若是你很差好玩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嗎?spa

爲了回答這個問題,咱們首先須要搞清楚printf這個函數自己是怎麼玩的?線程

(注:如下代碼都是由VC編譯器編譯並運行,結論只限於該編譯器,編譯選項是:Release模式下,關掉代碼優化)debug

1. __cdecl

所謂__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

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以前,程序內存中當前線程棧的狀態是怎樣的?

image

 

咱們能夠得出結論: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的值,此時線程棧狀態。

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上跑,狀況是怎樣?
相關文章
相關標籤/搜索