如果你不好好玩printf
昨天在跟Fiona讨论printf导致程序Crash的问题,就花了点时间看看究竟什么情况下会这样,有兴趣的童鞋可以看看:)
只要是玩过C或者C++的童鞋们,对printf肯定是再熟悉不过了。下面有几个方法,你知道每个方法输出是什么吗?
void Test1()
{
printf("hello %d");
} void Test2()
{
printf("hello %s");
} void Test3()
{
int a = 0;
printf("hello %s");
}
可以肯定的是,上面三个方法都是错误的写法,但我们在跑这三个方法的时候,程序一定会crash吗?
为了回答这个问题,我们首先需要搞清楚printf这个函数本身是怎么玩的?
(注:以下代码都是由VC编译器编译并运行,结论只限于该编译器,编译选项是:Release模式下,关掉代码优化)
1. __cdecl
所谓__cdecl,C语言默认的调用协定,就是说是由调用者来回收栈空间,参数是从右到左压入栈。(很清楚调用协定相关的童鞋,可以直接pass)
举个列子来说明:
int __cdecl my_cdecl(int a, int b, int c)
{
return a + b + c;
} int _tmain(int argc, _TCHAR* argv[])
{
my_cdecl(123, 456, 789);
return 0;
}
main函数对应的汇编代码:
PrintfTest!wmain:
010f1010 55 push ebp ;ebp入栈
010f1011 8bec mov ebp,esp ;更新ebp
010f1013 6815030000 push 315h ;789入栈
010f1018 68c8010000 push 1C8h ;456入栈
010f101d 6a7b push 7Bh ;123入栈
010f101f e8dcffffff call PrintfTest!my_cdecl (010f1000) ;调用my_cdecl函数
010f1024 83c40c add esp,0Ch ;回收栈空间,3*4 = 0Ch
010f1027 33c0 xor eax,eax
010f1029 5d pop ebp
010f102a c3 ret
上面的汇编代码验证了参数从右到左压栈——先压789,然后是456,最后是123;
以及main函数负责回收栈空间——add esp, 0Ch,3个int大小正好是12,在调用完my_cdecl函数后,将栈顶指针esp加12,保持了栈平衡。
2. printf
int __cdecl printf (
const char *format,
...
);
以上是printf函数的声明,printf含有一个可变参数,即参数的个数是可变的。其实,正是因为__cdecl的调用者来回收栈空间的特性,才能实现可变参数的调用。因为只有调用者才知道传了多少个参数进去,才能正确回收栈空间。
_stdcall这种由被调用者来回收栈空间的就玩不了可变参数了。
一个正确的printf的例子
void Test()
{
int a = 2014;
char* sz = "hello QQ";
printf("%s %d", sz, a);
}
很容易就知道输出:hello QQ 2014
我们看一下printf怎么玩的:
0:000> u PrintfTest!Test L10
PrintfTest!Test [d:\work\test\printftest\printftest\printftest.cpp @ 7]:
013b1000 55 push ebp
013b1001 8bec mov ebp,esp
;--------------------------------------------------------
;这段代码是给局部变量a,sz赋值
013b1003 83ec08 sub esp,8
013b1006 c745fcde070000 mov dword ptr [ebp-4],7DEh
013b100d c745f8f4203b01 mov dword ptr [ebp-8],offset PrintfTest!GS_ExceptionPointers+0x8 (013b20f4)
013b1014 8b45fc mov eax,dword ptr [ebp-4]
013b1017 50 push eax
013b1018 8b4df8 mov ecx,dword ptr [ebp-8]
013b101b 51 push ecx
;---------------------------------------------------------
013b101c 6800213b01 push offset PrintfTest!GS_ExceptionPointers+0x14 (013b2100) ;"%s %d"入栈
013b1021 ff15a0203b01 call dword ptr [PrintfTest!_imp__printf (013b20a0)] ;调用printf
013b1027 83c40c add esp,0Ch ;回收栈空间,三个参数,12个字节
013b102a 8be5 mov esp,ebp
013b102c 5d pop ebp
013b102d c3 ret
013b102e cc int 3
当代码在调用printf之前,程序内存中当前线程栈的状态是怎样的?

我们可以得出结论:printf首先从栈顶取出格式化字符串并解析,根据其中%的个数(%%除外)从栈顶(除了格式化字符串)依次从上往下取参数用来显示。
因为printf在并不知道传入的参数到底有多少个,也就没有办法判定传入的参数个数或者类型是否匹配格式化字符串,它只能从栈顶(除了格式化字符串)依次往下取,不管这个值是不是传入的参数。
所以,如果参数个数或者类型不匹配格式化字符串的时候,运行结果就完全依赖于当前栈的状态。
3. Test1
回到题目开头的Test1的例子:
0:000> u printftest!test1
PrintfTest!Test1 [d:\dev\test\printftest\printftest\printftest.cpp @ 8]:
001a1000 55 push ebp ;ebp入栈
001a1001 8bec mov ebp,esp
001a1003 6800211a00 push offset PrintfTest!GS_ExceptionPointers+0x8 (001a2100) ;格式化字符串"hello %d"入栈
001a1008 ff1590201a00 call dword ptr [PrintfTest!_imp__printf (001a2090)] ;调用printf
001a100e 83c404 add esp,4
001a1011 5d pop ebp
001a1012 c3 ret
001a1013 cc int 3
因为printf只传入了格式化字符串一个参数,在这之前压栈的是ebp,所以此时%d对应的参数就是压入的ebp的值,此时线程栈状态。

输出结果:

4. Test2
void Test2()
{
// 类似Test1,因为栈顶对应%s的值是指向的是栈上的一个合法地址,所以会打出乱码,但程序不会crash
printf("hello %s");
}
输出结果:

5. Test3
void Test3()
{
// 对应%s的正好是变量a的值,即相当于传了一个空指针给%s, printf对空指针有处理,打印结果为"hello <null>"
int a = 0;
printf("hello %s");
}
输出结果:

6. 怎样让程序Crash
上面三个例子程序都没有crash,难道说printf怎么玩都OK??当然不是,要玩死printf,只需要给一个非法地址给%s就行。
void Test4()
{
// 对应%s的正好是变量a的值,内存地址0x1是个非法地址,程序会crash
int a = 1;
printf("hello %s");
}

PS
有几个问题,有兴趣的同学可以一起讨论一下
- 上面的代码都是在VC编译器上,release,优化关闭的情况下跑的,如果是debug模式呢,或者是release优化开启?跑出来结果会一样吗,为什么?
- 在其他编译器上比如g++,Clang上跑,情况是怎样?
如果你不好好玩printf的更多相关文章
- 编程实践中C语言的一些常见细节
对于C语言,不同的编译器采用了不同的实现,并且在不同平台上表现也不同.脱离具体环境探讨C的细节行为是没有意义的,以下是我所使用的环境,大部分内容都经过测试,且所有测试结果基于这个环境获得,为简化起见, ...
- C语言的一些常见细节
C语言的一些常见细节 对于C语言,不同的编译器采用了不同的实现,并且在不同平台上表现也不同.脱离具体环境探讨C的细节行为是没有意义的,以下是我所使用的环境,大部分内容都经过测试,且所有测试结果基于这个 ...
- 03--(二)编程实践中C语言的一些常见细节
编程实践中C语言的一些常见细节(转载) 对于C语言,不同的编译器采用了不同的实现,并且在不同平台上表现也不同.脱离具体环境探讨C的细节行为是没有意义的,以下是我所使用的环境,大部分内容都经过测试,且所 ...
- 【转】PowerShell入门(四):如何高效地使用交互式运行环境?
转至:http://www.cnblogs.com/ceachy/archive/2013/02/05/PowerShell_Interacting_Environment.html 在开始关于脚本. ...
- 【零基础学习iOS开发】【02-C语言】10-函数
前面已经讲完了C语言中的基本语句和基本运算了,这讲呢,介绍C语言中的重头戏---函数.其实函数这个概念,在大部分高级语言中都是非常重要的,我也已经在<第一个C语言程序>一讲中对函数作了一个 ...
- 微软Code Hunt答案(00-05)——沉迷娱乐的我
昨天看到微软出的网游Code Hunt.o(∩_∩)o...哈哈,还不好好玩一吧,个人感觉不是一个模块比一个模块难的,Code Hunt是按功能划分.所以不要怕自己做不来.由于不同人特长不一样. 像A ...
- 吴恩达课后作业学习2-week2-优化算法
参考:https://blog.csdn.net/u013733326/article/details/79907419 希望大家直接到上面的网址去查看代码,下面是本人的笔记 我们需要做以下几件事: ...
- c++ 异常处理(1)
异常 (exception) 是 c++ 中新增的一个特性,它提供了一种新的方式来结构化地处理错误,使得程序可以很方便地把异常处理与出错的程序分离,而且在使用上,它语法相当地简洁,以至于会让人错觉觉得 ...
- 【ZH奶酪】如何用sklearn计算中文文本TF-IDF?
1. 什么是TF-IDF tf-idf(英语:term frequency–inverse document frequency)是一种用于信息检索与文本挖掘的常用加权技术.tf-idf是一种统计方法 ...
随机推荐
- Android热门网络框架Volley详解
.Volley简介 volley的英文意思为‘群发’.‘迸发’.Volley是2013年谷歌官方发布的一款Android平台上的网络通信库.Volley非常适合一些数据量不大,但需要频繁通信的网络操作 ...
- 可以直接拿来用的15个jQuery代码片段
jQuery里提供了许多创建交互式网站的方法,在开发Web项目时,开发人员应该好好利用jQuery代码,它们不仅能给网站带来各种动画.特效,还会提高网站的用户体验. 本文收集了15段非常实用的jQue ...
- poj1828
poj1828 [问题的描述]是这样的:程序猿的近亲 猴子(......)最近在进行王位争夺站. 题中使用二维坐标轴上的点(x,y)来代表猴子所占有的位置, 每只猴子占有一个坐标点.并且一个坐标点上面 ...
- Oracle分区表学习
(1) 表空间及分区表的概念表空间: 是一个或多个数据文件的集合,所有的数据对象都存放在指定的表空间中,但主要存放的是表, 所以称作表空间.分区表: 当表中的数据量不断增大,查询数据的速度就会变慢,应 ...
- CSRF 攻击的应对之道
转载自imb文库 CSRF(Cross Site Request Forgery, 跨站域请求伪造)是一种网络的攻击方式,该攻击可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点,从而在 ...
- 第一次用Github desktop(mac)提交代码遇到的问题
1.新建代码仓库 2.生成密钥 ssh-keygen -C 'your@email.address' -t rsa 3.到根目录下的.ssh文件夹下找到id_rsa.pub文件,将里面的内容复制到下图 ...
- Android JIN返回结构体
一.对应类型符号 Java 类型 符号 boolean Z byte B char C short S int I long J float ...
- 【BZOJ2741】【块状链表+可持久化trie】FOTILE模拟赛L
Description FOTILE得到了一个长为N的序列A,为了拯救地球,他希望知道某些区间内的最大的连续XOR和. 即对于一个询问,你需要求出max(Ai xor Ai+1 xor Ai+2 .. ...
- 泛型? extents super
?可以接受任何泛型集合,但是不能编辑集合值.所以一般只在方法参数中用 例子: ? extends Number 则类型只能是Number类的子孙类 ? super String 则类型只能是Str ...
- confluence的权限管理
上一篇解讲如何破解,安装confluence5.8.10,这次主要是看权限管理的实现.因为公司对知识的管理不仅是简单的分享,还要求不同权限的人看到不同的内容,所以在一开始就需要对权限这一块有所了解,以 ...