小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

函數(shù)調(diào)用過程探究

 昵稱15515903 2014-02-10

函數(shù)調(diào)用過程探究

引言

如何定義函數(shù)、調(diào)用函數(shù),是每個程序員學(xué)習(xí)編程的入門課。調(diào)用函數(shù)(caller)向被調(diào)函數(shù)(callee)傳入?yún)?shù),被調(diào)函數(shù)返回結(jié)果,看似簡單的過程,其實CPU和系統(tǒng)內(nèi)核在背后做了很多工作。下面我們通過反匯編工具,來看函數(shù)調(diào)用的底層實現(xiàn)。

 

基礎(chǔ)知識

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

棧(stack)

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

  • 每一個進程在用戶態(tài)對應(yīng)一個調(diào)用棧結(jié)構(gòu)(call stack)
  • 程序中每一個未完成運行的函數(shù)對應(yīng)一個棧幀(stack frame),棧幀中保存函數(shù)局部變量、傳遞給被調(diào)函數(shù)的參數(shù)等信息
  • 棧底對應(yīng)高地址,棧頂對應(yīng)低地址,棧由內(nèi)存高地址向低地址生長

一個進程的調(diào)用棧圖示如下:

 

寄存器(register)

寄存器位于CPU內(nèi)部,用于存放程序執(zhí)行中用到的數(shù)據(jù)和指令,CPU從寄存器中取數(shù)據(jù),相比從內(nèi)存中取快得多。寄存器又分通用寄存器和特殊寄存器。

通用寄存器有ax/bx/cx/dx/di/si,盡管這些寄存器在大多數(shù)指令中可以任意選用,但也有一些規(guī)定某些指令只能用某個特定“通用”寄存器,例如函數(shù)返回時需將返回值mov到ax寄存器中;特殊寄存器有bp/sp/ip等,特殊寄存器均有特定用途,例如sp寄存器用于存放以上提到的棧幀的棧頂?shù)刂罚酥?,不用于存放局部變量,或其他用途?/p>

 

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

  • ax(accumulator): 可用于存放函數(shù)返回值
  • bp(base pointer): 用于存放執(zhí)行中的函數(shù)對應(yīng)的棧幀的棧底地址
  • sp(stack poinger): 用于存放執(zhí)行中的函數(shù)對應(yīng)的棧幀的棧頂?shù)刂?/li>
  • ip(instruction pointer): 指向當(dāng)前執(zhí)行指令的下一條指令

 

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

 

函數(shù)調(diào)用例子

了解了棧和寄存器的概念,下面看一個函數(shù)調(diào)用實例:

復(fù)制代碼
//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; }
復(fù)制代碼

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

# gcc -g func_call.c -o func_call

 

-g選項使目標(biāo)文件func_call包含程序的調(diào)試信息。

 

反匯編分析

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

復(fù)制代碼
# 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)
復(fù)制代碼

start命令用于拉起被調(diào)試程序,并執(zhí)行至main函數(shù)的開始位置,程序被執(zhí)行之后與一個用戶態(tài)的調(diào)用棧關(guān)聯(lián)。

 

main函數(shù)

現(xiàn)進程跑在main函數(shù)中,我們disassemble命令顯示當(dāng)前函數(shù)的匯編信息:

復(fù)制代碼
(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.
復(fù)制代碼

 

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

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

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

 

一個函數(shù)被調(diào)用,首先默認要完成以下動作:

  • 將調(diào)用函數(shù)的棧幀棧底地址入棧,即將bp寄存器的值壓入調(diào)用棧中
  • 建立新的棧幀,將被調(diào)函數(shù)的棧幀棧底地址放入bp寄存器中

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

push %rbp
mov  %rsp, %rbp

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

是的,皆因main并不是程序拉起后第一個被執(zhí)行的函數(shù),它被_start函數(shù)調(diào)用,更詳細的資料參看這里。

 

一個函數(shù)調(diào)用另一個函數(shù),需先將參數(shù)準備好。main調(diào)用foo函數(shù),兩個參數(shù)傳入通用寄存器中:

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

 

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

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

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

 

萬事具備,是時候?qū)?zhí)行控制權(quán)交給foo函數(shù)了,call指令完成交接任務(wù):

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

一條call指令,完成了兩個任務(wù):

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

 

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

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

此時我們再來看rsp、rbp寄存器的值,它們保存了程序?qū)嶋H用到的物理內(nèi)存地址:

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

 

main函數(shù)君的執(zhí)行到此就暫時告一段落了,此時func_call的調(diào)用棧情況如下:

相關(guān)寄存器信息如下:

esi: 0x5   edi: 0x2

 

foo函數(shù)

foo函數(shù)被執(zhí)行之后,我們使用disassemble命令顯示其匯編指令:

復(fù)制代碼
(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)
復(fù)制代碼

前面兩條指令將main函數(shù)棧幀的棧底地址入棧,建立foo函數(shù)的棧幀。接著的三條指令擴展棧幀,將傳入的參數(shù)存為函數(shù)內(nèi)局部變量。最后三條指令與bar函數(shù)調(diào)用相對應(yīng),也是先將參數(shù)傳入esi、edi寄存器,然后執(zhí)行call指令。

 

繼續(xù)執(zhí)行si命令,讓程序執(zhí)行到call指令的位置:

復(fù)制代碼
(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)
復(fù)制代碼

 

foo函數(shù)調(diào)用bar函數(shù)之后,bar函數(shù)執(zhí)行之前,調(diào)用棧信息如下:

相關(guān)寄存器信息如下:

esi: 0x5   edi: 0x2

 

bar函數(shù)

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

復(fù)制代碼
(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)
復(fù)制代碼

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

 

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

 

至此,調(diào)用棧信息如下:

相關(guān)寄存器信息如下:

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

 

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

復(fù)制代碼
(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) 
復(fù)制代碼

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

 

函數(shù)返回過程

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

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

 

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

mov %rbp, %rsp
pop %rbp

這兩條指令將bp和sp寄存器中的值還原為函數(shù)調(diào)用前的值,是函數(shù)開頭兩條指令的逆向過程。ret指令修改了ip寄存器的值,將其設(shè)置為原函數(shù)棧幀中將要執(zhí)行的指令地址。bar函數(shù)的leave和ret執(zhí)行完之后,調(diào)用棧信息變?yōu)椋?/p>

rip寄存器的值為0x40051f

 

剩余的函數(shù)返回過程類似,直至所有函數(shù)執(zhí)行完成、調(diào)用棧被銷毀。

 

小結(jié)

本文通過一個簡單的函數(shù)調(diào)用實例,結(jié)合gdb單步調(diào)試和反匯編工具,對函數(shù)調(diào)用的底層實現(xiàn)過程進行了分析。

 

修改sp、bp寄存器記錄棧幀的高、低地址,以此完成函數(shù)調(diào)轉(zhuǎn);

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

棧這種簡單的數(shù)據(jù)結(jié)構(gòu)優(yōu)雅地完成了支撐計算機程序執(zhí)行的任務(wù)。

 

我們可以參照這樣的思路,在編碼實現(xiàn)功能需求時,分析所要實現(xiàn)的功能,選擇恰當(dāng)?shù)臄?shù)據(jù)結(jié)構(gòu)和實現(xiàn)方式,力求做到優(yōu)雅、簡潔。

 

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

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

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

 

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多