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为代价是非常值得的。

(完)

相关推荐
代码改变世界ctw1 天前
ARM汇编编程(AArch64架构)课程 - 第5章函数调用规范
汇编·arm开发·架构
liulilittle2 天前
C/C++ inline-hook(x86)高级函数内联钩子
c语言·开发语言·汇编·c++·hook·底层·钩子
代码改变世界ctw2 天前
ARM汇编编程(AArch64架构)课程 - 第8章:控制流与循环
汇编·arm开发
Jacen.L2 天前
【汇编逆向系列】九、函数传参之结构体 - SHL、SHR指令,小型结构体参数和返回值
汇编
2401_861615283 天前
跨平台的ARM 和 x86 Docker 镜像:汇编语言实验环境搭建
linux·汇编·ubuntu·docker·容器
大P哥阿豪3 天前
Go defer(二):从汇编的角度理解延迟调用的实现
开发语言·汇编·后端·golang
花小璇学linux3 天前
imx6ull-裸机学习实验1——汇编LED灯实验
linux·汇编·imx6ull·arm裸机开发
无小道3 天前
c++-引用(包括完美转发,移动构造,万能引用)
c语言·开发语言·汇编·c++
liulilittle5 天前
C++ i386/AMD64平台汇编指令对齐长度获取实现
c语言·开发语言·汇编·c++
工业互联网专业5 天前
汇编与接口技术:8259中断实验
汇编·单片机·嵌入式硬件·8259中断实验