函數(shù)調(diào)用棧

由于不是科班出生,又是自學(xué)開發(fā),對(duì)很多方面的知識(shí)都是只知其然而不知其所以然。加上最近公司事情不多,剛好乘此機(jī)會(huì)把長(zhǎng)毛了小本本又翻了出來,希望能每天學(xué)習(xí)一點(diǎn)點(diǎn),每天進(jìn)步一點(diǎn)點(diǎn)。

之前沒有接觸過底層方面,所以這篇文章,會(huì)通過菜鳥視角,從基礎(chǔ)概念到實(shí)戰(zhàn)演練,一步步的揭開函數(shù)調(diào)用背后,寄存器,堆棧都干了些什么。

1. 棧區(qū)(stack)

高地址向低地址生長(zhǎng)的一塊連續(xù)的內(nèi)存區(qū)域,所以棧頂?shù)刂泛蜅5淖畲笕萘慷际窍到y(tǒng)預(yù)先規(guī)定好的;

編譯器自動(dòng)管理;

方式類似數(shù)據(jù)結(jié)構(gòu)中的棧,后入先出(LIFO);

每個(gè)進(jìn)程在用戶態(tài)對(duì)應(yīng)一個(gè)調(diào)用棧結(jié)構(gòu);

存放函數(shù)參數(shù)和返回值,函數(shù)局部變量(不包括 static 聲明的變量,它們存放在靜態(tài)變量區(qū));

高效快速,但大小限制,數(shù)據(jù)不靈活(支持?jǐn)?shù)據(jù)類型有限,一般是整型,指針,浮點(diǎn)型等系統(tǒng)直接支持的數(shù)據(jù)類型);

2. 棧幀(stack frame)

函數(shù)調(diào)用經(jīng)常是嵌套的,在同一時(shí)刻,堆棧中會(huì)有多個(gè)函數(shù)的信息,每個(gè)未完成運(yùn)行的函數(shù)占用一個(gè)獨(dú)立連續(xù)區(qū)域(包含這個(gè)函數(shù)涉及的參數(shù),局部變量,返回地址等相關(guān)信息),稱為棧幀。

當(dāng)調(diào)用函數(shù)時(shí),就要壓入一個(gè)新的棧幀,發(fā)起調(diào)用函數(shù)的棧幀成為調(diào)用者棧幀,被調(diào)用函數(shù)的棧幀則稱為當(dāng)前棧幀(rsprbp 之間的內(nèi)存空間);被調(diào)用的函數(shù)運(yùn)行結(jié)束后回收棧幀,回到調(diào)用者棧幀。這一過程都是自動(dòng)的,由系統(tǒng)分配與銷毀,無需手動(dòng)調(diào)度。

3. 寄存器 (register)

x86-64中,所有寄存器都是64位,相對(duì)32位的x86來說,標(biāo)識(shí)符發(fā)生了變化,比如:從原來的 %ebp 變成了 %rbp。為了向后兼容性,%ebp 依然可以使用,不過指向了 %rbp 的低32位。

2017-07-31-memory-3.png

rip 指令地址寄存器,用來存儲(chǔ) CPU 即將要執(zhí)行的指令地址。每次 CPU 執(zhí)行完相應(yīng)的匯編指令之后,rip 寄存器的值就會(huì)自行累加;rip 無法直接賦值,call, ret, jmp 等指令可以修改 rip

rbp ?;刂芳拇嫫鳎4娈?dāng)前幀的棧底地址。

rsp 棧指針寄存器,保存當(dāng)前棧頂。

棧幀中,最重要的是幀指針 rbp 和棧指針 rsp,有了這兩個(gè)指針,我們就可以刻畫一個(gè)完整的棧幀。

3.1 寄存器保存慣例

調(diào)用者棧幀需要寄存器暫存數(shù)據(jù),被調(diào)用者棧幀也需要寄存器暫存數(shù)據(jù)。

如果調(diào)用者使用了 rbx,那被調(diào)用者就需要在使用之前把 rbx 保存起來,然后在返回調(diào)用者棧幀之前,恢復(fù) rbx。遵循該使用規(guī)則的寄存器就是被調(diào)用者保存寄存器,對(duì)于調(diào)用者來說, rbx 就是非易失的。

調(diào)用者使用 r10 存儲(chǔ)局部變量,為了能在子函數(shù)調(diào)用后還能使用 r10,調(diào)用者把 r10 先保存起來,然后在子函數(shù)返回之后,再恢復(fù) r10。遵循該使用規(guī)則的寄存器就是調(diào)用者保存寄存器,對(duì)于調(diào)用者來說, r10 就是易失的。

4. 函數(shù)調(diào)用棧

4.1 參數(shù)入棧

參數(shù)從右向左依次入棧(支持可變參數(shù))。

x86-64 中,有 6 個(gè)寄存器來存儲(chǔ)參數(shù),多于 6 個(gè)參數(shù),依然還是通過入棧實(shí)現(xiàn)。

4.2 返回地址入棧

實(shí)際代碼中我們是看不到 push rip 這句的;

它是包含在 call 指令之中的 call function = push rip + jmp function

x86 Instruction Set Reference

4.3 代碼區(qū)跳轉(zhuǎn)

它是包含在 call 指令之中的 call function = push rip + jmp function

4.4 棧幀調(diào)整

  1. 將調(diào)用幀的 push %rbp 入棧。
  2. 切換棧幀到當(dāng)前棧幀 movq %rsp, %rbp。
  3. 抬高棧頂,分配臨時(shí)數(shù)據(jù)區(qū) subq &xx, %rsp。

5 實(shí)例測(cè)試

Xcode 新建工程,main.c 文件:

#include <stdio.h> //line 9

char* get_memory(char *a, char *b) {
    char p[]="hello world";
    return p;
}

int main(int argc, const char * argv[]) {
    // insert code here...
    char* str = NULL;
    char* a = "good";
    int   b = 3;
    float  d = 12345.67890;
    
    str = get_memory("h", "w");
    printf("%s",str);
    return 0;
}

5.1 實(shí)例分析

選中 main.c 文件,x86-64環(huán)境 Product -> Perform Action -> Assemble 'main.c'。

生成的代碼中會(huì)有很多 . 開頭的,例如 .loc.section 等等,這些都是匯編器需要的,我們可以直接忽略,這篇文章對(duì)這些指令做了一些說明,清除掉它們和相關(guān)注釋后我們重點(diǎn)關(guān)注 main 函數(shù):

    .section    __TEXT,__text,regular,pure_instructions     //.section __TEXT 只讀和可執(zhí)行的代碼段
    .macosx_version_min 10, 12
    .file   1 ".../Project/test" ".../Project/test/test/main.c"
    .globl  _get_memory     //`_get_memory` 是一個(gè)外部符號(hào)(Symbol),對(duì)于二進(jìn)制文件外部可見。
    .p2align    4, 0x90     //指出了后面代碼的對(duì)齊方式。在我們的代碼中,后面的代碼會(huì)按照 16(2^4) 字節(jié)對(duì)齊,如果需要的話,用 0x90 補(bǔ)齊。
_get_memory:                            ## @get_memory
Lfunc_begin0:
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    subq    $48, %rsp
    leaq    -20(%rbp), %rax
    movq    ___stack_chk_guard@GOTPCREL(%rip), %rcx 
    movq    (%rcx), %rcx
    movq    %rcx, -8(%rbp)
    movq    %rdi, -32(%rbp)
    movq    %rsi, -40(%rbp)
Ltmp3:
    ##DEBUG_VALUE: get_memory:p <- %RAX
    movq    L_get_memory.p(%rip), %rcx
    movq    %rcx, -20(%rbp)
    movl    L_get_memory.p+8(%rip), %edx
    movl    %edx, -12(%rbp)
    movq    ___stack_chk_guard@GOTPCREL(%rip), %rcx
    movq    (%rcx), %rcx
    movq    -8(%rbp), %rsi
    cmpq    %rsi, %rcx
    movq    %rax, -48(%rbp)         ## 8-byte Spill
Ltmp4:
    ##DEBUG_VALUE: get_memory:p <- [%RBP+-48]
    jne LBB0_2  //如果 rsi 和 rcx 不相等,那么就跳轉(zhuǎn)到 LBB0_2
## BB#1:
    ##DEBUG_VALUE: get_memory:p <- [%RBP+-48]
    movq    -48(%rbp), %rax         ## 8-byte Reload
    addq    $48, %rsp
    popq    %rbp
    retq
LBB0_2:
    ##DEBUG_VALUE: get_memory:p <- [%RBP+-48]
    callq   ___stack_chk_fail
Ltmp5:
Lfunc_end0:
    .cfi_endproc

    .section    __TEXT,__literal4,4byte_literals
    .p2align    2
LCPI1_0:
    .long   1178658487              ## float 12345.6787

    .section    __TEXT,__text,regular,pure_instructions     
    .globl  _main
    .p2align    4, 0x90     
_main:                                  ## @main
Lfunc_begin1:
    .cfi_startproc                  //函數(shù)開始標(biāo)識(shí),用于初始化某些內(nèi)部數(shù)據(jù)結(jié)構(gòu)
## BB#0:
    pushq   %rbp                    //保存調(diào)用者的棧幀基址--控制鏈
Ltmp6:
    .cfi_def_cfa_offset 16          //此處距離 CFA 16 字節(jié)(用的 rsp 計(jì)算)
Ltmp7:
    .cfi_offset %rbp, -16           //rbp 的值,保存在距離 CFA 16 字節(jié)處
    movq    %rsp, %rbp              //設(shè)置新的棧幀基址
Ltmp8:
    .cfi_def_cfa_register %rbp      //修改計(jì)算 CFA 所用的寄存器,設(shè)成 rbp
    subq    $48, %rsp               //分配臨時(shí)數(shù)據(jù)區(qū)
    leaq    L_.str.1(%rip), %rax    //將 L_.str.1 的指針加載到 rax 寄存器 "h"
    leaq    L_.str.2(%rip), %rcx    //將 L_.str.2 的指針加載到 rcx 寄存器 "w"
    movss   LCPI1_0(%rip), %xmm0    ## xmm0 = mem[0],zero,zero,zero //將 LCPI1_0 的單精度值加載到 xmm0 寄存器的低雙字
    leaq    L_.str(%rip), %rdx      //將 L_.str  的指針加載到 rdx 寄存器 "good"
    movl    $0, -4(%rbp)            
    movl    %edi, -8(%rbp)          //將第一個(gè)參數(shù)(int argc)的值存在 rbp 低位偏移8字節(jié)
    movq    %rsi, -16(%rbp)         //將第二個(gè)參數(shù)(char *argv[])的值存在 rbp 低位偏移16字節(jié)
Ltmp9:
    movq    $0, -24(%rbp)           //第一個(gè)變量(char* str)值為0,存在 rbp 低位偏移24字節(jié)
    movq    %rdx, -32(%rbp)         //第二個(gè)變量(char* a)值為 rdx 的值(即前面 L_.str 的指針),存在 rbp 低位偏移32字節(jié)
    movl    $3, -36(%rbp)           //第三個(gè)變量(int b)值為3,存在 rbp 低位偏移36字節(jié)
    movss   %xmm0, -40(%rbp)        //第四個(gè)變量(float d)值為 xmm0 的值,存在 rbp 低位偏移40字節(jié)
    movq    %rax, %rdi              //將 get_memory 函數(shù)第一個(gè)參數(shù)值(之前存在 rax 寄存器的指針)設(shè)置到寄存器 edi
    movq    %rcx, %rsi              //將 get_memory 函數(shù)第二個(gè)參數(shù)值(之前存在 rcx 寄存器的指針)設(shè)置到寄存器 rsi
    callq   _get_memory             //調(diào)用 get_memory 函數(shù)
    leaq    L_.str.3(%rip), %rdi    //將 printf 函數(shù)第一個(gè)參數(shù)(L_.str.3 的指針)加載到 rdi 寄存器中 "%s"
    movq    %rax, -24(%rbp)         //將 get_memory 返回值設(shè)置給前面初始化過的第一個(gè)變量
    movq    -24(%rbp), %rsi         //將 printf 函數(shù)第第二個(gè)參數(shù)(char* str)設(shè)置到寄存器 rsi
    movb    $0, %al                 //printf 是可變參數(shù)函數(shù),ABI 調(diào)用約定指定,將會(huì)把使用來存儲(chǔ)參數(shù)的寄存器數(shù)量存儲(chǔ)在寄存器 al 中,這里是 0
    callq   _printf                 //調(diào)用 printf 函數(shù)
    xorl    %r8d, %r8d              //清 0 r8d 寄存器
    movl    %eax, -44(%rbp)         ## 4-byte Spill //printf 的返回值存在 rbp 低位偏移44字節(jié)
    movl    %r8d, %eax              //清 0 eax 低32位
    addq    $48, %rsp               //堆棧指針 rsp 上移 48 字節(jié)
    popq    %rbp                    //之前存儲(chǔ)至 rbp 中的值彈出
    retq                           
Ltmp10:
Lfunc_end1:
    .cfi_endproc                    //與開始時(shí)的 .cfi_startproc 對(duì)應(yīng),結(jié)束

    .section    __TEXT,__cstring,cstring_literals
L_get_memory.p:                         ## @get_memory.p
    .asciz  "hello world"
L_.str:                                 ## @.str
    .asciz  "good"
L_.str.1:                               ## @.str.1
    .asciz  "h"
L_.str.2:                               ## @.str.2
    .asciz  "w"
L_.str.3:                               ## @.str.3
    .asciz  "%s"

5.2 棧圖

2017-07-31-memory-4.png

5.3 說明

CFI 是調(diào)用框架指令(Call Frame Information)縮寫,提供的調(diào)用框架信息, 為實(shí)現(xiàn)堆?;乩@(stack unwiding)或異常處理(exception handling)提供了方便。

  • .cfi_startproc 用于函數(shù)開始,.cfi_endproc 用于函數(shù)結(jié)束,兩者配套使用。

  • .cfi_def_cfa_offset 16 指令表示此處(rsp)距離 CFA 16 字節(jié)。

    CFA(Canonical Frame Address)是標(biāo)準(zhǔn)框架地址,指調(diào)用者棧幀中調(diào)用點(diǎn)處的棧指針值。

    .cfi_def_cfa_offset offset modifies a rule for computing CFA(Canonical Frame Address). Register remains the same, but offset is new. Note that it is the absolute offset that will be added to a defined register to compute CFA address.

  • .cfi_offset %rbp, -16 指令表示 rbp 的值保存在距離 CFA 16 字節(jié)。

    rbp 是被調(diào)用者保存寄存器,按照慣例,被調(diào)者在使用之前要保存起來。

  • .cfi_offset register %rbp 指令表示,從這里開始,使用 rbp 作為計(jì)算 CFA 的基址寄存器:

    1. 在這之前的 cfi_def_cfa_offset 16 用的是 rsp
    2. 前一條指令 movq %rsp, %rbp 已經(jīng)將 rsp 設(shè)置為新的 rbp。
  • movl $3, -36(%rbp)

    1. % 用于直接尋址寄存器,$ 表示立即數(shù)。 movl $3, %rbp 表示把立即數(shù) 3 存到 rbp 中。
    2. () 用于內(nèi)存間接尋址,movl $3, (%rbp) 表示將立即數(shù) 3 保存到 rbp 所指向的內(nèi)存地址中。
    3. -36(%rbp) 表示先找到 rbp 所指向地址,再 -36 后所得到的地址。
  • 浮點(diǎn)數(shù)存儲(chǔ)方式

    LCPI1_0:
    .long   1178658487              ## float 12345.6787
    ...
    movss LCPI1_0(%rip), %xmm0
    ...
    

    單精度浮點(diǎn)數(shù)的存儲(chǔ)方式: | 1位符號(hào)數(shù) | 8位指數(shù) | 23位尾數(shù) |

    1. 12345.67890 被轉(zhuǎn)成了了十進(jìn)制數(shù) 1178658487
    2. 我們把1178658487 轉(zhuǎn)換成二進(jìn)制 0 10001100 10000001110011010110111;
    3. 第一位符號(hào)位 0 表示正數(shù);
    4. 第二位到第九位轉(zhuǎn)為十進(jìn)制 140;
    5. 實(shí)際指數(shù) = 140 - 127 = 13;

      由于指數(shù)需要表示正負(fù)兩種數(shù)據(jù),IEEE標(biāo)準(zhǔn)規(guī)定單精度指數(shù)以127為分割線,實(shí)際存儲(chǔ)的數(shù)據(jù)是指數(shù)加127所得結(jié)果,127為高位為零,后7位為1所得,其他雙精度也以此方式計(jì)算。
      6. 尾數(shù)加上 1.1.10000001110011010110111 擴(kuò)大2 ^ 23 次方為 110000001110011010110111 十進(jìn)制 12641975;
      7. 12641975 / 2 ^ (23 - 13(步驟5算出的指數(shù))) = 12641975 / 1024 = 12345.6787109375;

5.4 刨根

接下來,我們直接深入內(nèi)存,來驗(yàn)證一下我們上面是否在一本正經(jīng)的胡說八道。

Xcode 中勾選 Debug -> Debug Workflow -> Always Show Disassembly 后,在 main 方法打斷點(diǎn),就能進(jìn)入?yún)R編調(diào)試界面。

2017-07-31-memory-5.png
  • All 的位置,默認(rèn)選項(xiàng)是 auto,看不到寄存器狀態(tài)。
  • 也可以用 register read 指令查看寄存器狀態(tài)。

直接在 callq 0x100000c90 ; get_memory at main.c:11 位置,檢查進(jìn)入 get_memory 方法之前的寄存器和棧,是否和我們上面的棧圖吻合。

息欄可以看到各寄存器中保存的信息,可以直接看到 rbp 指向的地址 0x00007fff5fbff6e0

subq $0x30, %rsp 這句將棧抬高了 48 字節(jié),所以我們查看內(nèi)存的時(shí)候要從 0x7FFF5FBFF6B0 開始。

上方菜單欄 Debug -> Debug Workflow -> View Memory 打開內(nèi)存調(diào)試界面。

2017-07-31-memory-6.png

為了方便查看,我將圖中的內(nèi)存,按照我們之前的棧圖進(jìn)行了調(diào)整:

0x7fff5fbff6e0      rbp
0x7fff5fbff6dc      00 00 00 00                 0                 -4(%rbp)
0x7fff5fbff6d8      01 00 00 00                 1                 -8(%rbp)
0x7fff5fbff6d0      00 F7 BF 5F FF 7F 00 00     00007FFF5FBFF700  -16(%rbp)
0x7fff5fbff6c8      00 00 00 00 00 00 00 00     0                 -24(%rbp)
0x7fff5fbff6c0      5C 0F 00 00 01 00 00 00     0000000100000F5C  -32(%rbp)
0x7fff5fbff6bc      03 00 00 00                 3                 -36(%rbp)
0x7fff5fbff6b8      B7 E6 40 46                 12345.67890       -40(%rbp)
0x7fff5fbff6b4      00 00 00 00                 0                 -44(%rbp)
0x7fff5fbff6b0      00 00 00 00                 0                 -48(%rbp)

根據(jù)棧圖 -32(%rbp) 位置應(yīng)該存放的是 "good" 的內(nèi)存地址,同樣的步驟直接查看地址 0x0000000100000F5C

2017-07-31-memory-7.png

Xcode 已經(jīng)幫我們標(biāo)出來16進(jìn)制對(duì)應(yīng)的ASCII可顯示內(nèi)容了,當(dāng)然也可以到這里對(duì)照驗(yàn)證一下。

參考

用戶態(tài)和內(nèi)核態(tài)的概念區(qū)別

iOS內(nèi)核架構(gòu)淺談

X86-64寄存器和棧幀

C程序的函數(shù)棧作用機(jī)理

X86匯編調(diào)用框架淺析與CFI簡(jiǎn)介

幾種基本匯編指令詳解

xcode反匯編調(diào)試iOS模擬器程序

浮點(diǎn)數(shù)如何存儲(chǔ)

詳解大端模式和小端模式

Mach-O 可執(zhí)行文件

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原文地址:C語(yǔ)言函數(shù)調(diào)用棧(一)C語(yǔ)言函數(shù)調(diào)用棧(二) 0 引言 程序的執(zhí)行過程可看作連續(xù)的函數(shù)調(diào)用。當(dāng)一個(gè)函數(shù)執(zhí)...
    小豬啊嗚閱讀 4,972評(píng)論 1 19
  • 棧: 在函數(shù)調(diào)用時(shí),第一個(gè)進(jìn)棧的是主函數(shù)中函數(shù)調(diào)用后的下一條指令(函數(shù)調(diào)用語(yǔ)句的下一條可執(zhí)行語(yǔ)句)的地址,然后是函...
    zjfclimin閱讀 4,310評(píng)論 0 5
  • 閱讀經(jīng)典——《深入理解計(jì)算機(jī)系統(tǒng)》04 函數(shù)調(diào)用時(shí)的棧結(jié)構(gòu)變化是一個(gè)很有趣的話題,本文就來詳細(xì)剖析這個(gè)過程。 棧幀...
    金戈大王閱讀 23,553評(píng)論 14 36
  • 首先先看圖: 在main函數(shù)調(diào)用func_A的時(shí)候,首先在自己的棧幀中壓入函數(shù)返回地址,然后為func_A創(chuàng)建新棧...
    zjfclimin閱讀 7,549評(píng)論 1 2
  • 人生難得相伴游, 舒暢怡情自忘憂。 世界珍奇遺九寨, 山清水秀刻心留!
    張邦祥閱讀 162評(píng)論 0 2

友情鏈接更多精彩內(nèi)容