X64的函數調用規則

閒着沒事想研究一下gcc的函數調用方式和m$的__stdcall、__fastcall之類有何區別,本想是瞭解一下關於參數的入棧順序和清理方,就隨便寫了個C函數,編譯成.s文件,一看發現根本就沒有push和pop之類的指令...兩個int參數都是利用rsi和rdi傳遞!網上百度了一個關於m$平臺x64的調用約定

 看完ddk裏相關的部分,總結下吧,規則倒是不復雜,相對x86時代的stdcall cdecl fastcall 三分天下要簡明的多。按ddk裏的說法,m$就是要趁這次統一調用規則…… -__-

在x64下函數調用的前4個參數總是放在寄存器中傳遞,剩餘的參數則壓入堆棧中。而x86上則是全部壓入堆棧中(除了fastcall方式)。這4個用於存放參數的寄存器分別是:存放整數參數的RCX,RDX,R8,R9;存放浮點數參數的XMM0,XMM1,XMM2,XMM3。

按照所傳參數是整數還是浮點數的不同,寄存器的使用規則如下:

全部整數參數:
func1(int a, int b, int c, int d, int e);

參數a放入RCX,參數b放入RDC,參數c放入R8,參數d放入R9,參數e麼壓棧。

參數傳遞規則:按照參數表聲明的順序,從左向右,前4個參數依次放入RCX,RDX,R8,R9中。

全部浮點數參數:
func1(float a, float b, float c, double d, float e);
a放入XMM0,b放入XMM1,c放入XMM2,d放入XMM3,e壓棧
參數傳遞規則:按照參數聲明的順序,從左向右,前4個參數依次放入XMM0,XMM1,XMM2,XMM3中

整數和浮點數參數混合出現:
func3(float a, int b, double c, int d)
a放入XMM0中,b放入RDX,c放入XMM2,d放入R9。
這裏比較特殊,其實就是按照這個規則:
a b c d
RDX R9
XMM0 XMM2
也就是說4個整數寄存器嚴格的一一對應前4個參數,同樣前4個XMM寄存器嚴格的一一應前4個參數,如果是整數浮點數間隔出現,那麼就保持對應關係,選擇對應的寄存器即可。

指針參數:
指針的傳遞遵循整數參數傳遞方式。

結構體參數:
結構體特殊一點,按照ddk的描述,如果結構體長度小於64bit,則使用整數參數的傳遞規則。但如果是一個很大的結構體,那麼應該還是要在堆棧中申請臨時空間的(但ddk沒有明說這一點,參考x86的規則應該如此)。


未聲明函數的調用:
ddk裏特別列舉了這樣一個例子:
func1();
func2(){
func1(2, 1.0, 7)
}

在這種情況下,func1()的參數表其實不明確,那麼參數的傳遞要怎樣進行?這裏採用了一個比較保守的規則,就是:整數參數還是按照寄存器映射關係放入對應的寄存器中,浮點數在按照映射關係放入XMM寄存器後,還需要按照整數參數的寄存器映射關係放入整數寄存器中一次,這就是爲啥我說是“比較保守的規則”。就現在這個例子而言,結果如下:
2在RCX中,1.0在RDX和XMM1中,7在R8中。

然而gcc的用法卻與此截然不同:

 

版權爲 win_hate 所有, 轉載請保留作者名字

我這段時間要把以前的一個 x86_32 的 linux 程序移植到 x86_64(AMD) 的 linux 環境裏. 由於寫的是數學算法, 64 與 32 位有很大不同, 代碼實際上要重寫. 看了點資料後, 覺得 AMD64 的擴展於以前 16 到 32 位的擴展很類似, e**, 擴展爲 r**, 此外還多了8個通用寄存器 r8~r15.指令格式與32位的極爲相似. 我覺得比較容易, 所以沒再仔細看, 就開始動手寫了.

我的程序由若干個彙編模塊於與若干個c模塊構成, 很多c模塊要調用匯編模塊. 作爲試驗, 我先寫了個簡單的彙編函數, 然後用c來調用. 結果算出來的值始終是錯誤的. 這令我很惱火, 因爲函數很簡單, 沒有多少出錯的餘地. 後來我把程序反匯編出來, 錯誤馬上浮現出來了, 函數的參數居然是通過寄存器來傳遞的. 我憑以前的經驗, 從堆棧裏取參數, 算出的結果當然不對了. 我以前不是沒碰到過用寄存器傳遞參數的情況, 但所在的環境都不是 pc. 在 x86_32/linux 中, 即使用 -O3 優化選項, gcc 仍通過棧來傳遞參數的.

所以我們現在知道, 在 x86_64/linux/gcc3.2 中, 即使不打開優化選項, 函數的參數也會通過寄存器來傳遞, 這肯定是闊了的表現(通用寄存器多了).

我試驗了多個參數的情況,發現一般規則爲, 當參數少於7個時, 參數從左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。當參數爲 7 個以上時, 前 6 個與前面一樣, 但後面的依次從 "右向左" 放入棧中。

例如:
CODE

(1) 參數個數少於7個:
f (a, b, c, d, e, f);
a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9

g (a, b)
a->%rdi, b->%rsi

有趣的是, 實際上將參數放入寄存器的語句是從右到左處理參數表的, 這點與32位的時候一致.

CODE

2) 參數個數大於 7 個的時候
H(a, b, c, d, e, f, g);
a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%rax
g->8(%esp)
f->(%esp)
call H


易失寄存器:
%rax, %rcx, %rdx, %rsi, %rdi, %r8, %r9 爲易失寄存器, 被調用者不必恢復它們的值。
顯然,這裏出現的寄存器大多用於參數傳遞了, 值被改掉也無妨。而 %rax, %rdx 常用於
數值計算, %rcx 常用於循環計數,它們的值是經常改變的。其它的寄存器爲非易失的,也
就是 rbp, rbx, rsp, r10~r15 的值如果在彙編模塊中被改變了,在退出該模塊時,必須將
其恢復。

教訓:
用匯編寫模塊, 然後與 c 整合, 一定要搞清楚編譯器的行爲, 特別是參數傳遞的方式. 此外, 我現在比較擔心的一點是, 將來如果要把程序移植到 WIN/VC 環境怎麼辦? 以前我用cygwin的gcc來處理彙編模塊, 用vc來處理c模塊, 只需要很少改動. 現在的問題是, 如果VC用不同的參數傳遞方式, 那我不就麻煩了?

補充:
前面的參數 a, b, c, d 等, 都是整數, 長整數, 或指針, 也就是說, 能放到寄存器裏頭的. 如果你要傳遞一個很大的結構, 我估計編譯器也只能通過棧來傳遞了.

環境爲 AMD Athlon64, Mandrak linux 9.2, GCC3.3.1

 
對比上下兩篇文章可知,gcc和vc在x64的函數調用方式完全不同,gcc和vc編譯的模塊想要互相調用貌似不太可能,除非其中的一種推出兼容模式

發佈了67 篇原創文章 · 獲贊 19 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章