• linux可执行程序的编译、链接、装载


            在编译链接方面《程序员的自我修养-链接、装载与库》一书讲的很不错,这篇文章一部分是读此书后的记录,另一部分是自己对可执行程序在编译、链接装载产生的理解总结。

            很多时候,入门总是很容易,因为达到了效果,得到了结果。但这其中往往还有很多细节值值得思考,因为很多结果的实现是站在了别人的肩膀上。

            本文首先总结编译的4个的过程,熟悉编译中的几个步骤(预处理、编译、汇编、链接),再分析编译后的ELF文件(目标文件和可执行)的结构,弄清编译后文件的排布及含有哪些信息,从而分析静态链接中如何使用这些信息进行重定位操作。之后,分析程序的装载过程,及动态链接的过程。

    编译链接四步骤

    helloworld的编译链接执行

            对于大部分进行C/C++,JAVA,C#等各种程序开发的人员来说,入门之初应该都是从打印一行字符串开始。

    1. hello.c
    2. #include
    3. int main(void)
    4. {
    5. printf("hello world\r\n");
    6. return 0;
    7. }

    然后拿来gcc最简单的一条编译指令:

    gcc hello.c -o hello

    产生可执行文件hello后运行。

    1. root@xxzh:~/loader# ./hello
    2. hello world
    3. root@xxzh:~/loader#

    似乎很快就完成了入门实验,但这背后隐含了很多内容。

    编译链接过程

            如上直接-o输出了可执行程序hello。可谓是一步到位,但编译器这么痛快的用,还可以一步一步的用。因为上面的过程可以被拆分为多个步骤,通常为4个步骤。

    第一步:预编译(.c到.i)

    gcc -E hello.c -o hello.i

    看下执行后hello.i 有多少行,如下,856行。代码只有不到10行,预编译后产生了一个800+行的文件

    1. root@xxzh:~/loader# gcc -E hello.c -o hello.i
    2. root@xxzh:~/loader# wc -l hello.i
    3. 856 hello.i
    4. root@xxzh:~/loader#

    打开,hello.i 看最底部,自己编写的代码部分并没有变化,可见,预编译主要是处理代码中以“#”开头的预编译指令的。#include被替换为了一系列内容。

    如果在.c 在定义一个宏,#define A 123,然后printf的时候他们它。预编译后查看.i是:

    printf("hello world A=%d\r\n", 123);

    可见预编译直接把宏替换为了实际的数值。

    预编译主要任务:

    • 处理 # 开头的预处理指令
    • 删除注释
    • 最一些前期基础的工作,搜集信息,前期处理

    第二步:编译(.i到.S)

    1. gcc -S hello.i -o hello.s 或者
    2. gcc -S hello.c -o hello.s

    进行词法分析、语义分析、产生汇编代码

    如果在hello.c中的main下,随便输入一些字符串,在预处理阶段是不会报告任何错误的,当汇编的时候才会提示存在error。这就是汇编阶段的语法检查。

    预编译和编译是由编译器的cc1来完成的。gcc命令是对cc1的包装,根据传入的参数,执行不同的动作。

    编译的主要任务:        

    • 语法检查
    • 语义分析
    • 优化
    • 生成汇编文件

    第三步:汇编(.s到.o)

            由汇编器as来完成,将汇编代码,转换成机器指令。一个汇编对应一个代码形式的机器指令码,汇编的过程就是根据这个对应关系将汇编码转换成机器认识的机器码。

    1. ac hello.s -o hello.o 或者
    2. gcc -c hello.s -o hello.o

    注意 .o文件被称作为目标文件

    第四步:链接

            第三步不是已经产生机器认可的机器码了吗,为什么需要链接呢?执行一下看看,可见并不认为.o是可执行文件。是 LSB relocatable,而不是LSB executable。

    1. root@xxzh:~/loader# ./hello.o
    2. -bash: ./hello.o: cannot execute binary file: Exec format error
    3. root@xxzh:~/loader#
    4. root@xxzh:~/loader# file hello.o
    5. hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
    6. root@xxzh:~/loader# file hello
    7. hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=20d5f83a3c0961c0638b187ec7a30ad4411e92d5, not stripped
    8. root@xxzh:~/loader#

    再重新加上-v编译一下,打印出编译的详细过程:如下

    1. root@xxzh:~/loader# gcc hello.c -o hello -v
    2. Using built-in specs.
    3. COLLECT_GCC=gcc
    4. COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper
    5. Target: x86_64-linux-gnu
    6. Configured with: ../src/configure -v --with-pkgversion='Ubuntu 5.4.0-6ubuntu1~16.04.12' --with-bugurl=file:///usr/share/doc/gcc-5/README.Bugs --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-5 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-5-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-5-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-5-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
    7. Thread model: posix
    8. gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.12)
    9. COLLECT_GCC_OPTIONS='-o' 'hello' '-v' '-mtune=generic' '-march=x86-64'
    10. /usr/lib/gcc/x86_64-linux-gnu/5/cc1 -quiet -v -imultiarch x86_64-linux-gnu hello.c -quiet -dumpbase hello.c -mtune=generic -march=x86-64 -auxbase hello -version -fstack-protector-strong -Wformat -Wformat-security -o /tmp/ccmgY0bt.s
    11. GNU C11 (Ubuntu 5.4.0-6ubuntu1~16.04.12) version 5.4.0 20160609 (x86_64-linux-gnu)
    12. compiled by GNU C version 5.4.0 20160609, GMP version 6.1.0, MPFR version 3.1.4, MPC version 1.0.3
    13. GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
    14. ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
    15. ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/5/../../../../x86_64-linux-gnu/include"
    16. #include "..." search starts here:
    17. #include <...> search starts here:
    18. /usr/lib/gcc/x86_64-linux-gnu/5/include
    19. /usr/local/include
    20. /usr/lib/gcc/x86_64-linux-gnu/5/include-fixed
    21. /usr/include/x86_64-linux-gnu
    22. /usr/include
    23. End of search list.
    24. GNU C11 (Ubuntu 5.4.0-6ubuntu1~16.04.12) version 5.4.0 20160609 (x86_64-linux-gnu)
    25. compiled by GNU C version 5.4.0 20160609, GMP version 6.1.0, MPFR version 3.1.4, MPC version 1.0.3
    26. GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
    27. Compiler executable checksum: 8087146d2ee737d238113fb57fabb1f2
    28. COLLECT_GCC_OPTIONS='-o' 'hello' '-v' '-mtune=generic' '-march=x86-64'
    29. as -v --64 -o /tmp/cc2IuGAa.o /tmp/ccmgY0bt.s
    30. GNU assembler version 2.26.1 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.26.1
    31. COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/
    32. LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../:/lib/:/usr/lib/
    33. COLLECT_GCC_OPTIONS='-o' 'hello' '-v' '-mtune=generic' '-march=x86-64'
    34. /usr/lib/gcc/x86_64-linux-gnu/5/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/5/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper -plugin-opt=-fresolution=/tmp/ccsjSH0R.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --sysroot=/ --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -z relro -o hello /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o -L/usr/lib/gcc/x86_64-linux-gnu/5 -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/5/../../.. /tmp/cc2IuGAa.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o

            预处理的时候,只是把stdio.h进行处理,但是printf的具体实现在链接之前的过程并没有包含进来。链接就是把可执行程序所需要所有函数及变量汇聚起来,形成关系后,输出最后的可执行程序。

            在详细编译的日志中,可以看到调用的是collect2,然后最后-o了hello。程序中printf可以静态的包含到hello可执行程序中,也可以动态库的形式存在,也就是静态编译和动态编译。默认情况是动态编译,可以看到有-dynamic-linker标志。

            以上针对的是有操作系统,在linux下的分析。针对于裸机,打印这个就需要操作串口寄存器了。

    编译的几步工作

            词法检查、语法分析、语义分析、中间语言生产、目标代码生成与优化

    编译器与链接器的区别

    编译器:将源代码编译成未链接的目标文件(.o)

    链接器:将目标文件链接成最后的可执行文件

    目标文件

            object文件,编译后最终生成的问题,一个工程往往含有多个.c,每个.c单独的被编译成一个.o文件,称之为中间目标文件。如在makefile中,经常看到如下通配编译命令。

    1. .c.o:
    2. $(CC) $(INCLUDE) $(CFLAGS) -c $<

    重定位

            链接过程中,将各个目标符号地址确定的过程叫做重定位。目标符号包含函数及全局变量等。在编译多个.c的时候,若a.c调用了b.c中的函数,在编译a.o的时候并不知道b.o中函数的地址,因此在链接的时候需要确定下来,这就是重定位。当前a引用b中变量也存在这个过程。

    ELF文件格式

            全程:Executable Linkable Format。linux下可执行文件格式,其中,目标文件、可执行文件、静态库、动态库等都以ELF格式生成。

            file可以查看文件属性,有ELF 64-bit LSB shared object、ELF 64-bit LSB relocatable、ELF 64-bit LSB executable等。

    1. root@xxzh:/# file ./lib/x86_64-linux-gnu/libc-2.23.so
    2. ./lib/x86_64-linux-gnu/libc-2.23.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=c4fd86ec1eed57a09c79ce601f6c6e3796f574df, for GNU/Linux 2.6.32, stripped
    3. root@xxzh:~/loader# file hello.o
    4. hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
    5. root@xxzh:~/loader# file hello
    6. hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=20d5f83a3c0961c0638b187ec7a30ad4411e92d5, not stripped
    7. root@xxzh:~/loader#

    ELF头 

            读一下可执行文件、目标中间的elf头如下。

            包含信息:ELF魔幻数:特定的标识,识别为elf文件、机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、短表位置和长度、短表数量。

    1. root@xxzh:~/loader# arm-linux-gnueabihf-readelf -h hello
    2. ELF Header:
    3. Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
    4. Class: ELF32
    5. Data: 2's complement, little endian
    6. Version: 1 (current)
    7. OS/ABI: UNIX - System V
    8. ABI Version: 0
    9. Type: EXEC (Executable file)
    10. Machine: ARM
    11. Version: 0x1
    12. Entry point address: 0x102f1
    13. Start of program headers: 52 (bytes into file)
    14. Start of section headers: 8436 (bytes into file)
    15. Flags: 0x5000400, Version5 EABI, hard-float ABI
    16. Size of this header: 52 (bytes)
    17. Size of program headers: 32 (bytes)
    18. Number of program headers: 8
    19. Size of section headers: 40 (bytes)
    20. Number of section headers: 38
    21. Section header string table index: 35
    22. root@xxzh:~/loader#
    23. root@xxzh:~/loader# arm-linux-gnueabihf-readelf -h hello.o
    24. ELF Header:
    25. Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
    26. Class: ELF32
    27. Data: 2's complement, little endian
    28. Version: 1 (current)
    29. OS/ABI: UNIX - System V
    30. ABI Version: 0
    31. Type: REL (Relocatable file)
    32. Machine: ARM
    33. Version: 0x1
    34. Entry point address: 0x0
    35. Start of program headers: 0 (bytes into file)
    36. Start of section headers: 568 (bytes into file)
    37. Flags: 0x5000000, Version5 EABI
    38. Size of this header: 52 (bytes)
    39. Size of program headers: 0 (bytes)
    40. Number of program headers: 0
    41. Size of section headers: 40 (bytes)
    42. Number of section headers: 12
    43. Section header string table index: 9
    44. root@xxzh:~/loader#

    数据结构定义

    1. typedef struct
    2. {
    3. unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
    4. Elf32_Half e_type; /* Object file type */
    5. Elf32_Half e_machine; /* Architecture */
    6. Elf32_Word e_version; /* Object file version */
    7. Elf32_Addr e_entry; /* Entry point virtual address */
    8. Elf32_Off e_phoff; /* Program header table file offset */
    9. Elf32_Off e_shoff; /* Section header table file offset */
    10. Elf32_Word e_flags; /* Processor-specific flags */
    11. Elf32_Half e_ehsize; /* ELF header size in bytes */
    12. Elf32_Half e_phentsize; /* Program header table entry size */
    13. Elf32_Half e_phnum; /* Program header table entry count */
    14. Elf32_Half e_shentsize; /* Section header table entry size */
    15. Elf32_Half e_shnum; /* Section header table entry count */
    16. Elf32_Half e_shstrndx; /* Section header string table index */
    17. } Elf32_Ehdr;

    段表

            指示了该elf文件中,段的分布位置,段名、段偏移地址、段的大小。

    从上面头部可以知道段表的数量,通过readelf -S hello可以查看每个段表内容。

    1. root@xxzh:~/loader# arm-linux-gnueabihf-readelf -S hello
    2. There are 38 section headers, starting at offset 0x20f4:
    3. Section Headers:
    4. [Nr] Name Type Addr Off Size ES Flg Lk Inf Al
    5. [ 0] NULL 00000000 000000 000000 00 0 0 0
    6. [ 1] .interp PROGBITS 00010134 000134 000019 00 A 0 0 1
    7. [ 2] .note.ABI-tag NOTE 00010150 000150 000020 00 A 0 0 4
    8. [ 3] .note.gnu.build-i NOTE 00010170 000170 000024 00 A 0 0 4
    9. [ 4] .hash HASH 00010194 000194 000028 04 A 5 0 4
    10. [ 5] .dynsym DYNSYM 000101bc 0001bc 000050 10 A 6 1 4
    11. [ 6] .dynstr STRTAB 0001020c 00020c 000041 00 A 0 0 1
    12. [ 7] .gnu.version VERSYM 0001024e 00024e 00000a 02 A 5 0 2
    13. [ 8] .gnu.version_r VERNEED 00010258 000258 000020 00 A 6 1 4
    14. [ 9] .rel.dyn REL 00010278 000278 000008 08 A 5 0 4
    15. [10] .rel.plt REL 00010280 000280 000020 08 AI 5 22 4
    16. [11] .init PROGBITS 000102a0 0002a0 00000c 00 AX 0 0 4
    17. [12] .plt PROGBITS 000102ac 0002ac 000044 04 AX 0 0 4
    18. [13] .text PROGBITS 000102f0 0002f0 00013c 00 AX 0 0 4
    19. [14] .fini PROGBITS 0001042c 00042c 000008 00 AX 0 0 4
    20. [15] .rodata PROGBITS 00010434 000434 000011 00 A 0 0 4
    21. [16] .ARM.exidx ARM_EXIDX 00010448 000448 000008 00 AL 13 0 4
    22. [17] .eh_frame PROGBITS 00010450 000450 000004 00 A 0 0 4
    23. [18] .init_array INIT_ARRAY 00020454 000454 000004 04 WA 0 0 4
    24. [19] .fini_array FINI_ARRAY 00020458 000458 000004 04 WA 0 0 4
    25. [20] .jcr PROGBITS 0002045c 00045c 000004 00 WA 0 0 4
    26. [21] .dynamic DYNAMIC 00020460 000460 0000e8 08 WA 6 0 4
    27. [22] .got PROGBITS 00020548 000548 000020 04 WA 0 0 4
    28. [23] .data PROGBITS 00020568 000568 000008 00 WA 0 0 4
    29. [24] .bss NOBITS 00020570 000570 000004 00 WA 0 0 1
    30. [25] .comment PROGBITS 00000000 000570 00002d 01 MS 0 0 1
    31. [26] .ARM.attributes ARM_ATTRIBUTES 00000000 00059d 000033 00 0 0 1
    32. [27] .debug_aranges PROGBITS 00000000 0005d0 0000b0 00 0 0 8
    33. [28] .debug_info PROGBITS 00000000 000680 00049b 00 0 0 1
    34. [29] .debug_abbrev PROGBITS 00000000 000b1b 00018e 00 0 0 1
    35. [30] .debug_line PROGBITS 00000000 000ca9 000276 00 0 0 1
    36. [31] .debug_frame PROGBITS 00000000 000f20 000044 00 0 0 4
    37. [32] .debug_str PROGBITS 00000000 000f64 000373 01 MS 0 0 1
    38. [33] .debug_loc PROGBITS 00000000 0012d7 0000dd 00 0 0 1
    39. [34] .debug_ranges PROGBITS 00000000 0013b8 000048 00 0 0 8
    40. [35] .shstrtab STRTAB 00000000 001f87 00016c 00 0 0 1
    41. [36] .symtab SYMTAB 00000000 001400 000700 10 37 90 4
    42. [37] .strtab STRTAB 00000000 001b00 000487 00 0 0 1
    43. Key to Flags:
    44. W (write), A (alloc), X (execute), M (merge), S (strings)
    45. I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
    46. O (extra OS processing required) o (OS specific), p (processor specific)
    47. root@xxzh:~/loader#
    48. root@xxzh:~/loader#
    49. root@xxzh:~/loader# arm-linux-gnueabihf-readelf -S hello.o
    50. There are 12 section headers, starting at offset 0x238:
    51. Section Headers:
    52. [Nr] Name Type Addr Off Size ES Flg Lk Inf Al
    53. [ 0] NULL 00000000 000000 000000 00 0 0 0
    54. [ 1] .text PROGBITS 00000000 000034 000016 00 AX 0 0 2
    55. [ 2] .rel.text REL 00000000 0001bc 000018 08 I 10 1 4
    56. [ 3] .data PROGBITS 00000000 00004a 000000 00 WA 0 0 1
    57. [ 4] .bss NOBITS 00000000 00004a 000000 00 WA 0 0 1
    58. [ 5] .rodata PROGBITS 00000000 00004c 00000d 00 A 0 0 4
    59. [ 6] .comment PROGBITS 00000000 000059 00002e 01 MS 0 0 1
    60. [ 7] .note.GNU-stack PROGBITS 00000000 000087 000000 00 0 0 1
    61. [ 8] .ARM.attributes ARM_ATTRIBUTES 00000000 000087 000033 00 0 0 1
    62. [ 9] .shstrtab STRTAB 00000000 0001d4 000061 00 0 0 1
    63. [10] .symtab SYMTAB 00000000 0000bc 0000e0 10 11 12 4
    64. [11] .strtab STRTAB 00000000 00019c 00001e 00 0 0 1
    65. Key to Flags:
    66. W (write), A (alloc), X (execute), M (merge), S (strings)
    67. I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
    68. O (extra OS processing required) o (OS specific), p (processor specific)
    69. root@xxzh:~/loader#

    段表数据结构

    1. typedef struct elf32_shdr {
    2. Elf32_Word sh_name;
    3. Elf32_Word sh_type;
    4. Elf32_Word sh_flags;
    5. Elf32_Addr sh_addr;
    6. Elf32_Off sh_offset;
    7. Elf32_Word sh_size;
    8. Elf32_Word sh_link;
    9. Elf32_Word sh_info;
    10. Elf32_Word sh_addralign;
    11. Elf32_Word sh_entsize;
    12. } Elf32_Shdr;

    代码段

            .text 

    数据段

            .data 初始化了的全局静态变量和局部静态变量

    BSS段

            .bss 未初始化的全局变量和局部静态变量

    重定位表

            .rel.text text段重定位表

    字符串表

            .shstrtab 段名,变量名的统一存储。

    符号表

            .symtab 符号表

    静态链接

            将所有依赖打包到一起,可执行文件比较大,但不依赖外部函数库的链接方式。

    目标文件与可执行文件的段属性变化

            a.c中调用b.c中的函数,b作为a的库提供者。

    1. //a.c
    2. extern int share;
    3. int swap(int *a, int *b);
    4. int main()
    5. {
    6. int a = 100;
    7. swap(&a, &share);
    8. }
    9. //b.c
    10. int share = 12;
    11. int swap(int *a, int *b)
    12. {
    13. *a = *b;
    14. }

    编译产生.o 目标文件

    arm-linux-gnueabihf-gcc -c a.c b.c

    链接出ab可执行文件

    arm-linux-gnueabihf-ld  a.o b.o -e main -o ab

    查看段属性

    1. root@xxzh:~/loader# arm-linux-gnueabihf-objdump -h a.o
    2. a.o: file format elf32-littlearm
    3. Sections:
    4. Idx Name Size VMA LMA File off Algn
    5. 0 .text 00000024 00000000 00000000 00000034 2**1
    6. CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
    7. 1 .data 00000000 00000000 00000000 00000058 2**0
    8. CONTENTS, ALLOC, LOAD, DATA
    9. 2 .bss 00000000 00000000 00000000 00000058 2**0
    10. ALLOC
    11. 3 .comment 0000002e 00000000 00000000 00000058 2**0
    12. CONTENTS, READONLY
    13. 4 .note.GNU-stack 00000000 00000000 00000000 00000086 2**0
    14. CONTENTS, READONLY
    15. 5 .ARM.attributes 00000033 00000000 00000000 00000086 2**0
    16. CONTENTS, READONLY
    17. root@xxzh:~/loader# arm-linux-gnueabihf-objdump -h b.o
    18. b.o: file format elf32-littlearm
    19. Sections:
    20. Idx Name Size VMA LMA File off Algn
    21. 0 .text 00000020 00000000 00000000 00000034 2**1
    22. CONTENTS, ALLOC, LOAD, READONLY, CODE
    23. 1 .data 00000004 00000000 00000000 00000054 2**2
    24. CONTENTS, ALLOC, LOAD, DATA
    25. 2 .bss 00000000 00000000 00000000 00000058 2**0
    26. ALLOC
    27. 3 .comment 0000002e 00000000 00000000 00000058 2**0
    28. CONTENTS, READONLY
    29. 4 .note.GNU-stack 00000000 00000000 00000000 00000086 2**0
    30. CONTENTS, READONLY
    31. 5 .ARM.attributes 00000033 00000000 00000000 00000086 2**0
    32. CONTENTS, READONLY
    33. root@xxzh:~/loader# arm-linux-gnueabihf-objdump -h ab
    34. ab: file format elf32-littlearm
    35. Sections:
    36. Idx Name Size VMA LMA File off Algn
    37. 0 .text 00000044 00010094 00010094 00000094 2**2
    38. CONTENTS, ALLOC, LOAD, READONLY, CODE
    39. 1 .data 00000004 000200d8 000200d8 000000d8 2**2
    40. CONTENTS, ALLOC, LOAD, DATA
    41. 2 .comment 0000002d 00000000 00000000 000000dc 2**0
    42. CONTENTS, READONLY
    43. 3 .ARM.attributes 00000033 00000000 00000000 00000109 2**0
    44. CONTENTS, READONLY
    45. root@xxzh:~/loader#

    可见ab的text段大小 0x44=0x24+0x20。

    目标文件与可执行文件符号地址的变化

            如下,可见在产生.o的过程中,编译器并不知道a.c中调用的外部符号swap位于什么地址,于是使用0来代替。在链接后的执行文件ab中,swap已经被替换为实际的地址。

    1. root@xxzh:~/loader# arm-linux-gnueabihf-objdump -s -d a.o
    2. a.o: file format elf32-littlearm
    3. Contents of section .text:
    4. 0000 80b582b0 00af6423 7b603b1d 40f20001 ......d#{`;.@...
    5. 0010 c0f20001 1846fff7 feff0023 18460837 .....F.....#.F.7
    6. 0020 bd4680bd .F..
    7. Contents of section .comment:
    8. 0000 00474343 3a20284c 696e6172 6f204743 .GCC: (Linaro GC
    9. 0010 4320362e 322d3230 31362e31 31292036 C 6.2-2016.11) 6
    10. 0020 2e322e31 20323031 36313031 3600 .2.1 20161016.
    11. Contents of section .ARM.attributes:
    12. 0000 41320000 00616561 62690001 28000000 A2...aeabi..(...
    13. 0010 05372d41 00060a07 41080109 020a0412 .7-A....A.......
    14. 0020 04140115 01170318 0119011a 021c011e ................
    15. 0030 062201 .".
    16. Disassembly of section .text:
    17. 00000000
      :
    18. 0: b580 push {r7, lr}
    19. 2: b082 sub sp, #8
    20. 4: af00 add r7, sp, #0
    21. 6: 2364 movs r3, #100 ; 0x64
    22. 8: 607b str r3, [r7, #4]
    23. a: 1d3b adds r3, r7, #4
    24. c: f240 0100 movw r1, #0
    25. 10: f2c0 0100 movt r1, #0
    26. 14: 4618 mov r0, r3
    27. 16: f7ff fffe bl 0
    28. 1a: 2300 movs r3, #0
    29. 1c: 4618 mov r0, r3
    30. 1e: 3708 adds r7, #8
    31. 20: 46bd mov sp, r7
    32. 22: bd80 pop {r7, pc}
    33. root@xxzh:~/loader#
    34. root@xxzh:~/loader# arm-linux-gnueabihf-objdump -s -d b.o
    35. b.o: file format elf32-littlearm
    36. Contents of section .text:
    37. 0000 80b483b0 00af7860 39603b68 1a687b68 ......x`9`;h.h{h
    38. 0010 1a6000bf 18460c37 bd465df8 047b7047 .`...F.7.F]..{pG
    39. Contents of section .data:
    40. 0000 0c000000 ....
    41. Contents of section .comment:
    42. 0000 00474343 3a20284c 696e6172 6f204743 .GCC: (Linaro GC
    43. 0010 4320362e 322d3230 31362e31 31292036 C 6.2-2016.11) 6
    44. 0020 2e322e31 20323031 36313031 3600 .2.1 20161016.
    45. Contents of section .ARM.attributes:
    46. 0000 41320000 00616561 62690001 28000000 A2...aeabi..(...
    47. 0010 05372d41 00060a07 41080109 020a0412 .7-A....A.......
    48. 0020 04140115 01170318 0119011a 021c011e ................
    49. 0030 062201 .".
    50. Disassembly of section .text:
    51. 00000000 :
    52. 0: b480 push {r7}
    53. 2: b083 sub sp, #12
    54. 4: af00 add r7, sp, #0
    55. 6: 6078 str r0, [r7, #4]
    56. 8: 6039 str r1, [r7, #0]
    57. a: 683b ldr r3, [r7, #0]
    58. c: 681a ldr r2, [r3, #0]
    59. e: 687b ldr r3, [r7, #4]
    60. 10: 601a str r2, [r3, #0]
    61. 12: bf00 nop
    62. 14: 4618 mov r0, r3
    63. 16: 370c adds r7, #12
    64. 18: 46bd mov sp, r7
    65. 1a: f85d 7b04 ldr.w r7, [sp], #4
    66. 1e: 4770 bx lr
    67. root@xxzh:~/loader#
    68. root@xxzh:~/loader#
    69. root@xxzh:~/loader# arm-linux-gnueabihf-objdump -s -d ab
    70. ab: file format elf32-littlearm
    71. Contents of section .text:
    72. 10094 80b582b0 00af6423 7b603b1d 40f2d801 ......d#{`;.@...
    73. 100a4 c0f20201 184600f0 05f80023 18460837 .....F.....#.F.7
    74. 100b4 bd4680bd 80b483b0 00af7860 39603b68 .F........x`9`;h
    75. 100c4 1a687b68 1a6000bf 18460c37 bd465df8 .h{h.`...F.7.F].
    76. 100d4 047b7047 .{pG
    77. Contents of section .data:
    78. 200d8 0c000000 ....
    79. Contents of section .comment:
    80. 0000 4743433a 20284c69 6e61726f 20474343 GCC: (Linaro GCC
    81. 0010 20362e32 2d323031 362e3131 2920362e 6.2-2016.11) 6.
    82. 0020 322e3120 32303136 31303136 00 2.1 20161016.
    83. Contents of section .ARM.attributes:
    84. 0000 41320000 00616561 62690001 28000000 A2...aeabi..(...
    85. 0010 05372d41 00060a07 41080109 020a0412 .7-A....A.......
    86. 0020 04140115 01170318 0119011a 021c011e ................
    87. 0030 062201 .".
    88. Disassembly of section .text:
    89. 00010094
      :
    90. 10094: b580 push {r7, lr}
    91. 10096: b082 sub sp, #8
    92. 10098: af00 add r7, sp, #0
    93. 1009a: 2364 movs r3, #100 ; 0x64
    94. 1009c: 607b str r3, [r7, #4]
    95. 1009e: 1d3b adds r3, r7, #4
    96. 100a0: f240 01d8 movw r1, #216 ; 0xd8
    97. 100a4: f2c0 0102 movt r1, #2
    98. 100a8: 4618 mov r0, r3
    99. 100aa: f000 f805 bl 100b8
    100. 100ae: 2300 movs r3, #0
    101. 100b0: 4618 mov r0, r3
    102. 100b2: 3708 adds r7, #8
    103. 100b4: 46bd mov sp, r7
    104. 100b6: bd80 pop {r7, pc}
    105. 000100b8 :
    106. 100b8: b480 push {r7}
    107. 100ba: b083 sub sp, #12
    108. 100bc: af00 add r7, sp, #0
    109. 100be: 6078 str r0, [r7, #4]
    110. 100c0: 6039 str r1, [r7, #0]
    111. 100c2: 683b ldr r3, [r7, #0]
    112. 100c4: 681a ldr r2, [r3, #0]
    113. 100c6: 687b ldr r3, [r7, #4]
    114. 100c8: 601a str r2, [r3, #0]
    115. 100ca: bf00 nop
    116. 100cc: 4618 mov r0, r3
    117. 100ce: 370c adds r7, #12
    118. 100d0: 46bd mov sp, r7
    119. 100d2: f85d 7b04 ldr.w r7, [sp], #4
    120. 100d6: 4770 bx lr
    121. root@xxzh:~/loader#
    122. root@xxzh:~/loader#

    重定位表

            如上,在链接后,a.c调用b.c中swap被填上了正确的地址,但是链接的时候,如何知道a.c中哪些符号需要被重新修正地址呢,这些信息存储在a.o的重定位表中。

    查看a.o的重定位表,如下,可见只有text段存在重定位需要。且其偏移也给出了。

    1. root@xxzh:~/loader# arm-linux-gnueabihf-objdump -d a.o
    2. a.o: file format elf32-littlearm
    3. Disassembly of section .text:
    4. 00000000
      :
    5. 0: b580 push {r7, lr}
    6. 2: b082 sub sp, #8
    7. 4: af00 add r7, sp, #0
    8. 6: 2364 movs r3, #100 ; 0x64
    9. 8: 607b str r3, [r7, #4]
    10. a: 1d3b adds r3, r7, #4
    11. c: f240 0100 movw r1, #0
    12. 10: f2c0 0100 movt r1, #0
    13. 14: 4618 mov r0, r3
    14. 16: f7ff fffe bl 0
    15. 1a: 2300 movs r3, #0
    16. 1c: 4618 mov r0, r3
    17. 1e: 3708 adds r7, #8
    18. 20: 46bd mov sp, r7
    19. 22: bd80 pop {r7, pc}
    20. root@xxzh:~/loader#
    21. root@xxzh:~/loader#
    22. root@xxzh:~/loader# arm-linux-gnueabihf-objdump -r a.o
    23. a.o: file format elf32-littlearm
    24. RELOCATION RECORDS FOR [.text]:
    25. OFFSET TYPE VALUE
    26. 0000000c R_ARM_THM_MOVW_ABS_NC share
    27. 00000010 R_ARM_THM_MOVT_ABS share
    28. 00000016 R_ARM_THM_CALL swap
    29. root@xxzh:~/loader#

    符号解析

            如上知道了哪些要进行重定位,于是需要在多个目标文件中进行符号查找,从而确认重定位地址。

            如下,a.o中未定义的符号在b.o中定义。

    1. root@xxzh:~/loader# arm-linux-gnueabihf-readelf -s a.o
    2. Symbol table '.symtab' contains 12 entries:
    3. Num: Value Size Type Bind Vis Ndx Name
    4. 0: 00000000 0 NOTYPE LOCAL DEFAULT UND
    5. 1: 00000000 0 FILE LOCAL DEFAULT ABS a.c
    6. 2: 00000000 0 SECTION LOCAL DEFAULT 1
    7. 3: 00000000 0 SECTION LOCAL DEFAULT 3
    8. 4: 00000000 0 SECTION LOCAL DEFAULT 4
    9. 5: 00000000 0 NOTYPE LOCAL DEFAULT 1 $t
    10. 6: 00000000 0 SECTION LOCAL DEFAULT 6
    11. 7: 00000000 0 SECTION LOCAL DEFAULT 5
    12. 8: 00000000 0 SECTION LOCAL DEFAULT 7
    13. 9: 00000001 36 FUNC GLOBAL DEFAULT 1 main
    14. 10: 00000000 0 NOTYPE GLOBAL DEFAULT UND share
    15. 11: 00000000 0 NOTYPE GLOBAL DEFAULT UND swap
    16. root@xxzh:~/loader#
    17. root@xxzh:~/loader# arm-linux-gnueabihf-readelf -s b.o
    18. Symbol table '.symtab' contains 12 entries:
    19. Num: Value Size Type Bind Vis Ndx Name
    20. 0: 00000000 0 NOTYPE LOCAL DEFAULT UND
    21. 1: 00000000 0 FILE LOCAL DEFAULT ABS b.c
    22. 2: 00000000 0 SECTION LOCAL DEFAULT 1
    23. 3: 00000000 0 SECTION LOCAL DEFAULT 2
    24. 4: 00000000 0 SECTION LOCAL DEFAULT 3
    25. 5: 00000000 0 NOTYPE LOCAL DEFAULT 2 $d
    26. 6: 00000000 0 NOTYPE LOCAL DEFAULT 1 $t
    27. 7: 00000000 0 SECTION LOCAL DEFAULT 5
    28. 8: 00000000 0 SECTION LOCAL DEFAULT 4
    29. 9: 00000000 0 SECTION LOCAL DEFAULT 6
    30. 10: 00000000 4 OBJECT GLOBAL DEFAULT 2 share
    31. 11: 00000001 32 FUNC GLOBAL DEFAULT 1 swap
    32. root@xxzh:~/loader#

    链接脚本

            写一个简单的连接脚本,再来编译出ab可执行程序,然后查看虚拟地址空间地址。

    1. root@xxzh:~/loader# cat imx6ull.lds
    2. SECTIONS {
    3. . = 0x80100000;
    4. . = ALIGN(4);
    5. .text :
    6. {
    7. *(.text)
    8. }
    9. . = ALIGN(4);
    10. .rodata : { *(.rodata) }
    11. . = ALIGN(4);
    12. .data : { *(.data) }
    13. . = ALIGN(4);
    14. __bss_start = .;
    15. .bss : { *(.bss) *(.COMMON) }
    16. __bss_end = .;
    17. }
    1. root@xxzh:~/loader# arm-linux-gnueabihf-objdump -h ab
    2. ab: file format elf32-littlearm
    3. Sections:
    4. Idx Name Size VMA LMA File off Algn
    5. 0 .text 00000044 80100000 80100000 00010000 2**1
    6. CONTENTS, ALLOC, LOAD, READONLY, CODE
    7. 1 .data 00000004 80100044 80100044 00010044 2**2
    8. CONTENTS, ALLOC, LOAD, DATA
    9. 2 .comment 0000002d 00000000 00000000 00010048 2**0
    10. CONTENTS, READONLY
    11. 3 .ARM.attributes 00000033 00000000 00000000 00010075 2**0
    12. CONTENTS, READONLY
    13. root@xxzh:~/loader#

            如上,在链接后text段在0x80100000 开始了,通过链接脚本控制了可执行文件中段表的排布。  

    总结:

            所谓静态链接,最直观的解释就是将一系列目标文件的各个段表合并到一起,形成一个总的段表,然后根据符号及重定位表按照链接脚本进行地址修正,使得所有的符号都有一个正确的地址。所有依赖的符号实现均被打包在一起的一个可执行程序。

            往往静态链接后的可执行文件比较大,不依赖于外部函数库,在执行程序内部有所有符号的实现。

    程序的装载

            写好编译好的程序总要执行起来,装载是程序从磁盘上的可执行ELF文件加载到内存中,通过进程的方式开始动态执行的过程,也就是程序到进程的过程。

    进程虚拟地址空间

            每个进程都拥有自己独立的进程空间,也就是虚拟地址空间。对于linux 32位系统机有4G空间,其中1G给内核使用,3G给用户空间使用。windows下是2G/2G。操作系统要管理进行对地址的使用,访问了未给其的空间,系统将产生异常,结束进程。

    装载方式-页映射

            将磁盘中的数据和指令 以及 内存 安装 页为单位分成若干个页。装载的单元以页为单位进行。页的大小根据平台有4KB,8KB,2MB,4MB等,常用的以4K为主。

            假设,内存只有16K(4个页)。

            程序有32K(8个页) P0-P7> 内存大小,动态以页为单位进行装载。

            首先将程序入口所在页P0装入内存,然后用到程序中的某些内容在页内不存在则再加载一个页。若内存被占用满,则舍弃一个当前未使用的页。舍弃的算法有FIFO(先进先出)、LUR(最少使用算法)。

            控制装载的单位是操作系统中的存储管理器。

            如上有一个文件,就是程序的不同位置可能被装载到内存不同位置,于是每次装载都需要重定位操作,由于当前硬件MMU的存在,操作将以虚拟地址的方式进行。

    操作系统装载过程

            新程序需要执行的时候,由操作系统进行管理维护。操作系统需要:

    • 创建虚拟地址空间(创建虚拟地址到物理地址映射的数据结构集合,页目录)
    • 读取程序头部,建立虚拟地址空间和可执行文件的映射
    • CPU指令设置为可执行程序入口,启动执行

    创建进程地址空间

            实际创建的是物理到虚拟地址空间的映射的数据结构,页目录。

    VMA虚拟内存区域

            虚拟地址与执行文件直接的映射关系。

            进行虚拟地址空间中的一个段。一个段对应一个VMA。如代码段在虚拟地址空间中存在一个叫做text的VMA空间,并且有对应的虚拟地址空间范围,如0x08048000-0x08049000,对应了text端偏移0的位置。

            程序发生段错误的时候,通过查找VMA定位到可执行程序中的位置,进行页映射。

     CPU到程序入口

            切换内核堆栈和用户堆栈,CPU权限切换,跳转到elf头中给出的执行入口函数。

    执行期间的页错误

            CPU将控制权给到进程后,由于前期操作系统只是完成了映射,而为进行实际的物理地址分配。当代码段第一条执行开始执行,发现是一个空页面,没有实际的物理空间对应,于是发生页错误,cpu将控制器给到操作系统,操作系统通过页错误处理机制开始管理,操作系统查询数据结构,找到空页面所在VMA,根据此偏移找到在可执行程序中的偏移,在物理内存分配空间,建立虚拟内存与物理内存的映射关系,之后将控制器给到进程,重新从页错误的位置开始执行。

            程序的不断执行,页错误不断发生,操作系统不断将物理地址映射,当内存不足的时候可能将内存进行回收,再次需要的时候进行重映射,从而更好满足多个进程对内存的使用需求。是虚拟存储管理的内容。

    相似段表组合映射

            elf中可能存在多个端,如text,data,bss,init。由于映射是以页为单位,如果一个段对应一个VMA,那么将产生更多的VMA单元,也就设计更多的页被占用。

            为了节省空间,可以将具有相似权限的段section进行合并,产生一个segment。如init段和text都是只读的,组合成一个VMA映射,从而可能节省更多的空间。

            因此系统按照segment(程序头)来进行映射,而不是section。

    ELF的链接视图和执行视图

            ELF中存在段表,上面最初分析ELF的时候就是段表,即ELF中存在多个段,每个段(section)位置。ELF也可能存在程序头表,是以执行角度出发对ELF进行分段(segment),segment是将section进行一些合并,从而更好的在执行阶段进行映射,提示物理空间使用效率。是两种不同的划分角度。

            目标文件不需要装载,没有程序表头,最后可执行文件和动态库文件需要装载,都有程序表头。

    栈和堆的VMA

            栈和堆在虚拟地址空间也都是对应一个VMA。

            虚拟地址空间往往有代码VMA、数据VMA、堆VMA、栈VMA。

    相邻segment的共享映射

            内存的使用总是仔细的,任何浪费内存的思路都将在操作系统中优化。如果以segment为单位进行映射,如果5个segment占用总和是3个页面,那么这样映射 有2个页面将是浪费。于是将段对齐的情况下,进行组合映射。

            也就是说多个segment可能共用一个页面。

    BASH角度装载过程分析

            在终端执行一个可执行程序后,bash会fork一个进程,然后在该进程中调用系统调用开始装载。过程如下

    • bash fork()一个新进程
    • 新进程调用execve系列系统调用。
    • execve查找可执行文件路径,读取头部128字节。
    • execve判断类型(elf、#!脚本) serch_binary_handle()根据前4字节魔术找到类型
    • serch_binary_handle()调用load_elf_binary()进行后续装载工作
    • 检查并解析elf头部,如魔术、段表、程序表头信息
    • 寻找动态链接".interp"段(动态链接使用)
    • 根据程序表头进行映射(代码、数据、只读数据等)
    • 初始化elf环境(相关寄存器设置等,如EDX)
    • 系统调用返回地址改为程序入口地址
    • load_elf_binary->do_execve结束->sys_execve结束->elf入口开始执行。
    • 装载结束
  • 相关阅读:
    langchain 加载各种格式文件读取方法
    mfc140.dll是什么文件?如何修复mfc140.dll丢失的方法分享
    代码整洁之道
    微信小程序(一)
    全栈工程师必须要掌握的前端Html技能
    视频监控智能分析系统
    图卷积网络(Graph Convolutional Network, GCN)
    自己编译静态ffmpeg freetype2 not found问题解决
    Codeforces Round 901 (Div. 2) | JorbanS
    【C语言实现】----动态/文件/静态通讯录
  • 原文地址:https://blog.csdn.net/fengyuwuzu0519/article/details/126085986