zoukankan      html  css  js  c++  java
  • 如果你不好好玩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:worktestprintftestprintftestprintftest.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之前,程序内存中当前线程栈的状态是怎样的?

    image

    我们可以得出结论:printf首先从栈顶取出格式化字符串并解析,根据其中%的个数(%%除外)从栈顶(除了格式化字符串)依次从上往下取参数用来显示。

    因为printf在并不知道传入的参数到底有多少个,也就没有办法判定传入的参数个数或者类型是否匹配格式化字符串,它只能从栈顶(除了格式化字符串)依次往下取,不管这个值是不是传入的参数。

    所以,如果参数个数或者类型不匹配格式化字符串的时候,运行结果就完全依赖于当前栈的状态。

    3. Test1

    回到题目开头的Test1的例子:

    0:000> u printftest!test1
    PrintfTest!Test1 [d:devtestprintftestprintftestprintftest.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的值,此时线程栈状态。

    index

    输出结果:

    index1

    4. Test2

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

    输出结果:

    index2

    5. Test3

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

    输出结果:

    index3

    6. 怎样让程序Crash

    上面三个例子程序都没有crash,难道说printf怎么玩都OK??当然不是,要玩死printf,只需要给一个非法地址给%s就行。

    void Test4()
    {
        // 对应%s的正好是变量a的值,内存地址0x1是个非法地址,程序会crash
        int a = 1;
        printf("hello %s");
    }

    index4

    PS

    有几个问题,有兴趣的同学可以一起讨论一下

    • 上面的代码都是在VC编译器上,release,优化关闭的情况下跑的,如果是debug模式呢,或者是release优化开启?跑出来结果会一样吗,为什么?
    • 在其他编译器上比如g++,Clang上跑,情况是怎样?
  • 相关阅读:
    forward和redirect的区别详解
    j2ee部分jar包的作用
    Struts2的工作原理(图解)详解
    struts2的s:iterator 标签 详解
    Struts2 控制标签:<s:if>、<s:elseif>和<s:else>
    Struts2 资源配置文件国际化详解
    ActionContext和ServletActionContext区别以及action访问servlet API的三种方法
    js获取class
    怎么解决eclipse报PermGen space异常的问题
    SQL模糊查找语句详解
  • 原文地址:https://www.cnblogs.com/quark/p/3949954.html
Copyright © 2011-2022 走看看