More than code

More Than Code
The efficiency of your iteration of reading, practicing and thinking decides your understanding of the world.
  1. 首页
  2. C++
  3. 正文

Daily C/C++ printf中的一个小问题

2021年10月4日 532点热度 0人点赞 0条评论

printf中的一个小问题

今天的问题由这样一段小的代码引入

int main() {
    short x = -1;
    printf("%u", x);
    return 0;
}

直觉上来说,这段代码的输出应该是65535,因为65535是双字节无符号数所能表达的最大的值。

正常来说,我们输出负数使用的是%d,这里虽然我们使用了%u,但是他的输出结果并不是我们想象的65535,而是4294967295。这是四字节的数所能表示的最大值。

但是为什么是这样的呢?按照我曾经的理解,printf不过是用一个指针逐步解析我们的参数。所以即便这里的格式符是u,我们的x也是不变的,他在内存中的表示应该是ffff ffff ???? ????,后面的问号表示的不确定。

为了验证这个说法,我们首先看一下他的内存布局

20211004201902

20211004201842

是没错的

所以我们输出的时候应该会用u来解析这段内存,然后得到一个奇怪的数才对。但是这里我们总是得到了那个INT_MAX。所以肯定是哪里出现了类型转换

在探究问题的过程中,我参考了标准库中的printf的源码,以及printf依赖的vfprintf的源码。但是都没有找到答案

而且vfprintf的源码较为复杂,所以我使用了一个比较简单的方法来进行debug

从这里开始我将按照我曾经的步骤一步一步讲解解决问题的逻辑

首先我们用gdb来跟到vfprintf中来确认。因为在阅读vfprintf的代码的过程中,我发现他传入的参数是指针类型,所以应该不会发生显式的类型转换,但是为了验证我的猜测,我们还是实践看一看

这里我们单步来到vfprintf中

20211004194827

可以看到他有四个参数传入进来,分析printf中调用的方式我们可以猜测出,第一个参数是文件描述符,这里是stdout,第二个参数就是格式串,第三个是va_list,第四个是模式标志。这里我们只关心第三个参数,也就是变参结构体,里面储存了我们想要的信息。

输出出来看一下

20211004195312

看起来很奇怪,但是我们可以去搜索一下相关的资料

很幸运,我找到了一个讲解这个结构的文章,文章里面提到了reg_save_area加上gp_offset就是我们整形参数储存的位置

我们输出一下试试

20211004195518

可以看到,是四字节的-1。这里是vfprintf的入口处,所以转化是发生在进入vfprintf之前。

我们重新来,这次我们已经知道了最终变量存放的地址,所以我们这次单步执行,并且每次都去检查一下这个地址,最终我们可以锁定到这里

20211004195842

通过看源码我们可以知道,变换的位置是在va_begin中,也就是变参的预处理中

然后我们看这段反汇编的指令,从0到34这段是不是感觉很熟悉,这段代码在依次的把寄存器中的变量传入到栈中。联想到printf变参的原理,我们就可以了解到是这里把我们传入printf的参数一步一步压入到栈中,然后我们就可以通过指针的移动来解析这里的变量

然后再继续想,既然这一段是用来将变量压入栈中的话,根据以前CSAPP中模糊的记忆,rdi是作为第一个参数,这里是传给了r10。那么我们可以猜到,这一定就是格式串的地址了,我们尝试输出一下

20211004200455

大家可以回去看一下上面vfprintf中的格式串的地址,就是这个

那么rsi就是我们传入的x了,为了进一步的验证,我们输出一下rsp+28的值看看

20211004200642

果然就是我们刚才变量的地址

然后怀着激动的心情输出一下rsi看一下

20211004200724

是四个字节的-1,说明转换在这里之前发生。

那想必一定在main中了。我们去反汇编一下看看

20211004200825

可以看到,这里我们首先是把0xffff给了栈中的一个空间,movw也可以看出这里一定就是x的赋值的过程了。

然后他使用movswl进行带符号的拓展,将双字节拓展到四字节。然后将结果放到了eax中。之后又传入到esi中

那么到这里我们就可以明确我们的答案了,当将x作为参数传递时,进行了符号拓展,导致解析出了更大的无符号整数

那么想一下,为什么他要进行无符号拓展呢,我们的参数为什么一定要按照四字节的传入呢?而且在使用的过程中也可以发现,我们是在使用rax等寄存器。肯定是因为涉及到对齐效率相关的因素。而且寄存器一次传多少位都是那点时间,但是处理对齐的额外计算就不一定了。

那么同样的道理,假如我们使用%d来输出一个无符号的short时,会发生错误吗?

答案是不会的,因为无符号数使用的是无符号拓展,即便是标识符错了,也不会影响到我们最终解析的结果。

标签: c++
最后更新:2021年10月4日

sheep

think again

点赞
< 上一篇
下一篇 >

文章评论

取消回复

COPYRIGHT © 2021 heavensheep.xyz. ALL RIGHTS RESERVED.

THEME KRATOS MADE BY VTROIS