彙編調用C函數
從系統引導過程中的彙編程序跳轉到系統主函數中,或者在中斷處理的彙編代碼中跳轉到中斷處理函數(傳説中的中斷上部), 這些過程都是從彙編程序跳轉到C程序的,其中不可缺少的有:調用約定,參數傳遞方式,函數調用方式等。因為這些過程都是在系統內核中,所以,我們講解的是GNU C語言和AT&T彙編語言。話不多説,下面讓我們逐一介紹。
彙編調用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體系結構下,大部分編譯器都使用的是寄存器傳遞參數。因此,內存傳遞和寄存器傳遞的區別就不太重要了。
-
C/C++變量在內存中的分佈介紹
變量在內存地址的分佈為:堆-棧-代碼區-全局靜態-常量數據。同一區域的各變量按聲明的順序在內存的中依次由低到高分配空間(只有未賦值的全局變量是個例外)。本文是本站小編搜索整理的關於C/C++變量在內存中的分佈介紹,感興趣的朋友一起學習吧!!想了解更多相關信息...
-
C語言順序結構知識歸納
C語言的順序結構裏面有哪些知識需要學習的呢,下面小編為大家歸納了C語言順序結構知識,歡迎大家閲讀!C語言順序結構知識歸納一、表達式語句、函數調用語句和空語句1.C語言的語句共分五大類:表達式語句、控制語句、函數調用語句、空語句和複合語句。2.表達式語句的...
-
2017計算機二級C語言精選習題
多做題有助於同學們及時檢測自己的學習情況。希望提供的2017計算機二級C語言精選習題,能夠幫助大家鞏固所學知識,為今後的學習打好基礎!(1)OSI模型的'物理層負責下列哪一種功能?A)格式化報文B)為數據選擇通過網絡的路由C)定義連接到介質的特徵D)提供遠程文件訪...
-
C語言指針的長度和類型講解
對於初學者深入理解C語言程序設計有很好的參考價值,下面是小編為大家整理的C語言指針的長度和類型講解,歡迎參考~一般來説,如果考慮應用程序的兼容性和可移植性,指針的長度就是一個問題,在大部分現代平台上,數據指針的長度通常是一樣的,與指針類型無關,儘管C標準沒有規...