Windows/Linux x64彙編函數調用約定

Windows/Linux x64彙編函數調用約定

調用約定細節很準也很全 已驗證過了 所以轉載了 謝謝:

https://www.cnblogs.com/shines77/p/3788514.html

上面裏的參考文章很垃圾 已刪除  還不如看我博客的:

https://my.oschina.net/zengfr   

以下爲原文:

1. 前言

最近在寫一些字符串函數的優化,用到x64彙編,我也是第一次接觸,故跟大家分享一下。

2. 簡介

x86:又名 x32 ,表示 Intel x86 架構,即 Intel 的 32位 80386 彙編指令集。

x64:表示 AMD64 和 Intel 的 EM64T ,而不包括 IA64 。至於三者間的區別,可自行搜索。

 

x64 跟 x86 相比寄存器的變化,如圖:

 

從圖上可以看到,X64架構相對於X86架構的主要變化,是將原來所有的寄存器都擴大了一倍,例如EAX現在擴充成RAX,同時,又新增加了從R8~R15這8個64位的寄位器,有點RISC的味道(RISC特點就是寄存器多)。

 

3. x64 調用約定

在 x86 模式下,有三種常用調用約定,cdecl (C規範) / stdcall(WinAPI默認) / fastcall 函數調用約定。

而在 x64 模式下,調用約定只有一種,就是 fastcall,但是 Windows 下和 Linux 下還是略有不同的,下面分別介紹。

 

3.1 Windows 下的 x64

一些細節:

  • Windows 的 x64 下只有一種函數調用約定,即 __fastcall ,其他調用約定的關鍵字會被忽略,也就是說 ABI 只有 __fastcall ;
  • 一個函數在調用時,前四個參數是從左至右依次存放於 RCX、RDX、R8、R9 寄存器裏面,剩下的參數通過棧傳遞,從右至左順序入棧;
  • 如果是 int f(double a, double b, double c, double d, double e, double f) 這樣的函數,前四個浮點類型參數從左到右由 XMM0,XMM1,XMM2,XMM3 依次傳遞,剩下的參數通過棧傳遞,從右至左順序入棧;
  • 調用者負責在棧上分配32字節的“shadow space”,用於存放那四個存放調用參數的寄存器的值(亦即前四個調用參數);
  • 小於64位(bit)的參數傳遞時高位並不填充零(例如只傳遞ecx),也就是說結構體或union如果大小是1,2,4,8字節,用值傳遞(相應的寄存器),大於8字節(64位)必須按照地址(指針)傳遞;
  • 被調用函數的返回值是64位以內(包括64位)的整形或指針時,則返回值會被存放於RAX;
  • 如果返回值是浮點值,則返回值存放在XMM0;
  • 更大的返回值(比如結構體),由調用方在棧上分配空間,並由 RCX 持有該空間的指針並傳遞給被調用函數,因此整型參數使用的寄存器依次右移一格,實際只可以利用 RDX,R8,R9,3個寄存器,其餘參數通過棧傳遞。函數調用結束後,RAX 返回該空間的指針(即函數調用開始時的 RCX 值)。
  • 調用者 (caller) 負責清理棧,被調用函數 (callee) 不用清棧,可是爲什麼有時候我們看到調用者 (caller) 也沒有清棧呢?後面會講;
  • 除了 RCX,RDX,R8,R9 以外,RAX,R10,R11 和 XMM5,XMM6 也是“易揮發”的,不用特別保護,其餘寄存器需要保護。(x86下只有 eax, ecx, edx 是易揮發的)
  • 棧需要16字節對齊,“call”指令會入棧一個8字節的函數返回地址(函數調用指令後的下一個指令的地址)(注:即函數調用前原來的RIP指令寄存器的值),這樣一來,棧就對不齊了(因爲RCX、RDX、R8、R9四個寄存器剛好是32個字節,是16字節對齊的,現在多出來了8個字節)。所以,所有非葉子結點調用的函數,都必須調整棧RSP的地址爲16n+8,來使棧對齊。
  • 對於 R8~R15 寄存器,我們可以使用 r8, r8d, r8w, r8b 分別代表 r8 寄存器的64位、低32位、低16位和低8位。

 

關於 Windows x64 的調用約定,可以參考微軟的官方文檔:

x64 調用約定

https://docs.microsoft.com/zh-cn/cpp/build/x64-calling-convention?view=vs-2017

 

一些其他要注意的小問題:

  • 另外一些小問題要注意,AMD64不支持 push 32bit 寄存器的指令,push 和 pop 都要用64位寄存器,即 push rbx ,不能使用 push ebx 。
  • 另外要補充的一點是,在一般情況下,x64 平臺的 RBP 棧基指針被廢棄掉,只作爲普通寄存器來用,所有的棧操作都通過 RSP 指針來完成。

 

關於有時候我們看到調用者 (caller) 也沒有清棧的原因:

  都說 x64 下 __fastcall 由調用者 (caller) 清理棧區空間。但是我們有時候發現 main() 函數或被 main() 函數調用的函數中,沒有清理子函數棧空間的過程呢?

  這是由於 64 位平臺下棧區空間開闢問題導致。我在CSDN上看到這樣一句話:與通過 PUSH 和 POP 指令在堆棧中顯式添加和移除參數的 x86 編譯器不同,x64 模式下,編譯器會預留足夠的堆棧空間,以調用最大目標函數(參數方法)所使用的任何內容。隨後,在調用子函數時,它重複使用相同的堆棧區域來設置這些參數,從而實現不用調用者 (caller) 反覆清棧的過程。

  這句話什麼意思呢?它的意思就是我們在 x64 模式下一開始系統會爲 main() 函數開闢一個很大的棧區,但是 main() 函數並未消耗掉這麼大的棧區空間,這時候怎麼辦呢?子函數就會還繼續利用 main() 函數的預留的棧區空間,所以 main() 函數或其他被 main() 調用的函數,並不用對子函數棧區空間進行清理。

 

示例:

複製代碼
; 示例代碼 1.asm
; 語法:GoASM

DATA SECTION
text     db 'Hello x64!', 0
caption  db 'My First x64 Application', 0

CODE SECTION
START:

sub rsp, 28h           ; 堆棧預留 shadow space (32)字節 + 8 字節,讓棧對齊到 16 字節

xor r9d, r9d           ; r9
lea r8, caption        ; r8
lea rdx, text          ; rdx
xor rcx, rcx           ; rcx

call MessageBoxA

add rsp, 28h           ; 調用者自己恢復堆棧

ret
複製代碼

 

3.2 Linux 下的 x64

調用約定細節:

  • Linux 下的調用約定叫做 “System V AMD64 ABI”,此約定主要在 Solaris,GNU/Linux,FreeBSD 和其他非微軟OS上使用;
  • Linux 的 x64 下也只有一種函數調用約定,即 __fastcall ,其他調用約定的關鍵字會被忽略,也就是說 ABI 只有 __fastcall ;
  • 一個函數在調用時,如果參數個數小於等於 6 個時,前 6 個參數是從左至右依次存放於 RDI,RSI,RDX,RCX,R8,R9 寄存器裏面,剩下的參數通過棧傳遞,從右至左順序入棧;
  • 如果參數個數大於 6 個時,前 5 個參數是從左至右依次存放於 RDI,RSI,RDX,RCX,RAX 寄存器裏面,剩下的參數通過棧傳遞,從右至左順序入棧;
  • 對於系統調用,使用 R10 代替 RCX;
  • XMM0 ~ XMM7 用於傳遞浮點參數;
  • 小於64位(bit)的參數傳遞時高位並不填充零(例如只傳遞ecx),也就是說結構體或union如果大小是1,2,4,8字節,用值傳遞(相應的寄存器),大於8字節(64位)必須按照地址(指針)傳遞;
  • 被調用函數的返回值是64位以內(包括64位)的整形或指針時,則返回值會被存放於 RAX,如果返回值是128位的,則高64位放入 RDX;
  • 如果返回值是浮點值,則返回值存放在XMM0;
  • 更大的返回值(比如結構體),由調用方在棧上分配空間,並由 RCX 持有該空間的指針並傳遞給被調用函數,因此整型參數使用的寄存器依次右移一格,實際只可以利用 RDI,RSI,RDX,R8,R9,5個寄存器,其餘參數通過棧傳遞。函數調用結束後,RAX 返回該空間的指針(即函數調用開始時的 RCX 值)。
  • 可選地,被調函數推入 RBP,以使 caller-return-rip 在其上方8個字節,並將 RBP 設置爲已保存的 RBP 的地址。這允許遍歷現有堆棧幀,通過指定GCC的 -fomit-frame-pointer 選項可以消除此問題。
  • 調用者 (caller) 負責清理棧,被調用函數 (callee) 不用清棧;
  • 除了 RDI,RSI,RDX,RCX,R8,R9 以外,RAX,R10,R11 也是“易揮發”的,不用特別保護,其餘寄存器需要保護。
  • 在調用 call 指令之前,必須保證堆棧是16字節對齊的;
  • 對於 R8~R15 寄存器,我們可以使用 r8, r8d, r8w, r8b 分別代表 r8 寄存器的64位、低32位、低16位和低8位。

5. 更新歷史

2020/09/18: 重新整理,修正錯漏,並新增 Linux 下的 x64 調用約定。

2014/06/14: 初始版本。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章