可执行程序的编译、链接、装载"/>
linux可执行程序的编译、链接、装载
在编译链接方面《程序员的自我修养-链接、装载与库》一书讲的很不错,这篇文章一部分是读此书后的记录,另一部分是自己对可执行程序在编译、链接装载产生的理解总结。
很多时候,入门总是很容易,因为达到了效果,得到了结果。但这其中往往还有很多细节值值得思考,因为很多结果的实现是站在了别人的肩膀上。
本文首先总结编译的4个的过程,熟悉编译中的几个步骤(预处理、编译、汇编、链接),再分析编译后的ELF文件(目标文件和可执行)的结构,弄清编译后文件的排布及含有哪些信息,从而分析静态链接中如何使用这些信息进行重定位操作。之后,分析程序的装载过程,及动态链接的过程。
编译链接四步骤
helloworld的编译链接执行
对于大部分进行C/C++,JAVA,C#等各种程序开发的人员来说,入门之初应该都是从打印一行字符串开始。
hello.c#include<stdio.h>int main(void)
{printf("hello world\r\n");return 0;
}
然后拿来gcc最简单的一条编译指令:
gcc hello.c -o hello
产生可执行文件hello后运行。
root@xxzh:~/loader# ./hello
hello world
root@xxzh:~/loader#
似乎很快就完成了入门实验,但这背后隐含了很多内容。
编译链接过程
如上直接-o输出了可执行程序hello。可谓是一步到位,但编译器这么痛快的用,还可以一步一步的用。因为上面的过程可以被拆分为多个步骤,通常为4个步骤。
第一步:预编译(.c到.i cc1)
gcc -E hello.c -o hello.i
看下执行后hello.i 有多少行,如下,856行。代码只有不到10行,预编译后产生了一个800+行的文件
root@xxzh:~/loader# gcc -E hello.c -o hello.i
root@xxzh:~/loader# wc -l hello.i
856 hello.i
root@xxzh:~/loader#
打开,hello.i 看最底部,自己编写的代码部分并没有变化,可见,预编译主要是处理代码中以“#”开头的预编译指令的。#include<stdio.h>被替换为了一系列内容。
如果在.c 在定义一个宏,#define A 123,然后printf的时候他们它。预编译后查看.i是:
printf("hello world A=%d\r\n", 123);
可见预编译直接把宏替换为了实际的数值。
预编译主要任务:
预编译主要完成对“#”开头的预编译指令的处理
- 处理 # 开头的预处理指令
- 删除注释
- 最一些前期基础的工作,搜集信息,前期处理
第二步:编译(.i到.S cc1)
gcc -S hello.i -o hello.s 或者
gcc -S hello.c -o hello.s
进行词法分析、语义分析、产生汇编代码
如果在hello.c中的main下,随便输入一些字符串,在预处理阶段是不会报告任何错误的,当汇编的时候才会提示存在error。这就是汇编阶段的语法检查。
预编译和编译是由编译器的cc1来完成的。gcc命令是对cc1的包装,根据传入的参数,执行不同的动作。
编译的主要任务:
编译主要完成高级语言到汇编语言的转换。
- 词法检查
- 语法分析
- 语义分析
- 优化
- 生成汇编文件
- 中间语言生产
第三步:汇编(.s到.o as )
由汇编器as来完成,将汇编代码,转换成机器指令。一个汇编对应一个代码形式的机器指令码,汇编的过程就是根据这个对应关系将汇编码转换成机器认识的机器码。
ac hello.s -o hello.o 或者
gcc -c hello.s -o hello.o
注意 .o文件被称作为目标文件。
汇编主要完成汇编语言到机器语言的转换。
第四步:链接(ld)
第三步不是已经产生机器认可的机器码了吗,为什么需要链接呢?执行一下看看,可见并不认为.o是可执行文件。是 LSB relocatable,而不是LSB executable。
root@xxzh:~/loader# ./hello.o
-bash: ./hello.o: cannot execute binary file: Exec format error
root@xxzh:~/loader#
root@xxzh:~/loader# file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
root@xxzh:~/loader# file hello
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
root@xxzh:~/loader#
再重新加上-v编译一下,打印出编译的详细过程:如下
root@xxzh:~/loader# gcc hello.c -o hello -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper
Target: x86_64-linux-gnu
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
Thread model: posix
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.12)
COLLECT_GCC_OPTIONS='-o' 'hello' '-v' '-mtune=generic' '-march=x86-64'/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
GNU C11 (Ubuntu 5.4.0-6ubuntu1~16.04.12) version 5.4.0 20160609 (x86_64-linux-gnu)compiled by GNU C version 5.4.0 20160609, GMP version 6.1.0, MPFR version 3.1.4, MPC version 1.0.3
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/5/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:/usr/lib/gcc/x86_64-linux-gnu/5/include/usr/local/include/usr/lib/gcc/x86_64-linux-gnu/5/include-fixed/usr/include/x86_64-linux-gnu/usr/include
End of search list.
GNU C11 (Ubuntu 5.4.0-6ubuntu1~16.04.12) version 5.4.0 20160609 (x86_64-linux-gnu)compiled by GNU C version 5.4.0 20160609, GMP version 6.1.0, MPFR version 3.1.4, MPC version 1.0.3
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: 8087146d2ee737d238113fb57fabb1f2
COLLECT_GCC_OPTIONS='-o' 'hello' '-v' '-mtune=generic' '-march=x86-64'as -v --64 -o /tmp/cc2IuGAa.o /tmp/ccmgY0bt.s
GNU assembler version 2.26.1 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.26.1
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/
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/
COLLECT_GCC_OPTIONS='-o' 'hello' '-v' '-mtune=generic' '-march=x86-64'/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中,经常看到如下通配编译命令。
.c.o:$(CC) $(INCLUDE) $(CFLAGS) -c $<
重定位
链接过程中,将各个目标符号地址确定的过程叫做重定位。目标符号包含函数及全局变量等。在编译多个.c的时候,若a.c调用了b.c中的函数,在编译a.o的时候并不知道b.o中函数的地址,因此在链接的时候需要确定下来,这就是重定位。当前a引用b中变量也存在这个过程。
ELF文件格式
首先,综合列出ELF常用命令
- 查看elf头内容
readelf -h
- 查看段表分布
readelf -S
- 查看主要段大小
size a.o
- 段内容十六进制展示
objdump -s ab
- 指令段反汇编
objdump -d ab
- 查看符号表
readelf -s
全程:Executable Linkable Format。linux下可执行文件格式,其中,目标文件、可执行文件、静态库、动态库等都以ELF格式生成。
file可以查看文件属性,有ELF 64-bit LSB shared object、ELF 64-bit LSB relocatable、ELF 64-bit LSB executable等。
root@xxzh:/# file ./lib/x86_64-linux-gnu/libc-2.23.so
./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
root@xxzh:~/loader# file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
root@xxzh:~/loader# file hello
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
root@xxzh:~/loader#
ELF头
读一下可执行文件、目标中间的elf头如下。
包含信息:ELF魔幻数:特定的标识,识别为elf文件、机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、短表位置和长度、短表数量。readelf -h 查看ELF文件头
root@xxzh:~/loader# arm-linux-gnueabihf-readelf -h hello
ELF Header:Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32Data: 2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: EXEC (Executable file)Machine: ARMVersion: 0x1Entry point address: 0x102f1Start of program headers: 52 (bytes into file)Start of section headers: 8436 (bytes into file)Flags: 0x5000400, Version5 EABI, hard-float ABISize of this header: 52 (bytes)Size of program headers: 32 (bytes)Number of program headers: 8Size of section headers: 40 (bytes)Number of section headers: 38Section header string table index: 35
root@xxzh:~/loader#
root@xxzh:~/loader# arm-linux-gnueabihf-readelf -h hello.o
ELF Header:Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32Data: 2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: REL (Relocatable file)Machine: ARMVersion: 0x1Entry point address: 0x0Start of program headers: 0 (bytes into file)Start of section headers: 568 (bytes into file)Flags: 0x5000000, Version5 EABISize of this header: 52 (bytes)Size of program headers: 0 (bytes)Number of program headers: 0Size of section headers: 40 (bytes)Number of section headers: 12Section header string table index: 9
root@xxzh:~/loader#
数据结构定义
typedef struct
{unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */Elf32_Half e_type; /* Object file type */Elf32_Half e_machine; /* Architecture */Elf32_Word e_version; /* Object file version */Elf32_Addr e_entry; /* Entry point virtual address */Elf32_Off e_phoff; /* Program header table file offset */Elf32_Off e_shoff; /* Section header table file offset */Elf32_Word e_flags; /* Processor-specific flags */Elf32_Half e_ehsize; /* ELF header size in bytes */Elf32_Half e_phentsize; /* Program header table entry size */Elf32_Half e_phnum; /* Program header table entry count */Elf32_Half e_shentsize; /* Section header table entry size */Elf32_Half e_shnum; /* Section header table entry count */Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;
段表
指示了该elf文件中,段的分布位置,段名、段偏移地址、段的大小。
从上面头部可以知道段表的数量,通过readelf -S hello可以查看每个段表内容。
root@xxzh:~/loader# arm-linux-gnueabihf-readelf -S hello
There are 38 section headers, starting at offset 0x20f4:Section Headers:[Nr] Name Type Addr Off Size ES Flg Lk Inf Al[ 0] NULL 00000000 000000 000000 00 0 0 0[ 1] .interp PROGBITS 00010134 000134 000019 00 A 0 0 1[ 2] .note.ABI-tag NOTE 00010150 000150 000020 00 A 0 0 4[ 3] .note.gnu.build-i NOTE 00010170 000170 000024 00 A 0 0 4[ 4] .hash HASH 00010194 000194 000028 04 A 5 0 4[ 5] .dynsym DYNSYM 000101bc 0001bc 000050 10 A 6 1 4[ 6] .dynstr STRTAB 0001020c 00020c 000041 00 A 0 0 1[ 7] .gnu.version VERSYM 0001024e 00024e 00000a 02 A 5 0 2[ 8] .gnu.version_r VERNEED 00010258 000258 000020 00 A 6 1 4[ 9] .rel.dyn REL 00010278 000278 000008 08 A 5 0 4[10] .rel.plt REL 00010280 000280 000020 08 AI 5 22 4[11] .init PROGBITS 000102a0 0002a0 00000c 00 AX 0 0 4[12] .plt PROGBITS 000102ac 0002ac 000044 04 AX 0 0 4[13] .text PROGBITS 000102f0 0002f0 00013c 00 AX 0 0 4[14] .fini PROGBITS 0001042c 00042c 000008 00 AX 0 0 4[15] .rodata PROGBITS 00010434 000434 000011 00 A 0 0 4[16] .ARM.exidx ARM_EXIDX 00010448 000448 000008 00 AL 13 0 4[17] .eh_frame PROGBITS 00010450 000450 000004 00 A 0 0 4[18] .init_array INIT_ARRAY 00020454 000454 000004 04 WA 0 0 4[19] .fini_array FINI_ARRAY 00020458 000458 000004 04 WA 0 0 4[20] .jcr PROGBITS 0002045c 00045c 000004 00 WA 0 0 4[21] .dynamic DYNAMIC 00020460 000460 0000e8 08 WA 6 0 4[22] .got PROGBITS 00020548 000548 000020 04 WA 0 0 4[23] .data PROGBITS 00020568 000568 000008 00 WA 0 0 4[24] .bss NOBITS 00020570 000570 000004 00 WA 0 0 1[25] ment PROGBITS 00000000 000570 00002d 01 MS 0 0 1[26] .ARM.attributes ARM_ATTRIBUTES 00000000 00059d 000033 00 0 0 1[27] .debug_aranges PROGBITS 00000000 0005d0 0000b0 00 0 0 8[28] .debug_info PROGBITS 00000000 000680 00049b 00 0 0 1[29] .debug_abbrev PROGBITS 00000000 000b1b 00018e 00 0 0 1[30] .debug_line PROGBITS 00000000 000ca9 000276 00 0 0 1[31] .debug_frame PROGBITS 00000000 000f20 000044 00 0 0 4[32] .debug_str PROGBITS 00000000 000f64 000373 01 MS 0 0 1[33] .debug_loc PROGBITS 00000000 0012d7 0000dd 00 0 0 1[34] .debug_ranges PROGBITS 00000000 0013b8 000048 00 0 0 8[35] .shstrtab STRTAB 00000000 001f87 00016c 00 0 0 1[36] .symtab SYMTAB 00000000 001400 000700 10 37 90 4[37] .strtab STRTAB 00000000 001b00 000487 00 0 0 1
Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings)I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)O (extra OS processing required) o (OS specific), p (processor specific)
root@xxzh:~/loader#
root@xxzh:~/loader#
root@xxzh:~/loader# arm-linux-gnueabihf-readelf -S hello.o
There are 12 section headers, starting at offset 0x238:Section Headers:[Nr] Name Type Addr Off Size ES Flg Lk Inf Al[ 0] NULL 00000000 000000 000000 00 0 0 0[ 1] .text PROGBITS 00000000 000034 000016 00 AX 0 0 2[ 2] .rel.text REL 00000000 0001bc 000018 08 I 10 1 4[ 3] .data PROGBITS 00000000 00004a 000000 00 WA 0 0 1[ 4] .bss NOBITS 00000000 00004a 000000 00 WA 0 0 1[ 5] .rodata PROGBITS 00000000 00004c 00000d 00 A 0 0 4[ 6] ment PROGBITS 00000000 000059 00002e 01 MS 0 0 1[ 7] .note.GNU-stack PROGBITS 00000000 000087 000000 00 0 0 1[ 8] .ARM.attributes ARM_ATTRIBUTES 00000000 000087 000033 00 0 0 1[ 9] .shstrtab STRTAB 00000000 0001d4 000061 00 0 0 1[10] .symtab SYMTAB 00000000 0000bc 0000e0 10 11 12 4[11] .strtab STRTAB 00000000 00019c 00001e 00 0 0 1
Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings)I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)O (extra OS processing required) o (OS specific), p (processor specific)
root@xxzh:~/loader#
段表数据结构
typedef struct elf32_shdr { Elf32_Word sh_name; Elf32_Word sh_type; Elf32_Word sh_flags; Elf32_Addr sh_addr; Elf32_Off sh_offset; Elf32_Word sh_size; Elf32_Word sh_link; Elf32_Word sh_info; Elf32_Word sh_addralign;Elf32_Word sh_entsize;
} Elf32_Shdr;
代码段
.text
数据段
.data 初始化了的全局静态变量和局部静态变量
BSS段
.bss 未初始化的全局变量和局部静态变量
分段的优点
- 更安全:代码段只读、数据段可读写
- 节省空间:代码段共享
重定位表
.rel.text text段重定位表
字符串表
.shstrtab 段名,变量名的统一存储。
符号表
.symtab 符号表
静态链接
将所有依赖打包到一起,可执行文件比较大,但不依赖外部函数库的链接方式。
目标文件与可执行文件的段属性变化
a.c中调用b.c中的函数,b作为a的库提供者。
//a.c
extern int share;int swap(int *a, int *b);int main()
{int a = 100;swap(&a, &share);
}//b.cint share = 12;int swap(int *a, int *b)
{*a = *b;
}
编译产生.o 目标文件
arm-linux-gnueabihf-gcc -c a.c b.c
链接出ab可执行文件
arm-linux-gnueabihf-ld a.o b.o -e main -o ab
查看段属性
root@xxzh:~/loader# arm-linux-gnueabihf-objdump -h a.o a.o: file format elf32-littlearmSections:
Idx Name Size VMA LMA File off Algn0 .text 00000024 00000000 00000000 00000034 2**1CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE1 .data 00000000 00000000 00000000 00000058 2**0CONTENTS, ALLOC, LOAD, DATA2 .bss 00000000 00000000 00000000 00000058 2**0ALLOC3 ment 0000002e 00000000 00000000 00000058 2**0CONTENTS, READONLY4 .note.GNU-stack 00000000 00000000 00000000 00000086 2**0CONTENTS, READONLY5 .ARM.attributes 00000033 00000000 00000000 00000086 2**0CONTENTS, READONLY
root@xxzh:~/loader# arm-linux-gnueabihf-objdump -h b.ob.o: file format elf32-littlearmSections:
Idx Name Size VMA LMA File off Algn0 .text 00000020 00000000 00000000 00000034 2**1CONTENTS, ALLOC, LOAD, READONLY, CODE1 .data 00000004 00000000 00000000 00000054 2**2CONTENTS, ALLOC, LOAD, DATA2 .bss 00000000 00000000 00000000 00000058 2**0ALLOC3 ment 0000002e 00000000 00000000 00000058 2**0CONTENTS, READONLY4 .note.GNU-stack 00000000 00000000 00000000 00000086 2**0CONTENTS, READONLY5 .ARM.attributes 00000033 00000000 00000000 00000086 2**0CONTENTS, READONLY
root@xxzh:~/loader# arm-linux-gnueabihf-objdump -h abab: file format elf32-littlearmSections:
Idx Name Size VMA LMA File off Algn0 .text 00000044 00010094 00010094 00000094 2**2CONTENTS, ALLOC, LOAD, READONLY, CODE1 .data 00000004 000200d8 000200d8 000000d8 2**2CONTENTS, ALLOC, LOAD, DATA2 ment 0000002d 00000000 00000000 000000dc 2**0CONTENTS, READONLY3 .ARM.attributes 00000033 00000000 00000000 00000109 2**0CONTENTS, READONLY
root@xxzh:~/loader#
可见ab的text段大小 0x44=0x24+0x20。
目标文件与可执行文件符号地址的变化
如下,可见在产生.o的过程中,编译器并不知道a.c中调用的外部符号swap位于什么地址,于是使用0来代替。在链接后的执行文件ab中,swap已经被替换为实际的地址。
root@xxzh:~/loader# arm-linux-gnueabihf-objdump -s -d a.oa.o: file format elf32-littlearmContents of section .text:0000 80b582b0 00af6423 7b603b1d 40f20001 ......d#{`;.@...0010 c0f20001 1846fff7 feff0023 18460837 .....F.....#.F.70020 bd4680bd .F..
Contents of section ment:0000 00474343 3a20284c 696e6172 6f204743 .GCC: (Linaro GC0010 4320362e 322d3230 31362e31 31292036 C 6.2-2016.11) 60020 2e322e31 20323031 36313031 3600 .2.1 20161016.
Contents of section .ARM.attributes:0000 41320000 00616561 62690001 28000000 A2...aeabi..(...0010 05372d41 00060a07 41080109 020a0412 .7-A....A.......0020 04140115 01170318 0119011a 021c011e ................0030 062201 .". Disassembly of section .text:00000000 <main>:0: b580 push {r7, lr}2: b082 sub sp, #84: af00 add r7, sp, #06: 2364 movs r3, #100 ; 0x648: 607b str r3, [r7, #4]a: 1d3b adds r3, r7, #4c: f240 0100 movw r1, #010: f2c0 0100 movt r1, #014: 4618 mov r0, r316: f7ff fffe bl 0 <swap>1a: 2300 movs r3, #01c: 4618 mov r0, r31e: 3708 adds r7, #820: 46bd mov sp, r722: bd80 pop {r7, pc}
root@xxzh:~/loader#
root@xxzh:~/loader# arm-linux-gnueabihf-objdump -s -d b.ob.o: file format elf32-littlearmContents of section .text:0000 80b483b0 00af7860 39603b68 1a687b68 ......x`9`;h.h{h0010 1a6000bf 18460c37 bd465df8 047b7047 .`...F.7.F]..{pG
Contents of section .data:0000 0c000000 ....
Contents of section ment:0000 00474343 3a20284c 696e6172 6f204743 .GCC: (Linaro GC0010 4320362e 322d3230 31362e31 31292036 C 6.2-2016.11) 60020 2e322e31 20323031 36313031 3600 .2.1 20161016.
Contents of section .ARM.attributes:0000 41320000 00616561 62690001 28000000 A2...aeabi..(...0010 05372d41 00060a07 41080109 020a0412 .7-A....A.......0020 04140115 01170318 0119011a 021c011e ................0030 062201 .". Disassembly of section .text:00000000 <swap>:0: b480 push {r7}2: b083 sub sp, #124: af00 add r7, sp, #06: 6078 str r0, [r7, #4]8: 6039 str r1, [r7, #0]a: 683b ldr r3, [r7, #0]c: 681a ldr r2, [r3, #0]e: 687b ldr r3, [r7, #4]10: 601a str r2, [r3, #0]12: bf00 nop14: 4618 mov r0, r316: 370c adds r7, #1218: 46bd mov sp, r71a: f85d 7b04 ldr.w r7, [sp], #41e: 4770 bx lr
root@xxzh:~/loader#
root@xxzh:~/loader#
root@xxzh:~/loader# arm-linux-gnueabihf-objdump -s -d abab: file format elf32-littlearmContents of section .text:10094 80b582b0 00af6423 7b603b1d 40f2d801 ......d#{`;.@...100a4 c0f20201 184600f0 05f80023 18460837 .....F.....#.F.7100b4 bd4680bd 80b483b0 00af7860 39603b68 .F........x`9`;h100c4 1a687b68 1a6000bf 18460c37 bd465df8 .h{h.`...F.7.F].100d4 047b7047 .{pG
Contents of section .data:200d8 0c000000 ....
Contents of section ment:0000 4743433a 20284c69 6e61726f 20474343 GCC: (Linaro GCC0010 20362e32 2d323031 362e3131 2920362e 6.2-2016.11) 6.0020 322e3120 32303136 31303136 00 2.1 20161016.
Contents of section .ARM.attributes:0000 41320000 00616561 62690001 28000000 A2...aeabi..(...0010 05372d41 00060a07 41080109 020a0412 .7-A....A.......0020 04140115 01170318 0119011a 021c011e ................0030 062201 .". Disassembly of section .text:00010094 <main>:10094: b580 push {r7, lr}10096: b082 sub sp, #810098: af00 add r7, sp, #01009a: 2364 movs r3, #100 ; 0x641009c: 607b str r3, [r7, #4]1009e: 1d3b adds r3, r7, #4100a0: f240 01d8 movw r1, #216 ; 0xd8100a4: f2c0 0102 movt r1, #2100a8: 4618 mov r0, r3100aa: f000 f805 bl 100b8 <swap>100ae: 2300 movs r3, #0100b0: 4618 mov r0, r3100b2: 3708 adds r7, #8100b4: 46bd mov sp, r7100b6: bd80 pop {r7, pc}000100b8 <swap>:100b8: b480 push {r7}100ba: b083 sub sp, #12100bc: af00 add r7, sp, #0100be: 6078 str r0, [r7, #4]100c0: 6039 str r1, [r7, #0]100c2: 683b ldr r3, [r7, #0]100c4: 681a ldr r2, [r3, #0]100c6: 687b ldr r3, [r7, #4]100c8: 601a str r2, [r3, #0]100ca: bf00 nop100cc: 4618 mov r0, r3100ce: 370c adds r7, #12100d0: 46bd mov sp, r7100d2: f85d 7b04 ldr.w r7, [sp], #4100d6: 4770 bx lr
root@xxzh:~/loader#
root@xxzh:~/loader#
重定位表
如上,在链接后,a.c调用b.c中swap被填上了正确的地址,但是链接的时候,如何知道a.c中哪些符号需要被重新修正地址呢,这些信息存储在a.o的重定位表中。
查看a.o的重定位表,如下,可见只有text段存在重定位需要。且其偏移也给出了。
root@xxzh:~/loader# arm-linux-gnueabihf-objdump -d a.o a.o: file format elf32-littlearmDisassembly of section .text:00000000 <main>:0: b580 push {r7, lr}2: b082 sub sp, #84: af00 add r7, sp, #06: 2364 movs r3, #100 ; 0x648: 607b str r3, [r7, #4]a: 1d3b adds r3, r7, #4c: f240 0100 movw r1, #010: f2c0 0100 movt r1, #014: 4618 mov r0, r316: f7ff fffe bl 0 <swap>1a: 2300 movs r3, #01c: 4618 mov r0, r31e: 3708 adds r7, #820: 46bd mov sp, r722: bd80 pop {r7, pc}
root@xxzh:~/loader#
root@xxzh:~/loader#
root@xxzh:~/loader# arm-linux-gnueabihf-objdump -r a.oa.o: file format elf32-littlearmRELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000c R_ARM_THM_MOVW_ABS_NC share
00000010 R_ARM_THM_MOVT_ABS share
00000016 R_ARM_THM_CALL swaproot@xxzh:~/loader#
符号解析
如上知道了哪些要进行重定位,于是需要在多个目标文件中进行符号查找,从而确认重定位地址。
如下,a.o中未定义的符号在b.o中定义。
root@xxzh:~/loader# arm-linux-gnueabihf-readelf -s a.o Symbol table '.symtab' contains 12 entries:Num: Value Size Type Bind Vis Ndx Name0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FILE LOCAL DEFAULT ABS a.c2: 00000000 0 SECTION LOCAL DEFAULT 1 3: 00000000 0 SECTION LOCAL DEFAULT 3 4: 00000000 0 SECTION LOCAL DEFAULT 4 5: 00000000 0 NOTYPE LOCAL DEFAULT 1 $t6: 00000000 0 SECTION LOCAL DEFAULT 6 7: 00000000 0 SECTION LOCAL DEFAULT 5 8: 00000000 0 SECTION LOCAL DEFAULT 7 9: 00000001 36 FUNC GLOBAL DEFAULT 1 main10: 00000000 0 NOTYPE GLOBAL DEFAULT UND share11: 00000000 0 NOTYPE GLOBAL DEFAULT UND swap
root@xxzh:~/loader#
root@xxzh:~/loader# arm-linux-gnueabihf-readelf -s b.oSymbol table '.symtab' contains 12 entries:Num: Value Size Type Bind Vis Ndx Name0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FILE LOCAL DEFAULT ABS b.c2: 00000000 0 SECTION LOCAL DEFAULT 1 3: 00000000 0 SECTION LOCAL DEFAULT 2 4: 00000000 0 SECTION LOCAL DEFAULT 3 5: 00000000 0 NOTYPE LOCAL DEFAULT 2 $d6: 00000000 0 NOTYPE LOCAL DEFAULT 1 $t7: 00000000 0 SECTION LOCAL DEFAULT 5 8: 00000000 0 SECTION LOCAL DEFAULT 4 9: 00000000 0 SECTION LOCAL DEFAULT 6 10: 00000000 4 OBJECT GLOBAL DEFAULT 2 share11: 00000001 32 FUNC GLOBAL DEFAULT 1 swap
root@xxzh:~/loader#
链接脚本
写一个简单的连接脚本,再来编译出ab可执行程序,然后查看虚拟地址空间地址。
root@xxzh:~/loader# cat imx6ull.lds
SECTIONS {. = 0x80100000;. = ALIGN(4);.text :{*(.text)}. = ALIGN(4);.rodata : { *(.rodata) }. = ALIGN(4);.data : { *(.data) }. = ALIGN(4);__bss_start = .;.bss : { *(.bss) *(.COMMON) }__bss_end = .;
}
root@xxzh:~/loader# arm-linux-gnueabihf-objdump -h abab: file format elf32-littlearmSections:
Idx Name Size VMA LMA File off Algn0 .text 00000044 80100000 80100000 00010000 2**1CONTENTS, ALLOC, LOAD, READONLY, CODE1 .data 00000004 80100044 80100044 00010044 2**2CONTENTS, ALLOC, LOAD, DATA2 ment 0000002d 00000000 00000000 00010048 2**0CONTENTS, READONLY3 .ARM.attributes 00000033 00000000 00000000 00010075 2**0CONTENTS, READONLY
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入口开始执行。
- 装载结束
动态链接
编译后链接改为运行装载时链接:
当程序被装载的时候,系统动态链接器(/lib/ld-2.22.so)将程序中所有动态链接库装载到进程地址空间,并将程序中所有未决议的符号绑定到相应的动态链接库中,进行重定位操作。
编译时需要动态库参与编译,使得连接器从中获取未决议符号是静态链接还是动态链接。
系统运行应以程序之前,首先把权限给到动态链接器,由其完成动态链接工作后,再把控制权交给应以程序。
静态链接的缺点
- 浪费磁盘空间
多个程序使用一个lib.o,那么内存中存在多个lib.o
- 程序发布后的维护麻烦
程序存在bug后,需要重新编译整个工程
动态链接库
--share -fPIC
arm-linux-gnueabihf-gcc -fPIC -shared -o libb.so b.c
root@xxzh:~/loader# arm-linux-gnueabihf-readelf -l libb.so Elf file type is DYN (Shared object file)
Entry point 0x424
There are 5 program headers, starting at offset 52Program Headers:Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg AlignLOAD 0x000000 0x00000000 0x00000000 0x00544 0x00544 R E 0x10000LOAD 0x000544 0x00010544 0x00010544 0x0011c 0x00120 RW 0x10000DYNAMIC 0x000550 0x00010550 0x00010550 0x000e0 0x000e0 RW 0x4NOTE 0x0000d4 0x000000d4 0x000000d4 0x00024 0x00024 R 0x4GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10Section to Segment mapping:Segment Sections...00 .note.gnu.build-id .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .eh_frame 01 .init_array .fini_array .jcr .dynamic .got .data .bss 02 .dynamic 03 .note.gnu.build-id 04
root@xxzh:~/loader#
load地址是0,可见共享对象的最终装载地址在编译时是不确定的,在装载时,装载器根据地址空间使用情况,动态分配虚拟地址空间给共享对象。
地址无关码
动态链接思路
静态链接中:
-
读取elf头部,检查合法性
-
读取elf中“segment”的虚拟地址、文件地址、属性并映射到虚拟地址空间
-
控制权交给可执行文件入口
动态链接基本思路
动态链接基本逻辑和上面相似,但是在最后不能把控制权直接交给可执行程序,因为动态链接下,可执行程序依赖的共享对象符号并没有重定位,外部符号引用处于无效地址状态。因此映射完可执行程序后,系统首先启动一个动态链接器(ld.so)。
以同样的方式将ld.so加载到进程地址空间中,控制器交给动态链接器的入口,动态链接器执行一系列自身初始化,然后根据当前环境,开始对可执行文件进行动态链接工作,所有动态链接完成后,动态链接器将控制权交给可执行程序开始运行。
动态链接信息
interp段
段中指定所使用的动态链接器,elf中已经包含了使用的动态链接器
root@xxzh:# objdump -s groundctrlgroundctrl: file format elf32-littleContents of section .interp:10134 2f6c6962 2f6c642d 6c696e75 782d6172 /lib/ld-linux-ar10144 6d68662e 736f2e33 00 mhf.so.3.
dynamic段
类比成动态链接下的“elf头”,包含动态链接所需基本信息,包含依赖哪些共享对象,动态链接符号表位置,动态链接重定位表位置,共享对象初始化代码的位置等。查看该段内容:
root@xxzh:~/loader# arm-linux-gnueabihf-readelf -d libb.so Dynamic section at offset 0x550 contains 24 entries:Tag Type Name/Value0x00000001 (NEEDED) Shared library: [libc.so.6]0x0000000c (INIT) 0x3ec0x0000000d (FINI) 0x5380x00000019 (INIT_ARRAY) 0x105440x0000001b (INIT_ARRAYSZ) 4 (bytes)0x0000001a (FINI_ARRAY) 0x105480x0000001c (FINI_ARRAYSZ) 4 (bytes)0x00000004 (HASH) 0xf80x00000005 (STRTAB) 0x2880x00000006 (SYMTAB) 0x1580x0000000a (STRSZ) 206 (bytes)0x0000000b (SYMENT) 16 (bytes)0x00000003 (PLTGOT) 0x106300x00000002 (PLTRELSZ) 16 (bytes)0x00000014 (PLTREL) REL0x00000017 (JMPREL) 0x3dc0x00000011 (REL) 0x39c0x00000012 (RELSZ) 64 (bytes)0x00000013 (RELENT) 8 (bytes)0x6ffffffe (VERNEED) 0x37c0x6fffffff (VERNEEDNUM) 10x6ffffff0 (VERSYM) 0x3560x6ffffffa (RELCOUNT) 30x00000000 (NULL) 0x0
root@xxzh:~/loader#
root@xxzh:~/loader#
dynsym段-动态符号表
保存动态链接相关的符号,不保存内部符号。
.dynstr表:动态符号字符串表
.hash表:加快符号查找
重定位表
.rel.dyn:对数据引用的修正,修正位置位于“.got”以及数据段
.rel.plt:对函数引用的修正,修正位于“.got.plt”
root@xxzh:~/loader# arm-linux-gnueabihf-readelf -r abRelocation section '.rel.dyn' at offset 0x470 contains 2 entries:Offset Info Type Sym.Value Sym. Name
00020768 00000815 R_ARM_GLOB_DAT 00000000 __gmon_start__
00026f9c 00000c14 R_ARM_COPY 00026f9c shareRelocation section '.rel.plt' at offset 0x480 contains 4 entries:Offset Info Type Sym.Value Sym. Name
00020758 00000716 R_ARM_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.4
0002075c 00000816 R_ARM_JUMP_SLOT 00000000 __gmon_start__
00020760 00000f16 R_ARM_JUMP_SLOT 00000000 swap
00020764 00001016 R_ARM_JUMP_SLOT 00000000 abort@GLIBC_2.4
root@xxzh:~/loader#
可执行程序ab中的swap在动态链接库中提供,重定位时,swap位于libb.so中,假设链接器在全局符号表中找到了“swap”函数地址为0x12345678,那么链接器将地址0x12345678 写入到.rel.plt的00020760偏移位置去。
动态链接前堆栈初始化-辅助信息数组
系统把控制权交给链接器进行链接之前,通过堆栈传递给链接器一些信息。如:可执行文件的段、段属性、程序入口等。这些称谓辅助信息数组。为Elf32_auxv_t。位于堆栈环境变量的后面。
动态链接步骤
主要有3部分:
- 启动动态链接器本身
- 装载共享对象
- 重定位和初始化
动态链接器自举
普通的共享对象,重定位由动态链接器完成,共享对象也可以包含其他共享对象,由动态链接器负责装载和链接。
动态链接器是否需要重定位(自身完成,即自举),它是否可以依赖其他共享对象(不可以)?
操作系统把控制权给到动态链接器后,进入到动态链接器的入口,也就是自举的开始。
过程:
找到自身的GOT,保存了".dynamic"段偏移,找到自身的重定位表和符号表。
装载共享对象
动态链接符号表+可执行程序符号表=全局符号表
根据可执行文件所依赖的共享对象(NEEDED),放入到装载集合中,如ab中需要的./libb.so、libc.so.6
root@xxzh:~/loader# arm-linux-gnueabihf-readelf -d abDynamic section at offset 0x65c contains 25 entries:Tag Type Name/Value0x00000001 (NEEDED) Shared library: [./libb.so]0x00000001 (NEEDED) Shared library: [libc.so.6]0x0000000c (INIT) 0x104a00x0000000d (FINI) 0x106380x00000019 (INIT_ARRAY) 0x206500x0000001b (INIT_ARRAYSZ) 4 (bytes)0x0000001a (FINI_ARRAY) 0x206540x0000001c (FINI_ARRAYSZ) 4 (bytes)0x00000004 (HASH) 0x101940x00000005 (STRTAB) 0x103480x00000006 (SYMTAB) 0x102280x0000000a (STRSZ) 225 (bytes)0x0000000b (SYMENT) 16 (bytes)0x00000015 (DEBUG) 0x00x00000003 (PLTGOT) 0x2074c0x00000002 (PLTRELSZ) 32 (bytes)0x00000014 (PLTREL) REL0x00000017 (JMPREL) 0x104800x00000011 (REL) 0x104700x00000012 (RELSZ) 16 (bytes)0x00000013 (RELENT) 8 (bytes)0x6ffffffe (VERNEED) 0x104500x6fffffff (VERNEEDNUM) 10x6ffffff0 (VERSYM) 0x1042a0x00000000 (NULL) 0x0
root@xxzh:~/loader#
动态链接器在装载集合中取出一个名字,找到文件并打开共享对象,然后读取elf文件头和"dynamic"段,将其代码段和数据段等映射到地址空间中,如果此时依赖的其他共享对象,则将起放入装载集合。直到所有依赖的共享对象都被装载进来。
当一个共享对象被装载后,其符号表被加入到全局符号中,当所有共享对象被装载,全局符号表中将包含所有的动态链接所需符号。
如果多个共享文件中存在多个同名的函数,那么后加入的将被忽略(全局符号介入)。
重定位和初始化
遍历重定位表,将需要重定位的位置进行修正。
如果共享对象有“init”段,则动态链接器执行“init”段中代码。实现共享库自身初始化。
C运行库与自定义CRT
C运行库,是C可执行程序在操作系统上运行的基础,提供了运行环境及库。
最初当我们编译一个hello world的时候,只写了一个hello.c里面main中几行代码,调用printf输出字符串。然后在linux下执行。其实在编译hello.c时候,就依赖CRT。
root@xxzh:~/loader# gcc -static hello.c -o hello -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper
Target: x86_64-linux-gnu
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
Thread model: posix
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.12)
COLLECT_GCC_OPTIONS='-static' '-o' 'hello' '-v' '-mtune=generic' '-march=x86-64'/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/ccSoIejv.s
GNU C11 (Ubuntu 5.4.0-6ubuntu1~16.04.12) version 5.4.0 20160609 (x86_64-linux-gnu)compiled by GNU C version 5.4.0 20160609, GMP version 6.1.0, MPFR version 3.1.4, MPC version 1.0.3
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/5/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:/usr/lib/gcc/x86_64-linux-gnu/5/include/usr/local/include/usr/lib/gcc/x86_64-linux-gnu/5/include-fixed/usr/include/x86_64-linux-gnu/usr/include
End of search list.
GNU C11 (Ubuntu 5.4.0-6ubuntu1~16.04.12) version 5.4.0 20160609 (x86_64-linux-gnu)compiled by GNU C version 5.4.0 20160609, GMP version 6.1.0, MPFR version 3.1.4, MPC version 1.0.3
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: 8087146d2ee737d238113fb57fabb1f2
COLLECT_GCC_OPTIONS='-static' '-o' 'hello' '-v' '-mtune=generic' '-march=x86-64'as -v --64 -o /tmp/cc7paYgB.o /tmp/ccSoIejv.s
GNU assembler version 2.26.1 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.26.1
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/
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/
COLLECT_GCC_OPTIONS='-static' '-o' 'hello' '-v' '-mtune=generic' '-march=x86-64'/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/ccSHWdgH.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_eh -plugin-opt=-pass-through=-lc --sysroot=/ --build-id -m elf_x86_64 --hash-style=gnu --as-needed -static -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/crtbeginT.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/cc7paYgB.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o
root@xxzh:~/loader#
root@xxzh:~/loader#
可以看到在链接阶段,不仅仅是hello.o,包含了crtn.o、crti.o等等,这些是CRT提供的。所以编译出来的代码要比实际大很多。
自定义CRT
- 静态库的形式提供(libcrt.a + crt.h)
- gcc编译的时候通过-e指定入口函数,默认_start
- 入口_start中完成初始化及对main的调用
- 提供C库,通过系统调用等实现malloc、read、write(asm汇编)
- main执行后清理资源
总结
- 可执行程序的生成的过程经历了 预编译、编译、汇编、链接 四个过程
- 中间文件、可执行程序、动静态库都是ELF格式的文件
- ELF有链接视图(section 段表头、段表)和装载视图(segment 程序表头)两种不同方式
- 静态链接:是合并多个中间的文件的相似段,根据不同中间文件的偏移,对符号进行重定位
- ./hello的执行,经历了fork和exec
- 动态链接:是动态链接器自举,并根据可执行文件的全局符号表不断加载依赖的共享对象,直到所有共享对象被加载,再根据全局符号表进行重定位
- C运行时库CRT,是一个模板程序,和用户的main程序及依赖柔和在一起,产生可执行程序。为用户程序运行提供前期的初始化及库函数。
- CRT封装了不同的系统调用接口API给应用程序编程使用
更多推荐
linux可执行程序的编译、链接、装载
发布评论