一道“简单”的二进制题目
这道题本来想当作面试题给出,但认真考虑了一下,还是算了,怕被找上门真人快打……找好的懂底层的开发真难啊。
题目
请指出下列代码是否存在问题。如有,请指明错误,并预测程序运行输出,并说明原因。
// 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
分别从rdi
和xmm0
中取参数,使用的索引是两套体系,因此无论传入参数顺序如何变化,都不影响取值顺序。 - 后两次调用
prinf
时,需要从rdi
和rsi
中取值,rdi
为之前设定的值,但rsi
没有被特定赋值过,因此无法确定这个值。
尝试更改编译参数加入-Wall
,会发现出现了编译警告。此外,类似的问题在ARM64等其他平台下仍然存在,可以自行检索查询。