局部變量

  在接下來的幾篇文章中, 我將利用"火眼金睛"來分析一個個的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


书籍推荐