一道“简单”的二进制题目

这道题本来想当作面试题给出,但认真考虑了一下,还是算了,怕被找上门真人快打……找好的懂底层的开发真难啊。

题目

请指出下列代码是否存在问题。如有,请指明错误,并预测程序运行输出,并说明原因。

// gcc test.c -o test -g && ./test
#include <stdio.h>

int main(void){
  unsigned int i = 0x12345678;
  float j = 0x9abcdef0;
  printf("%d\t%f\t", j, i);
  printf("%d\t%f\n", i, j);

  printf("%p\t%x\t", j, i);
  printf("%p\t%x\n", i, j);
}

答案

这道题的来源是最近同事调试exp中遇到的小问题,主要涉及到了:

  • 调用约定
  • printf原理
  • 浮点数存储

首先说下正确的答案:

存在错误,在第三次和第四次时传入参数类型和格式化字符串不符。打印输出的第一行可能是: 305419896 2596069120.000000 305419896 2596069120.000000 第二行中第一个、第三个输出数字为0x12345678,第二个和第四个不可预测。


分析

一般来说比较容易从源代码发现的问题是:

  • 第三行、第四行的格式化字符串中,参数给定错误。
  • 声明的变量j由于浮点数自身问题,会丢失精度,变成0x9abcdf00,也就是2596069120.000000。这里涉及到的就是浮点数存储问题,不再赘述。

但观察输出,又有了几个新问题,分别是:

  • 第一次、第二次与第三次、第四次调用printf时,参数顺序有了变化,但为何没有体现在输出结果中
  • 第三次、第四次调用printf时,输出为何是不可预测的

printf的特殊之处在于,他是一个变参函数。变参函数的传参方式,在System V AMD64 ABI(Page 20)中有如下描述:

  • 整数类型按照rdi->rsi->rdx->rcx->r8->r9的顺序
  • 浮点数类型按照xmm0->xmm1->...->xmm6->xmm7的顺序,部分类型只占用半个寄存器的,可以将寄存器拆成两半使用
  • 其他参数和复杂类型(如直接传结构体)从栈走

而在printf->vfprintf内部的代码中,处理这些变参的方法如下:

    LABEL (form_integer):                                                      \
      /* Signed decimal integer.  */                                              \
      base = 10;                                                              \
                                                                              \
      if (is_longlong)                                                              \
        {                                                                      \
          long long int signed_number;                                              \
                                                                              \
          if (fspec == NULL)                                                      \
            signed_number = va_arg (ap, long long int);                              \
          else                                                                      \
            signed_number = args_value[fspec->data_arg].pa_long_long_int;     \

//...
    LABEL (form_float):                                                              \
      {                                                                              \
        /* Floating-point number.  This is handled by printf_fp.c.  */              \
  //...
            if (is_long_double)                                                      \
              the_arg.pa_long_double = va_arg (ap, long double);              \
            else                                                              \
              the_arg.pa_double = va_arg (ap, double);                              \
            ptr = (const void *) &the_arg;                                      \
                                                                              \

va_arg总是按照这个宏里指定的类型提取参数。


说到这,上面两个问题的答案都明确了:

  • 前两次调用printf时,%d%f分别从rdixmm0中取参数,使用的索引是两套体系,因此无论传入参数顺序如何变化,都不影响取值顺序。
  • 后两次调用prinf时,需要从rdirsi中取值,rdi为之前设定的值,但rsi没有被特定赋值过,因此无法确定这个值。

尝试更改编译参数加入-Wall,会发现出现了编译警告。此外,类似的问题在ARM64等其他平台下仍然存在,可以自行检索查询。