局部變量

  在接下來的幾篇文章中, 我將利用"火眼金睛"來分析一個個的C程序, 為大家揭開 C 語言的奧祕。

怪題

有一天,一個朋友發現一段奇怪的 C 程序:

int i = 3;
int ans = (++i)+(++i)+(++i);

書上說答案是 18,我還以為是 4+5+6=15 呢。

驗證

然後我就想著驗證一下結果到底是怎樣的, 我寫瞭如下的測試程序(inc.c):

#include <stdio.h>

int main()
{
    int i = 3;
    int ans = (++i)+(++i)+(++i);

    printf("%d\n",ans);
    return 0;
}

在 linux 中編譯、運行,結果如下:

[lqy@localhost temp]$ gcc -o inc inc.c
[lqy@localhost temp]$ ./inc
16
[lqy@localhost temp]$

既然又出現了一個令人匪夷所思的 16!

揭祕

好吧,先不管 18 了,看看這個 16 是怎麼來的:

gcc -S -o inc.s inc.c

生成了彙編源文件 inc.s, 現在我們只關心其中的粗體部分:

    .file    "inc.c"
    .section    .rodata
.LC0:
    .string    "%d\n"
    .text
.globl main
    .type    main, @function
main:
    pushl    %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    subl    $32, %esp
    movl    $3, 28(%esp)
    addl    $1, 28(%esp)
    addl    $1, 28(%esp)
    movl    28(%esp), %eax
    addl    %eax, %eax
    addl    $1, 28(%esp)
    addl    28(%esp), %eax
    movl    %eax, 24(%esp)
    movl    $.LC0, %eax
    movl    24(%esp), %edx
    movl    %edx, 4(%esp)
    movl    %eax, (%esp)
    call    printf
    movl    $0, %eax
    leave
    ret
    .size    main, .-main
    .ident    "GCC: (GNU) 4.5.1 20100924 (Red Hat 4.5.1-4)"
    .section    .note.GNU-stack,"",@progbits

  現在我我不得不解釋一下這種彙編的格式了, 它是 AT&T 格式的 x86 彙編, 我們在 windows 上見到的一般是 Intel 格式的彙編, AT&T 彙編與 Intel 格式的彙編有些差異, 不過還是很好理解的。

  首先是寄存器的命名格式不同, 在 Intel 格式的寄存器前加了個 %:eax 變成了 %eax 然後就是雙元指令的操作數的傳遞方向與 Intel 的剛好相反: mov eax,ebx(相當於 eax=ebx;) 變成了 movl %ebx,%eax (左向右傳值) 還有其他一些差別,不過還是容易看明白的。

給粗體部分加點註釋吧:

movl    $3, 28(%esp)    # i = 3;
addl    $1, 28(%esp)    # ++i; // 4
addl    $1, 28(%esp)    # ++i; // 5
movl    28(%esp), %eax    # eax = i;
addl    %eax, %eax        # eax = eax + eax; // 10
addl    $1, 28(%esp)    # ++i; // 6
addl    28(%esp), %eax    # eax = eax + i; // 16
movl    %eax, 24(%esp)    # ans = eax;

然後,我們就知道了 16 是怎麼來的了。

為什麼我就肯定 28(%esp) 就是變量 i 呢? 因為只有它被寫入了 3,沒有別的內存被寫入 3, 而且從之後操作它的各條指令也可以確定它就是 C 代碼中的變量 i(好悲催啊, 堂堂一個局部變量變成了無名無姓的相對寄存器尋址的一塊內存!)。 類似的,局部變量 ans 變成了 24(%esp), 可以推理得出:局部變量最後都變成了相對 esp 尋址的內存塊 (之後的篇章會看到這個推論還不是很正確)。

延伸

同樣的程序,在 VC 上編譯運行, Debug 模式的運行結果是 16,Release 模式的運行結果是 18; 而在 Visual Studio 2010 中 Debug 和 Release 模式下都是 18。

VC 和 VS 也可以看反彙編代碼, 在調試過程中遇到斷點中斷的時候, VC 使用 Alt + 8 打開反彙編窗體, VS 右擊 C 源代碼編輯區選擇"轉到反彙編",打開反彙編窗體。

為什麼 Intel 自己造的 CPU,AT&T 還出一套與 Intel 不同格式的彙編語言呢?沒法子,AT&T 也不是好惹的, 人家做了兩樣東西至今影響全世界:Unix、C語言。

小結

從這個例子中我們應該吸取經驗: 被實施遞增(遞減)操作的變量不應該在表達式中多次出現, 否則結果就不受我們控制了,而是被編譯器自由發揮:

C 標準規定:兩個序列點之間, 程序執行的順序可以是任意的。 這樣做給了編譯器優化的空間。[1]

如果想得到結果 15 的話,程序可以改成這樣:

#include <stdio.h>

int main()
{
    int i = 3;
    int ans = (i+2)*3;
    i += 3;

    printf("%d\n",ans);
    return 0;
}

這個程序不論在 windows 下還是在 linux 下, 不論是 Release 還是 Debug,結果一定是 15。

  關於 解剖 C 語言,才出了兩篇,已經能夠為我們解惑了, 悟空很有潛力啊!元芳,你怎麼看?

[回目錄][content]

[1] 由 ohyeah 指出:http://rs.xidian.edu.cn/forum.php?mod=redirect&goto=findpost&ptid=412474&pid=8298351