masm64栈帧结构的详解

本文详细介绍了x64架构中栈帧的结构,包括函数参数区、返回地址区和局部变量区的布局,以及栈帧的两种构建方法:默认栈帧构建和手工栈帧构建。在默认构建中,栈帧包含了参数区、局部变量区和非易失性寄存器保护区,而在手工构建中,栈帧结构可以根据需要灵活调整。此外,文章还讨论了没有栈帧情况下的函数调用,强调了即使没有栈帧,关键指令也不能省略以确保参数的正确访问。
摘要由CSDN通过智能技术生成

masm64栈帧结构的详解

x64与x86的重要区别之一,就是栈的平衡机制不同。所以了解x64栈帧结构及构建方法,是非常重要的。

1. 栈帧结构

以下是函数A调用函数B时,函数B的栈帧结构。

 rspB   : dq ? dup(?)  ;用于存放调用其他函数时需要传递的参数。
 local  : dq ? dup(?)  ;局部变量区(需进行16字节对齐)。
 rbpB   : dq rbpA      ;保存了函数A的rbp值。
 retaddr: dq ?         ;保存了返回地址
 rspA   : dq rcx       ;函数A传入函数B的第1个参数
          dq rdx       ;函数A传入函数B的第2个参数(只有1个参数时无意义)
          dq r8        ;函数A传入函数B的第3个参数(只有2个参数时无意义)
          dq r9        ;函数A传入函数B的第4个参数(只有3个参数时无意义)
          dq ?         ;函数A传入函数B的第5个参数(只有4个参数时无意义或无该单元)
          dq ?         ;函数A传入函数B的第6个参数(只有5个参数时无意义或无该单元)
          ...

注: rspA和rbpA是函数A的rsp和rbp值。rspB和rbpB是函数B的rsp和rbp值

rspA所指的首4个参数分别是rcx,rdx,r8,r9四个寄存器参数的存放单元,只有当函数B使用了函数参数名才会将这4个参数值存入到对应的单元,否则并不会将首4个参数存入到该单元中,但单元空间是保留可用的,用户可以自己将4个参数保存到对应的单元中。

1.1 函数参数区

参数区从rspA开始向下,该栈空间由函数A构建,用于向函数B传递函数参数,如果使用默认栈帧构建宏(如STACKFRAME宏)则为128字节长度(16个QWORD变量字节长度)。对于函数B来说,虽然不知道函数A栈帧参数区空间尺寸,但只少有4个QWORD参数空间,这是约定的。

如果函数B不使用参数名,则前4个参数不会存入到栈中,但可以通过rbp来访问这些栈空间单元,如下:

 [rbp+16] : 参数RCX存放单元(如果无参数名,则并没有保存实际的值,但单元空间可用。下同)
 [rbp+24] : 参数RDX存放单元
 [rbp+32] : 参数R8存放单元
 [rbp+40] : 参数R9存放单元

1.2 返回地址区

retaddr所指的8字节保存了返回地址,是函数A在执行call指令时产生的。

1.3 函数B构建的栈帧

从rspB到retaddr(不含retaddr)的空间是函数B构建的。

(1) rbpB的8字节保存了函数A的rbp值,保护原rbp,并将原rsp赋值给rbp,这样可以通过rbp来访问传入的函数参数和局部变量。

(2) 局部变量区。local所指的区为局部变量区,用于局部变量。默认栈帧构建宏(如STACKFRAME宏)将其划分为2个区域,即局部变量和非易失性寄存器保护区。

(3) 函数参数区。rspB所指的区为参数区,当函数B调用其他函数时,其用于保存需要传递给其他函数的参数。如果使用默认栈帧构建宏(如STACKFRAME宏),则为128字节长度(16个QWORD变量字节长度),如果手工构建栈帧则只少有4个QWORD参数空间,这是约定的。

2. 栈帧的构建

栈帧的构建有两种方法,两种方法得到的栈帧是有所区别的。

2.1 默认栈帧构建

在masm64宏代码中有一个STACKFRAME宏,默认情况下该宏会包含在用户的源代码文件中,编译器会在函数的首尾自动调用该宏所指定的另外两个宏,即UseStackFrame和EndStackFrame宏,称之谓"序言/尾声"。在函数的开始处调用UseStackFrame宏来构建栈帧,在函数的尾部调用EndStackFrame宏来释放栈帧并恢复rbp值。

STACKFRAME宏构建的栈帧结构如下(以函数B为例)

 rspB   : dq 16 dup(?)  ;128字节空间。用于存放调用其他函数时需要传递的参数。
 local  : dq ? dup(?)   ;局部变量区域(需进行16字节对齐)。如果无局部变量,则无该区域。
 save   : dq 12 dup(?)  ;96字节空间。用于保存非易失性寄存器。
 rbpB   : dq rbpA       ;保存了调用者(函数A)的rbp值。

即局部变量区被划分为2块,local所指的区域尺寸是函数中定义的局部变量总字节长度(经16字节对齐后的长度)。其中save区用于保存非易失性寄存器的值。因为save区没有关联变量名,如果要使用save区单元,则需通过rbp来访问,并且从下到上的顺序访问,如下:

 mov [rbp-8],rbx    ;保存rbx
 mov [rbp-16],rsi   ;
    ...
 mov [rbp-96],r15   ;这是save区的顶部

2.2 手工栈帧构建

要手工构建栈帧,必须在函数首关闭默认栈帧构建宏,然后再在函数尾开启默认栈帧构建宏。示例代码如下:

;***********************************************
 NOSTACKFRAME  ;关闭默认栈帧构建宏
;===============================================
; 手功构建栈帧示例
;===============================================
funcB proc a1:QWORD,a2:QWORD,a3:QWORD,a4:QWORD,a5:QWORD,a6:QWORD
 LOCAL ss_a1:QWORD
 LOCAL ss_a2:QWORD
 LOCAL ss_a3:QWORD
 LOCAL ss_a4:QWORD
 LOCAL ss_a5:QWORD
 LOCAL ss_a6:QWORD
 ENTER 128,0     ;构建参数区,保存原rbp,设置新的rbp (这里也使用默认的128字节空间)
 sub rsp,96+6*8  ;构建局部变量区(需进行16字节对齐)

 ...
 
 LEAVE    ;释放栈帧并恢复rbp
 ret
funcB endp

 STACKFRAME ;开启默认栈帧构建宏
;*******************************************

上例代码所构建的栈帧结构如下:

    rspB   : dq 16 dup(?)  ;128字节空间。用于存放调用其他函数时需要传递的参数。
    save   : dq 12 dup(?)  ;96字节空间。用于保存非易失性寄存器。
    local  : dq ss_a6      ;局部变量ss_a6存贮单元。
             dq ss_a5      ;局部变量ss_a5存贮单元。
             dq ss_a4      ;局部变量ss_a4存贮单元。
             dq ss_a3      ;局部变量ss_a3存贮单元。
             dq ss_a2      ;局部变量ss_a2存贮单元。
             dq ss_a1      ;局部变量ss_a1存贮单元。
    rbpB   : dq rbpA       ;保存了调用者(函数A)的rbp值。

与默认构建的栈帧结构有所区别,即save区在local区的上面,这样save区的访问就不方便了。所以手工构建栈帧时一般不需要save区,上例首部代码改为如下即可:

    ENTER 128,0     ;第2个参数必须为0。
    sub rsp,6*8     ;6*8为ss_a1到ss_a6的变量字节长度。

例子中将参数区空间尺寸设置为128字节,也可以根据实际情况设置。如果函数不调用其他函数,则不需要构建参数区,上例首部代码改为如下即可:

    ENTER 0,0
    sub rsp,6*8

代码中ENTER和LEAVE两个指令的作用如下:

(1) ENTER N,0 指令的等效代码如下:

    push rbp
    mov rbp,rsp
    sub rsp,N

所以也可以使用以下代码来构建栈帧:

    push rbp
    mov rbp,rsp
    sub rsp,128+6*8

(2) LEAVE 指令的等效代码如下:

    mov rsp,rbp
    pop rbp

即LEAVE指令释放了由ENTER指令构建的栈空间,也释放了由"sub rsp,n"所构建的栈空间。

3. 没有栈帧的情况

如果函数A调用函数B时,而函数B是没有栈帧的,则栈的结构如下:

rspB   : dq ?         ;保存了返回地址
rspA   : dq rcx       ;函数A传入函数B的第1个参数
         dq rdx       ;函数A传入函数B的第2个参数(只有1个参数时无意义)
         dq r8        ;函数A传入函数B的第3个参数(只有2个参数时无意义)
         dq r9        ;函数A传入函数B的第4个参数(只有3个参数时无意义)
         dq ?         ;函数A传入函数B的第5个参数(只有4个参数时无意义或无该单元)
         dq ?         ;函数A传入函数B的第6个参数(只有5个参数时无意义或无该单元)
         ...          ;..............

这类函数没有局部变量,也不能调用其他函数,如果你调用其他函数,则其他函数就会访问参数,可是你并没有参数区,这就极易造成程序崩溃。

虽然没有栈帧,但也不能省略函数首尾的关键指令,否则不能访问函数A传入的参数。正确的代码示例如下:

 NOSTACKFRAME
funcB proc a1:QWORD,a2:QWORD,a3:QWORD,a4:QWORD,a5:QWORD,a6:QWORD
 ENTER 0,0   ;这样才能访问参数

 mov rax,a1  ;可以访问参数
 ...
 
 LEAVE       ;恢复rbp这是必须的,否则函数A就崩溃了。
 ret
funcB endp
 STACKFRAME
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值