基础知识
函数调用约定
Windows
- 整数和浮点数参数
32位cdecl、stdcall、fastcall等几种,但64位统一为一种变形的fastcall。64位的fastcall中最多可以把函数的4个参数存储到寄存器中传递:
参数 | 整数型 | 浮点数型 |
---|---|---|
1st | RCX | XMM0 |
2st | RDX | XMM1 |
3st | R8 | XMM2 |
4st | R9 | XMM2 |
超过4个参数,使用栈来传递,传递顺序依照“从右向左”。此外,函数返回时传递的参数过程中所用的栈由调用者清理。看上去64位的fastcall就像32位下的cdcel和fastcall的结合。函数的前4个参数虽然使用寄存器传递,但在栈中仍为这4个参数预留了空间(32个字节)。
Note: 当整数和浮点数参数混合出现时,
eg: void func(float a, int b, double c, int d);
a放入XMM0中,b放入RDX,c放入XMM2,d放入R9。
这个存放的顺序很怪异,其实这是严格按照表中整数和浮点数的4个参数一一对应,需要为没用的参数预留空间,比如c参数没有放到XMM1中,XMM1被预留位置了。
- 指针参数
指针参数的传递遵循整数参数传递的方式。
-
结构体参数
结构体参数比较特殊,如果结构体长度小于64bit,则使用整数参数的传递规则。但如果是一个很大的结构体,那么应该还是要在堆栈中申请临时空间的(但ddk没有明说这一点,参考x86的规则应该如此)。 -
未声明函数调用
func1();
func2()
{
func1(2, 1.0, 7);
}
在这种情况下,func1()的参数表其实不明确,那么参数的传递要怎样进行?这里采用了一个比较保守的规则,就是:整数参数还是按照寄存器映射关系放入对应的寄存器中,浮点数在按照映射关系放入XMM寄存器后,还需要按照整数参数的寄存器映射关系放入整数寄存器中一次,这就是“比较保守的规则”的意思。就现在这个例子而言,结果如下:2在RCX中,1.0在RDX和XMM1中,7在R8中。
细节
- Windows 的 x64 下只有一种函数调用约定,即 __fastcall ,其他调用约定的关键字会被忽略,也就是说 ABI 只有 __fastcall ;
- 一个函数在调用时,前四个参数是从左至右依次存放于 RCX、RDX、R8、R9 寄存器里面,剩下的参数通过栈传递,从右至左顺序入栈;
- 如果是 int f(double a, double b, double c, double d, double e, double f) 这样的函数,前四个浮点类型参数从左到右由 XMM0,XMM1,XMM2,XMM3 依次传递,剩下的参数通过栈传递,从右至左顺序入栈;
- 调用者负责在栈上分配32字节的“shadow space”,用于存放那四个存放调用参数的寄存器的值(亦即前四个调用参数);
- 小于64位(bit)的参数传递时高位并不填充零(例如只传递ecx),也就是说结构体或union如果大小是1,2,4,8字节,用值传递(相应的寄存器),大于8字节(64位)必须按照地址(指针)传递;
- 被调用函数的返回值是64位以内(包括64位)的整形或指针时,则返回值会被存放于RAX;
- 如果返回值是浮点值,则返回值存放在XMM0;
- 更大的返回值(比如结构体),由调用方在栈上分配空间,并由 RCX 持有该空间的指针并传递给被调用函数,因此整型参数使用的寄存器依次右移一格,实际只可以利用 RDX,R8,R9,3个寄存器,其余参数通过栈传递。函数调用结束后,RAX 返回该空间的指针(即函数调用开始时的 RCX 值)。
- 调用者 (caller) 负责清理栈,被调用函数 (callee) 不用清栈,可是为什么有时候我们看到调用者 (caller) 也没有清栈呢?后面会讲;
- 除了 RCX,RDX,R8,R9 以外,RAX,R10,R11 和 XMM5,XMM6 也是“易挥发”的,不用特别保护,其余寄存器需要保护。(x86下只有 eax, ecx, edx 是易挥发的)
- 栈需要16字节对齐,“call”指令会入栈一个8字节的函数返回地址(函数调用指令后的下一个指令的地址)(注:即函数调用前原来的RIP指令寄存器的值),这样一来,栈就对不齐了(因为RCX、RDX、R8、R9四个寄存器刚好是32个字节,是16字节对齐的,现在多出来了8个字节)。所以,所有非叶子结点调用的函数,都必须调整栈RSP的地址为16n+8,来使栈对齐。
- 对于 R8~R15 寄存器,我们可以使用 r8, r8d, r8w, r8b 分别代表 r8 寄存器的64位、低32位、低16位和低8位。
Linux
细节
调用约定细节:
- Linux 下的调用约定叫做 “System V AMD64 ABI”,此约定主要在 Solaris,GNU/Linux,FreeBSD 和其他非微软OS上使用;
- Linux 的 x64 下也只有一种函数调用约定,即 __fastcall ,其他调用约定的关键字会被忽略,也就是说 ABI 只有 __fastcall ;
- 一个函数在调用时,如果参数个数小于等于 6 个时,前 6 个参数是从左至右依次存放于 RDI,RSI,RDX,RCX,R8,R9 寄存器里面,剩下的参数通过栈传递,从右至左顺序入栈;
- 如果参数个数大于 6 个时,前 5 个参数是从左至右依次存放于 RDI,RSI,RDX,RCX,RAX 寄存器里面,剩下的参数通过栈传递,从右至左顺序入栈;
- 对于系统调用,使用 R10 代替 RCX;
- XMM0 ~ XMM7 用于传递浮点参数;
- 小于64位(bit)的参数传递时高位并不填充零(例如只传递ecx),也就是说结构体或union如果大小是1,2,4,8字节,用值传递(相应的寄存器),大于8字节(64位)必须按照地址(指针)传递;
- 被调用函数的返回值是64位以内(包括64位)的整形或指针时,则返回值会被存放于 RAX,如果返回值是128位的,则高64位放入 RDX;
- 如果返回值是浮点值,则返回值存放在XMM0;
更大的返回值(比如结构体),由调用方在栈上分配空间,并由 RCX 持有该空间的指针并传递给被调用函数,因此整型参数使用的寄存器依次右移一格,实际只可以利用 RDI,RSI,RDX,R8,R9,5个寄存器,其余参数通过栈传递。函数调用结束后,RAX 返回该空间的指针(即函数调用开始时的 RCX 值)。 - 可选地,被调函数推入 RBP,以使 caller-return-rip 在其上方8个字节,并将 RBP 设置为已保存的 RBP 的地址。这允许遍历现有堆栈帧,通过指定GCC的 -fomit-frame-pointer 选项可以消除此问题。
- 调用者 (caller) 负责清理栈,被调用函数 (callee) 不用清栈;
- 除了 RDI,RSI,RDX,RCX,R8,R9 以外,RAX,R10,R11 也是“易挥发”的,不用特别保护,其余寄存器需要保护。
- 在调用 call 指令之前,必须保证堆栈是16字节对齐的;
- 对于 R8~R15 寄存器,我们可以使用 r8, r8d, r8w, r8b 分别代表 r8 寄存器的64位、低32位、低16位和低8位。
字节大小
取决于是64位编译模式还是32位编译模式(注意,和机器位数没有直接关系)
在64位编译模式下,指针的占用内存大小是8字节
在32位编译模式下,指针占用内存大小是4字节
DWORD 4个字节
寄存器基础
通用寄存器 (general register)
通用寄存器(general-purpose registers, GPRs) ,每一个用户空间的程序,或者内核程序都用到的,基本的寄存器。
因为X86-64是从32位的X86,甚至16位、8位演变而来的,为了软件可以向前兼容,所以,这些寄存器都有不同的版本。话不多说,看下表:
64-bit | 32-bit | 16-bit | 8-bit (Low / High) |
---|---|---|---|
RAX | EAX | AX | AL / AH |
RBX | EBX | BX | BL / BH |
RCX | ECX | CX | CL / CH |
RDX | EDX | DX | DL / DH |
RSI | ESI | SI | SIL |
RDI | EDI | DI | DIL |
RBP | EBP | BP | BPL |
RSP | ESP | SP | SPL |
R8 | R8D | R8W | R8B |
R9 | R9D | R9W | R9B |
R10 | R10D | R10W | R10B |
R11 | R11D | R11W | R11B |
R12 | R12D | R12W | R12B |
R13 | R13D | R13W | R13B |
R14 | R14D | R14W | R14B |
R15 | R15D | R15W | R15B |
通用寄存器总共: 68.
案例分析
v8 = *(_DWORD *)(*a3 + 92i64);
i64为MS后缀,可直接去掉
movd xmm6, dword ptr [rax+38h]
- 从地址[rax+38h]取出double word字节(32位)的数据存入xmm6的寄存器
- movd: 移动双字
- xmm6: 浮点寄存器
- dword ptr: 双字指针
cvtsi2sd xmm1, dword ptr [rbx+11Ch]
- cvtsi2sd: 将源操作数(第二个操作数)中的有符号双字整数转换成目标操作数(第一个操作数)中的双精度浮点值。源操作数可以是通用寄存器或 32 位内存位置。目标操作数是 XMM 寄存器。结果存储到目标操作数的低位四字,高位四字保持不变。