如果在 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 我還沒講呢! 二進制文件的分析後面也有用到,但是很少,用到的時候再說吧。