上一篇 博文提到了 X64 下 MSVC 如何传递参数,但是没有涉及到当参数个数大于 4 的时候如何分配内存空间的问题,接下来我们来探究这个问题。
RSP 和 RBP
按照上面提到的博文,我们进行如下实验:
- 所有参数都是
struct Arg
, 并且sizeof(Arg) == 16
。 - 实验中,参数的个数
k = 6, 7, 8
,先观察汇编后rsp
和rbp
寄存器的变化。
实验:
1. k = 6
; 28 : void Call() {
$LN3:
00000 40 55 push rbp
00002 56 push rsi
00003 57 push rdi
00004 48 81 ec f0 03
00 00 sub rsp, 1008 ; 000003f0H
0000b 48 8d 6c 24 30 lea rbp, QWORD PTR [rsp+48]
00010 48 8b fc mov rdi, rsp
- k = 7
; 28 : void Call() {
$LN3:
00000 40 55 push rbp
00002 56 push rsi
00003 57 push rdi
00004 48 81 ec 60 04
00 00 sub rsp, 1120 ; 00000460H
0000b 48 8d 6c 24 40 lea rbp, QWORD PTR [rsp+64]
00010 48 8b fc mov rdi, rsp
- k = 8
; 28 : void CallFuck() {
$LN3:
00000 40 55 push rbp
00002 56 push rsi
00003 57 push rdi
00004 48 81 ec c0 04
00 00 sub rsp, 1216 ; 000004c0H
0000b 48 8d 6c 24 40 lea rbp, QWORD PTR [rsp+64]
这里我们需要明确一点,就是 rbp
的意义和 rsp
的意义是什么。
rbp
是属于当前函数的栈空间基地址,rsp
是包含当前函数为被调用函数准备的栈空间的基地址。
这点可以在汇编代码中看出来。
我们可以看出,当 k=6 的时候,MSVC 利用 lea rbp, QWORD PTR [rsp+48]
使得 rbp == rsp + 48
k = 7, 8 时 rbp == rsp + 64
根据 X64 下的传参规则,当 sizeof(struct)
不为 8, 16, 32, 64 bits 时,将指向参数本体的指针放在对应的位置,因此,一个参数(这里指这个指针,64 bits = 8 bytes)在栈上所占用的内存应该为 M = 8 * (#arg - 4)
。
所以, k = 6, M = 16; k = 7, M = 24; k = 8, M = 32; 再加上分配的 32 bytes 影子空间,那么应该是
k = 6, rbp == rsp + 48
k = 7, rbp == rsp + 56
k = 8, rbp == rsp + 64·
实际情况呢,k = 7 我们和编译器结果不一样,实际情况是 rbp == rsp + 64
,原因在于
为这些聚合类型作为指针的传递 (包括__m128),调用方分配的临时内存将是 16 字节对齐。
因此这些指针虽然单个大小是 8,所以在奇数个的时候为了对齐要额外增加 8 !
Calling Convention
根据这个简单的调用约定,我们可以画出64位下函数调用时到底是个怎么样的内存结构。(时机应该是 call
指令执行完毕)
这张图片认真理解,就解决了所有调用时对于内存空间如何分配的疑问。
See also:
本篇基于实验得出,除了某些叫法不同,和微软的官方文档是一致的。