昨天在跟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吗?spa
为了回答这个问题,咱们首先须要搞清楚printf这个函数自己是怎么玩的?线程
(注:如下代码都是由VC编译器编译并运行,结论只限于该编译器,编译选项是:Release模式下,关掉代码优化)debug
所谓__cdecl,C语言默认的调用协定,就是说是由调用者来回收栈空间,参数是从右到左压入栈。(很清楚调用协定相关的童鞋,能够直接pass)3d
举个列子来讲明:指针
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函数对应的汇编代码:code
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;orm
以及main函数负责回收栈空间——add esp, 0Ch,3个int大小正好是12,在调用完my_cdecl函数后,将栈顶指针esp加12,保持了栈平衡。blog
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在并不知道传入的参数到底有多少个,也就没有办法断定传入的参数个数或者类型是否匹配格式化字符串,它只能从栈顶(除了格式化字符串)依次往下取,无论这个值是否是传入的参数。
因此,若是参数个数或者类型不匹配格式化字符串的时候,运行结果就彻底依赖于当前栈的状态。
回到题目开头的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的值,此时线程栈状态。
输出结果:
void Test2() { // 相似Test1,由于栈顶对应%s的值是指向的是栈上的一个合法地址,因此会打出乱码,但程序不会crash printf("hello %s"); }
输出结果:
void Test3() { // 对应%s的正好是变量a的值,即至关于传了一个空指针给%s, printf对空指针有处理,打印结果为"hello <null>" int a = 0; printf("hello %s"); }
输出结果:
上面三个例子程序都没有crash,难道说printf怎么玩都OK??固然不是,要玩死printf,只须要给一个非法地址给%s就行。
void Test4() { // 对应%s的正好是变量a的值,内存地址0x1是个非法地址,程序会crash int a = 1; printf("hello %s"); }
有几个问题,有兴趣的同窗能够一块儿讨论一下