來自: 圖靈社區(qū)
內(nèi)容選自圖靈社區(qū)電子書《一個64位操作系統(tǒng)的實現(xiàn)》
作者:田宇
鏈接:http://www.ituring.com.cn/article/192591(點擊尾部閱讀原文前往)
比如:從系統(tǒng)引導(dǎo)過程中的匯編程序跳轉(zhuǎn)到系統(tǒng)主函數(shù)中,或者在中斷處理的匯編代碼中跳轉(zhuǎn)到中斷處理函數(shù)(傳說中的中斷上部), 這些過程都是從匯編程序跳轉(zhuǎn)到C程序的,其中不可缺少的有:調(diào)用約定,參數(shù)傳遞方式,函數(shù)調(diào)用方式等。因為這些過程都是在系統(tǒng)內(nèi)核中,所以,我們講解的是GNU C語言和AT&T匯編語言。話不多說,下面讓我們逐一介紹。
函數(shù)的調(diào)用方式
函數(shù)的調(diào)用方式其實沒那么復(fù)雜,基本上就是jmp、call、ret或者他們的變種而已。讓我們先看下面的程序。
int test()
{
int i = 0;
i = 1 + 2;
return i;
}
int main()
{
test();
return 0;
}
這段程序基本上沒有什么難點,很簡單,對吧?唯一要注意的地方是main函數(shù)的返回值,這里個人建議大家要使用int類型作為主函數(shù)的返回值,而不要使用void,或者其他類型。雖然,在主函數(shù)執(zhí)行到return 0之后就跟我們沒有什么關(guān)系了。但是,有的編譯器要求主函數(shù)要有個返回值,或者,在某些場合里,系統(tǒng)環(huán)境會用到主函數(shù)的返回值??紤]到上述原因,要使用int類型作為主函數(shù)的返回值,如果處于某個特殊的或者可預(yù)測的環(huán)境下,那就無所謂了。
說了這么多,反匯編一下這段代碼,看看匯編語言是怎么調(diào)用test函數(shù)的。工具objdump,用于反匯編二進制程序,它有很多參數(shù),可以反匯編出各類想要的信息。
objdump工具命令:
objdump -d test
下面是反匯編后的部分代碼,把相關(guān)的系統(tǒng)運行庫等一些與上面C程序不相關(guān)的代碼忽略掉。經(jīng)過刪減后的反匯編代碼如下:
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 :這一行,這里就是主函數(shù),前面的000000000040048b其實是函數(shù)main的地址。一共16個數(shù),16 * 4 = 64,對!這就是64位地址寬度啦。
乍一看,有好多個“%”符號,還記得2.2.1節(jié)里講的AT&T匯編語法嗎?這就是那里面說——引用寄存器的時候要在前面加“%”符號。
還有一些匯編指令的后綴,如:“l(fā)”、“q”。“l(fā)”的意思是雙字(long型),“q”的意思是四字(64位寄存器的后綴就是這個)。
如果您仔細觀察,是不是會發(fā)現(xiàn)有些寄存器rbp,rsp等,感覺會跟ebp和esp有關(guān)系呢?答對了,esp寄存器是32位寄存器,而rsp寄存器是64位寄存器。這是Intel對寄存器的一種向下繼承性,從最開始一字節(jié)的al,ah,到兩字節(jié)的ax(16位),四字節(jié)的eax(32位),再到八字節(jié)的rax(64位),寄存器的長度在不斷的擴展,對于相關(guān)指令的使用,也從“b”、“l(fā)”,“q”,也是不斷的向下繼承或擴展。
這里有一條指令leaveq,它等效于 movq %rbp, %rsp; popq %rbp;
callq 400474 這句的意思就是跳轉(zhuǎn)到test函數(shù)里執(zhí)行。其實匯編調(diào)用C函數(shù)就這么簡單,如果把這條callq指令改成jmpq指令也是可以的。這要從call和jmp的區(qū)別上說起,call會把在其之后的那條指令的地址壓入棧,在上面反匯編后的代碼中,就是0000000000400499,然后再跳轉(zhuǎn)到test函數(shù)里執(zhí)行。而jmpq就不會把地址0000000000400499壓入棧中。當(dāng)函數(shù)執(zhí)行完畢,調(diào)用retq指令返回的時候,會把棧中的返回地址彈出到rip寄存器中,這樣就返回到main函數(shù)中繼續(xù)執(zhí)行了。
實現(xiàn)jmpq代替callq的偽代碼如下所示:
pushq $0x0000000000400499
jmpq 400474
對于callq 400474 這條指令也可以使用retq來實現(xiàn)。它的實現(xiàn)原理是:指令retq會將棧中的返回地址彈出,并放入到rip寄存器中,然后處理器從rip寄存器所指的地址內(nèi)取指令后繼續(xù)執(zhí)行。根據(jù)這個原理,可以先將返回地址0000000000400499壓入棧中。然后再將test函數(shù)的入口地址0000000000400474壓入棧中,接著使用retq指令,以調(diào)用返回的形式,從main函數(shù)“返回”到test函數(shù)中。
實現(xiàn)retq代替callq的偽代碼如下所示:
pushq $0x0000000000400499
pushq $0x0000000000400474
retq
這些看起來是不是沒有想象的那么難?其實把匯編的原理掌握清楚了,這些都是可以靈活運用的,希望這段內(nèi)容能啟發(fā)讀者的靈感~!
調(diào)用約定
對于不同的公司,不同的語言以及不同的需求,都是用各自不同的調(diào)用約定,而且他們往往差異很大。在IBM兼容機對市場進行洗牌后,微軟操作系統(tǒng)和編程工具占據(jù)了統(tǒng)治地位,除了微軟之外,還有零星的一些公司,以及開源項目GCC,都各自維護著自己的標準。下面是比較流行的幾款調(diào)用標準,咱們寫的大多數(shù)程序都出自這個標準之一。
stdcall
1、在進行函數(shù)調(diào)用的時候,函數(shù)的參數(shù)是從右向左依次放入棧中的。
如:
int function(int first,int second)
這個函數(shù)的參數(shù)入棧順序,首先是參數(shù)second,然后是參數(shù)first。
2、函數(shù)的棧平衡操作是由被調(diào)用函數(shù)執(zhí)行的,使用的指令是 retn X,X表示參數(shù)占用的字節(jié)數(shù),CPU在ret之后自動彈出X個字節(jié)的堆??臻g。例如上面的function函數(shù),當(dāng)我們把function的函數(shù)參數(shù)壓入棧中后,當(dāng)function函數(shù)執(zhí)行完畢后,由function函數(shù)負責(zé)將傳遞給它的參數(shù)first和second從棧中彈出來。
3、在函數(shù)名的前面用下劃線修飾,在函數(shù)名的后面由@來修飾,并加上棧需要的字節(jié)數(shù)。如上面的function函數(shù),會被編譯器轉(zhuǎn)換為_function@8。
cdecl
1、在進行函數(shù)調(diào)用的時候,和stdcall一樣,函數(shù)的參數(shù)是從右向左依次放入棧中的。
2、函數(shù)的棧平衡操作是由調(diào)用函數(shù)執(zhí)行的,這點是與stdcall不同之處。stdcall使用retn X平衡棧,cdecl則使用leave、pop、增加棧指針寄存器的數(shù)據(jù)等方法平衡棧。
3、每一個調(diào)用它的函數(shù)都包含有清空棧的代碼,所以編譯產(chǎn)生的可執(zhí)行文件會比調(diào)用stdcall約定產(chǎn)生的文件大。
cdecl是GCC的默認調(diào)用約定。但是,GCC在x64位系統(tǒng)環(huán)境下,使用寄存器作為函數(shù)調(diào)用的參數(shù)。按照從左向右的順序,頭六個整型參數(shù)放在寄存器RDI, RSI, RDX, RCX, R8和R9上,同時XMM0到XMM7用來放置浮點變元,返回值保存在RAX中,并且由調(diào)用者負責(zé)平衡棧。
fastcall
1.函數(shù)調(diào)用約定規(guī)定,函數(shù)的參數(shù)在可能的情況下使用寄存器傳遞參數(shù),通常是前兩個 DWORD類型的參數(shù)或較小的參數(shù)使用ECX和EDX寄存器傳遞,其余參數(shù)按照從右向左的順序入棧。
2、函數(shù)的棧平衡操作是由被調(diào)用函數(shù)在返回之前負責(zé)清除棧中的參數(shù)。
還有很多調(diào)用規(guī)則,如:thiscall、naked call、pascal等,有興趣的讀者可以自己去研究一下。
參數(shù)傳遞方式
函數(shù)參數(shù)的傳遞方式無外乎兩種,一種是通過寄存器傳遞,另一種是通過內(nèi)存?zhèn)鬟f。這兩種傳遞方式在我們平時的開發(fā)中并不會被關(guān)注,因為不在特殊情況下,這兩種傳遞方式,都可以滿足要求。但是,我們要寫的是操作系統(tǒng),在操作系統(tǒng)里面有很多苛刻的環(huán)境要求,這使得我們不得不了解這些參數(shù)傳遞方式,來解決這些問題。
寄存器傳遞
寄存器傳遞就是將函數(shù)的參數(shù)放到寄存器里傳遞,而不是放到棧里傳遞。這樣的好處主要是執(zhí)行速度快,編譯后生成的代碼量少。但只有少部分調(diào)用規(guī)定默認是通過寄存器傳遞參數(shù),大部分編譯器是需要特殊指定使用寄存器傳遞參數(shù)的。
在X86體系結(jié)構(gòu)下,系統(tǒng)調(diào)用一般會使用寄存器傳遞,由于作者看過的內(nèi)核種類有限,也不能確定所有的內(nèi)核都是這么處理的,但是Linux內(nèi)核肯定是這么做的。因為應(yīng)用程序的執(zhí)行空間和系統(tǒng)內(nèi)核的執(zhí)行空間是不一樣的,如果想從應(yīng)用層把參數(shù)傳遞到內(nèi)核層的話,最方便快捷的方法是通過寄存器傳遞參數(shù),否則需要使用很大的周折才能把數(shù)據(jù)傳遞過去,原因會在以后的章節(jié)中詳細講述。
內(nèi)存?zhèn)鬟f
內(nèi)存?zhèn)鬟f參數(shù)很好理解,在大多數(shù)情況下參數(shù)傳遞都是通過內(nèi)存入棧的形式實現(xiàn)的。
在X86體系結(jié)構(gòu)下的Linux內(nèi)核中,中斷或異常的處理會使用內(nèi)存?zhèn)鬟f參數(shù)。因為,在中斷產(chǎn)生后,到中斷處理的上半部,中間的過渡代碼是用匯編實現(xiàn)的。匯編跳轉(zhuǎn)到C語言的過程中,C語言是用堆棧保存參數(shù)的,為了無縫銜接,匯編就需要把參數(shù)壓入棧中,然后再跳轉(zhuǎn)到C語言實現(xiàn)的中斷處理程序中。
以上這些都是在X86體系結(jié)構(gòu)下的參數(shù)傳遞方式,在X64體系結(jié)構(gòu)下,大部分編譯器都使用的是寄存器傳遞參數(shù)。因此,內(nèi)存?zhèn)鬟f和寄存器傳遞的區(qū)別就不太重要了。
●本文編號58,以后想閱讀這篇文章直接輸入58即可。
●輸入m可以獲取到文章目錄
推薦《15個技術(shù)類公眾微信》
聯(lián)系客服