x64汇编下过程参数解析

简介


好久没上博客, 突然发现我的粉丝数变2700+了, 真是这几个月涨的粉比我之前好几年的都多, 于是心血来潮来写一篇, 记录一下x64下的调用约定(这里的调用约定只针对windows平台)

Windows下的x64程序的调用约定有别于x86下的"stdcall调用约定"以及"cdecl调用约定", 它有如下特点:

复制代码
1. 前4个参数使用RCX, RDX, R8, R9进行传参 ⭐
2. 从第5个参数开始, 使用栈传参, 返回值用RAX ⭐
3. 从右往左入栈 ⭐
4. 浮点数用XMM0-XMM3寄存器传参, 浮点数返回值用XMM0寄存器
5. 栈由调用者清理

这里只需要关心标星⭐的点即可

分类讨论

如果参数少于4个的情况下, Windows x64调用约定将使用RCX, RDX, R8, R9进行传参, 这时就很简单:

cpp 复制代码
; 描述: strlen的实现
; RCX: 字符串地址
; 返回值: 字符串长度
StrLen proc 
    mov rax, rcx 
    jmp L1Cmp 
L1:
    inc rax 
L1Cmp:
    mov dl, [rax]
    test dl, dl 
    jnz L1 

    sub rax, rcx 
    ret  
StrLen endp 

当参数多于4个的时候, 这里将分4种情况对其进行讨论:

  • 不构造栈帧 且 没有局部变量
  • 不构造栈帧 且 有局部变量
  • 构造栈帧 且 没有局部变量
  • 构造栈帧 且 有局部变量

这里给的案例C原型如下:

c 复制代码
// 目的是为了计算6个数的和
extern "C" int MultiAdd(int iNum1, int iNum2, int iNum3, int iNum4, int iNum5, int iNum6);

情况1. 不构造栈帧 且 没有局部变量

原理图:

案例:

cpp 复制代码
; RCX:  参数1
; RDX:  参数2
; R8:   参数3
; R9:   参数4
; 参数5和参数6通过栈传递
MultiAdd proc 
    lea rax, [r8 + r9]
    add rax, rcx 
    add rax, rdx 
    ; +8是为了越过"返回地址"
    add rax, [rsp + 8]
    add rax, [rsp + 10h]
    ret 
MultiAdd endp 

解释: 由于这种情况下, 不进行任何栈操作, 所以额外的参数永远是在[rsp + 8]的位置开始的, 依次+8

情况2. 不构造栈帧 且 有局部变量

原理图:

案例:

cpp 复制代码
; RCX:  参数1
; RDX:  参数2
; R8:   参数3
; R9:   参数4
; 参数5和参数6通过栈传递
MultiAdd proc 
    sub rsp, 20h 
    lea rax, [r8 + r9]
    add rax, rcx 
    add rax, rdx 
    ; +28h实际上是8+20h
    ; 8是为了越过"返回地址", 20h是越过局部栈空间
    add rax, [rsp + 28h]
    add rax, [rsp + 30h]
    add rsp, 20h 
    ret 
MultiAdd endp 

解释: 在局部过程中开辟了栈空间或者在栈中保存了参数, 需要让RSP越过对应的空间才能访问到过程的形参, 假设额外栈空间大小为n, 那过程额外的参数永远是在[rsp + 8 + n]的位置开始的, 依次+8

情况3. 构造栈帧 且 没有局部变量

原理图:

案例:

cpp 复制代码
; RCX:  参数1
; RDX:  参数2
; R8:   参数3
; R9:   参数4
; 参数5和参数6通过栈传递
MultiAdd proc 
    ; 开辟栈帧
    push rbp 
    mov rbp, rsp 

    lea rax, [r8 + r9]
    add rax, rcx 
    add rax, rdx 
    ; +10h是越过了保存在栈帧上的rbp以及返回地址
    add rax, [rsp + 10h]
    add rax, [rsp + 18h]
    
    mov rsp, rbp 
    pop rbp 
    ret 
MultiAdd endp 

解释: 如果没有在局部过程中开辟额外栈空间, 那栈帧其实没必要构造的, 因为会让栈中额外多出8字节的开销。过程额外的参数永远是在[rsp + 10h]的位置开始的, 依次+8

情况4. 构造栈帧 且 有局部变量

这里又可以分为2种寻址方式

  • RSP寻址
  • RBP寻址
a. RSP寻址:

RSP寻址就是以RSP作为基地址进行偏移来寻址
原理图:

案例:

cpp 复制代码
; RCX:  参数1
; RDX:  参数2
; R8:   参数3
; R9:   参数4
; 参数5和参数6通过栈传递
MultiAdd proc 
    ; 开辟栈帧
    push rbp 
    mov rbp, rsp 
    sub rsp, 20h 

    lea rax, [r8 + r9]
    add rax, rcx 
    add rax, rdx 
    ; +30h实际上是10h+20h
    ; 10h是越过了保存在栈帧上的rbp以及返回地址
    ; 20h是越过了开辟的局部空间
    add rax, [rsp + 30h]
    add rax, [rsp + 38h]
    
    mov rsp, rbp 
    pop rbp 
    ret 
MultiAdd endp 

解释: 如果你开辟了栈帧, 还用RSP来寻址, 那就得不偿失了, 因为开辟栈帧主要就是为了方便创建局部变量以及访问参数, 但虽然得不偿失也未尝不可。只是比较麻烦。要越过保存在栈上的RBP以及返回地址, 还有自己开辟的局部空间。
假设额外栈空间大小为n, 那过程额外的参数永远是在[rsp + 10h + n]的位置开始的, 依次+8

b. RBP寻址

RBP寻址就是以RBP作为基地址进行偏移来寻址, 这个访问过程的参数非常方便
原理图:

案例:

cpp 复制代码
; RCX:  参数1
; RDX:  参数2
; R8:   参数3
; R9:   参数4
; 参数5和参数6通过栈传递
MultiAdd proc 
    ; 开辟栈帧
    push rbp 
    mov rbp, rsp 
    sub rsp, 20h 

    lea rax, [r8 + r9]
    add rax, rcx 
    add rax, rdx 
    ; 10h实际上是RBP以及返回地址
    add rax, [rbp + 10h]
    add rax, [rbp + 18h]
    
    mov rsp, rbp 
    pop rbp 
    ret 
MultiAdd endp 

解释:

如果用RBP进行寻址, 那就非常方便了, 以8字节的开销保存RBP为代价是非常值得的。

(完)

相关推荐
浩浩测试一下10 小时前
汇编 16位32位64位通用寄存器(逆向分析)
汇编·windows·stm32·单片机·嵌入式硬件·逆向·二进制
浩浩测试一下19 小时前
汇编常用的(JCC 串 判断)指令 通用寄存器 标志寄存器 段寄存器(逆向分析)
汇编·通用寄存器·逆向二进制·标志寄存器·段寄存器·串 jcc 常用指令
浩浩测试一下2 天前
汇编 标志位寄存器 (逆向分析 )
c语言·汇编·逆向·windows编程·标志寄存器
浩浩测试一下2 天前
汇编 数组与串指令(逆向分析)
汇编·逆向·二进制·免杀·串指令·汇编数组
浩浩测试一下2 天前
汇编 内联汇编与混合编程 (逆向分析)
汇编·混合编程·windows编程·内联汇编·二进制逆向·c语言混合汇编
浩浩测试一下2 天前
汇编 结构体与宏
汇编··免杀·结构体·windows编程·逆向二进制
浩浩测试一下3 天前
汇编中的JCC指令 (逆向分析)
汇编·逆向·标志位·jcc指令·跳转指令·标志位寄存器
浩浩测试一下3 天前
汇编中的段与段寄存器(大小)段序 (逆向分析)
汇编·逆向·二进制·字节序·windows编程·内存地址排序
浩浩测试一下4 天前
汇编 call与ret 函数与堆栈 (逆向分析)
汇编·push·函数·pop·call·ret·堆栈逆向
山屿落星辰4 天前
昇腾NPU算子开发:从“手写汇编“到“搭积木“
汇编