照妖鏡和火眼金睛

  如果在 linux 下編寫 C 程序, 那麼你將獲得兩個犀利的法寶:


照妖鏡

一個C程序(max.c):

#define MAX(a,b) ((a)>=(b)?(a):(b))

int main(){
	int c=MAX(1,2); // 注注注註釋
	return 0;
}

程序很簡單,就是定義和使用一個MAX宏, 宏在正式編譯前是會被替換為本來面目的, 我們現在看到的不是它的真身。讓我們用照妖鏡來照照:

gcc -E -o max2.c max.c

這裡的 -o max2.c 是讓 gcc 把要輸出東西輸出到 max2.c 文件中。

妖怪!快快現形吧:

# 1 "max.c"
# 1 "<built-in>"
# 1 "<命令行>"
# 1 "max.c"


int main(){
 int c=((1)>=(2)?(1):(2));
 return 0;
}

  上面就是max2.c中的內容,MAX(1,2) 被替換成了 ((1)>=(2)?(1):(2)),這隻孽畜終於現形了!

  照妖鏡的作用就是替換宏,但是宏好像大家都不太用。 不過宏在 現代 linux 內核源代碼中簡直是運用到了極致, 甚至可以說 linux 內核是由 C、宏、彙編 寫出來的。 宏是可以嵌套的,也就是說宏的 參數 或 右部 中還可以出現能夠被替換的宏, 所以情況就相當複雜了——十個字符的簡單的一條語句, 當被還原為本來面目時,可能就變成七八十個字符了, 要分析這樣的語句,照妖鏡就大顯神威了。

  關於宏,後面會獨立出一篇來介紹。


火眼金睛

  照妖鏡應該是不如火眼金睛的, 火眼金睛可以看到及其微小的細節。下面我寫了個Hello World (hello.c):

#include <stdio.h>

int main(){
	printf("Hello, World!\n");
	return 0;
}

Hello World 就不用解釋了吧,鼎鼎有名啊!O(∩_∩)O~, 然後我們用火眼金睛來看一下:

gcc -S -o hello.s hello.c

Hello World 的彙編版就出來了(hello.s):

	.file	"hello.c"
	.section	.rodata
.LC0:
	.string	"Hello, World!"
	.text
.globl main
	.type	main, @function
main:
	pushl	%ebp
	movl	%esp, %ebp
	andl	$-16, %esp
	subl	$16, %esp
	movl	$.LC0, (%esp)
	call	puts
	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

  其內容我們後面再慢慢分析, 現在只要知道怎麼用“火眼金睛”就行了, 接下來的幾篇都得靠悟空了。


  照妖鏡和火眼金睛其實都是靠截斷編譯過程 得到中間產物的,gcc的完整編譯過程是:

預處理->編譯->彙編->鏈接

  使用不同的編譯選項可以得出不同的中間產物:

編譯階段 命令 截斷後的產物
C源程序
預處理 gcc -E 替換了宏的C源程序(沒有了#define,#include…), 刪除了註釋
編譯 gcc -S 彙編源程序
彙編 gcc -c 目標文件,二進制文件, 允許有不在此文件中的外部變量、函數
鏈接 gcc 可執行程序,一般由多個目標文件或庫鏈接而成, 二進制文件,所有變量、函數都必須找得到

  也許有同學發現了 -c 我還沒講呢! 二進制文件的分析後面也有用到,但是很少,用到的時候再說吧。


书籍推荐