<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12
      代碼改變世界

      函數調用過程探究

      2012-05-22 01:30  bangerlee  閱讀(50905)  評論(7)    收藏  舉報

      引言

      如何定義函數、調用函數,是每個程序員學習編程的入門課。調用函數(caller)向被調函數(callee)傳入參數,被調函數返回結果,看似簡單的過程,其實CPU和系統內核在背后做了很多工作。下面我們通過反匯編工具,來看函數調用的底層實現。

       

      基礎知識

      我們先來看幾個概念,這有助于理解后面反匯編的輸出結果。

      棧(stack)

      棧,相信大家都十分熟悉,push/pop,只允許在一端進行操作,后進先出(LIFO),凡是學過編程的人都能列出一二三點。但就是這個最簡單的數據結構,構成了計算機中程序執行的基礎,用于內核中程序執行的棧具有以下特點:

      • 每一個進程在用戶態對應一個調用棧結構(call stack)
      • 程序中每一個未完成運行的函數對應一個棧幀(stack frame),棧幀中保存函數局部變量、傳遞給被調函數的參數等信息
      • 棧底對應高地址,棧頂對應低地址,棧由內存高地址向低地址生長

      一個進程的調用棧圖示如下:

       

      寄存器(register)

      寄存器位于CPU內部,用于存放程序執行中用到的數據和指令,CPU從寄存器中取數據,相比從內存中取快得多。寄存器又分通用寄存器和特殊寄存器。

      通用寄存器有ax/bx/cx/dx/di/si,盡管這些寄存器在大多數指令中可以任意選用,但也有一些規定某些指令只能用某個特定“通用”寄存器,例如函數返回時需將返回值mov到ax寄存器中;特殊寄存器有bp/sp/ip等,特殊寄存器均有特定用途,例如sp寄存器用于存放以上提到的棧幀的棧頂地址,除此之外,不用于存放局部變量,或其他用途。

       

      對于有特定用途的幾個寄存器,簡要介紹如下:

      • ax(accumulator): 可用于存放函數返回值
      • bp(base pointer): 用于存放執行中的函數對應的棧幀的棧底地址
      • sp(stack poinger): 用于存放執行中的函數對應的棧幀的棧頂地址
      • ip(instruction pointer): 指向當前執行指令的下一條指令

       

      不同架構的CPU,寄存器名稱被添以不同前綴以指示寄存器的大小。例如對于x86架構,字母“e”用作名稱前綴,指示各寄存器大小為32位;對于x86_64寄存器,字母“r”用作名稱前綴,指示各寄存器大小為64位。

       

      函數調用例子

      了解了棧和寄存器的概念,下面看一個函數調用實例:

      //func_call.c
      int
      bar(int c, int d) { int e = c + d; return e; } int foo(int a, int b) { return bar(a, b); } int main(void) { foo(2, 5); return 0; }

      該程序很簡單,main->foo->bar,編譯得到可執行文件func_call:

      # gcc -g func_call.c -o func_call

       

      -g選項使目標文件func_call包含程序的調試信息。

       

      反匯編分析

      下面我們使用gdb對func_call進行反匯編,跟蹤main->foo->bar函數調用過程。

      # gdb func_call
      //此處省略gdb版本信息
      Reading symbols from /tmp/lx/func_call...done.
      (gdb) start
      Temporary breakpoint 1 at 0x400525: file func_call.c, line 14.
      Starting program: /tmp/lx/func_call 
      
      Temporary breakpoint 1, main () at func_call.c:14
      14            foo(2, 5);
      (gdb)

      start命令用于拉起被調試程序,并執行至main函數的開始位置,程序被執行之后與一個用戶態的調用棧關聯。

       

      main函數

      現進程跑在main函數中,我們disassemble命令顯示當前函數的匯編信息:

      (gdb) disassemble /rm
      Dump of assembler code for function main:
      13        {
      0x0000000000400521 <main+0>:     55                push %rbp
      0x0000000000400522 <main+1>:     48 89 e5          mov %rsp,%rbp
      
      14               foo(2, 5);
      0x0000000000400525 <main+4>:     be 05 00 00 00    mov $0x5,%esi
      0x000000000040052a <main+9>:     bf 02 00 00 00    mov $0x2,%edi
      0x000000000040052f <main+14>:    e8 d2 ff ff ff    callq 0x400506 <foo>
      
      15               return 0;
      0x0000000000400534 <main+19>:    b8 00 00 00 00    mov $0x0,%eax
      
      16        }
      0x0000000000400539 <main+24>:     c9               leaveq 
      0x000000000040053a <main+25>:     c3               retq
      
      End of assembler dump.

       

      disassemble命令的/m指示顯示匯編指令的同時,顯示相應的程序源碼;/r指示顯示十六進制的計算機指令(raw instruction)。

      以上輸出每行指示一條匯編指令,除程序源碼外共有四列,各列含義為:

      1. 0x0000000000400521: 該指令對應的虛擬內存地址
      2. <main+0>: 該指令的虛擬內存地址偏移量
      3. 55: 該指令對應的計算機指令
      4. push %rbp: 匯編指令

       

      一個函數被調用,首先默認要完成以下動作:

      • 將調用函數的棧幀棧底地址入棧,即將bp寄存器的值壓入調用棧中
      • 建立新的棧幀,將被調函數的棧幀棧底地址放入bp寄存器中

      以下兩條指令即完成上面動作:

      push %rbp
      mov  %rsp, %rbp

      也許你會問:咦?以上disassemble的輸出不是main函數的匯編指令嗎,怎么輸出中也有上面兩條指令?難道main也是一個“被調函數”?

      是的,皆因main并不是程序拉起后第一個被執行的函數,它被_start函數調用,更詳細的資料參看這里

       

      一個函數調用另一個函數,需先將參數準備好。main調用foo函數,兩個參數傳入通用寄存器中:

      mov $0x5, %esi
      mov $0x2, %edi

       

      對于參數傳遞的方式,x86和x86_64定義了不同的函數調用規約(calling convention)。相比x86_64將參數傳入通用寄存器的方式,x86將參數壓入調用棧中,x86下對應foo函數傳參的匯編指令,有以下形式的輸出:

      sub $0x8, %esp
      mov $0x5, -0x4(%ebp)
      mov $0x2, -0x8(%ebp)

      參數的調用棧位置通過ebp保存的棧幀棧底地址索引,棧從內存高地址向低地址生長,所以索引值為負數,減少esp寄存器的值表示擴展棧幀。

       

      萬事具備,是時候將執行控制權交給foo函數了,call指令完成交接任務:

      0x000000000040052f <main+14>:     e8 d2 ff ff ff    callq  0x400506 <foo>

      一條call指令,完成了兩個任務:

      1. 將調用函數(main)中的下一條指令(這里為0x400534)入棧,被調函數返回后將取這條指令繼續執行,64位rsp寄存器的值減8
      2. 修改指令指針寄存器rip的值,使其指向被調函數(foo)的執行位置,這里為0x400506

       

      執行完start命令后,現在程序停在0x400522的位置,下面我們通過gdb的si指令,讓程序執行完call指令:

      (gdb) si 3
      foo (a=0, b=4195328) at func_call.c:8
      8    {
      (gdb) 

      此時我們再來看rsp、rbp寄存器的值,它們保存了程序實際用到的物理內存地址:

      (gdb) info registers rbp rsp
      rbp            0x7fffffffe8e0    0x7fffffffe8e0
      rsp            0x7fffffffe8d8    0x7fffffffe8d8
      (gdb)

       

      main函數君的執行到此就暫時告一段落了,此時func_call的調用棧情況如下:

      相關寄存器信息如下:

      esi: 0x5   edi: 0x2

       

      foo函數

      foo函數被執行之后,我們使用disassemble命令顯示其匯編指令:

      (gdb) disassemble /rm
      Dump of assembler code for function foo:
      8    {
      0x0000000000400506 <foo+0>:     55             push   %rbp
      0x0000000000400507 <foo+1>:     48 89 e5       mov    %rsp,%rbp
      0x000000000040050a <foo+4>:     48 83 ec 08    sub    $0x8,%rsp
      0x000000000040050e <foo+8>:     89 7d fc       mov    %edi,-0x4(%rbp)
      0x0000000000400511 <foo+11>:    89 75 f8       mov    %esi,-0x8(%rbp)
      
      9        return bar(a, b);
      0x0000000000400514 <foo+14>:     8b 75 f8      mov    -0x8(%rbp),%esi
      0x0000000000400517 <foo+17>:     8b 7d fc      mov    -0x4(%rbp),%edi
      0x000000000040051a <foo+20>:     e8 cd ff ff ff    callq  0x4004ec <bar>
      
      10    }
      0x000000000040051f <foo+25>:     c9    leaveq 
      0x0000000000400520 <foo+26>:     c3    retq   
      
      End of assembler dump.
      (gdb)

      前面兩條指令將main函數棧幀的棧底地址入棧,建立foo函數的棧幀。接著的三條指令擴展棧幀,將傳入的參數存為函數內局部變量。最后三條指令與bar函數調用相對應,也是先將參數傳入esi、edi寄存器,然后執行call指令。

       

      繼續執行si命令,讓程序執行到call指令的位置:

      (gdb) si 8
      bar (c=32767, d=-139920736) at func_call.c:2
      2    {
      (gdb) info registers rbp rsp
      rbp            0x7fffffffe8d0    0x7fffffffe8d0
      rsp            0x7fffffffe8c0    0x7fffffffe8c0
      (gdb)

       

      foo函數調用bar函數之后,bar函數執行之前,調用棧信息如下:

      相關寄存器信息如下:

      esi: 0x5   edi: 0x2

       

      bar函數

      此時程序執行至bar函數,同樣,我們先用disassemble看一下bar函數的匯編指令:

      (gdb) disassemble /rm
      Dump of assembler code for function bar:
      2    {
      0x00000000004004ec <bar+0>:     55          push   %rbp
      0x00000000004004ed <bar+1>:     48 89 e5    mov    %rsp,%rbp
      0x00000000004004f0 <bar+4>:     89 7d ec    mov    %edi,-0x14(%rbp)
      0x00000000004004f3 <bar+7>:     89 75 e8    mov    %esi,-0x18(%rbp)
      
      3        int e = c + d;
      0x00000000004004f6 <bar+10>:     8b 55 e8    mov    -0x18(%rbp),%edx
      0x00000000004004f9 <bar+13>:     8b 45 ec    mov    -0x14(%rbp),%eax
      0x00000000004004fc <bar+16>:     01 d0       add    %edx,%eax
      0x00000000004004fe <bar+18>:     89 45 fc    mov    %eax,-0x4(%rbp)
      
      4        return e;
      0x0000000000400501 <bar+21>:     8b 45 fc    mov    -0x4(%rbp),%eax
      
      5    }
      0x0000000000400504 <bar+24>:     c9    leaveq 
      0x0000000000400505 <bar+25>:     c3    retq   
      
      End of assembler dump.
      (gdb)

      對于最前面兩條指令我們應該很熟悉了:將foo函數棧幀的棧底地址入棧,建立bar函數的棧幀。但后面兩條指令與foo函數中對應位置的指令就不一樣了,這里為什么不擴展棧幀,不像foo函數匯編指令那樣將參數的值存入調用棧呢?

       

      原因就是bar函數是最后一個被調用的函數了,foo函數中的局部變量在bar函數返回后還有可能被操作,而bar函數的局部變量已失去保存的必要。以上“{}”中剩余的指令利用edx和eax寄存器完成加法操作,最后結果保存在eax寄存器中,以作為結果返回。

       

      至此,調用棧信息如下:

      相關寄存器信息如下:

      esi: 0x5   edi: 0x2   edx: 0x5   eax: 0x7

       

      這時我們再來使用gdb的x命令查看內存信息:

      (gdb) x/16x 0x7fffffffe8a0 
      0x7fffffffe8a0:    0x00000005    0x00000002    0x00400595    0x00000000
      0x7fffffffe8b0:    0xf7ffa658    0x00000007    0xffffe8d0    0x00007fff
      0x7fffffffe8c0:    0x0040051f    0x00000000    0x00000005    0x00000002
      0x7fffffffe8d0:    0xffffe8e0    0x00007fff    0x00400534    0x00000000
      (gdb) 

      以上命令顯示16個4bytes內存地址指示的值,且值以十六進制顯示。比較下,看這里的輸出與上面的調用棧信息是否一致?

       

      函數返回過程

      函數調用過程對應著調用棧的建立,而函數返回則是進行調用棧的銷毀,返回比調用過程簡單多了,畢竟破壞比建設來的容易。在main、foo和bar函數的匯編顯示中,我們都可以看到leave和ret兩條指令:

      0x0000000000400504 <bar+24>:     c9    leaveq 
      0x0000000000400505 <bar+25>:     c3    retq

       

      leave指令等價于以下兩條指令:

      mov %rbp, %rsp
      pop %rbp

      這兩條指令將bp和sp寄存器中的值還原為函數調用前的值,是函數開頭兩條指令的逆向過程。ret指令修改了ip寄存器的值,將其設置為原函數棧幀中將要執行的指令地址。bar函數的leave和ret執行完之后,調用棧信息變為:

      rip寄存器的值為0x40051f

       

      剩余的函數返回過程類似,直至所有函數執行完成、調用棧被銷毀。

       

      小結

      本文通過一個簡單的函數調用實例,結合gdb單步調試和反匯編工具,對函數調用的底層實現過程進行了分析。

       

      修改sp、bp寄存器記錄棧幀的高、低地址,以此完成函數調轉;

      push/mov操作保存caller變量、指令信息,保證callee返回之后caller繼續正常執行;

      ??

      棧這種簡單的數據結構優雅地完成了支撐計算機程序執行的任務。

       

      我們可以參照這樣的思路,在編碼實現功能需求時,分析所要實現的功能,選擇恰當的數據結構和實現方式,力求做到優雅、簡潔。

       

      ------------------------------------------------------------

      本文基于Suse11sp1(x86_64),該發行版可從這里下載。

      # cat /etc/SuSE-release;uname -r
      SUSE Linux Enterprise Desktop 11 (x86_64)
      VERSION = 11
      PATCHLEVEL = 1
      2.6.32.12-0.7-default

       

      Reference:  函數調用

                       Chapter 5, the stack, Self-service Linux

       

       

      主站蜘蛛池模板: 婷婷色综合视频在线观看| 福利一区二区1000| 日韩中文字幕v亚洲中文字幕| 亚洲成亚洲成网| 久久被窝亚洲精品爽爽爽| 最新午夜男女福利片视频| 国产精品国产自产拍在线| 精品国产av一区二区果冻传媒| 九九热视频在线观看一区| 老司机免费的精品视频| 亚洲第一无码专区天堂| 国产成人AV男人的天堂| 99久久无色码中文字幕| 国产三级精品片| 熟妇高潮精品一区二区三区| 精品无码久久久久久尤物| 亚洲高潮喷水无码AV电影| 久久精品国产99国产精品澳门| 精品国产av一区二区果冻传媒| 国产性三级高清在线观看| 亚洲精品无码你懂的| 久久香蕉国产线熟妇人妻| 日韩成人一区二区二十六区| 亚洲精品无码成人A片九色播放| 国产精品成人午夜久久| 18禁无遮挡啪啪无码网站| 九九re线精品视频在线观看视频| 国产乱码精品一区二区三| 国产精品污www在线观看| 国产精品第一页一区二区| 国产成人一区二区三区在线| 国产精品推荐手机在线| 四虎永久免费高清视频| 国产亚洲视频在线播放香蕉| 美女一区二区三区亚洲麻豆| 99精品热在线在线观看视| 婷婷六月综合缴情在线| 东京热人妻中文无码| 欧美性猛交xxxx免费看| 乱女乱妇熟女熟妇综合网| 男女扒开双腿猛进入爽爽免费看|