测试环境
clang version 3.8.1-24 (tags/RELEASE_381/final)
Target: x86_64-pc-linux-gnu
Thread model: posix
Linux version 4.9.0-deepin13-amd64 (yangbo@deepin.com) (gcc version 6.3.0 20170321 (Debian 6.3.0-11) ) #1 SMP PREEMPT Deepin 4.9.57-1 (2017-10-19)
奇异现象复现
代码
#include <stdio.h> int main() { double a = 6.0; printf("%lx\n" , a); }
执行结果
这段代码用的运行结果是随机的,无规律的,这是非常奇怪的
分析
先看glibc-2.26中
stdio-common/printf.c
的源码int __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函数部分汇编代码
subq $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
处设置断点,检查用于传参的前四个通用目的寄存器
(红框内是前四个传参的通用目的寄存器)
- 执行gdb 的
next
指令 ,运行callq printf
这条指令,检查输出
可以看到,与
rsi
寄存器的内容一样。可以初步确认,因为格式字符串是”%lx”,所以printf在通用目的寄存器读取可变参数手动修改汇编代码,在callq printf之前加上一条
movq $16, %rsi
(注意,此处是十进制,而printf使用的格式字符串是”%lx”,所以程序输出的是十六进制)movabsq $.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函数会使用这些寄存器,所以这样检查必然是不行的。