<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          圖解!深入淺出函數(shù)調(diào)用棧

          共 3687字,需瀏覽 8分鐘

           ·

          2022-07-24 12:29

          今天來和你聊聊函數(shù)調(diào)用,為什么會想到這個話題,因為最近突然想到,當(dāng)然學(xué)習(xí)C語言的時候遇到的攔路虎——函數(shù),函數(shù)在我們高中階段的定義可能是有定義域X對應(yīng)到值域Y的一種關(guān)系,而在這類函數(shù)定義中,我們總能拿到一個結(jié)果Y。而一個函數(shù)調(diào)用另一個函數(shù),這個過程到底是怎么回事呢?

          如果你有過程序的調(diào)試經(jīng)歷,你肯定非常熟悉下面這個畫面。這是一個代碼斷點的調(diào)試功能。

          可以看到,它是從start方法開始到main方法的,如果我們繼續(xù)往下,main又調(diào)DemoClass的print方法,還能調(diào)其他的方法。那我們打一個斷點就能獲取整個函數(shù)的調(diào)用鏈,這是基于什么原理呢?為什么這個斷點能找到它所在的函數(shù)位置呢?

          下面就來一起看看C函數(shù)調(diào)用棧,全文脈絡(luò)。

          首先來回顧下一些基礎(chǔ)知識。

          前置知識

          • 棧是一種容器,具有后進先出的特性,我們函數(shù)調(diào)用的過程設(shè)計就利用了棧的特性,調(diào)用一個新的函數(shù)時,進行壓棧Push,這個函數(shù)執(zhí)行完進行出棧Pop。
          • 函數(shù)棧幀是一種數(shù)據(jù)結(jié)構(gòu),它保存這一個函數(shù)調(diào)用所需的信息,比如參數(shù),局部變量,返回地址等等。
          • 在32位操作系統(tǒng)進行C函數(shù)調(diào)用時,ESP寄存器總是指向棧頂?shù)刂?,EBP寄存器指向的存儲舊EBP的起始地址。

          程序地址空間

          先來看張圖。程序運行時主要分為用戶空間和內(nèi)核空間,我們主要來講講用戶空間。

          • 棧空間:它用于維護函數(shù)調(diào)用的上下文,也就是我們本文的重點,離開了棧,那么函數(shù)調(diào)用就無法實現(xiàn)了。它通常 在用戶空間的的最高地址開始,向下增長,也就是由高往低增長。
          • 堆空間:堆空間是用來容納應(yīng)用程序動態(tài)分配內(nèi)存的區(qū)域,當(dāng)我們用malloc函數(shù)時就是在這片區(qū)域分配內(nèi)存,它是由低地址往高地址增長,也就是向上增長。
          • 可讀可寫區(qū):這里主要包含程序的data段,以及未初始化的變量段。
          • 只讀區(qū):這里包含了text段以及rodata段,好像在安卓系統(tǒng)上text段是可寫的,這里有待探究,有深入研究過的讀者可以一起探討。
          • 預(yù)留空間:也叫保留區(qū),,它不是一個單一的內(nèi)存區(qū)域,而是對內(nèi)存中受保護而禁止訪問區(qū)域的總稱,很小塊。

          什么是調(diào)用棧

          前置知識中提到了棧其實是一種容器,一種數(shù)據(jù)結(jié)構(gòu)。這是我們計算機程序里的重要概念,在技術(shù)系統(tǒng)中,棧則是一個具有容器屬性的動態(tài)內(nèi)存區(qū)域,程序可以將數(shù)據(jù)壓入棧中,也可以出棧。在程序地址空間里也提到棧的空間總是向下增長的。

          而函數(shù)調(diào)用棧,是將一個個函數(shù)的所用的信息,稱之為活動記錄或者棧幀,按照調(diào)用的順序依次壓入棧中,等最上層的函數(shù)執(zhí)行完了,就彈出相應(yīng)的棧幀,棧幀主要包括以下幾個內(nèi)容:

          • 函數(shù)的返回地址和參數(shù)
          • 本地變量
          • 調(diào)用前后上下文

          前面提到了EBP寄存器指向了一個舊的EBP起始地址,ESP執(zhí)行棧頂,一個棧幀的具體結(jié)構(gòu)如下圖。

          上圖,參數(shù)內(nèi)容之后便是當(dāng)前函數(shù)的棧幀,EBP固定執(zhí)行舊的EBP起始地址,而舊的EBP存儲著上一個函數(shù)的執(zhí)行地址,這樣等到末尾出棧之后就能按層級返回上一級函數(shù)了,而ESP總是執(zhí)行棧頂,會隨著函數(shù)的調(diào)用或這些不斷變化。

          那么EBP可以用來做什么呢?

          根據(jù)上圖,可以很容易想到,EBP可以根據(jù)地址的加減,來獲取響應(yīng)的棧幀內(nèi)容,比如獲取返回地址 ebp-4就是 返回地址,參數(shù)也可以用ebp-8、ebp-12來獲取,為什么是-4呢,我們在開頭約定了是在32位機器下,4個字節(jié)就是32位了。所以EBP寄存器可以用來追蹤我們的函數(shù)調(diào)用鏈,從而定位相關(guān)出錯問題。

          調(diào)用過程

          上面介紹了調(diào)用棧,這里具體來看看一個函數(shù)調(diào)用鏈的怎么形成的。

          • 根據(jù)棧幀的結(jié)構(gòu)圖,首先將參數(shù)入棧。

          • 執(zhí)行完這個函數(shù)之后,返回回來得接著執(zhí)行,所以要將當(dāng)前指令的下一條指令壓入棧中,然后跳到函數(shù)體執(zhí)行。

          • 將EBP壓入棧中,此時的ebp還是保存著調(diào)用函數(shù)的ebp,也就是Old EBP。

          • 此時EBP其實指向棧頂?shù)?,所以將EBP的值賦給ESP,ESP就指向棧頂了。

          • 在棧區(qū)分配空間,保存old函數(shù)用到的寄存器數(shù)據(jù)。因為被調(diào)用函數(shù)執(zhí)行完之后,要回到之前的函數(shù)執(zhí)行,那么之前函數(shù)用到的數(shù)據(jù)得保護起來,以便于后續(xù)正常執(zhí)行。

          • 被調(diào)用函數(shù)執(zhí)行完,恢復(fù)相關(guān)寄存器數(shù)據(jù),同時恢復(fù)ESP以前的數(shù)據(jù),回收分配的空間,以及恢復(fù)EBP的數(shù)據(jù)。

          • 最后從棧幀中取到返回地址,并回到調(diào)用函數(shù)處下一條指令執(zhí)行。

          上面的幾個步驟就是一個函數(shù)調(diào)用另一個函數(shù)的過程了,如果是多個函數(shù)調(diào)用,形成一條鏈,也是類似的。

          調(diào)用約束

          在一個函數(shù)調(diào)用另外一個函數(shù)的時候,有一些數(shù)據(jù)即可以由調(diào)用者保存,也可以有被調(diào)用者保存,那么這個時候其實就出現(xiàn)了兩種約束:調(diào)用者約束和被調(diào)用者約束。

          如果在調(diào)用函數(shù)里要使用某個寄存器,可能需要先把它的值保存下來,防止破壞了別的代碼保存在這里的數(shù)據(jù)。這種約定叫做被調(diào)用者約束,也就是使用寄存器的人要保護好寄存器里原有的信息。某個函數(shù)如果使用了某個寄存器,但它又要調(diào)用別的函數(shù),為了防止別的函數(shù)把自己放在寄存器中的數(shù)據(jù)覆蓋掉,要自己保存在棧楨中。這種約定叫做調(diào)用者約束。

          舉例說明

          準備代碼

          我們這里準備了一個帶有參數(shù)X的函數(shù)test,然后利用main函數(shù)去調(diào)用它。

          #include<stdio.h>
          int test(int x) {
              int a = 10;
              int b = 20;
              int c = 30;
              int d = 40;
              return x + a + b + c + d;
          }
          int main() {
              int a = test(0);
              printf("a: %d\n",a);
              return 0;
          }

          編譯

          利用gcc編譯器,編譯成匯編代碼,因為機器碼我們根本讀不懂,而匯編代碼和CPU指令幾乎是一對一的。相關(guān)編譯指令:

          gcc -S -o main.s main.c

          變成匯編后的代碼,由于筆者是用蘋果電腦編譯的,所以沒有出現(xiàn)上面提到的EBP這些,rbp可以看成上面提到的 EBP,rsp可以看成ESP。

          _test:                                 
           pushq %rbp 
           movq %rsp, %rbp
           movl %edi, -4(%rbp)
            ....
           addl -20(%rbp), %eax
           popq %rbp
           retq

          解讀代碼

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

          上面的匯編代碼我們可以看到第一步:保存了%ebp的值,隨著將%esp的值賦給%ebp,使新的%ebp指向棧頂。

          看文字可能不太明確,我們來看一副對比圖。

          那其實這是被調(diào)用者做的事情,調(diào)用者也做了兩件事:第一,將被調(diào)用函數(shù)的參數(shù)按照從右到左的順序壓入棧中。第二,將返回地址壓入棧中。這兩件事是調(diào)用者負責(zé)的,所以壓入的棧就屬于調(diào)用者的棧幀。

          函數(shù)返回

          我們可以注意到最后有一個 retq 指令,它其實就是相當(dāng)于 pop + jum。

          它首先將數(shù)據(jù)(返回地址)彈出棧并保存到EIP中,然后處理器根據(jù)這個地址無條件地跳到相應(yīng)位置獲取新的指令。

          函數(shù)返回的過程就是調(diào)用ret這個返回指令,將執(zhí)行完的函數(shù)的數(shù)據(jù)在棧中清理干凈,然后回到調(diào)用前的地方繼續(xù)執(zhí)行,上面我們不也提到了,在函數(shù)調(diào)用另一個函數(shù)前會保存下一條執(zhí)行嗎?回來之后就可以繼續(xù)執(zhí)行。

          總結(jié)

          回到開篇的問題:在debug編譯條件下,編譯器會對代碼進行很多調(diào)試信息的插入操作,這些信息能夠為我們debug提供重要的支持,包括行號等等信息,當(dāng)然也離不開EBP和ESP這兩個在??臻g最重要的寄存器,我們能夠斷點調(diào)試都是因為編譯器的強大,編譯之美呀。后續(xù)也會和你分享編譯方面的知識,記得長期持有我這只潛力股。

          本文從函數(shù)調(diào)用的過程以及原理知識一起探討了C函數(shù)調(diào)用的前前后后,希望對你有所幫助。

          end


          瀏覽 38
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91人人妻| 黄色国产视频网站 | 国产精品无码在线看 | 大寄吧查进去的视频 | 欧美黄色免费看 |