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