Win64 栈帧的性能和注意事项

Win64 栈帧的性能和注意事项

在前文《masm64栈帧结构的详解》中,从Masm64的角度介绍了Win64栈帧结构及构建方法,本文介绍这种栈帧结构的性能和编程注意事项。
在Win64中,API函数采用新的调用约定,也即新的栈帧平衡机制,本人称其为"静态栈帧"。这种栈帧带来了很大变化,它改善了函数调用的性能,并且有更多的灵活性。对于使用汇编语言的编程人员来说,必须熟知这些变化,以适应新的技术环境。

1. 近距离使用push/pop指令

在Win32中,push/pop指令常用在函数体的首尾,以保存和恢复寄存器,但往往因为push/pop指令没有正确配对,造成程序崩溃。而采用静态栈帧,就消除了这类错误,提高了代码的安全性。

但这并不是说push/pop指令不能使用,其实还可以使用,但必须"近距离使用"。所谓近距离使用,就是用于变量的复制,而不是用于寄存器的保护。

如将变量val1的值复制到val2中,这就是近距离使用的意思了:

    push val1
    pop val2

以下这种情况是不可以的,因为不符合栈空间16字节对齐的要求,在调用子函数(即Test1)时直接出错。

    push rbx
    invoke Test1,101,102,103,104
    pop rbx

以下这种情况也是不可以的,虽然符合栈空间16字节对齐的要求,可以调用子函数(即Test1),但寄存器RAX和RBX的值并没有被保护,这是因为在调用子函数时,原压入栈中的2个寄存器值被函数参数替换。

    push rax
    push rbx
    invoke Test1,101,102,103,104
    pop rbx  ;rbx=101(被替换为第1个参数值)
    pop rax  ;rax=102(被替换为第2个参数值)

2. 函数参数传递的优化和注意事项

在Win32中使用push指令将函数参数压入栈中,但即要求被调用者负责释放,这种"不公平"的约定增加了系统级的错误机会。而在Win64中,在函数的首部一次性构建了所需的全部栈空间,谁构建栈空间就由谁来负责释放,责任明确。并且使用mov指令将函数参数置入栈中,避免了因使用push指令而引起的函数间的栈平衡问题。

另一方面,mov指令的执行速度大约比push指令快1倍,而且mov指令置入栈中的函数参数不会被子函数清除,可以重复利用,并且还约定前4个数参分别由RCX、RDX、R8、R9四个寄存器来传递,这进一步提高了函数调用的速度。一个程序是由大量的函数块构成的,每个函数又有很多参数需要传递,所以对于提高程序运行速度应该是有利的。

这里要注意的是,使用RCX、RDX、R8、R9四个寄存器用作参数时,一定要注意"对号入座",否则就出错了。例:

    invoke Test,rcx,rdx,r8,r9,arg5,arg6  ;这是正确的

    invoke Test,rdx,r8,arg3,arg4,rcx,r9  ;这是错误的

另外要注意的是,在Masm64中如果将RAX用作参数传递,必须是前4个参数之一,因为Masm64在处理后面参数(第5个参数开始)要使用寄存器RAX,这样RAX的值就被改变了。例:

invoke Test,rcx,rdx,r8,rax,arg5,arg6  ;这是正确的
invoke Test,rcx,rdx,r8,r9,arg5,rax    ;这是错误的(在处理arg5后,RAX值被改变)

3. 函数参数的"双向传递"效果

在Win32中,函数参数是不能双向传递的,除非你使用变量指针作为参数,来显式地接收函数运算的结果。而在Win64中,函数参数具有双向传递的效果,如函数TestA调用函数TestB,如果在TestB函数中改变了参数的值,则能在TestA函数中得到该变化后的值,这相当前逆传给调用者,这对于需要返回多个运算结构是很有用的。例:

以下例子中,TestA调用TestB计算最小与最大值,再分别使用两种方法调用TestC和TestD计算中位数。例子本身没有什么价值,只是演示函数参数双向传递的技巧。

    ;===============================
    ; 计算中位数---使用默认栈帧
    ;===============================
    TestD proc min:QWORD,max:QWORD
     mov rax,max
     inc rax
     sub rax,min
     shr rax,1
     ret
    TestD endp
    ;===============================
    ; 计算中位数---使用自定义栈帧
    ;===============================
     NOSTACKFRAME
    TestC proc min:QWORD,max:QWORD
     ENTER 0,0
     mov rax,max
     inc rax
     sub rax,min
     shr rax,1
     leave
     ret
    TestC endp
     STACKFRAME
    ;==================================================
    ; 求数列的最大和最小值---逆向返回计算结果
    ; 返回:
    ;    RAX=数值个数
    ;    a1=最小值
    ;    a2=最大值
    ;==================================================
   TestB proc a1:QWORD,a2:QWORD,a3:QWORD,a4:QWORD,a5:QWORD,a6:QWORD
     xor rax,rax
     mov rdx,-1
     mov rcx,6
     lea r8,a1
    ss_lp1:
     cmp rax,[r8] 
     jnbe @F
     mov rax,[r8]
    @@:
     cmp rdx,[r8]
     jbe @F
     mov rdx,[r8]
    @@:
     add r8,8
     dec rcx
     jnz ss_lp1
    ;---返回结果值---
     mov a1,rdx  ;返回最小值  
     mov a2,rax  ;返回最大值
     mov rax,6
     ret
    TestB endp
    ;====================================
    ; 参数双向传递例---主函数
    ;===================================
    TestA proc
     LOCAL ss_a1:QWORD
     LOCAL ss_a2:QWORD
     LOCAL ss_a3:QWORD
     LOCAL ss_a4:QWORD
     LOCAL ss_a5:QWORD
     LOCAL ss_a6:QWORD
     LOCAL ss_min:QWORD
     LOCAL ss_max:QWORD
     LOCAL ss_mid1:QWORD
     LOCAL ss_mid2:QWORD
    ;---初始化---
     mov ss_a1,3
     mov ss_a2,1
     mov ss_a3,5
     mov ss_a4,4
     mov ss_a5,6
     mov ss_a6,2
    ;---调用TestB计算最小与最大值---
     invoke TestB,ss_a1,ss_a2,ss_a3,ss_a4,ss_a5,ss_a6
    ;---获取返回值(注意:参数ss_a1 - ss_a6的值并没有改变)---
     mov rax,[rsp]
     mov ss_min,rax
     mov rax,[rsp+8]
     mov ss_max,rax
    ;--------------------------------------------------
    ; 调用TestC函数计算中位数
    ; 因为TestC使用自定义栈帧,不会改变当前rsp中的参数,
    ; 所以用call直接调用
    ;--------------------------------------------------
     call TestC
     mov ss_mid1,rax
    ;--------------------------------------------------
    ; 调用TestD函数计算中位数
    ; 因为TestD使用默认栈帧,会改变当前rsp中的参数,
    ; 所以用invoke调用。
    ;--------------------------------------------------
     mov r10,rsp
     invoke TestD,QWORD PTR [r10],QWORD PTR [r10+8]
     mov ss_mid2,rax  ;计算结果与ss_mid1是一样的
    ;--------------------------------------------------
    ; 也可以使用以下方法调用TestD函数
    ;--------------------------------------------------
     mov rcx,[rsp]
     mov rdx,[rsp+8]
     call TestD
     mov ss_mid2,rax  ;计算结果与ss_mid1是一样的
     ret
    TestA endp

4. 函数的跨节点返回

所谓跨节点返回,是指函数不返回到父函数(即调用者),而直接返回到父函数的父函数,或更远。如TestA调用TestB,TestB调用TestC,TestC调用TestD,而TestD直接返回到TestB或TestA。这种技术在编写调试类软件时是很有用的。

在Win32中这种跨节点返回是不可能的事,而在Win64中采用了静态栈帧,而且每个函数体内用RBP来指向本函数的栈底,RSP当然是栈顶,这样跨节点返回就很容易了。当然不能返回到当前线程外面,也不要返回到API,并且要注意返回码的问题。

例:

    ;=========================
    ; 跨节点返回例子
    ;=========================
    TestD proc
     leave        ;返回到TestB
    ; leave       ;如果不注释掉的话,连续2个leave指令就返回到TestA
     xor rax,rax  ;默认的返回码
     ret
    TestD endp
    ;=========================
    ; 跨节点返回例子
    ;=========================
     NOSTACKFRAME
    TestC proc
     ENTER 0,16
     call TestD
     leave
     ret
    TestC endp
     STACKFRAME
    ;=========================
    ; 跨节点返回例子
    ;=========================
    TestB proc
     invoke TestC
     mov rax,2
     invoke MessageBox,0,str$(rax),"rax",MB_OK  ;显示rax值
     ret
    TestB endp
    ;=========================
    ; 跨节点返回例子
    ;=========================
    TestA proc
     call TestB
     mov rax,1
     invoke MessageBox,0,str$(rax),"rax",MB_OK  ;显示rax值
     ret
    TestA endp
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值