照妖鏡和火眼金睛

  如果在 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 文件中</em>。

妖怪!快快現形吧:

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