printf的一个异常现象引发的对x86-64体系下可变参数传参的探究

测试环境

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函数会使用这些寄存器,所以这样检查必然是不行的。

猜你喜欢

转载自blog.csdn.net/h_zx_h_zx/article/details/79105534