天天看點

GCC在AMD64平台下的參數傳遞

from: http://hi.baidu.com/bluebanboom/blog/item/381959af65ff36fbfaed5068.html

閑着沒事的時候就拿自己動手寫作業系統裡的例子來編譯,但我裝的是AMD64的ubuntu,而實際上這并沒帶來多少好處,反倒帶來不少麻煩!

在編譯第五章中彙編和C混合程式設計的例子時遇到了麻煩,因為用的nasm不支援64位的elf格式,我就找到了yasm,yasm是nasm的完全重寫,

輸出格式支援64位的elf,但是編譯連結後運作程式始終提示段錯誤。

一開始我以為可能yasm的輸出與gcc的輸出不一樣,為了驗證我的想法,我寫了一個簡單,不需要傳遞參數的函數,結果連結後的

程式居然可以運作。看來可能是參數傳遞上出了問題。

一開始,我用hte來反彙編看函數調用過程,但是沒個比較,也不知道問題出在了哪兒,而且hte的反彙編效果很差。

還好,我還有個32位的debian而且,在debian下,程式順利編譯通過了。

我就在debian下用ida反彙編檢視

兩個程式反彙編的結果一對照,發現

在debian下編譯的程式是通過堆棧來傳遞參數的

而在ubuntu amd64下編譯的程式是通過寄存器來傳遞的參數

而原來的彙編代碼是通過堆棧來處理參數,當然會出問題。

修改後的foo.asm

============================

extern choose ; int choose(int a, int b);

[section .data] ; 資料在此

num1st  dd 5

num2nd  dd 6

[section .text] ; 代碼在此

global _start ; 我們必須導出 _start 這個入口,以便讓連結器識别。

global myprint ; 導出這個函數為了讓 bar.c 使用

_start:

 ;mov edi,3  ; ┓

 ;mov esi,4 ; ┃

 mov esi,[num2nd]

 mov edi,[num1st]

 ;push num1st

 ;push  num2nd

 call choose  ; ┣ choose(num1st, num2nd);

 add esp, 4  ; ┛

 mov ebx, 0

 mov eax, 1  ; sys_exit

 int 0x80  ; 系統調用

; void myprint(char* msg, int len)

myprint:

 ;mov edx, [esp + 8] ; len

 ;mov ecx, [esp + 4] ; msg

 mov edx, esi ; len

 mov ecx, edi

 mov ebx, 1

 mov eax, 4  ; sys_write

 int 0x80  ; 系統調用

 ret

================================================

然後我想知道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

繼續閱讀