测试环境
1 | clang version 3.8.1-24 (tags/RELEASE_381/final) |
奇异现象复现
代码
1
2
3
4
5
6
7
int main()
{
double a = 6.0;
printf("%lx\n" , a);
}执行结果
这段代码用的运行结果是随机的,无规律的,这是非常奇怪的
先说原因
- printf因为使用的格式化字符串是”%lx”所以从通用目的寄存器读取可变参数,但是
a
因为是double类型,所以放在xmm0寄存器。
分析
先看glibc-2.26中
stdio-common/printf.c
的源码1
2
3
4
5
6
7
8
9
10
11
12int
__printf (const char *format, ...)
{
va_list arg;
int done;
va_start (arg, format);
done = vfprintf (stdout, format, arg);
va_end (arg);
return done;
}可以看到,使用的是stdarg的机制实现可变参数传参。
如果可变参数完全使用栈帧传递,那么结果不可能是随机的。那么只可能是使用寄存器传参
复习一下CSAPP第三章
可以看到,浮点参数的传参使用的是SIMD寄存器,而整形使用的是通用目的寄存器
- 那么猜测,这应该是问题所在。printf因为使用的格式化字符串是”%lx”所以从通用目的寄存器读取可变参数,但是
a
因为是double类型,所以放在xmm0寄存器。
GDB调试
使用
clang -S d.c && clang d.s -g
命令编译上面那段问题代码。这样我们就可以在gdb里针对汇编指令设置断点main函数部分汇编代码
1
2
3
4
5
6
7
8subq $16, %rsp
movabsq $.L.str, %rdi # .L.str就是"%lx\n"
movsd .LCPI0_0, %xmm0
# 字面量的浮点放在内存,.LCPI0_0引用的就是 double 类型的 6.0
movsd %xmm0, -8(%rbp)
movsd -8(%rbp), %xmm0
movb $1, %al
callq printf可以看到,double a 确实放在了xmm0,
用GDB在
callq printf
处设置断点(注意,运行到断点处,callq printf指令还没有执行),检查用于传参的前四个通用目的寄存器(红框内是前四个传参的通用目的寄存器)
执行gdb 的
next
指令 ,运行callq printf
这条指令,检查输出可以看到,与
rsi
寄存器的内容一样。可以初步确认,因为格式字符串是”%lx”,所以printf在通用目的寄存器读取可变参数手动修改汇编代码,在callq printf之前加上一条
movq $16, %rsi
(注意,此处是十进制,而printf使用的格式字符串是”%lx”,所以程序输出的是十六进制)1
2
3
4
5
6
7movabsq $.L.str, %rdi
movsd .LCPI0_0, %xmm0 # xmm0 = mem[0],zero
movsd %xmm0, -8(%rbp)
movsd -8(%rbp), %xmm0 # xmm0 = mem[0],zero
movb $1, %al
movq $16, %rsi # 这一条就是加上去的
callq printf运行,结果是
符合预期,与rsi寄存器的东西一样
分析结果得到证实
探究过程出现的一些问题
- 在不合时宜的时刻检查寄存器的值
- 执行完
callq printf
后才检查xmm0、xmm1的内容,企图找到double a - 执行完
callq printf
后才检查rdi、rsi的值。
- 执行完
- 因为printf函数会使用这些寄存器,所以这样检查必然是不行的
关于vc++的一些补充
-
注意:这里的b不是在xmm0,而是在xmm1,d也是如此
Visual Studio 2015 的 Varargs文档
如果参数是通过 vararg(例如省略号参数)传递的,则基本上会应用正常的参数传递过程,包括溢出第五个及后续参数。 此外,被调用方的责任是转储采用其地址的参数。仅处理浮点值时,如果被调用方要使用整数寄存器中的值,整数寄存器和浮点寄存器才会同时包含浮点值
if parameters are passed via varargs (for example, ellipsis arguments), then essentially the normal parameter passing applies including spilling the fifth and subsequent arguments. It is again the callee’s responsibility to dump arguments that have their address taken. For floating-point values only, both the integer and the floating-point register will contain the float value in case the callee expects the value in the integer registers.
按照我的理解,加粗部分应该是说,如果实参里有integer也有float-point,那么我们在整形寄存器也可以读取到对应序号的浮点寄存器的值,比如test(3, 2.0, 1),那么2.0既存在于RDX,也存在于XMM1, 1既存在于R8,也存在于xmm2。这样,我们使用stdarg的va_arg(ap, long long)读取第二个参数2.0时,就不会出错。如果是gcc,就会出错,因为gcc并不会把浮点放在整形寄存器。
这应该是微软为了兼容以前的老代码,以前可变参都是放在栈上,所以改变va_arg的第二个实参type也不会读错,只会形成强制类型转换。(由于手头没有vc++的编译器,只能借助跟师兄的远程合作来探究,所以这里只有部分猜测被证实,读者可以自己测试一下是否在对应序号的整形寄存器和浮点寄存器存在相同的内容)