在接下來的幾篇文章中, 我將利用"火眼金睛"來分析一個個的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(#tip1)
如果想得到結果 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