假設今天有一段基本的程式
#include <stdio.h>
int main() {
printf("Hello World!\n");
return 0;
}
它會經過以下幾個階段,最後產生可以執行的 Binary
大致可以表示成:
hello.c
↓ Preprocessor
hello.i
↓ Compiler
hello.s
↓ Assembler
hello.o
↓ Linker
hello
在這個階段中,像是 #include <stdio.h> , #define 等等
都會在這個階段中被處理
像 #include 會將對應 Header 的內容展開,而 Macro 也會在這個階段進行替換。
這邊可以使用:
gcc -E hello.c -o hello.i
只執行 preprocessing 的部分。
這個階段是把 C code 轉成 Assembly
而 Assembly 的部分會根據你的
有不同的結果
可以使用:
gcc -S hello.c -o hello.s
然後 Compiler 在編譯的過程中可能對程式做很多轉換以及最佳化
這也帶出 Reverse Engineering 很重要的一個觀念:
我們看到的 Assembly,不一定能一對一對應回原本的 Source Code
雖然說 Assembly 已經是低階語言了
但由於 CPU 執行的是 Machine Code
所以還需要 Assembler 將 Assembly 轉成 Machine Code
可以使用:
gcc -c hello.c -o hello.o
執行完之後會得到 hello.o
這是一個 Object File
其中已經包含了編譯後的 Machine Code
但它還不是一個完整的 Executable
上述的 hello.o 如果使用 file 指令去檢視的話,可能會出現
hello.o: ELF 64-bit LSB relocatable, x86-64, ...
會發現他有一個關鍵字 relocatable,並不是 executable
這是因為程式裡還有一些「這個 Function 到底在哪裡」之類的資訊尚未被解析。
比如說像是 printf() 根本就不在我們實際寫的程式中,那他的程式碼在哪裡?
像這些未解決的 Symbol 會在下個階段被解決
在這個階段中 Linker 會把 Object File 之間的 Symbol Reference 解析起來
並處理程式所依賴的 Library,最後產生可以執行的 Binary
程式會使用到 C Standard Library 裡面的函數,但這些函數並不是寫在我們自己的 hello.c 裡
可以透過:
gcc hello.o -o hello
進行 Linking
透過 file 指令查看會顯示:
ELF 64-bit LSB pie executable, x86-64, ...
這樣才是我們平常可以直接 ./hello 執行的程式
正常開發時,我們從高階的 Source Code 產生低階的 Machine Code
而 Reverse Engineering 則是從 Binary、Assembly 等低階資訊出發,重新理解程式原本的邏輯
如果剛剛有注意到 file 的輸出的話,應該會一直看到一個東西:ELF
像是:
ELF 64-bit LSB relocatable
以及:
ELF 64-bit LSB pie executable
所以明天就來看看:ELF 到底是什麼?