Linux系统下剖析hello world背后的秘密

652人浏览   2024-04-25 06:36:37
  • Hi,小伙伴们,大家好!今天给大家讲解Linux系统编程中几个重要的概念。必须理解了这几个重要概念,才能更快的入门Linux系统编程,这是不可或缺的基础知识。看似简单,如果不花一番功夫很难真正的理解它们。需要不断的对它们进行思考和理解,只有这样才能写出高可靠性的Linux程序。

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. helloworld可执行程序是什么文件?

  • 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表组成。在上面的section列表中,比较熟悉的应该是text段、data段和bss段。

  • text段为代码段,用于保存可执行指令。
  • data段为数据段,用于保存有非0初始值的全局变量和静态变量。
  • bss段用于保存没有初始值或初值为0的全局变量和静态变量,当程序加载时,bss段中的变量会被初始化为0。

除此之外还有其他常见的段:

  • debug段:用于保存调试信息,如果不使用-g选项,则不会生成。
  • dynamic段:用于保存动态链接信息。
  • fini段:用于保存进程退出时的执行程序。当进程结束时,系统会自动执行这部分代码。
  • init段:用于保存进程启动时的执行程序。当进程启动时,系统会自动执行这部分代码。
  • rodata段:用于保存只读数据,如const修饰的全局变量、字符串常量。
  • symtab段:用于保存符号表。

3.helloworld是如何在系统上运行的?

  • 当我们在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系统编程,才能在以后遇到问题时更快的分析问题。好了,这篇就先到这里吧,我们后续章节继续。加油,热爱技术的你!


相关推荐