1.来自hello world
的思考
- 初学一种编程语言时,往往第一次编码时就是写一个最简单的
hell world
,如果不这么做好像就违背了约定俗成的传统了。我们学习Linux系统编程也不例外,只不过这次我们要刨析它背后的故事。哈哈,话不多说,直接上代码:
#include <stdio.h>
int main(void)
{
printf("Hello world!\n");
return 0;
}
linux系统上使用gcc生成可执行程序:gcc -g -W helloworld.c -o helloworld
整个过程看似简单,背后涉及预处理、编译、汇编和链接等多个过程。但是gcc作为一个工具集合自动完成了这些步骤。下面我们就来分析看看其中所涉及的几个步骤。
- 预处理预处理用于处理预处理命令。对于上面的代码来说,唯一的预处理命令是
#include
。它的作用是将头文件的内容包含到本文件中。该头文件中的所有代码都会在#include
处展开。可以通过gcc -E helloworld.c
在预处理后自动停止后面的操作,并把预处理的结果输出到标准输出。因此使用gcc -E helloworld.c > helloworld.i
,可得到预处理后的文件。理解了预处理,就明白为什么不能在头文件中定义全局变量,这是因为定义全局变量的代码会存在于所有以#include包含该头文件的文件中,也就是说所有的这些文件,都会定义一个同样的全局变量,这样就会发生冲突。 - 编译编译过程是对源代码进行语法分析,并优化产生对应的汇编代码的过程。同样使用gcc也可得到汇编代码
gcc -S helloworld.c -o helloworld.s
。gcc的-S选项会让gcc在编译完成后而停止,这样就会产生对应的汇编文件。 - 汇编汇编的过程比较简单,就是将源代码翻译成可执行的指令,并生成目标文件。对应的gcc命令为
gcc -c helloworld.c -o helloworld.o
。 - 链接链接是生成可执行程序的最后步骤,也是比较复杂的一步。它就是将各个目标文件,包括库文件链接成一个可执行程序。在这个过程中,在Linux环下,该工作是由GNU的链接器ld完成的。
2. hello world
可执行程序是什么文件?
- Linux下可执行程序是二进制的,其格式一般为ELF格式
- 用readelf命令查看其helloworld可执行程序的ELF格式:
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400430
Start of program headers: 64 (bytes into file)
Start of section headers: 7352 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 36
Section header string table index: 33
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8
0000000000000060 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400318 00000318
000000000000003d 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400356 00000356
0000000000000008 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400360 00000360
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400380 00000380
0000000000000018 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000400398 00000398
0000000000000030 0000000000000018 AI 5 24 8
[11] .init PROGBITS 00000000004003c8 000003c8
000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 00000000004003f0 000003f0
0000000000000030 0000000000000010 AX 0 0 16
[13] .plt.got PROGBITS 0000000000400420 00000420
0000000000000008 0000000000000000 AX 0 0 8
[14] .text PROGBITS 0000000000400430 00000430
0000000000000182 0000000000000000 AX 0 0 16
[15] .fini PROGBITS 00000000004005b4 000005b4
0000000000000009 0000000000000000 AX 0 0 4
[16] .rodata PROGBITS 00000000004005c0 000005c0
0000000000000011 0000000000000000 A 0 0 4
[17] .eh_frame_hdr PROGBITS 00000000004005d4 000005d4
0000000000000034 0000000000000000 A 0 0 4
[18] .eh_frame PROGBITS 0000000000400608 00000608
00000000000000f4 0000000000000000 A 0 0 8
[19] .init_array INIT_ARRAY 0000000000600e10 00000e10
0000000000000008 0000000000000000 WA 0 0 8
[20] .fini_array FINI_ARRAY 0000000000600e18 00000e18
0000000000000008 0000000000000000 WA 0 0 8
[21] .jcr PROGBITS 0000000000600e20 00000e20
0000000000000008 0000000000000000 WA 0 0 8
[22] .dynamic DYNAMIC 0000000000600e28 00000e28
00000000000001d0 0000000000000010 WA 6 0 8
[23] .got PROGBITS 0000000000600ff8 00000ff8
0000000000000008 0000000000000008 WA 0 0 8
[24] .got.plt PROGBITS 0000000000601000 00001000
0000000000000028 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000601028 00001028
0000000000000010 0000000000000000 WA 0 0 8
[26] .bss NOBITS 0000000000601038 00001038
0000000000000008 0000000000000000 WA 0 0 1
[27] .comment PROGBITS 0000000000000000 00001038
0000000000000035 0000000000000001 MS 0 0 1
[28] .debug_aranges PROGBITS 0000000000000000 0000106d
0000000000000030 0000000000000000 0 0 1
[29] .debug_info PROGBITS 0000000000000000 0000109d
0000000000000091 0000000000000000 0 0 1
[30] .debug_abbrev PROGBITS 0000000000000000 0000112e
0000000000000044 0000000000000000 0 0 1
[31] .debug_line PROGBITS 0000000000000000 00001172
0000000000000041 0000000000000000 0 0 1
[32] .debug_str PROGBITS 0000000000000000 000011b3
00000000000000d6 0000000000000001 MS 0 0 1
[33] .shstrtab STRTAB 0000000000000000 00001b69
000000000000014c 0000000000000000 0 0 1
[34] .symtab SYMTAB 0000000000000000 00001290
00000000000006c0 0000000000000018 35 52 8
[35] .strtab STRTAB 0000000000000000 00001950
0000000000000219 0000000000000000 0 0 1
ELF文件的主要是由各个section及symbol表组成。在上面的secti
ELF文件的主要是由各个section及symbol表组成。在上面的section列表中,比较熟悉的应该是text段、data段和bss段。
- text段为代码段,用于保存可执行指令。
- data段为数据段,用于保存有非0初始值的全局变量和静态变量。
- bss段用于保存没有初始值或初值为0的全局变量和静态变量,当程序加载时,bss段中的变量会被初始化为0。
除此之外还有其他常见的段:
- debug段:用于保存调试信息,如果不使用-g选项,则不会生成。
- dynamic段:用于保存动态链接信息。
- fini段:用于保存进程退出时的执行程序。当进程结束时,系统会自动执行这部分代码。
- init段:用于保存进程启动时的执行程序。当进程启动时,系统会自动执行这部分代码。
- rodata段:用于保存只读数据,如const修饰的全局变量、字符串常量。
- symtab段:用于保存符号表。
3.hello world
是如何在系统上运行的?
- 当我们在Linux系统运行
helloworld
时,它是如何运行的。或者说./hellworld
都经历了哪些操作过程。下面在Ubuntu环境下,可以使用strace跟踪系统调用,从而可以帮助我们研究系统程序加载、 运行和退出的过程。
strace ./helloworld
execve("./helloworld", ["./helloworld"], [/* 70 vars */]) = 0
brk(NULL) = 0x25ab000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=114859, ...}) = 0
mmap(NULL, 114859, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fbb541f8000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`\t\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1868984, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fbb541f7000
mmap(NULL, 3971488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fbb53c26000
mprotect(0x7fbb53de6000, 2097152, PROT_NONE) = 0
mmap(0x7fbb53fe6000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000) = 0x7fbb53fe6000
mmap(0x7fbb53fec000, 14752, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fbb53fec000
close(3) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fbb541f6000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fbb541f5000
arch_prctl(ARCH_SET_FS, 0x7fbb541f6700) = 0
mprotect(0x7fbb53fe6000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ) = 0
mprotect(0x7fbb54215000, 4096, PROT_READ) = 0
munmap(0x7fbb541f8000, 114859) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
brk(NULL) = 0x25ab000
brk(0x25cc000) = 0x25cc000
write(1, "Hello world!\n", 13Hello world!
) = 13
exit_group(0) = ?
+++ exited with 0 +++
在Linux系统上, 当我们执行命令时,首先是由shell调用fork,然后在子进程中来执行这个命令。strace是helloworld开始执行后的输出。首先是调用execve来加载helloworld,然后ld会分别检查ld.so.nohwcap和ld.so.preload。其中,如果ld.so.nohwcap存在,则ld会加载其中未优化版本的库。如果ld.so.preload存在,则ld会加载其中的库。之后利用mmap将ld.so.cache映射到内存中,ld.so.cache中保存了库的路径,这样就完成了所有的准备工作。然后ld加载c库open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC)
,利用mmap及mprotect设置程序的各个内存区域,到这里,程序运行的环境已经完成。后面的write会向文件描述符1(即标准输出)输出Hello world!
,返回值为13,它表示write成功的字符数。最后调用exit_group退出程序,参数为0,说明程序退出的状态。
4.总结
至此,一个简单的helloworld
从编码到产生可执行程序,再到运行,背后涉及的‘故事’就讲完了。看似简单的一个helloword
,没想到背后竟然隐藏着这么多“秘密”,与其说“秘密”不如说是涉及了这么多东西。因此,在学习Linux系统编程时,我们不仅要知其然,更要知其所以然,只有这样才能深刻地理解Linux系统编程,才能在以后遇到问题时更快地分析问题。好了,这篇就先到这里吧,我们后续章节继续。加油,热爱技术的你!