1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 3687字,需瀏覽 8分鐘

         ·

        2022-07-24 12:29

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

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

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

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

        首先來(lái)回顧下一些基礎(chǔ)知識(shí)。

        前置知識(shí)

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

        程序地址空間

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

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

        什么是調(diào)用棧

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

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

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

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

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

        那么EBP可以用來(lái)做什么呢?

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

        調(diào)用過(guò)程

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

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

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

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

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

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

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

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

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

        調(diào)用約束

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

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

        舉例說(shuō)明

        準(zhǔn)備代碼

        我們這里準(zhǔn)備了一個(gè)帶有參數(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編譯器,編譯成匯編代碼,因?yàn)闄C(jī)器碼我們根本讀不懂,而匯編代碼和CPU指令幾乎是一對(duì)一的。相關(guān)編譯指令:

        gcc -S -o main.s main.c

        變成匯編后的代碼,由于筆者是用蘋(píng)果電腦編譯的,所以沒(méi)有出現(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指向棧頂。

        看文字可能不太明確,我們來(lái)看一副對(duì)比圖。

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

        函數(shù)返回

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

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

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

        總結(jié)

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

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

        end


        瀏覽 38
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            久久人色 | 国产女人夜夜春夜夜高潮 | 99黄色视频 | 久久人妖TS三区系列电影 | 亚洲性受 | 欧美激情12p | www.逼逼 | 国产清纯白嫩初高生视频在线观看 | 久久另类TS人妖一区二区 | 精品久久人妻 |