[翻譯]Where the top of the stack is on x86

原作者Eli Bendersky's website (https://eli.thegreenplace.net/)

背景

對軟件進(jìn)行逆向分析時,通常對函數(shù)堆??臻g的分析是第一步,也是重要的步驟,文章主要對x-86架構(gòu)的棧空間問題進(jìn)行了基礎(chǔ)性的描述,作為初學(xué)逆向很值得學(xué)習(xí):

The stack analogy ??臻g類比

回到基本概念,??臻g通常被比喻一推疊放起來的碟子,無論放新的盤子還是取走一個盤子,都是從這推疊放的頂端操作,因此堆棧的操作部分也成為棧頂top of stack,棧的操作也類似分為入棧push和出棧pop。


plates

Hardware stacks 實(shí)際硬件中的棧

一般來講,棧是來自于memory的一段空間,入棧就是在把新的數(shù)據(jù)放到棧頂,而出棧就是把棧頂上的數(shù)據(jù)取走,然而這么說并沒有明確究竟棧頂和memory的關(guān)系和位置。

The stack in x86

這里將解釋上面困惑的原因,在于x86架構(gòu)下棧的方向是“頭朝下”,即從某一地址開始向低地址方向生長:


stack1

因此,我們說x86架構(gòu)的棧頂值得是從memory中申請到的這段堆棧的地址的最低部分。當(dāng)我們討論它時,讓我們看看x86匯編編程的一些常見習(xí)語如何映射到該圖形表示形式。

Pushing and popping data with the stack pointer 通過SP指針壓棧和出棧

x86架構(gòu)保留以個特殊的寄存器用于棧的操作--ESP(Extend Stack Point)。SP指針的作用即一直指向棧頂:


stack2

在上圖中,0x9080ABCC即是棧頂?shù)刂?,esp指向的數(shù)據(jù)"foo"就位于棧頂位置。

使用“push”想棧頂壓入數(shù)據(jù),push操作首先對esp的地址值減4,并且將操作數(shù)存放到esp指向的位置,因此:
push eax 等價于

sub esp, 4
mov [esp], eax

按照之前棧結(jié)構(gòu)圖示中的esp位置,假設(shè)eax保存新的壓棧數(shù)據(jù)0xDEADBEEF,push操作之后??臻g變?yōu)椋?br>

stack3

類似的,執(zhí)行pop指令是從頂取走數(shù)據(jù)并替換其操作數(shù),先保存棧頂位置之后地址值增加,換句話:
pop eax等價于

mov eax, [esp]
add esp, 4

接上面的??臻g結(jié)構(gòu)(push操作后),pop eax后的結(jié)果為:

stack4

這樣,數(shù)值0xDEADBEEF就被寫入了eax,同時發(fā)現(xiàn)0xDEADBEEF 的地址為0x9080ABC8。

Stack frames and calling conventions 棧結(jié)構(gòu)和條用規(guī)則

分析以下C編寫的匯編程序,可以發(fā)現(xiàn)很多有趣的模式。我們關(guān)心的主要是函數(shù)的參數(shù)是如何通過過棧傳入的(當(dāng)然在其他的一些架構(gòu)或者調(diào)用方式中,參數(shù)是放在寄存器里),以及本地參數(shù)在??臻g的分布規(guī)則。
分析以下C程序代碼

int foobar(int a, int b, int c)
{
    int xx = a + 2;
    int yy = b + 3;
    int zz = c + 4;
    int sum = xx + yy + zz;
    return xx * yy * zz + sum;
}
int main()
{
    return foobar(77, 88, 99);
}

當(dāng)foobar函數(shù)被調(diào)用時,包括傳入foobar的參數(shù)、函數(shù)的本地變量以及其他數(shù)據(jù)都將被保存到棧空間中。這一系列的數(shù)據(jù)都被稱為函數(shù)的棧幀,那么在return函數(shù)執(zhí)行前,foobar函數(shù)的棧幀結(jié)構(gòu)為:

stackframe1

當(dāng)調(diào)用函數(shù)時,首先將綠色的數(shù)據(jù)分配一段空間存儲參數(shù),藍(lán)色的數(shù)據(jù)則是函數(shù)本身的壓棧操作用于保存本地變量,使用gcc進(jìn)行編譯:
gcc -masm=intel -S z.c -o z.s
得到函數(shù)foobar的匯編代碼,我們對其進(jìn)行了注釋:

_foobar:
    ; ebp must be preserved across calls. Since
    ; this function modifies it, it must be
    ; saved.
    ;
    push    ebp

    ; From now on, ebp points to the current stack
    ; frame of the function
    ;
    mov     ebp, esp

    ; Make space on the stack for local variables
    ;
    sub     esp, 16

    ; eax <-- a. eax += 2. then store eax in xx
    ;
    mov     eax, DWORD PTR [ebp+8]
    add     eax, 2
    mov     DWORD PTR [ebp-4], eax

    ; eax <-- b. eax += 3. then store eax in yy
    ;
    mov     eax, DWORD PTR [ebp+12]
    add     eax, 3
    mov     DWORD PTR [ebp-8], eax

    ; eax <-- c. eax += 4. then store eax in zz
    ;
    mov     eax, DWORD PTR [ebp+16]
    add     eax, 4
    mov     DWORD PTR [ebp-12], eax

    ; add xx + yy + zz and store it in sum
    ;
    mov     eax, DWORD PTR [ebp-8]
    mov     edx, DWORD PTR [ebp-4]
    lea     eax, [edx+eax]
    add     eax, DWORD PTR [ebp-12]
    mov     DWORD PTR [ebp-16], eax

    ; Compute final result into eax, which
    ; stays there until return
    ;
    mov     eax, DWORD PTR [ebp-4]
    imul    eax, DWORD PTR [ebp-8]
    imul    eax, DWORD PTR [ebp-12]
    add     eax, DWORD PTR [ebp-16]

    ; The leave instruction here is equivalent to:
    ;
    ;   mov esp, ebp
    ;   pop ebp
    ;
    ; Which cleans the allocated locals and restores
    ; ebp.
    ;
    leave
    ret

可以看到首先需要在內(nèi)存上開辟一段??臻g,用于函數(shù)中各個參數(shù)(此函數(shù)為return, a, b, c),ebp永遠(yuǎn)指向返回地址,參戰(zhàn)按照ebp不斷增加;之后函數(shù)將本地變量(xx,yy,zz,sum)壓棧。函數(shù)參數(shù)在堆棧中高于ebp(因此訪問它們時為正偏移量),而局部變量在堆棧中低于ebp。

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

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