当我们写完了C语言代码后,通过gcc将其编译成可执行文件运行,这中间具体经过的步骤包括预处理、编译、汇编、链接四个步骤。
最简单的hello.c源文件内容如下:
# include <stdio.h> // 这是一行注释 int main(void) { printf("hello world!n"); printf("%sn", __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("%sn", "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战队 审校,电子工业出版社)