從原始檔到可執行檔案:原始檔的預處理、編譯、彙編、連結
當我們寫完了C語言程式碼後,通過gcc將其編譯成可執行檔案執行,這中間具體經過的步驟包括預處理、編譯、彙編、連結四個步驟。
最簡單的hello.c原始檔內容如下:
# include <stdio.h>
// 這是一行註釋
int main(void)
{
printf("hello world!\n");
printf("%s\n", __DATE__);
return 0;
}
預處理
處理原始檔中以“#”開頭的元素,比如#include
#define
,將其轉換後直接插入原始檔中,處理後的檔案通常以.i
作為副檔名。這一步具體包括:
- 展開標頭檔案:
#include
- 巨集替換:
#define
- 條件編譯:
#if
#elif
else
ifdef
ifndef
- 刪除註釋
- 新增行號
- 預定義符號常量:
__DATE__
__TIME__
__TIMESTAMP__
__LINE__
__FILE__
__STDC__
gcc可以通過如下指令得到預處理後的檔案:gcc -E hello.c -o hello.i
,hello.i
檔案很長,這裡擷取一小部分:
# 4 "hello.c"
int main(void)
{
printf("hello world!\n");
printf("%s\n", "May 3 2022");
return 0;
}
可以看到註釋已經被刪除了,符號常量__DATE__
也已經被展開。
編譯
編譯階段包括詞法分析、語法分析、語義分析、中間程式碼生成、目的碼生成與優化,編譯完成後會生成彙編程式碼,通常副檔名為.s
。
gcc可以通過如下指令得到編譯後的彙編程式碼:gcc -S hello.c -o hello.s
預設生成的彙編程式碼是AT&T格式的,可採用如下指令得到intel格式的彙編程式碼:gcc -S hello.c -o hello.s -masm=intel
,intel格式的hello.s
內容如下:
.file "hello.c"
.intel_syntax noprefix
.text
.section .rodata
.LC0:
.string "hello world!"
.LC1:
.string "May 3 2022"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
lea rdi, .LC0[rip]
call puts@PLT
lea rdi, .LC1[rip]
call puts@PLT
mov eax, 0
pop rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
彙編
彙編是根據彙編指令與機器指令的對應關係將彙編檔案翻譯成目標檔案,如果從原始檔開始,gcc命令是gcc -c hello.c -o hello.o
,如果從彙編檔案開始,gcc命令是gcc -c hello.s -o hello.o
。通過file
命令檢視目標檔案hello.o
:file hello.o
,終端顯示為:hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
,說明這是一個ELF檔案,關於ELF檔案的內容將在下一篇部落格中介紹。
hello.o
檔案內容無法直接在編輯器中顯示,但可以通過objdump顯示:objdump -sd hello.o -M intel
。
hello.o: 檔案格式 elf64-x86-64
Contents of section .text:
0000 f30f1efa 554889e5 488d3d00 000000e8 ....UH..H.=.....
0010 00000000 488d3d00 000000e8 00000000 ....H.=.........
0020 b8000000 005dc3 .....].
Contents of section .rodata:
0000 68656c6c 6f20776f 726c6421 004d6179 hello world!.May
0010 20203320 32303232 00 3 2022.
Contents of section .comment:
0000 00474343 3a202855 62756e74 7520392e .GCC: (Ubuntu 9.
0010 342e302d 31756275 6e747531 7e32302e 4.0-1ubuntu1~20.
0020 30342e31 2920392e 342e3000 04.1) 9.4.0.
Contents of section .note.gnu.property:
0000 04000000 10000000 05000000 474e5500 ............GNU.
0010 020000c0 04000000 03000000 00000000 ................
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 27000000 00450e10 8602430d ....'....E....C.
0030 065e0c07 08000000 .^......
Disassembly of section .text:
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push rbp
5: 48 89 e5 mov rbp,rsp
8: 48 8d 3d 00 00 00 00 lea rdi,[rip+0x0] # f <main+0xf>
f: e8 00 00 00 00 call 14 <main+0x14>
14: 48 8d 3d 00 00 00 00 lea rdi,[rip+0x0] # 1b <main+0x1b>
1b: e8 00 00 00 00 call 20 <main+0x20>
20: b8 00 00 00 00 mov eax,0x0
25: 5d pop rbp
26: c3 ret
此時由於還未連結,目標檔案中符號的虛擬地址無法確定。此時,如果執行hello.o
會報錯:可執行檔案格式錯誤。
連結
連結包括靜態連結和動態連結兩種,gcc預設使用動態連結,新增編譯選項-static
可以進行靜態連結,這一階段將目標檔案與其依賴庫進行連結,主要包括地址和空間分配(Address and Storage Allocation)、符號繫結(Symbol Binding)、重定位(Relocation)等。gcc命令:gcc hello.c -o hello
。經過objdump後,部分內容如下:
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push rbp
114e: 48 89 e5 mov rbp,rsp
1151: 48 8d 3d ac 0e 00 00 lea rdi,[rip+0xeac] # 2004 <_IO_stdin_used+0x4>
1158: e8 f3 fe ff ff call 1050 <puts@plt>
115d: 48 8d 3d ad 0e 00 00 lea rdi,[rip+0xead] # 2011 <_IO_stdin_used+0x11>
1164: e8 e7 fe ff ff call 1050 <puts@plt>
1169: b8 00 00 00 00 mov eax,0x0
116e: 5d pop rbp
116f: c3 ret
跟未經過連結的目標檔案相比,虛擬地址已經確定了,執行hello
便可以得到結果:
hello world!
May 3 2022
參考資料
CTF競賽權威指南(Pwn篇)(楊超 編著,吳石 eee戰隊 審校,電子工業出版社)