糯米文學吧

位置:首頁 > 計算機 > C語言

彙編調用C函數

C語言1.37W

系統引導過程中的彙編程序跳轉到系統主函數中,或者在中斷處理的彙編代碼中跳轉到中斷處理函數(傳説中的中斷上部), 這些過程都是從彙編程序跳轉到C程序的,其中不可缺少的有:調用約定,參數傳遞方式,函數調用方式等。因為這些過程都是在系統內核中,所以,我們講解的是GNU C語言和AT&T彙編語言。話不多説,下面讓我們逐一介紹。

彙編調用C函數
彙編調用C函數

 函數的調用方式

函數的調用方式其實沒那麼複雜,基本上就是jmp、call、ret或者他們的變種而已。讓我們先看下面的程序。

int test()

{

int i = 0;

i = 1 + 2;

return i;

}

int main()

{

test();

return 0;

}

這段程序基本上沒有什麼難點,很簡單,對吧?唯一要注意的地方是main函數的返回值,這裏個人建議大家要使用int類型作為主函數的返回值,而不要使用void,或者其他類型。雖然,在主函數執行到return 0之後就跟我們沒有什麼關係了。但是,有的編譯器要求主函數要有個返回值,或者,在某些場合裏,系統環境會用到主函數的返回值。考慮到上述原因,要使用int類型作為主函數的返回值,如果處於某個特殊的或者可預測的環境下,那就無所謂了。

説了這麼多,反彙編一下這段代碼,看看彙編語言是怎麼調用test函數的。工具objdump,用於反彙編二進制程序,它有很多參數,可以反彙編出各類想要的信息。

  objdump工具命令:

objdump -d test

下面是反彙編後的部分代碼,把相關的系統運行庫等一些與上面C程序不相關的代碼忽略掉。經過刪減後的反彙編代碼如下:

0000000000400474:

400474: 55 push %rbp

400475: 48 89 e5 mov %rsp,%rbp

400478: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)

40047f: c7 45 fc 03 00 00 00 movl $0x3,-0x4(%rbp)

400486: 8b 45 fc mov -0x4(%rbp),%eax

400489: c9 leaveq

40048a: c3 retq

000000000040048b

:

40048b: 55 push %rbp

40048c: 48 89 e5 mov %rsp,%rbp

40048f: b8 00 00 00 00 mov $0x0,%eax

400494: e8 db ff ff ff callq 400474

400499: b8 00 00 00 00 mov $0x0,%eax

40049e: c9 leaveq

40049f: c3 retq

大家先看000000000040048b :這一行,這裏就是主函數,前面的000000000040048b其實是函數main的地址。一共16個數,16 * 4 = 64,對!這就是64位地址寬度啦。

乍一看,有好多個“%”符號,還記得2.2.1節裏講的AT&T彙編語法嗎?這就是那裏面説——引用寄存器的時候要在前面加“%”符號。

還有一些彙編指令的後綴,如:“l”、“q”。“l”的意思是雙字(long型),“q”的意思是四字(64位寄存器的後綴就是這個)。

如果您仔細觀察,是不是會發現有些寄存器rbp,rsp等,感覺會跟ebp和esp有關係呢?答對了,esp寄存器是32位寄存器,而rsp寄存器是64位寄存器。這是Intel對寄存器的一種向下繼承性,從最開始一字節的al,ah,到兩字節的ax(16位),四字節的eax(32位),再到八字節的rax(64位),寄存器的長度在不斷的擴展,對於相關指令的使用,也從“b”、“l”,“q”,也是不斷的向下繼承或擴展。

這裏有一條指令leaveq,它等效於 movq %rbp, %rsp; popq %rbp;

callq 400474 這句的意思就是跳轉到test函數裏執行。其實彙編調用C函數就這麼簡單,如果把這條callq指令改成jmpq指令也是可以的。這要從call和jmp的區別上説起,call會把在其之後的那條指令的地址壓入棧,在上面反彙編後的代碼中,就是0000000000400499,然後再跳轉到test函數裏執行。而jmpq就不會把地址0000000000400499壓入棧中。當函數執行完畢,調用retq指令返回的時候,會把棧中的返回地址彈出到rip寄存器中,這樣就返回到main函數中繼續執行了。

實現jmpq代替callq的偽代碼如下所示:

pushq $0x0000000000400499

jmpq 400474

對於callq 400474 這條指令也可以使用retq來實現。它的實現原理是:指令retq會將棧中的返回地址彈出,並放入到rip寄存器中,然後處理器從rip寄存器所指的地址內取指令後繼續執行。根據這個原理,可以先將返回地址0000000000400499壓入棧中。然後再將test函數的入口地址0000000000400474壓入棧中,接着使用retq指令,以調用返回的形式,從main函數“返回”到test函數中。

實現retq代替callq的偽代碼如下所示:

pushq $0x0000000000400499

pushq $0x0000000000400474

  retq

這些看起來是不是沒有想象的那麼難?其實把彙編的原理掌握清楚了,這些都是可以靈活運用的,希望這段內容能啟發讀者的靈感~!

  調用約定

對於不同的公司,不同的語言以及不同的需求,都是用各自不同的調用約定,而且他們往往差異很大。在IBM兼容機對市場進行洗牌後,微軟操作系統和編程工具佔據了統治地位,除了微軟之外,還有零星的一些公司,以及開源項目GCC,都各自維護着自己的標準。下面是比較流行的幾款調用標準,咱們寫的大多數程序都出自這個標準之一。

  stdcall

1、在進行函數調用的時候,函數的參數是從右向左依次放入棧中的。

如:

int function(int first,int second)

這個函數的參數入棧順序,首先是參數second,然後是參數first。

2、函數的棧平衡操作是由被調用函數執行的,使用的`指令是 retn X,X表示參數佔用的字節數,CPU在ret之後自動彈出X個字節的堆棧空間。例如上面的function函數,當我們把function的函數參數壓入棧中後,當function函數執行完畢後,由function函數負責將傳遞給它的參數first和second從棧中彈出來。

3、在函數名的前面用下劃線修飾,在函數名的後面由@來修飾,並加上棧需要的字節數。如上面的function函數,會被編譯器轉換為_function@8。

  cdecl

1、在進行函數調用的時候,和stdcall一樣,函數的參數是從右向左依次放入棧中的。

2、函數的棧平衡操作是由調用函數執行的,這點是與stdcall不同之處。stdcall使用retn X平衡棧,cdecl則使用leave、pop、增加棧指針寄存器的數據等方法平衡棧。

3、每一個調用它的函數都包含有清空棧的代碼,所以編譯產生的可執行文件會比調用stdcall約定產生的文件大。

cdecl是GCC的默認調用約定。但是,GCC在x64位系統環境下,使用寄存器作為函數調用的參數。按照從左向右的順序,頭六個整型參數放在寄存器RDI, RSI, RDX, RCX, R8和R9上,同時XMM0到XMM7用來放置浮點變元,返回值保存在RAX中,並且由調用者負責平衡棧。

fastcall

1.函數調用約定規定,函數的參數在可能的情況下使用寄存器傳遞參數,通常是前兩個 DWORD類型的參數或較小的參數使用ECX和EDX寄存器傳遞,其餘參數按照從右向左的順序入棧。

2、函數的棧平衡操作是由被調用函數在返回之前負責清除棧中的參數。

還有很多調用規則,如:thiscall、naked call、pascal等,有興趣的讀者可以自己去研究一下。

  參數傳遞方式

函數參數的傳遞方式無外乎兩種,一種是通過寄存器傳遞,另一種是通過內存傳遞。這兩種傳遞方式在我們平時的開發中並不會被關注,因為不在特殊情況下,這兩種傳遞方式,都可以滿足要求。但是,我們要寫的是操作系統,在操作系統裏面有很多苛刻的環境要求,這使得我們不得不瞭解這些參數傳遞方式,來解決這些問題。

  寄存器傳遞

寄存器傳遞就是將函數的參數放到寄存器裏傳遞,而不是放到棧裏傳遞。這樣的好處主要是執行速度快,編譯後生成的代碼量少。但只有少部分調用規定默認是通過寄存器傳遞參數,大部分編譯器是需要特殊指定使用寄存器傳遞參數的。

在X86體系結構下,系統調用一般會使用寄存器傳遞,由於作者看過的內核種類有限,也不能確定所有的內核都是這麼處理的,但是Linux內核肯定是這麼做的。因為應用程序的執行空間和系統內核的執行空間是不一樣的,如果想從應用層把參數傳遞到內核層的話,最方便快捷的方法是通過寄存器傳遞參數,否則需要使用很大的周折才能把數據傳遞過去,原因會在以後的章節中詳細講述。

  內存傳遞

內存傳遞參數很好理解,在大多數情況下參數傳遞都是通過內存入棧的形式實現的。

在X86體系結構下的Linux內核中,中斷或異常的處理會使用內存傳遞參數。因為,在中斷產生後,到中斷處理的上半部,中間的過渡代碼是用匯編實現的。彙編跳轉到C語言的過程中,C語言是用堆棧保存參數的,為了無縫銜接,彙編就需要把參數壓入棧中,然後再跳轉到C語言實現的中斷處理程序中。

以上這些都是在X86體系結構下的參數傳遞方式,在X64體系結構下,大部分編譯器都使用的是寄存器傳遞參數。因此,內存傳遞和寄存器傳遞的區別就不太重要了。

標籤:彙編 調用 函數