问题引子
stackoverflow 上有这个问题,标准里说makecontext
的可变参数都必须是int
类型,然后发现 makecontext
的源码里有如下一段注释说明。
/* Handle arguments. The standard says the parameters must all be int values. This is an historic accident and would be done differently today. For x86-64 all integer values are passed as 64-bit values and therefore extending the API to copy 64-bit values instead of 32-bit ints makes sense. It does not break existing functionality and it does not violate the standard which says that passing non-int values means undefined behavior. */
注释说明里,提到可变参数的整形都是当作 64bit
来处理的,就来深入探究一下x64
可变参数的实现原理,包括可变参数当中含有浮点数的情况。
实验代码
实验的代码如下
#include <stdio.h>
#include <stdarg.h>
int f(int x, float y, short a, double b, ...)
{
va_list ap;
va_start(ap, b);
char cc = va_arg(ap, int);
float dd = va_arg(ap, double);
int ee = va_arg(ap, int);
double ff = va_arg(ap, double);
int last = va_arg(ap, int);
printf("%d %f %d %lf %d\n", (int)cc, dd, ee, ff, last);
for(int i = 0; i < last; ++i)
{
int tmp = va_arg(ap, int);
printf("%d\n", tmp);
}
va_end(ap);
}
int main()
{
f(1, 2.2f, 3, 4.4, 5, 6.6f, 7, 8.8, 3, 10, 11, 12, 'x', 'y');
return 0;
}
va_list 结构
typedef struct {
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
} va_list[1];
g 表示通用寄存器,f 表示浮点数寄存器。
reg_save_area
指向栈上的一个特殊位置,这个位置位于栈中间。这里提供6 + 8
个位置(前 6 个位置用来保存通用寄存器传递的参数,每个 8 个字节;后8个位置用来保存xmm
寄存器传递的浮点数参数,每个16字节)。所有通过寄存器传递的可变参数最后都会复制到这里。比如第5,6
两个参数(可变参数)通过通用寄存器传递。那么将占用第5,6
两个位置,前4个位置将不会被使用。如果没有可变参数是通过寄存器传递(即可变参数之前已经有不少于 6 个确定的参数),那么这 6 个位置依然存在(占用空间),只是不被使用。
gp_offset
表示第一个前边提到的6个位置中保存的可变参数相对于overflow_arg_area
的偏移量。如果从第3
个数开始是可变参数,那么偏移就是2 * 8 == 16
,即下图当中的 0x10
.
overflow_arg_area
表示通过栈传递的参数的起始地址。确定的参数和可变参数一起,前边 6 个通过寄存器传递。后边的都是通过栈传递。最后栈传递的参数最先入栈,第 7 个参数最后入栈,拥有最低的地址。所以overflow_arg_area
指向第 7 个参数。
在 x64
下,浮点数通过 xmm
寄存器来传递。xmm
浮点数寄存器传递的可变参数最终也会复制到栈上。fp_offset
表示复制到栈上的可变浮点数参数的起始地址偏离reg_save_area
的大小,上边因为中间刚好隔着 6 个8字节的通用寄存器传递的参数。所以如果从第 3 个浮点数(不算非浮点数)开始,那么就是 8 * 6 + 2 * 16 = 80
,即下图当中的0x50
.
所以原理就是通过 reg_save_area + gp_offset
访问通过通用寄存器传递的可变参数,这些可变参数会复制到栈中间,通过reg_save_area + fp_offset
访问浮点数寄存器传递的可变参数,这些可变参数也会复制到栈中间。剩下的参数,是本函数的上级调用者传入调用者的栈,通过栈来传递的,通过overflow_arg_area
来访问。
栈上内存布局
汇编代码
对应的汇编代码和注释如下
gef➤ disassemble /m main
Dump of assembler code for function main:
26 {
=> 0x0000000000400854 <+0>: push %rbp
0x0000000000400855 <+1>: mov %rsp,%rbp
27 f(1, 2.2f, 3, 4.4, 5, 6.6f, 7, 8.8, 3, 10, 11, 12);
0x0000000000400858 <+4>: movsd 0x118(%rip),%xmm2 # 0x400978
0x0000000000400860 <+12>: movsd 0x118(%rip),%xmm1 # 0x400980
0x0000000000400868 <+20>: movsd 0x118(%rip),%xmm0 # 0x400988
0x0000000000400870 <+28>: pushq $0xc
0x0000000000400872 <+30>: pushq $0xb
0x0000000000400874 <+32>: mov $0xa,%r9d #通用寄存器传递第 6 个非浮点参数
0x000000000040087a <+38>: mov $0x3,%r8d #通用寄存器传递第 5 个非浮点参数
0x0000000000400880 <+44>: movapd %xmm2,%xmm3
0x0000000000400884 <+48>: mov $0x7,%ecx #通用寄存器传递第 4 个非浮点参数
0x0000000000400889 <+53>: movapd %xmm1,%xmm2
0x000000000040088d <+57>: mov $0x5,%edx #通用寄存器传递第 3 个非浮点参数
0x0000000000400892 <+62>: movapd %xmm0,%xmm1 #xmm 寄存器用来传递浮点数参数。在call 指令前,按顺序从 xmm0 开始,传递从左到右的浮点数参数
0x0000000000400896 <+66>: mov $0x3,%esi #通用寄存器传递第 2 个非浮点参数
0x000000000040089b <+71>: movss 0xed(%rip),%xmm0 # 0x400990
0x00000000004008a3 <+79>: mov $0x1,%edi #通用寄存器传递第 1 个非浮点参数
0x00000000004008a8 <+84>: mov $0x4,%eax #eax 传递浮点数参数的个数。在调用者当中更具 eax 当中的值来进行判断是否需要将浮点数寄存器复制到栈当中。
0x00000000004008ad <+89>: callq 0x400596 <f> #当然如果没有浮点数参数,直接 mov $0,%eax 这样就不需要进行复制
0x00000000004008b2 <+94>: add $0x10,%rsp
28 return 0;
0x00000000004008b6 <+98>: mov $0x0,%eax
29 }
0x00000000004008bb <+103>: leaveq
0x00000000004008bc <+104>: retq
gef➤ disassemble /m f
Dump of assembler code for function f:
4 {
0x0000000000400596 <+0>: push %rbp
0x0000000000400597 <+1>: mov %rsp,%rbp
0x000000000040059a <+4>: sub $0x110,%rsp
0x00000000004005a1 <+11>: mov %edi,-0xf4(%rbp) #通用寄存器传递的前 2 个参数是命名参数,直接复制到栈的顶部
0x00000000004005a7 <+17>: movss %xmm0,-0xf8(%rbp)
0x00000000004005af <+25>: movsd %xmm1,-0x108(%rbp) #xmm 传递的前两个浮点数参数也是命名参数,直接复制到栈的顶部
0x00000000004005b7 <+33>: mov %rdx,-0xa0(%rbp)
0x00000000004005be <+40>: mov %rcx,-0x98(%rbp)
0x00000000004005c5 <+47>: mov %r8,-0x90(%rbp)
0x00000000004005cc <+54>: mov %r9,-0x88(%rbp) #通用寄存器传递的后 4 个参数是可变参数,可变参数复制到 自身 栈中一块单独的保存通用寄存器传递的参数特定区域
0x00000000004005d3 <+61>: test %al,%al #调用者负责设置好 eax 的值,表明传递的所有的浮点数参数的个数
0x00000000004005d5 <+63>: je 0x4005ef <f+89>
0x00000000004005d7 <+65>: movaps %xmm2,-0x60(%rbp)
0x00000000004005db <+69>: movaps %xmm3,-0x50(%rbp) #xmm 传递的剩下的浮点数参数是可变参数,可变参数也复制到 自身 栈上一块单独保存浮点数寄存器的特定区域
0x00000000004005df <+73>: movaps %xmm4,-0x40(%rbp)
0x00000000004005e3 <+77>: movaps %xmm5,-0x30(%rbp)
0x00000000004005e7 <+81>: movaps %xmm6,-0x20(%rbp) #
0x00000000004005eb <+85>: movaps %xmm7,-0x10(%rbp)
0x00000000004005ef <+89>: mov %esi,%eax #edi 和 esi 传递的都是命名参数。所以直接复制到栈顶。
0x00000000004005f1 <+91>: mov %ax,-0xfc(%rbp) #通用寄存器传递的参数和浮点数传递的参数保存在栈上的位置相邻
5 va_list ap;
6 va_start(ap, b);
0x00000000004005f8 <+98>: movl $0x10,-0xe8(%rbp) #va_list.gp_offset 第一个通用寄存器传递的可变参数偏离保存的寄存器区域的偏移量
0x0000000000400602 <+108>: movl $0x50,-0xe4(%rbp) #va_list.fp_offset 第一个浮点数寄存器传递的可变参数偏离保存的寄存器区域的偏移量
0x000000000040060c <+118>: lea 0x10(%rbp),%rax #这里是 rbp + 说明是调用者的栈,0x10 刚好跳过保存的调用者的 rbp (当前 rbp 指向旧的 rbp) 和 返回地址
0x0000000000400610 <+122>: mov %rax,-0xe0(%rbp) #va_list.overflow_arg_area 指向
0x0000000000400617 <+129>: lea -0xb0(%rbp),%rax
0x000000000040061e <+136>: mov %rax,-0xd8(%rbp) #va_list.reg_save_area 指向上一个栈帧上保存的寄存器(保存通用寄存器和浮点数寄存器传递的参数)区
7
8 char cc = va_arg(ap, int);
0x0000000000400625 <+143>: mov -0xe8(%rbp),%eax #eax = va_list.gp_offset
0x000000000040062b <+149>: cmp $0x2f,%eax #因为通用寄存器只有 6 个位置 48 个字节,所以这里计算偏移量是否超过47,超过说明剩下的可变参数不在这个本栈帧内的这个特殊区域
0x000000000040062e <+152>: ja 0x400653 <f+189>
0x0000000000400630 <+154>: mov -0xd8(%rbp),%rax #va_list.reg_save_area
0x0000000000400637 <+161>: mov -0xe8(%rbp),%edx
0x000000000040063d <+167>: mov %edx,%edx
0x000000000040063f <+169>: add %rdx,%rax #eax = va_list.gp_offset + va_list.reg_save_area,刚好指向特定区域保存的寄存器传递的参数
0x0000000000400642 <+172>: mov -0xe8(%rbp),%edx
0x0000000000400648 <+178>: add $0x8,%edx
0x000000000040064b <+181>: mov %edx,-0xe8(%rbp) #va_list.gp_offset = va_list.gp_offset + 0x8,偏移量 + 8 指向下一个保存的寄存器参数
0x0000000000400651 <+187>: jmp 0x400665 <f+207>
0x0000000000400653 <+189>: mov -0xe0(%rbp),%rax
0x000000000040065a <+196>: lea 0x8(%rax),%rdx
0x000000000040065e <+200>: mov %rdx,-0xe0(%rbp)
0x0000000000400665 <+207>: mov (%rax),%eax #eax 指向保存的寄存器参数,取值
0x0000000000400667 <+209>: mov %al,-0xb5(%rbp) # 复制给栈上的局部变量 cc
9 float dd = va_arg(ap, double);
0x000000000040066d <+215>: mov -0xe4(%rbp),%eax #eax = va_list.fp_offset
0x0000000000400673 <+221>: cmp $0xaf,%eax #通用寄存器 6 * 8 浮点数寄存器 8 * 16 一共就是 48 + 128 = 128 个字节,这里大于0xaf(127) 说明剩下的浮点数参数都在上个栈上
0x0000000000400678 <+226>: ja 0x40069d <f+263>
0x000000000040067a <+228>: mov -0xd8(%rbp),%rax #va_list.reg_save_area
0x0000000000400681 <+235>: mov -0xe4(%rbp),%edx
0x0000000000400687 <+241>: mov %edx,%edx
0x0000000000400689 <+243>: add %rdx,%rax #rax = va_list.reg_save_area + va_list.fp_offset 刚好指向特殊区域当中的保存的浮点数寄存器
0x000000000040068c <+246>: mov -0xe4(%rbp),%edx
0x0000000000400692 <+252>: add $0x10,%edx
0x0000000000400695 <+255>: mov %edx,-0xe4(%rbp) #偏移 + 16,指向下一个 va_list.fp_offset = va_list.fp_offset + 16
0x000000000040069b <+261>: jmp 0x4006af <f+281>
0x000000000040069d <+263>: mov -0xe0(%rbp),%rax
0x00000000004006a4 <+270>: lea 0x8(%rax),%rdx
0x00000000004006a8 <+274>: mov %rdx,-0xe0(%rbp)
0x00000000004006af <+281>: movsd (%rax),%xmm0 #这 3 条指令,先用双精度复制到 xmm0 中转
0x00000000004006b3 <+285>: cvtsd2ss %xmm0,%xmm2 #再将双精度转换成单精度,复制给 xmm2 中转
0x00000000004006b7 <+289>: movss %xmm2,-0xbc(%rbp) #最后赋值给局部变量 dd
10 int ee = va_arg(ap, int);
0x00000000004006bf <+297>: mov -0xe8(%rbp),%eax
0x00000000004006c5 <+303>: cmp $0x2f,%eax
0x00000000004006c8 <+306>: ja 0x4006ed <f+343>
0x00000000004006ca <+308>: mov -0xd8(%rbp),%rax
0x00000000004006d1 <+315>: mov -0xe8(%rbp),%edx
0x00000000004006d7 <+321>: mov %edx,%edx
0x00000000004006d9 <+323>: add %rdx,%rax
0x00000000004006dc <+326>: mov -0xe8(%rbp),%edx
0x00000000004006e2 <+332>: add $0x8,%edx
0x00000000004006e5 <+335>: mov %edx,-0xe8(%rbp)
0x00000000004006eb <+341>: jmp 0x4006ff <f+361>
0x00000000004006ed <+343>: mov -0xe0(%rbp),%rax
0x00000000004006f4 <+350>: lea 0x8(%rax),%rdx
0x00000000004006f8 <+354>: mov %rdx,-0xe0(%rbp)
0x00000000004006ff <+361>: mov (%rax),%eax
0x0000000000400701 <+363>: mov %eax,-0xc0(%rbp)
11 double ff = va_arg(ap, double);
0x0000000000400707 <+369>: mov -0xe4(%rbp),%eax
0x000000000040070d <+375>: cmp $0xaf,%eax
0x0000000000400712 <+380>: ja 0x400737 <f+417>
0x0000000000400714 <+382>: mov -0xd8(%rbp),%rax
0x000000000040071b <+389>: mov -0xe4(%rbp),%edx
0x0000000000400721 <+395>: mov %edx,%edx
0x0000000000400723 <+397>: add %rdx,%rax
0x0000000000400726 <+400>: mov -0xe4(%rbp),%edx
0x000000000040072c <+406>: add $0x10,%edx
0x000000000040072f <+409>: mov %edx,-0xe4(%rbp)
0x0000000000400735 <+415>: jmp 0x400749 <f+435>
0x0000000000400737 <+417>: mov -0xe0(%rbp),%rax
0x000000000040073e <+424>: lea 0x8(%rax),%rdx
0x0000000000400742 <+428>: mov %rdx,-0xe0(%rbp)
0x0000000000400749 <+435>: movsd (%rax),%xmm0
0x000000000040074d <+439>: movsd %xmm0,-0xc8(%rbp)
12 int last = va_arg(ap, int);
0x0000000000400755 <+447>: mov -0xe8(%rbp),%eax
0x000000000040075b <+453>: cmp $0x2f,%eax
0x000000000040075e <+456>: ja 0x400783 <f+493>
0x0000000000400760 <+458>: mov -0xd8(%rbp),%rax
0x0000000000400767 <+465>: mov -0xe8(%rbp),%edx
0x000000000040076d <+471>: mov %edx,%edx
0x000000000040076f <+473>: add %rdx,%rax
0x0000000000400772 <+476>: mov -0xe8(%rbp),%edx
0x0000000000400778 <+482>: add $0x8,%edx
0x000000000040077b <+485>: mov %edx,-0xe8(%rbp)
0x0000000000400781 <+491>: jmp 0x400795 <f+511>
0x0000000000400783 <+493>: mov -0xe0(%rbp),%rax
0x000000000040078a <+500>: lea 0x8(%rax),%rdx
0x000000000040078e <+504>: mov %rdx,-0xe0(%rbp)
0x0000000000400795 <+511>: mov (%rax),%eax
0x0000000000400797 <+513>: mov %eax,-0xcc(%rbp)
13
14 printf("%d %f %d %lf %d\n", (int)cc, dd, ee, ff, last);
0x000000000040079d <+519>: cvtss2sd -0xbc(%rbp),%xmm0
0x00000000004007a5 <+527>: movsbl -0xb5(%rbp),%eax
0x00000000004007ac <+534>: mov -0xcc(%rbp),%ecx
0x00000000004007b2 <+540>: movsd -0xc8(%rbp),%xmm1
0x00000000004007ba <+548>: mov -0xc0(%rbp),%edx
0x00000000004007c0 <+554>: mov %eax,%esi
0x00000000004007c2 <+556>: mov $0x400958,%edi
0x00000000004007c7 <+561>: mov $0x2,%eax
0x00000000004007cc <+566>: callq 0x4004a0 <printf@plt>
15
16 for(int i = 0; i < last; ++i)
0x00000000004007d1 <+571>: movl $0x0,-0xb4(%rbp)
0x00000000004007db <+581>: jmp 0x400843 <f+685>
0x000000000040083c <+678>: addl $0x1,-0xb4(%rbp)
0x0000000000400843 <+685>: mov -0xb4(%rbp),%eax
0x0000000000400849 <+691>: cmp -0xcc(%rbp),%eax
0x000000000040084f <+697>: jl 0x4007dd <f+583>
17 {
18 int tmp = va_arg(ap, int);
0x00000000004007dd <+583>: mov -0xe8(%rbp),%eax #eax = va_list.gp_offset
0x00000000004007e3 <+589>: cmp $0x2f,%eax #栈上保存的是否耗尽
0x00000000004007e6 <+592>: ja 0x40080b <f+629>
0x00000000004007e8 <+594>: mov -0xd8(%rbp),%rax
0x00000000004007ef <+601>: mov -0xe8(%rbp),%edx
0x00000000004007f5 <+607>: mov %edx,%edx
0x00000000004007f7 <+609>: add %rdx,%rax
0x00000000004007fa <+612>: mov -0xe8(%rbp),%edx
0x0000000000400800 <+618>: add $0x8,%edx
0x0000000000400803 <+621>: mov %edx,-0xe8(%rbp)
0x0000000000400809 <+627>: jmp 0x40081d <f+647>
0x000000000040080b <+629>: mov -0xe0(%rbp),%rax #如果耗尽跳转到这里 eax = va_list.overflow_arg_area 指向上个栈帧
0x0000000000400812 <+636>: lea 0x8(%rax),%rdx #
0x0000000000400816 <+640>: mov %rdx,-0xe0(%rbp) #va_list.overflow_arg_area = va_list.overflow_arg_area + 8
0x000000000040081d <+647>: mov (%rax),%eax #取值
0x000000000040081f <+649>: mov %eax,-0xd0(%rbp)
19 printf("%d\n", tmp);
0x0000000000400825 <+655>: mov -0xd0(%rbp),%eax
0x000000000040082b <+661>: mov %eax,%esi
0x000000000040082d <+663>: mov $0x40096d,%edi
0x0000000000400832 <+668>: mov $0x0,%eax
0x0000000000400837 <+673>: callq 0x4004a0 <printf@plt>
20 }
21
22 va_end(ap);
23 }
0x0000000000400851 <+699>: nop
0x0000000000400852 <+700>: leaveq
0x0000000000400853 <+701>: retq
x64
可变参数,通用类型的都是按照 8 字节来保存的,所以不存在需要都是整数的说法。
总结
1、通过va_arg
来获取参数值,广义的整形(char, short...)指定的类型长度不能小于 int
,浮点型不能是 float
,否则有警告,会进行类型提升,而且查生成的汇编代码也有问题。
warning: ‘char’ is promoted to ‘int’ when passed through ‘...’
warning: ‘float’ is promoted to ‘double’ when passed through ‘...’
2、只有通过寄存器传递的可变参数,才会赋值到栈上这块特殊的区域,毕竟具名参数都复制到了栈顶。多余的参数(6个之外的整形参数,8个之外的浮点参数)都是在 call
指令之前入栈,也就是本函数作为被调用者,在调用者的栈上,而且这些直接通过栈传递的整形都是占用 8 个字节,浮点型,无论是单精度还是双精度也是占用 8 个字节。
如果是下边这种方式调用,因为前边已经有了 8 个浮点数参数,14.14f
将通过栈传递。
f(1, 2.2f, 3, 4.4, 5, 6.6f, 7, 8.8, 3, 10, 11, 12, 'x', 'y', 9.9, 10.1, 11.11, 12.12, 13.3f, 14.4f);
对应的部分反汇编如下
0x00000000004008e4 <+0>: push %rbp
0x00000000004008e5 <+1>: mov %rsp,%rbp
0x00000000004008e8 <+4>: movsd 0x158(%rip),%xmm7 # 0x400a48
0x00000000004008f0 <+12>: movsd 0x158(%rip),%xmm6 # 0x400a50
0x00000000004008f8 <+20>: movsd 0x158(%rip),%xmm5 # 0x400a58
0x0000000000400900 <+28>: movsd 0x158(%rip),%xmm4 # 0x400a60
0x0000000000400908 <+36>: movsd 0x158(%rip),%xmm3 # 0x400a68
0x0000000000400910 <+44>: movsd 0x158(%rip),%xmm2 # 0x400a70
0x0000000000400918 <+52>: movsd 0x158(%rip),%xmm1 # 0x400a78
0x0000000000400920 <+60>: movsd 0x158(%rip),%xmm0 # 0x400a80
0x0000000000400928 <+68>: lea -0x8(%rsp),%rsp
0x000000000040092d <+73>: movsd %xmm0,(%rsp) #13.3f 放到栈上
0x0000000000400932 <+78>: movsd 0x14e(%rip),%xmm0 # 0x400a88
0x000000000040093a <+86>: lea -0x8(%rsp),%rsp
0x000000000040093f <+91>: movsd %xmm0,(%rsp) # 0x400a88 #14.4f 放到栈上
0x0000000000400944 <+96>: pushq $0x79 #虽然是 char 直接占用 8 个字节
0x0000000000400946 <+98>: pushq $0x78
0x0000000000400948 <+100>: pushq $0xc
0x000000000040094a <+102>: pushq $0xb #直接占用 8 个字节
0x000000000040094c <+104>: mov $0xa,%r9d
0x0000000000400952 <+110>: mov $0x3,%r8d
0x0000000000400958 <+116>: mov $0x7,%ecx
0x000000000040095d <+121>: mov $0x5,%edx
0x0000000000400962 <+126>: mov $0x3,%esi
0x0000000000400967 <+131>: movss 0x121(%rip),%xmm0 # 0x400a90 #第一个浮点数参数 2.2f 传给寄存器
0x000000000040096f <+139>: mov $0x1,%edi
0x0000000000400974 <+144>: mov $0x8,%eax
0x0000000000400979 <+149>: callq 0x400596 <f>
0x000000000040097e <+154>: add $0x30,%rsp
0x0000000000400982 <+158>: mov $0x0,%eax
0x0000000000400987 <+163>: leaveq
0x0000000000400988 <+164>: retq
之前通过 va_arg
获取参数时,如果在调用者的栈上,无论是整形还是浮点型,va_list.overflow_arg_area
都是增加 8 个字节也说明了这一点。
lea 0x8(%rax),%rdx mov %rdx,-0xe0(%rbp) #va_list.overflow_arg_area = va_list.overflow_arg_area + 8
x64
下,通过栈传递的整形和浮点型参数都是占用 8 个字节,毕竟使用的是 pushq
指令。但是通过寄存器传递的具名参数,复制到栈上,因为是先开辟栈空间,然后再 mov
复制进栈,都是能合并的合并来节省内存。
通过寄存器传递的可变参数最终复制到栈上的特定区域,整形都是 8 个字节,浮点型因为是 xmm
寄存器,都是 16 个字节。