<address id="ousso"></address>
<form id="ousso"><track id="ousso"><big id="ousso"></big></track></form>
  1. C語言

    調用C函數

    時間:2025-02-02 10:11:10 C語言 我要投稿

    匯編調用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++函數調用的方式07-29

    C++調用C函數的方法05-21

    java調用c函數的實例09-16

    C語言函數的遞歸調用08-26

    C語言函數的運用及調用10-09

    C函數的調用過程07-15

    C++如何調用matlab函數06-29

    C語言函數的遞歸和調用08-22

    C語言函數調用與參數傳遞08-05

    <address id="ousso"></address>
    <form id="ousso"><track id="ousso"><big id="ousso"></big></track></form>
    1. 日日做夜狠狠爱欧美黑人