Windows x64 棧幀結構


0x01 前言

  Windows 64位下函數調用約定變為了快速調用約定,前4個參數采用rcx、rdx、r8、r9傳遞,多余的參數從右向左依次使用堆棧傳遞。本次文章是對於Windows 64位下函數調用的分析,分析各種參數情況下調用者和被調用函數的棧結構。

 

0x02 4參數時函數調用流程

   64位下函數的調用約定全部用FASTCALL,就是前4個參數依次用rcx,rdx,r8,r9傳遞,多余的參數從右至左壓參。

 1)測試用例

  我們先用c語言寫一個調用4參數的函數 

int Add(int a,int b,int c,int d);

int _tmain(int argc, _TCHAR* argv[])
{
    int a = 0;
    Add(1,2,3,4);
    return 0;
}

int Add(int a,int b,int c,int d)
{
    int xx = a+b+c+d;
    int yy = a+b-c-d;
    int zz = -a-b+c+d;
    return xx;
}

  2)分析過程  

  使用Vs2010 ,64位下調試,打開寄存器窗口,Alt+8 反匯編

  ①Main中調用Add函數

000000013F931049  mov         r9d,4  
000000013F93104F  mov         r8d,3  
000000013F931055  mov         edx,2  
000000013F93105A  mov         ecx,1  
000000013F93105F  call        Add (13F931005h)   ;指令為 push rip    ;RSP-8   
                                                 ;       jmp Add

  可以看到首先將1,2,3,4放在寄存器中,然后調用call指令,call指令可以分解為將下一條指令壓參,然后jmp到函數地址,注意在執行push指令的時候,RSP-8

  

  ②Add函數

int Add(int a,int b,int c,int d)
{
000000013F251080  mov         dword ptr [rsp+20h],r9d  
000000013F251085  mov         dword ptr [rsp+18h],r8d  
000000013F25108A  mov         dword ptr [rsp+10h],edx  
000000013F25108E  mov         dword ptr [rsp+8],ecx  
000000013F251092  push        rdi                ;保存前棧底   RSP-8
000000013F251093  sub         rsp,10h            ;開辟棧區 16字節 RSP-10h
000000013F251097  mov         rdi,rsp            ;新棧幀棧底rdi=rsp
000000013F25109A  mov         ecx,4              ;循環次數
000000013F25109F  mov         eax,0CCCCCCCCh  
000000013F2510A4  rep stos    dword ptr [rdi]    ;將rdi開始賦值eax中的值,循環4次
000000013F2510A6  mov         ecx,dword ptr [rsp+20h]  ;此處是第一個參數a  
    int xx = a+b+c+d;
000000013F2510AA  mov         eax,dword ptr [b]  
000000013F2510AE  mov         ecx,dword ptr [a]  
000000013F2510B2  add         ecx,eax  
000000013F2510B4  mov         eax,ecx  
000000013F2510B6  add         eax,dword ptr [c]  
000000013F2510BA  add         eax,dword ptr [d]  
000000013F2510BE  mov         dword ptr [rsp],eax  ;rsp 保存 xx
    int yy = a+b-c-d;
000000013F2510C1  mov         eax,dword ptr [b]  
000000013F2510C5  mov         ecx,dword ptr [a]  
000000013F2510C9  add         ecx,eax  
000000013F2510CB  mov         eax,ecx  
000000013F2510CD  sub         eax,dword ptr [c]  
000000013F2510D1  sub         eax,dword ptr [d]  
000000013F2510D5  mov         dword ptr [yy],eax   ;rsp+4 保存yy
    int zz = -a-b+c+d;
000000013F2510D9  mov         eax,dword ptr [a]  
000000013F2510DD  neg         eax  
000000013F2510DF  sub         eax,dword ptr [b]  
000000013F2510E3  add         eax,dword ptr [c]  
000000013F2510E7  add         eax,dword ptr [d]  
000000013F2510EB  mov         dword ptr [zz],eax  //rsp+8 保存
    return xx;
000000013F2510EF  mov         eax,dword ptr [rsp]  ;將返回值保存在eax寄存器中
}
000000013F2510F2  add         rsp,10h    ;恢復開辟的棧區
000000013F2510F6  pop         rdi        ;恢復前棧幀的棧底
000000013F2510F7  ret                    ;pop rip  將之前保存的call下一條指令彈出給rip , 繼續執行 
                                         ;RSP - 8  等於調用call之前的值 

  可以看到前4句將寄存器中傳遞的參數賦值給rsp+8h,rsp+10h,rsp+18h,rsp+20h,這是因為雖然使用寄存器傳參,但是在棧區函數還是會開辟0x20大小的區域保存傳遞過來的參數,不過使用寄存器傳參會比使用堆棧傳參更有效率。

  push rdi;保存前棧幀棧底

  sub rsp,10h;開辟棧區保存局部變量,由於是三個變量12字節,對齊內存是16字節,sub rsp,10h

  mov rdi,rsp;保存當前函數棧的棧底

      mov ecx,4

  mov eax,0CCCCCCCCh 

  rep stos dword ptr [rdi]    這三句是將rdi(棧底)指向的值,循環4次(rcx),賦值為0CCCCCCCCh(eax),這里是初始化棧區開辟的0x10字節的內容,注意release和debug版本的變化,debug版本會自動將變量初始化為0CCCCCCCCh,但是release版本不會初始化,如果忘記初始化則會編譯報錯。

  函數最后返回參數需要保存在eax中,add  rsp,10h要將之前堆棧開辟的棧區恢復,pop  rdi;要將之前push的main函數棧底恢復到rdi中。ret指令相當於pop rip,將call時壓入的rip(call的下一條指令)恢復,這樣一次函數調用的流程便結束了。

 

  3)內存分析

      ①我們查看main函數的棧底RDI和棧頂RSP

   ②保存上一個函數棧底,將rsp賦值給rdi,作為新函數Add()函數的棧底

 

  此時RSP經過 call 中的push rip 減去8,push edi 減去8,sub rsp,10h 一共減去20h,rsp賦值給rdi,為當前Add的棧底

 

 

  rdi經過rep stos 指令將eax中值初始化到rdi中,共4*4字節,rdi初始化之后加10h,此時我們看內存中的情況如上圖所示

  棧幀情況如下

 

 0x03 5參數時函數調用流程以及調用者棧分析

      我們在試試5參數的函數調用情況,同時我們知道函數會把4個寄存器中的值賦值到棧上面的區域,要開辟4*8=0x20h的區域,在調試的時候沒有發現對於rsp的操作,於是猜測是在上一個函數中已經開辟好了額外的空間存儲參數的數據。

  1)測試用例

  我們在main中調用5參數的Sub()函數查看5參數調用流程

  同時在Sub()中調用Add()函數,查看調用者棧的使用情況

#include "stdafx.h"
int Sub(int a,int b,int c,int d,int e);
int _tmain(int argc, _TCHAR* argv[])
{
    int a = 0;
    Sub(1,2,3,4,5);
    return 0;
}

int Add(int a,int b,int c,int d,int e)
{
    int xx = a+b+c+d;
    int yy = a+b-c-d;
    int zz = -a-b+c+d;
    return xx;
}

int Sub(int a,int b,int c,int d,int e)
{
    int xx = a+b+e+d;
    int yy = a+b-c-d;
    int zz = -a-b+c+d;
    Add(b,c,d,e,xx);
    return xx;
}

 

  2)分析過程

  ①main函數中調用5參數函數

    Sub(1,2,3,4,5);
000000013F4F2EF9  mov         dword ptr [rsp+20h],5   ;當前rsp + 20 就是存儲4個參數之后的位置  
000000013F4F2F01  mov         r9d,4  
000000013F4F2F07  mov         r8d,3  
000000013F4F2F0D  mov         edx,2  
000000013F4F2F12  mov         ecx,1  
000000013F4F2F17  call        Sub (13F4F100Fh)

  這里多余的一個參數直接保存在rsp+20h的地址中,使用棧傳遞參數,我們下面在調用者的棧分析中會說明rsp+20h是什么

  

  ②Sub()函數作為調用者,調用Add()函數的過程分析

int Sub(int a,int b,int c,int d,int e)
{
000000013F211110  mov         dword ptr [rsp+20h],r9d  ;第四個參數  此時的rsp為上一個函數的rsp
000000013F211115  mov         dword ptr [rsp+18h],r8d  ;第三個參數
000000013F21111A  mov         dword ptr [rsp+10h],edx  ;第二個參數
000000013F21111E  mov         dword ptr [rsp+8],ecx  ;第一個參數
000000013F211122  push        rdi  ;保存main函數棧底
000000013F211123  sub         rsp,40h  ;開辟本函數棧區,這里是三個局部變量對齊為0x10,和下一個函數的0x20+0x8。全部對齊為0x40
000000013F211127  mov         rdi,rsp  ;保存本函數棧底
000000013F21112A  mov         ecx,10h  ;rep次數
000000013F21112F  mov         eax,0CCCCCCCCh  ;rep初始化值
000000013F211134  rep stos    dword ptr [rdi]  ;初始化本函數棧區
000000013F211136  mov         ecx,dword ptr [rsp+50h]  
    int xx = a+b+e+d;
000000013F21113A  mov         eax,dword ptr [b]  
000000013F21113E  mov         ecx,dword ptr [a]  
000000013F211142  add         ecx,eax  
000000013F211144  mov         eax,ecx  
000000013F211146  add         eax,dword ptr [e]  
000000013F21114A  add         eax,dword ptr [d]  
000000013F21114E  mov         dword ptr [xx],eax  ;xx=a+b+c+d 值為10
    int yy = a+b-c-d;
000000013F211152  mov         eax,dword ptr [b]  
000000013F211156  mov         ecx,dword ptr [a]  
000000013F21115A  add         ecx,eax  
000000013F21115C  mov         eax,ecx  
000000013F21115E  sub         eax,dword ptr [c]  
000000013F211162  sub         eax,dword ptr [d]  
000000013F211166  mov         dword ptr [yy],eax  ;yy=a+b-c-d 值為-4
    int zz = -a-b+c+d;
000000013F21116A  mov         eax,dword ptr [a]  
000000013F21116E  neg         eax  
000000013F211170  sub         eax,dword ptr [b]  
000000013F211174  add         eax,dword ptr [c]  
000000013F211178  add         eax,dword ptr [d]  
000000013F21117C  mov         dword ptr [zz],eax  ;zz=-a-b+c+d 值為4
    Add(b,c,d,e,xx);
000000013F211180  mov         eax,dword ptr [xx]  
000000013F211184  mov         dword ptr [rsp+20h],eax  ;第五個參數  保存在Sub函數的rsp+20h處
000000013F211188  mov         r9d,dword ptr [e]  ;第四個參數
000000013F21118D  mov         r8d,dword ptr [d]  ;第三個參數
000000013F211192  mov         edx,dword ptr [c]  ;第二個參數
000000013F211196  mov         ecx,dword ptr [b]  ;第一個參數
000000013F21119A  call        Add (13F211005h)  
    return xx;
000000013F21119F  mov         eax,dword ptr [xx]  
}

  我在函數中調用了Add()函數,結果rsp - 0x40 開辟了0x40大小的棧區空間,這里的0x10是保存三個int型的局部變量,0x30中保存Add的4個寄存器中的值使用了0x20,還有0x08用作保存第5個參數,剩下的用於內存對齊。

 

  3)內存分析

  ①我們在Sub函數的棧頂RSP初始化完成之后,查看RSP的值

  ②在內存中輸入RSP地址,查看棧區內存,當我們對局部變量xx,yy,zz賦值完成之后棧區如下圖所示

  可以看出Sub函數棧中,棧頂RSP+0x30、0x34、0x38分別保存着局部變量xx/yy/zz,+0x3c的地方有4字節用於內存對齊。

    

  ③我們再看看Sub()函數中調用Add()函數返回之后Sub函數棧的內容

  此時在Add()函數返回之后,Add()開辟的函數棧已經銷毀,但是Sub()函數依然保留這傳遞給Add()的參數,從RSP開始依次0x20內存區域保存4個寄存器傳遞的參數的值,2,3,4,5。在RSP+0x20的地方保存了第五個參數的值0xc,這里就是在調用的時候直接使用RSP+20的原因,這里的賦值是在Add()函數開始將4個寄存器中的值拷貝到這里的,可以參考Sub()函數開始將寄存器中值拷貝到main函數棧區。

 

 0x04 少於4參數時函數調用流程

    1)我們編寫c語言測試三參數函數調用

#include "stdafx.h"
int Sub(int a,int b,int c);
int _tmain(int argc, _TCHAR* argv[])
{
    int a = 0;
    Sub(1,2,3);
    return 0;
}

int Add(int a,int b,int c)//2,3,3
{
    int xx = a+b+c;
    int yy = a+b-c;
    int zz = -a-b+c;
    return xx;
}

int Sub(int a,int b,int c)//1,2,3
{
    int xx = a+b;//3
    int yy = a+b-c;//0
    int zz = -b+c;//1
    Add(b,c,xx);//2,3,3
    return xx;
}

  

  2)分析過程

  我們直接查看Sub()函數的匯編代碼

int Sub(int a,int b,int c)//1,2,3
{
000000013F8E10F0  mov         dword ptr [rsp+18h],r8d  ;r9寄存器沒有用到
000000013F8E10F5  mov         dword ptr [rsp+10h],edx  
000000013F8E10F9  mov         dword ptr [rsp+8],ecx  
000000013F8E10FD  push        rdi  
000000013F8E10FE  sub         rsp,30h  ;開辟了0x10用於局部變量,0x20用於Add()函數的參數
000000013F8E1102  mov         rdi,rsp  
000000013F8E1105  mov         ecx,0Ch  
000000013F8E110A  mov         eax,0CCCCCCCCh  
000000013F8E110F  rep stos    dword ptr [rdi]  
000000013F8E1111  mov         ecx,dword ptr [rsp+40h]  
    int xx = a+b;//3
000000013F8E1115  mov         eax,dword ptr [b]  
000000013F8E1119  mov         ecx,dword ptr [a]  
000000013F8E111D  add         ecx,eax  
000000013F8E111F  mov         eax,ecx  
000000013F8E1121  mov         dword ptr [xx],eax  ;xx = a+b 3
    int yy = a+b-c;//0
000000013F8E1125  mov         eax,dword ptr [b]  
000000013F8E1129  mov         ecx,dword ptr [a]  
000000013F8E112D  add         ecx,eax  
000000013F8E112F  mov         eax,ecx  
000000013F8E1131  sub         eax,dword ptr [c]  
000000013F8E1135  mov         dword ptr [yy],eax  ;yy = a+b-c 0
    int zz = -b+c;//1
000000013F8E1139  mov         eax,dword ptr [b]  
000000013F8E113D  neg         eax  
000000013F8E113F  add         eax,dword ptr [c]  
000000013F8E1143  mov         dword ptr [zz],eax  ;zz = -b+c 1
    Add(b,c,xx);//2,3,3
000000013F8E1147  mov         r8d,dword ptr [xx]  ;第三參數
000000013F8E114C  mov         edx,dword ptr [c]  ;第二參數
000000013F8E1150  mov         ecx,dword ptr [b]  ;第一參數
000000013F8E1154  call        Add (13F8E1014h)  
    return xx;
000000013F8E1159  mov         eax,dword ptr [xx]  
}

  我在函數中調用了Add()函數,結果rsp - 0x30 開辟了0x30大小的棧區空間,這里有0x10是保存三個int型的局部變量,可以看到雖然只是用了三個寄存器傳遞參數,但是Sub()函數依然開辟了0x20保存4個參數的棧內存。

 

  3)內存分析

  ①我們在Sub()函數初始化RSP完成之后,查看RSP的值

 

  ②通過Sub()函數的RSP,我們查看Add()調用之后的Sub()函數棧區內存

   這里RSP依次保存三個參數,第四個參數內存初始化為cccccccch,然后就是Sub函數自身的局部變量,最后4個字節為內存對齊的開銷。

 

0x05 總結

  本文編寫了幾個小Demo,驗證了64位下函數調用時棧的分配情況。

  1.函數在開始會將寄存器上的參數拷貝到棧中保存,這塊內存由調用函數開辟

  2.少於或等於4參數情況,調用者函數會分配多余0x20字節內存用於保存調用函數的參數,保存由寄存器傳遞的參數。

  3.多余4參數時,調用者函數會分配0x20+多余參數個數 x 8 字節的內存用於保存調用函數的參數。其中0x20保存寄存器賦值的參數,多余的通過棧傳遞。

  4.函數的call指令,會保存下一條指令入棧,接着跳轉到函數的開頭。

  5.ret指令,會彈出之前保存的call之后的指令到eip/rip上,返回執行call之后的內容。

  6.函數棧是連續的,函數在開始會保存上一個函數棧幀,在結束時還原上一個函數棧幀。

 

  有什么不足之處,請指出!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM