一个误用snprintf的bug分析
2017-05-13 03:15
281 查看
转载地址: http://blog.csdn.net/wuchun/article/details/38455609
int snprintf(char *str, size_t size, const char *fomat, ...)
正常来说,只要会用printf函数写输出语句,应该都能会用该函数。但是,稍有不留神,很可能踩到其中的坑。现将前段时间遇到的一个问题进行简单地分析。
[cpp] view
plain copy
#include <stdio.h>
#include <iostream>
class A
{
public:
int64_t b;
char* pstr;
int a;
};
int main()
{
char source[128] = "hello world";
char tstr[128] = "\0";
A a;
a.a=0x0001;
a.b=0x00030004;
a.pstr = source;
snprintf(tstr, sizeof(tstr), "b=%ld,str=%s,a=%d\n", a.b, a.pstr, a.a);
printf("%s",tstr);
//printf("%s\nsource address:%ld\n",tstr, source); //查找问题时增加
return 0;
}
如上所示,功能比较简单,就是使用snprintf函数将类A中的变量值格式化输出到tstr字符串中。
首先将其编译成64位程序:(注:本文中所用gcc版本为4.1.2)
g++ -g -m64 -o tsn tsn.cpp
运行tsn,结果为:
b=196612,str=hello world,a=1
一切正常。
但当用g++ -g -m32 -o tsn tsn.cpp将其编译为32位程序后,问题出现了,运行tsn后,结果为:
b=196612,str=(null),a=-2270288
从结果来看,应该是a.pstr和a.a引用的地址出了问题,于是在代码中增加了一行语句,打印a.pstr所指向的source地址,修改后,编译成32位,运行结果如下:
b=196612,str=(null),a=-2270288
source address:-2270288
从结果发现a的值变成了source的地址,这时突然意识到snprintf是一个可变参数的函数,其应该也是使用va_arg来读取堆栈中可变参数,而va_arg来读取参数时,是按照指定大小来访问,很可能是指定的访问大小出现了问题所导致(注:snprintf函数内部是否通过va_arg来实现,本人并未进行研究,只是在找问题时猜测其可能性),果然,发现在A中的变量b定义的是int64_t,而在格式化输出时用的却是%ld,正确地应当使用%lld。
首先,使用objdump -S tsn,生成64位程序汇编指令,去掉不相关内容后,如下所示
[cpp] view
plain copy
4007f3: c7 45 f0 01 00 00 00 movl $0x1,0xfffffffffffffff0(%rbp)
4007fa: 48 c7 45 e0 04 00 03 movq $0x30004,0xffffffffffffffe0(%rbp)
400801: 00
400802: 48 8d 85 60 ff ff ff lea 0xffffffffffffff60(%rbp),%rax
400809: 48 89 45 e8 mov %rax,0xffffffffffffffe8(%rbp)
40080d: 8b 45 f0 mov 0xfffffffffffffff0(%rbp),%eax
400810: 48 8b 55 e8 mov 0xffffffffffffffe8(%rbp),%rdx
400814: 48 8b 4d e0 mov 0xffffffffffffffe0(%rbp),%rcx
400818: 48 8d bd e0 fe ff ff lea 0xfffffffffffffee0(%rbp),%rdi
40081f: 41 89 c1 mov %eax,%r9d
400822: 49 89 d0 mov %rdx,%r8
400825: ba 58 09 40 00 mov $0x400958,%edx
40082a: be 80 00 00 00 mov $0x80,%esi
40082f: b8 00 00 00 00 mov $0x0,%eax
400834: e8 af fd ff ff callq 4005e8 <snprintf@plt>
生成32位汇编指令如下所示:
[cpp] view
plain copy
80486a8: c7 45 f4 01 00 00 00 movl $0x1,0xfffffff4(%ebp)
80486af: c7 45 e8 04 00 03 00 movl $0x30004,0xffffffe8(%ebp)
80486b6: c7 45 ec 00 00 00 00 movl $0x0,0xffffffec(%ebp)
80486bd: 8d 85 68 ff ff ff lea 0xffffff68(%ebp),%eax
80486c3: 89 45 f0 mov %eax,0xfffffff0(%ebp)
80486c6: 8b 4d f4 mov 0xfffffff4(%ebp),%ecx
80486c9: 8b 5d f0 mov 0xfffffff0(%ebp),%ebx
80486cc: 8b 45 e8 mov 0xffffffe8(%ebp),%eax
80486cf: 8b 55 ec mov 0xffffffec(%ebp),%edx
80486d2: 89 4c 24 18 mov %ecx,0x18(%esp)
80486d6: 89 5c 24 14 mov %ebx,0x14(%esp)
80486da: 89 44 24 0c mov %eax,0xc(%esp)
80486de: 89 54 24 10 mov %edx,0x10(%esp)
80486e2: c7 44 24 08 10 88 04 movl $0x8048810,0x8(%esp)
80486e9: 08
80486ea: c7 44 24 04 80 00 00 movl $0x80,0x4(%esp)
80486f1: 00
80486f2: 8d 85 e8 fe ff ff lea 0xfffffee8(%ebp),%eax
80486f8: 89 04 24 mov %eax,(%esp)
80486fb: e8 b4 fd ff ff call 80484b4 <snprintf@plt>
通过以上两段汇编代码发现,在调用snprintf前,64位下并没有将参数push到栈中,而只是将参数存入了寄存器中,由于各个寄存器是相互独立的,所以即使在格式化输出时,指定的参数大小不一致,也能“正常”输出(其实看到的“正常”,是因为b的值较小,仍能使用低32位表示),而在32位下,由于参数进行的是堆栈操作,所以当指定大小有误时,输出也就会存在问题:由于b在栈中分配了8个字节,而格式化时输出时,却认为其只有4个字节,从而导致接下来,在取pstr值时,实际上取到的是b值中全为0的高32位,而取a值时,取到的则是pstr的值。
想到这里时,想起以前看过一篇文章《X86-64寄存器和栈帧》,以及在《深入理解计算机系统》的部分章节中均有相关介绍。因此,也进一步佐证了前面的分析应该是正确的。
2、体系结构的不一样,对同一个函数的行为,会产生较大地区别。
前言
snprintf函数的功能是格式化输出到字符串中,函数原型为:int snprintf(char *str, size_t size, const char *fomat, ...)
正常来说,只要会用printf函数写输出语句,应该都能会用该函数。但是,稍有不留神,很可能踩到其中的坑。现将前段时间遇到的一个问题进行简单地分析。
问题
以下是抽取出来的问题代码段:[cpp] view
plain copy
#include <stdio.h>
#include <iostream>
class A
{
public:
int64_t b;
char* pstr;
int a;
};
int main()
{
char source[128] = "hello world";
char tstr[128] = "\0";
A a;
a.a=0x0001;
a.b=0x00030004;
a.pstr = source;
snprintf(tstr, sizeof(tstr), "b=%ld,str=%s,a=%d\n", a.b, a.pstr, a.a);
printf("%s",tstr);
//printf("%s\nsource address:%ld\n",tstr, source); //查找问题时增加
return 0;
}
如上所示,功能比较简单,就是使用snprintf函数将类A中的变量值格式化输出到tstr字符串中。
首先将其编译成64位程序:(注:本文中所用gcc版本为4.1.2)
g++ -g -m64 -o tsn tsn.cpp
运行tsn,结果为:
b=196612,str=hello world,a=1
一切正常。
但当用g++ -g -m32 -o tsn tsn.cpp将其编译为32位程序后,问题出现了,运行tsn后,结果为:
b=196612,str=(null),a=-2270288
从结果来看,应该是a.pstr和a.a引用的地址出了问题,于是在代码中增加了一行语句,打印a.pstr所指向的source地址,修改后,编译成32位,运行结果如下:
b=196612,str=(null),a=-2270288
source address:-2270288
从结果发现a的值变成了source的地址,这时突然意识到snprintf是一个可变参数的函数,其应该也是使用va_arg来读取堆栈中可变参数,而va_arg来读取参数时,是按照指定大小来访问,很可能是指定的访问大小出现了问题所导致(注:snprintf函数内部是否通过va_arg来实现,本人并未进行研究,只是在找问题时猜测其可能性),果然,发现在A中的变量b定义的是int64_t,而在格式化输出时用的却是%ld,正确地应当使用%lld。
分析
问题看似找到了,而且也解决了,但是为什么将其编译64位时不会出现问题呢?于是决定查看二者的汇编指令差别在哪里。首先,使用objdump -S tsn,生成64位程序汇编指令,去掉不相关内容后,如下所示
[cpp] view
plain copy
4007f3: c7 45 f0 01 00 00 00 movl $0x1,0xfffffffffffffff0(%rbp)
4007fa: 48 c7 45 e0 04 00 03 movq $0x30004,0xffffffffffffffe0(%rbp)
400801: 00
400802: 48 8d 85 60 ff ff ff lea 0xffffffffffffff60(%rbp),%rax
400809: 48 89 45 e8 mov %rax,0xffffffffffffffe8(%rbp)
40080d: 8b 45 f0 mov 0xfffffffffffffff0(%rbp),%eax
400810: 48 8b 55 e8 mov 0xffffffffffffffe8(%rbp),%rdx
400814: 48 8b 4d e0 mov 0xffffffffffffffe0(%rbp),%rcx
400818: 48 8d bd e0 fe ff ff lea 0xfffffffffffffee0(%rbp),%rdi
40081f: 41 89 c1 mov %eax,%r9d
400822: 49 89 d0 mov %rdx,%r8
400825: ba 58 09 40 00 mov $0x400958,%edx
40082a: be 80 00 00 00 mov $0x80,%esi
40082f: b8 00 00 00 00 mov $0x0,%eax
400834: e8 af fd ff ff callq 4005e8 <snprintf@plt>
生成32位汇编指令如下所示:
[cpp] view
plain copy
80486a8: c7 45 f4 01 00 00 00 movl $0x1,0xfffffff4(%ebp)
80486af: c7 45 e8 04 00 03 00 movl $0x30004,0xffffffe8(%ebp)
80486b6: c7 45 ec 00 00 00 00 movl $0x0,0xffffffec(%ebp)
80486bd: 8d 85 68 ff ff ff lea 0xffffff68(%ebp),%eax
80486c3: 89 45 f0 mov %eax,0xfffffff0(%ebp)
80486c6: 8b 4d f4 mov 0xfffffff4(%ebp),%ecx
80486c9: 8b 5d f0 mov 0xfffffff0(%ebp),%ebx
80486cc: 8b 45 e8 mov 0xffffffe8(%ebp),%eax
80486cf: 8b 55 ec mov 0xffffffec(%ebp),%edx
80486d2: 89 4c 24 18 mov %ecx,0x18(%esp)
80486d6: 89 5c 24 14 mov %ebx,0x14(%esp)
80486da: 89 44 24 0c mov %eax,0xc(%esp)
80486de: 89 54 24 10 mov %edx,0x10(%esp)
80486e2: c7 44 24 08 10 88 04 movl $0x8048810,0x8(%esp)
80486e9: 08
80486ea: c7 44 24 04 80 00 00 movl $0x80,0x4(%esp)
80486f1: 00
80486f2: 8d 85 e8 fe ff ff lea 0xfffffee8(%ebp),%eax
80486f8: 89 04 24 mov %eax,(%esp)
80486fb: e8 b4 fd ff ff call 80484b4 <snprintf@plt>
通过以上两段汇编代码发现,在调用snprintf前,64位下并没有将参数push到栈中,而只是将参数存入了寄存器中,由于各个寄存器是相互独立的,所以即使在格式化输出时,指定的参数大小不一致,也能“正常”输出(其实看到的“正常”,是因为b的值较小,仍能使用低32位表示),而在32位下,由于参数进行的是堆栈操作,所以当指定大小有误时,输出也就会存在问题:由于b在栈中分配了8个字节,而格式化时输出时,却认为其只有4个字节,从而导致接下来,在取pstr值时,实际上取到的是b值中全为0的高32位,而取a值时,取到的则是pstr的值。
想到这里时,想起以前看过一篇文章《X86-64寄存器和栈帧》,以及在《深入理解计算机系统》的部分章节中均有相关介绍。因此,也进一步佐证了前面的分析应该是正确的。
总结
1、本文中所提到的问题,在所有通过va_start函数实现的可变参数函数中,应该都会存在。因为在含有可变参数的函数缺少对类型安全性的检查。2、体系结构的不一样,对同一个函数的行为,会产生较大地区别。
相关文章推荐
- 一个误用snprintf的bug分析
- 由AFX_IDW_PANE_FIRST宏的含义分析界面库XTP的一个bug
- SQL2005 Anerlysis Service的处理维度中一个BUG的分析
- 一个典型kernel bug的追踪之(一):出错现场分析
- shell脚本中case条件控制语句的一个bug分析
- shell脚本中case条件控制语句的一个bug分析
- 打算写一个文章对 Sun JDK 的 bug 进行分析
- ShareSDK造成App崩溃的一个BUG原因分析以及Fix方法
- 也谈Elecard码流分析软件中的一个小bug
- 展讯android LEDS模块分析----一个bug
- C 一个经典bug分析
- SQL2005 Anerlysis Service的处理维度中一个BUG的分析
- 实战分析一个崩溃的bug
- 分析localtime返回值造成的一个bug
- 一个时序混乱导致的bug分析过程记录
- SQL2005 Anerlysis Service的处理维度中一个BUG的分析
- 分享一个IPhone bug的分析过程(兼xcode编译器应该改进的地方)
- 一个并发环境下的BUG分析
- 一个panic bug的分析过程(一)
- 一个bug案例分析