<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>

          C++函數(shù)調(diào)用過程深入分析

          共 3047字,需瀏覽 7分鐘

           ·

          2021-04-06 21:21

          0. 引言

          函數(shù)調(diào)用的過程實際上也就是一個中斷的過程,那么C++中到底是怎樣實現(xiàn)一個函數(shù)的調(diào)用的呢?參數(shù)入棧、函數(shù)跳轉(zhuǎn)、保護現(xiàn)場、回復(fù)現(xiàn)場等又是怎樣實現(xiàn)的呢?本文將對函數(shù)調(diào)用的過程進(jìn)行深入的分析和詳細(xì)解釋,并在VC 6.0環(huán)境下進(jìn)行演示。分析不到位或者存在錯誤的地方請批評指正,請與作者聯(lián)系。

          首先對三個常用的寄存器做一下說明,EIP是指令指針,即指向下一條即將執(zhí)行的指令的地址;EBP為基址指針,常用來指向棧底;ESP為棧指針,常用來指向棧頂。

          看下面這個簡單的程序并在VC 6.0中查看并分析匯編代碼。

          圖1


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

          g_func函數(shù)調(diào)用的匯編代碼如圖2:

          圖2

          首先是三條push指令,分別將三個參數(shù)壓入棧中,可以發(fā)現(xiàn)參數(shù)的壓棧順序是從右向左的。這時我們可以查看棧中的數(shù)據(jù)驗證一下。如圖3所示,從右邊的實時寄存器表中我們可以看到ESP(棧頂指針)值為0x0012FEF0,然后從中間的內(nèi)存表中找到內(nèi)存地址0x0012FEF0處,我們可以看到內(nèi)存中依次存儲了0x00000001(即參數(shù)1),0x00000002(即參數(shù)2),0x00000003(即參數(shù)3),即此時棧頂存儲的是三個參數(shù)值,說明壓棧成功。

          圖3

          然后可以看到call指令跳轉(zhuǎn)到地址0x00401005,那么該地址處是什么呢?我們繼續(xù)跟蹤一下,在圖4中我們看到這里又是一條跳轉(zhuǎn)指令,跳轉(zhuǎn)到0x00401030。我們再看一下地址0x00401030處,在圖5中可以看到這才是真正的g_func函數(shù),0x00401030是該函數(shù)的起始地址,這樣就實現(xiàn)了到g_func函數(shù)的跳轉(zhuǎn)。

          圖4

          圖5

          2. 保存現(xiàn)場

          此時我們再來查看一下棧中的數(shù)據(jù),如圖6所示,此時的ESP(棧頂)值為0x0012FEEC,在內(nèi)存表中我們可以看到棧頂存放的是0x00401093,下面還是前面壓棧的參數(shù)1,2,3,也就是執(zhí)行了call指令后,系統(tǒng)默認(rèn)的往棧中壓入了一個數(shù)據(jù)(0x00401093),那么它究竟是什么呢?我們再看到圖3,call指令后面一條指令的地址就是0x00401093,實際上就是函數(shù)調(diào)用結(jié)束后需要繼續(xù)執(zhí)行的指令地址,函數(shù)返回后會跳轉(zhuǎn)到該地址。這也就是我們常說的函數(shù)中斷前的“保護現(xiàn)場”。這一過程是編譯器隱含完成的,實際上是將EIP(指令指針)壓棧,即隱含執(zhí)行了一條push eip指令,在中斷函數(shù)返回時再從棧中彈出該值到EIP,程序繼續(xù)往下執(zhí)行。

          圖6

          繼續(xù)往下看,進(jìn)入g_func函數(shù)后的第一條指令是push ebp,即將ebp入棧。因為每一個函數(shù)都有自己的棧區(qū)域,所以棧基址也是不一樣的。現(xiàn)在進(jìn)入了一個中斷函數(shù),函數(shù)執(zhí)行過程中也需要ebp寄存器,而在進(jìn)入函數(shù)之前的main函數(shù)的ebp值怎么辦呢?為了不被覆蓋,將它壓入棧中保存。

          下一條mov ebp, esp 將此時的棧頂?shù)刂纷鳛樵摵瘮?shù)的棧基址,確定g_func函數(shù)的棧區(qū)域(ebp為棧底,esp為棧頂)。

          再往下的指令是sub esp, 48h,指令的字面意思是將棧頂指針往上移動48h Byte。那為什么要移動呢?這中間的內(nèi)存區(qū)域用來做什么呢?這個區(qū)域為間隔空間,將兩個函數(shù)的棧區(qū)域隔開一段距離,如圖7所示。而該間隔區(qū)域的大小固定為40h,即64Byte,然后還要預(yù)留出存儲局部變量的內(nèi)存區(qū)域。g_func函數(shù)有兩個局部變量x和y,所以esp需移動的長度為40h+8=48h。

          圖7

          接下來的幾行指令(如下)是將剛才留出的48h的內(nèi)存區(qū)域賦值為0CCCCCCCCh。

          00401039 lea edi,[ebp-48h]

          0040103C mov ecx,12h

          00401041 mov eax,0CCCCCCCCh

          00401046 rep stos dword ptr [edi] 。

          接下來三條壓棧指令,分別將EBX,ESI,EDI壓入棧中,這也是屬于“保護現(xiàn)場”的一部分,這些是屬于main函數(shù)執(zhí)行的一些數(shù)據(jù)。EBX,ESI,EDI分別為基址寄存器,源變址寄存器,目的變址寄存器。

          3. 執(zhí)行子函數(shù)

          繼續(xù)往下看,接下來是局部變量的x和y的賦值,看匯編指令中是怎樣去計算x和y的內(nèi)存地址的呢?如圖8所示,是基于ebp去計算的,分別是[ebp-4]和[ebp-8]。我們查看內(nèi)存表可以看到相應(yīng)的內(nèi)存區(qū)域已經(jīng)存入了0x11111111和0x22222222。

          圖8

          此時我們對整個內(nèi)存區(qū)域中存儲的內(nèi)容應(yīng)該非常清晰了(如圖9所示)。

          圖9

          4. 恢復(fù)現(xiàn)場

          這時子函數(shù)部分的代碼已經(jīng)執(zhí)行完,繼續(xù)往下看,編譯器將會做一些事后處理的工作(如圖10所示)。首先是三條出棧指令,分別從棧頂讀取EDI,ESI和EBX的值。從圖9的內(nèi)存數(shù)據(jù)分布我們可以得知此時棧頂?shù)臄?shù)據(jù)確實是EDI,ESI和EBX,這樣就恢復(fù)了調(diào)用前的EDI,ESI和EBX值,這是“恢復(fù)現(xiàn)場”的一部分。

          圖10

          第四條指令是mov esp, ebp 即將ebp的值賦給esp。那這是什么意思呢?看看圖9的內(nèi)存數(shù)據(jù)分布,我們就能很明白了,這條語句是讓ESP指向EBP所指的內(nèi)存單元,也就是讓ESP跳過了一段區(qū)域,很明顯跳過的恰好是間隔區(qū)域和局部數(shù)據(jù)區(qū)域,因為函數(shù)已經(jīng)退出了,這兩個區(qū)域都已經(jīng)沒有用處了。實際上這條語句是進(jìn)入函數(shù)時創(chuàng)建間隔區(qū)域的語句 sub esp, 48h的相反操作。

          再往下是pop ebp,我們從圖9的內(nèi)存數(shù)據(jù)分布可以看出此時棧頂確實是存儲的前EBP值,這樣就恢復(fù)了調(diào)用前的EBP值,這也是“恢復(fù)現(xiàn)場”的一部分。該指令執(zhí)行完后,內(nèi)存數(shù)據(jù)分布如圖11所示。

          圖11

          再往下是一條ret指令,即返回指令,他會怎么處理呢?注意在執(zhí)行ret指令前的ESP值和EIP值(如圖12所示),ESP指向棧頂?shù)?x00401093,EIP的值是0x0040105C(即ret指令的地址)。

          圖12

          執(zhí)行ret指令后我們來查看ESP和EIP值(如圖13所示),此時ESP為0012FEF0,即往下移動了4Byte。顯然此處編譯器隱含的執(zhí)行了一條pop指令。再來看一下EIP的值,變?yōu)榱?x00401093,這個值怎么這么熟悉呢!它實際上就是棧頂?shù)?Byte數(shù)據(jù),所以這里隱含執(zhí)行的指令應(yīng)該是pop eip。而這個值就是前面講到過的,在調(diào)用call指令前壓棧的call的下一條指令的地址。從圖13中可以看出,正是因為EIP的值變成了0x00401093,所以程序跳轉(zhuǎn)到了call指令后面的一條指令,又回到了中斷前的地方,這就是所謂的恢復(fù)斷點。

          圖13

          還沒有完全結(jié)束,此時還有最后一條指令add esp, 0Ch。這個就很簡單了,從圖13中可以看出現(xiàn)在棧頂?shù)臄?shù)據(jù)是1,2,3,也就是函數(shù)調(diào)用前壓入的三個實參。這是函數(shù)已經(jīng)執(zhí)行完了,顯然這三個參數(shù)沒有用處了。所以add esp, 0Ch就是讓棧頂指針往下移動12Byte的位置。為什么是12Byte呢,很簡單,因為入棧的是3個int數(shù)據(jù)。這樣由于函數(shù)調(diào)用在棧中添加的所有數(shù)據(jù)都已清除,棧頂指針(ESP)真正回到了函數(shù)調(diào)用前的位置,所有寄存器的值也恢復(fù)到了函數(shù)調(diào)用之前。

          https://blog.csdn.net/dongtingzhizi/article/details/6680050

          - EOF -

          瀏覽 43
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  秋霞无码一区二区三区 | 国产福利视频在线 | 久久国产精品久久久久久电车 | 午夜无码操逼逼 | 成人18岁毛片 |