1. 程式人生 > >從反彙編理解堆疊及printf

從反彙編理解堆疊及printf

#include <stdio.h>
int main()
{
    long long a = 1, b = 2, c = 3;
    printf("%d %d %d\n", a,b,c);
    return 0;
}

//Tencent某年實習生筆試題目

結果是:

1 0 2

Process returned 0 (0x0)   execution time : 0.136 s
Press any key to continue.

該段轉自:

http://blog.csdn.net/yang_yulei/article/details/8086934

sprintf/fprintf/printf/sscanf/fscanf/scanf等這一類的函式,它們的呼叫規則(calling conventions)是cdecl,cdecl呼叫規則的函式,所有引數從右到左依次入棧,這些引數由呼叫者清除,稱為手動清棧。被呼叫函式不會要求呼叫者傳遞多少引數,呼叫者傳遞過多或者過少的引數,甚至完全不同的引數都不會產生編譯階段的錯誤。函式引數的傳遞都是放在棧裡面的,而且是從右邊的引數開始壓棧,printf()是不會對傳遞的引數進行型別檢查的,它只有一個format specification fields的字串,而引數是不定長的,所以也沒辦法對傳遞的引數做型別檢查,也沒辦法對引數的個數進行檢查。所以了,壓棧的時候,引數列表裡的所有引數都壓入棧中了,它不知道有多少個引數,所以它都壓棧。


所以對於問題1中的程式碼,壓入棧之後應該是這樣的:
棧裡面壓進去了三個數,a,b和c因為a佔2*4個bytes,b佔1*4個byte,所以現在棧裡面有3*4個bytes。c、b先後壓入棧,a最後壓入棧。因為這是little endian,即每個數字的高位元組在高地址,低位元組在低地址。而棧的記憶體生長方向是從大到小的,也就是棧底是高地址,棧頂是低地址,所以a的低位元組在低地址(低地址值為0x00000001,高地址值為0x00000000)。(有條件的同學可以在big endian的機器上驗證一下)
那麼輸出的時候,format specification fields字串其匹配棧裡面的內容,首先一個%d取出4個bytes出來輸出,然後後面又有一個%d再取出4個bytes出來列印。所以結果就是這樣了。也就是說剛開始壓入棧的b的值在輸出的時候根本都沒有用到,c更沒用到。

printf在壓棧時,對於長度小於32位的引數,自動擴充套件成32位(由CPU的位數決定的)。
故在根據格式串解釋時,對於%c %hd這樣的小於32位資料的格式串,系統也會自動提取32位資料解釋,而不會提取8位或16位來解釋。(因為你把人家壓入的時候就規定了擴充套件成32位嘛),但輸出結果仍是8位或者16位值
至於浮點引數壓棧的規則:float(4 位元組)型別擴充套件成double(8 位元組)入棧。所以在輸入時,需要區分float(%f)與double(%lf),而在輸出時,用%f即可。printf函式將按照double型的規則對壓入堆疊的float(已擴充套件成double)和double型資料進行輸出。


另附一段程式的反彙編作為理解;

0x00401334push   %ebp   //ebp入棧作為保護
0x00401335mov    %esp,%ebp//棧頂地址給ebp,ebp是讀取棧內容的暫存器
0x00401337and    $0xfffffff0,%esp
0x0040133Asub    $0x40,%esp
0x0040133Dcall   0x401970 <__main>
0x00401342movl   $0x1,0x38(%esp)//棧底是0x1,+4空間是0x0
0x0040134Amovl   $0x0,0x3c(%esp)
0x00401352movl   $0x2,0x30(%esp)
0x0040135Amovl   $0x0,0x34(%esp)
0x00401362movl   $0x3,0x28(%esp)
0x0040136Amovl   $0x0,0x2c(%esp)//上面到main是按定義順序入棧,這裡是小頭先入棧,後面會做調整
0x00401372mov    0x28(%esp),%eax//以下為調整
0x00401376mov    0x2c(%esp),%edx
0x0040137Amov    %eax,0x14(%esp)
0x0040137Emov    %edx,0x18(%esp)//按照大地址先入棧的順序入棧,先入c
0x00401382mov    0x30(%esp),%eax
0x00401386mov    0x34(%esp),%edx
0x0040138Amov    %eax,0xc(%esp)
0x0040138Emov    %edx,0x10(%esp)//b
0x00401392mov    0x38(%esp),%eax
0x00401396mov    0x3c(%esp),%edx
0x0040139Amov    %eax,0x4(%esp)
0x0040139Emov    %edx,0x8(%esp)//最後入棧a,調整後的順序是c b a,大地址在棧底,a最先定義可以先出棧
0x004013A2movl   $0x403024,(%esp)
0x004013A9call   0x401be0 <printf>//下面是printf,就按之前的說明執行
0x004013AEmov    $0x0,%eax
0x004013B3leave
0x004013B4ret