Linux编译链接原理
1.一般流程
一个C/C++文件要经过预处理(preprocessing)、编译(compilation)、汇编(assembly)、和连接(linking)才能变成可执行文件。
①预处理就是将要包含(include)的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些代码输出到一个“.i”文件中等待进一步处理。
1)预编译(生成.i文件)
1>将所有的“#define”删除,并且展开所有宏;
2>处理掉所有条件预编译指令,如:“#if”、“#ifdef”、“#elif”、“#else”、“#endif”;
3>处理“#include”指令,这是一个递归过程;
4>删除所有的注释“//”和“/ /”;
5>添加行号和文件名标示
6>保留所有的#pragma编译器指令,待编译器使用;
例如:
#ifdef _X86
#pragma message("_X86 macro activated!")
#endif
如果定义了X86这个宏,应用程序在编译时就会在编译输出窗口里显示"86 macro activated!"。
#pragma code_seg( ["section-name" [, "section-class"] ] )
它能够设置程序中函数代码存放的代码段,当开发驱动程序的时候就会使用到它。
②编译就是把C/C++代码(比如”.i”文件)“翻译”成汇编代码。
把预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相对应的汇编代码文件 。
③汇编就是将第二步输出的汇编代码翻译成符合一定格式的机器代码,在Linux系统上一般表现位ELF目标文件(OBJ文件)。
将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令 。
④链接就是将汇编生成的OBJ文件、系统库的OBJ文件、库文件链接起来,最终生成可以在特定平台运行的可执行程序。
1>地址和空间分配
2>符号决议
3>重定位
用法:gcc [选项] 文件名
选项 | 含义 |
---|---|
-v | 查看gcc编译器的版本,显示gcc执行时的详细过程 |
-o | Place the output into <file> ; |
-E | Preprocess only; do not compile, assemble or link; |
-S | Compile only; do not assemble or link; |
-c | Compile and assemble, but do not link; |
2.链接原理
gcc -c -o test2.o test2.c
不作最后一步链接,得到test2.o二进制OBJ文件,REL (可重定位文件) ,入口点地址: 0x0
gcc -v -o test2 test2.o
查看链接过程:
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.8/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v
--with-pkgversion='Ubuntu 4.8.4-2ubuntu1~14.04.3'
--with-bugurl=file:///usr/share/doc/gcc-4.8/README.Bugs
--enable-languages=c,c++,java,go,d,fortran,objc,obj-c++
--prefix=/usr
--program-suffix=-4.8
--enable-shared
--enable-linker-build-id
--libexecdir=/usr/lib
--without-included-gettext
--enable-threads=posix
--with-gxx-include-dir=/usr/include/c++/4.8
--libdir=/usr/lib
--enable-nls
--with-sysroot=/
--enable-clocale=gnu
--enable-libstdcxx-debug
--enable-libstdcxx-time=yes
--enable-gnu-unique-object
--disable-libmudflap
--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-4.8-amd64/jre
--enable-java-home
--with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-4.8-amd64
--with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-4.8-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
--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 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04.3)
COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/4.8/:
/usr/lib/gcc/x86_64-linux-gnu/4.8/:
/usr/lib/gcc/x86_64-linux-gnu/:
/usr/lib/gcc/x86_64-linux-gnu/4.8/:
/usr/lib/gcc/x86_64-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/4.8/:
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/:
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../lib/:
/lib/x86_64-linux-gnu/:
/lib/../lib/:
/usr/lib/x86_64-linux-gnu/:
/usr/lib/../lib/:
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../:
/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-o' 'test2' '-mtune=generic' '-march=x86-64'
/usr/lib/gcc/x86_64-linux-gnu/4.8/collect2
--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 test2
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/crt1.o
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/4.8/crtbegin.o
-L/usr/lib/gcc/x86_64-linux-gnu/4.8
-L/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu
-L/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../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/4.8/../../..
test2.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/4.8/crtend.o
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/crtn.o
crt1.o、crti.o、crtbegin.o、crtend.o、crtn.o是gcc加入的系统标准启动文件,对于一般应用程序,这些启动是必需的。 -lc:链接libc库文件,其中libc库文件中就实现了printf等函数。
目标文件
1)可执行文件
1>Windows下面PE格式
2>Linux下面是ELF格式,其格式分类见附录:ELF文件类型
3>目标文件、动态链接库、静态链接库都是按照可执行文件格式存储的
2)目标文件
1>目标文件的组成:
a.目标文件属性:是否可执行,动态或静态链接,段信息等等
b.程序指令
c.程序数据:数据段和.bss段
2>程序和数据分段的目的
a.可以标记为不同属性,分开存储:程序为只读,数据为可读可写
b.可以分开进行缓存
c.指令可以重复使用,即指令共享
3>ELF文件详细介绍
a.文件头:ELF魔数,文件机器字节长度,数据存储方式,版本,运行平台,硬件平台,入口地址,
程序头入口和长度,段表的位置和长度及数量等
b.段表:保存这些段的基本信息,段名,长度,在文件中的偏移,读写权限及段的其他属性
c.重定位表:即为“.rel.text”段
d.字符串表
2.1链接方式
动态链接和静态链接
①动态链接:动态链接使用动态链接库进行链接,生成的程序在执行的时候需要加载所需的动态库才能运行。 动态链接生成的程序体积较小,但是必须依赖所需的动态库,否则无法执行。
gcc -o test2_shared test2.o
huangxiang6@Cpl-WH-30:~/data/test/test7.23.1$ file test2_shared
test2_shared: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=722fe535c394163b4861eb7783720c7c49f02898, not stripped
huangxiang6@Cpl-WH-30:~/data/test/test7.23.1$ ls -l test2_shared
-rwxrwxr-x 1 huangxiang6 huangxiang6 8674 7月 24 09:31 test2_shared
② 静态链接:静态链接使用静态库进行链接,生成的程序包含程序运行所需要的全部库,可以直接运行,不过静态链接生成的程序体积较大。
gcc -static -o test2_static test2.o
huangxiang6@Cpl-WH-30:~/data/test/test7.23.1$ gcc -static -o test2_static test2.o
huangxiang6@Cpl-WH-30:~/data/test/test7.23.1$ file test2_static
test2_static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=16fa735dc83f8f9e160cc131214590c894e83068, not stripped
huangxiang6@Cpl-WH-30:~/data/test/test7.23.1$ ls -l test2_static
-rwxrwxr-x 1 huangxiang6 huangxiang6 877477 7月 24 09:35 test2_static
3.编译链接过程选项
3.1 预处理选项
预处理器选项(Preprocessor Option)
下列选项针对C预处理器,预处理器用在正式编译以前,对C 源文件进行某种处理。如果指定了-E
选项,GCC只进行预处理工作,下面的某些选项必须和-E选项一起才有意义,因为他们的输出结果不能用于编译。
-include file
在处理常规输入文件之前,首先处理文件file,其结果是,文件file的内容先得到编译。 命令行上任何-D和-U
选项永远在-include file之前处理, 无论他们在命令行上的顺序如何。然而-include和-imacros选项按书写顺
序处理。
-imacros file
在处理常规输入文件之前,首先处理文件file,但是忽略输出结果。由于丢弃了文件file的 输出内容, -
imacros file选项的唯一效果就是使文件file中的宏定义生效,可以用于其他输入文件。在处理-imacrosfile选
项之前,预处理器首先处理 -D 和 -U 选项,并不在乎他们在命令行上的顺序。然而-include和 -imacros选项
按书写顺序处理。
-idirafter dir
把目录dir添加到第二包含路径中。如果某个头文件在主包含路径(用-I添加的路径)中没有找到,预处理器就搜
索第二包含路径。
-iprefix prefix
指定prefix作为后续 -iwithprefix 选项的前缀。
-iwithprefix dir
把目录添加到第二包含路径中。目录名由prefix和dir合并而成,这里 prefix被先前的-iprefix选项指定。
-nostdinc
不要在标准系统目录中寻找头文件。只搜索-I选项指定的目录(以及当前目录,如果合适)。
结合使用-nostdinc和-I-选项,可以把包含文件搜索限制在显式指定的目录。
-nostdinc++
不要在C++专用标准目录中寻找头文件,但是仍然搜索其他标准目录。(当建立libg++时使用这个选项。)
-undef
不要预定义任何非标准宏。 (包括系统结构标志)。
-E
仅运行C预处理器。预处理所有指定的C源文件,结果送往标准输出或指定的输出文件。
-C
告诉预处理器不要丢弃注释。配合-E选项使用。
-P
告诉预处理器不要产生#line命令。配合-E选项使用。
-M [ -MG ]
告诉预处理器输出一个适合make的规则,用于描述各目标文件的依赖关系。对于每个源文件,预处理器输出
一个make规则,该规则的目标项(target)是源文件对应的目标文件名,依赖项(dependency)是源文件中
#include引用的所有文件。生成的规则可以是单行。但如果太长,就用 /- 换行符续成多行。规则显示在标准
输出,不产生预处理过的C程序。-M隐含了-E选项。-MG要求把缺失的头文件按存在对待,并且假定他们和源
程序文件在同一目录下,必须和-M选项一起用。
-MM [ -MG ]
与-M选项类似,但是输出结果仅涉及用户头文件,象这样#include file。忽略系统头文件如#include
。-MD和-M选项类似,但是把依赖信息输出在文件中,文件名通过把输出文件名末尾的.o替换为.d产
生。同时继续指定的编译工作----MD不象-M那样阻止正常的编译任务。
3.2 编译选项
3.3 汇编器选项(ASSEMBLER OPTION )
-Wa , option
把选项option传递给汇编器.如果option含有逗号,就在逗号处分割成多个选项.
3.4 链接器选项
下面的选项用于编译器连接目标文件
object-file-name
如果某些文件没有特别明确的后缀a special recognized suffix, GCC就认为他们是目标文件或库文件. (根据文
件内容,连接器能够区分目标文件和库文件).如果GCC执行连接操作,这些目标文件将成为连接器的输入文件.
-llibrary
连接名为library的库文件.连接器在标准搜索目录中寻找这个库文件,库文件的真正名字是liblibrary.a.连接器会 当做文件名得到准确说明一样引用这个文件.搜索目录除了一些系统标准目录外,还包括用户以-L选项指定的路径.一般说来用这个方法找到的文件是库文件—即由目标文件组成的归档文件(archive file).连接器处理归档文件的 方法是:扫描归档文件,寻找某些成员,这些成员的符号目前已被引用,不过还没有被定义.但是,如果连接器找到普通的 目标文件,而不是库文件,就把这个目标文件按平常方式连接进来.指定-l选项和指定文件名的唯一区别是, -l选项用lib和.a把library包裹起来,而且搜索一些目录。
-static
在支持动态连接(dynamic linking)的系统上,阻止连接共享库.该选项在其他系统上无效.
-shared
生成一个共享目标文件,他可以和其他目标文件连接产生可执行文件.只有部分系统支持该选项.
3.5 目录选项(DIRECTORY OPTION)
下列选项指定搜索路径,用于查找头文件,库文件,或编译器的某些成员:
-Idir
在头文件的搜索路径列表中添加dir 目录.
-I-
任何在**-I-前面用-I选项指定的搜索路径只适用于#include** file这种情况;他们不能用来搜索**#include <**file>
包含的头文件.如果用**-I选项指定的搜索路径位于-I-**选项后面,就可以在这些路径中搜索所有的 #include指令.
(一般说来**-I**选项就是这么用的.)还有, -I-选项能够阻止当前目录(存放当前输入文件的地方)成为搜索#include
file的第一选择.没有办法克服**-I-选项的这个效应.你可以指定-I.**搜索那个目录,它在调用编译器时是当前目录.
这和预处理器的默认行为不完全一样,但是结果通常 令人满意.**-I-**不影响使用系统标准目录,因此, -I-和-
nostdinc是不同的选项.
-Ldir
在**-l**选项的搜索路径列表中添加dir目录.
-Bprefix
这个选项指出在何处寻找可执行文件,库文件,以及编译器自己的数据文件.
3.6 警告选项(WARNING OPTION)
警告是针对程序结构的诊断信息,程序不一定有错误,而是存在风险,或者可能存在 错误.下列选项控制GNU CC产生的警告的数量和类型:
-fsyntax-only
检查程序中的语法错误,但是不产生输出信息.
-w
禁止所有警告信息.
-Wnested-externs
如果某extern声明出现在函数内部,编译器就发出警告.
4.ELF文件
对象文件(Object files)有三个种类:
-
可重定位的对象文件(Relocatable file)
这是由汇编器汇编生成的 .o 文件。后面的链接器(link editor)拿一个或一些 Relocatable object files 作为输入,经链接处理后,生成一个可执行的对象文件 (Executable file) 或者一个可被共享的对象文件(Shared object file)。可以使用 ar 工具将众多的 .o Relocatable object files 归档(archive)成 .a 静态库文件。内核可加载模块 .ko 文件也是 Relocatable object file。 -
可执行的对象文件(Executable file)
文本编辑器vi、调式用的工具gdb、播放mp3歌曲的软件mplayer等等都是Executable object file。在 Linux 系统里面,存在两种可执行的东西。除了 Executable object file,另外一种就是可执行的脚本(如shell脚本)。这些脚本不是 Executable object file,它们只是文本文件,但是执行这些脚本所用的解释器就是 Executable object file,比如 bash shell 程序。 -
可被共享的对象文件(Shared object file)
这些就是所谓的动态库文件,也即 .so 文件。如果拿前面的静态库来生成可执行程序,那每个生成的可执行程序中都会有一份库代码的拷贝。如果在磁盘中存储这些可执行程序,那就会占用额外的磁盘空间;另外如果拿它们放到Linux系统上一起运行,也会浪费物理内存。如果将静态库换成动态库,那么这些问题都不会出现。动态库在发挥作用的过程 中,必须经过两个步骤: a) 链接编辑器(link editor)拿它和其他Relocatable object file以及其他shared object file作为输入,
经链接处理后,生存另外的 shared object file 或者 executable file。
b)在运行时,动态链接器(dynamic linker)拿它和一个Executable file以及另外一些 Shared object
file 来一起处理,在Linux系统里面创建一个进程映像。
4.1 ELF文件格式
ELF文件格式提供了两种视图,分别是链接视图和执行视图。
segment段是section节的一个集合,sections按照一定规则映射到segment 链接视图是以节(section)为单位,执行视图是以段(segment)为单位。链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。上图左侧的视角是从链接来看的,右侧的视角是执行来看的。总个文件可以分为四个部分:
- ELF header: 描述整个文件的组织。
- Program Header Table: 描述文件中的各种segments,用来告诉系统如何创建进程映像的。
- sections 或者 segments:segments是从运行的角度来描述elf文件,sections是从链接的角度来描述elf文件,也就是说,在链接阶段,可以忽略program header table来处理此文件,在运行阶段可以忽略section header table来处理此程序(所以很多加固手段删除了section header table)。从图中可以看出,segments与sections是包含的关系,一个segment包含若干个section。
- Section Header Table: 包含了文件各个segction的属性信息。
程序头部表(Program Header Table),如果存在的话,告诉系统如何创建进程映像。
节区头部表(Section Header Table)包含了描述文件节区的信息,比如大小、偏移等。
查看可执行文件中有哪些section:
huangxiang6@Cpl-WH-30:~/data/test/test7.23.1$ readelf -S test2
共有 30 个节头,从偏移量 0x1190 开始:
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
...
[11] .init PROGBITS 0000000000400488 00000488
000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 00000000004004b0 000004b0
0000000000000070 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400520 00000520
00000000000001e2 0000000000000000 AX 0 0 16
...
[24] .data PROGBITS 0000000000601048 00001048
0000000000000010 0000000000000000 WA 0 0 8
[25] .bss NOBITS 0000000000601058 00001058
0000000000000008 0000000000000000 WA 0 0 1
...
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
查看可执行文件的执行视图:
huangxiang6@Cpl-WH-30:~/data/test/test7.23.1$ readelf -l test2
Elf 文件类型为 EXEC (可执行文件)
入口点 0x400520
共有 9 个程序头,开始于偏移量 64
程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x000000000000085c 0x000000000000085c R E 200000
...
GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x00000000000001f0 0x00000000000001f0 R 1
Section to Segment mapping:
段节...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got
当ELF文件被加载到内存中后,系统会将多个具有相同权限(flg值)section合并一个segment。操作系统往往以页为基本单位来管理内存分配,一般页的大小为4096B,即4KB的大小。同时,内存的权限管理的粒度也是以页为单位,页内的内存是具有同样的权限等属性,并且操作系统对内存的管理往往追求高效和高利用率这样的目标。ELF文件在被映射时,是以系统的页长度为单位的,那么每个section在映射时的长度都是系统页长度的整数倍,如果section的长度不是其整数倍,则导致多余部分也将占用一个页。而我们从上面的例子中知道,一个ELF文件具有很多的section,那么会导致内存浪费严重。所以区分节视图和段视图,可以减少页面内部的碎片,节省了空间,显著提高内存利用率。
4.2 重定位表
重定位表在ELF文件中扮演很重要的角色,首先得理解重定位的概念,程序从代码到可执行文件这个过程中,要经历编译器,汇编器和链接器对代码的处理。然而编译器和汇编器通常为每个文件创建程序地址从0开始的目标代码,但是几乎没有计算机会允许从地址0加载程序。如果一个程序是由多个子程序组成的,那么所有的子程序必需要加载到互不重叠的地址上。重定位就是为程序不同部分分配加载地址,调整程序中的数据和代码以反映所分配地址的过程。简单的言之,则是将程序中的各个部分映射到合理的地址上来。
换句话来说,重定位是将符号引用与符号定义进行连接的过程。例如,当程序调用了一个函数时,相关的调用指令必须把控制传输到适当的目标执行地址。 具体来说,就是把符号的value进行重新定位。但是重定位文件必须包含如何修改其节区内容的信息,从而允许可执行文件和共享目标文件保存进程的程序映象的正确信息。这就是重定位表项做的工作。
readelf -r test2.o
命令来读取重定位表:
huangxiang6@Cpl-WH-30:~/data/test/test7.23.1$ readelf -r test2.o
重定位节 '.rela.text' 位于偏移量 0x660 含有 8 个条目:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000010 000a00000002 R_X86_64_PC32 0000000000000000 getpid - 4
00000000001b 00050000000a R_X86_64_32 0000000000000000 .rodata + 0
000000000020 000b00000002 R_X86_64_PC32 0000000000000000 puts - 4
000000000037 00050000000a R_X86_64_32 0000000000000000 .rodata + 13
00000000003f 000c00000002 R_X86_64_PC32 0000000000000000 strcmp - 4
000000000055 00050000000a R_X86_64_32 0000000000000000 .rodata + 18
00000000005d 000c00000002 R_X86_64_PC32 0000000000000000 strcmp - 4
00000000006b 000d00000002 R_X86_64_PC32 0000000000000000 sleep - 4
重定位节 '.rela.eh_frame' 位于偏移量 0x720 含有 1 个条目:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
常见的重定位表类型:
- .rel.text:重定位的地方在.text段内,以offset指定具体要定位位置。在链接时候由链接器完成。.rel.text属于普通重定位辅助段 ,他由编译器编译产生,存在于obj文件内。连接器连接时,他用于最终可执行文件或者动态库的重定位。通过它修改原obj文件的.text段后,合并到最终可执行文件或者动态文件的.text段。其类型一般为R_386_32和R_386_PC32。
- .rel.dyn:重定位的地方在.got段内。主要是针对外部数据变量符号。例如全局数据。重定位在程序运行时定位,一般是在.init段内。定位过程:获得符号对应value后,根据rel.dyn表中对应的offset,修改.got表对应位置的value。另外,.rel.dyn 含义是指和dyn有关,一般是指在程序运行时候,动态加载。区别于rel.plt,rel.plt是指和plt相关,具体是指在某个函数被调用时候加载。这个Section的作用是,在重定位过程中,动态链接器根据r_offset找到.got对应表项,来完成对.got表项值的修改。
.rel.dyn和.rel.plt是动态定位辅助段。由连接器产生,存在于可执行文件或者动态库文件内。借助这两个辅助段可以动态修改对应.got和.got.plt段,从而实现运行时重定位。
- .rel.plt:重定位的地方在.got.plt段内(注意也是.got内,具体区分而已)。 主要是针对外部函数符号。一般是函数首次被调用时候重定位。首次调用时会重定位函数地址,把最终函数地址放到.got内,以后读取该.got就直接得到最终函数地址。我个人理解这个Section的作用是,在重定位过程中,动态链接器根据r_offset找到.got对应表项,来完成对.got表项值的修改。
- .plt段(过程链接表):所有外部函数调用都是经过一个对应桩函数,这些桩函数都在.plt段内。具体调用外部函数过程是:
调用对应桩函数—>桩函数取出.got表表内地址—>然后跳转到这个地址.如果是第一次,这个跳转地址默认是桩函数本身跳转处地址的下一个指令地址(目的是通过桩函数统一集中取地址和加载地址),后续接着把对应函数的真实地址加载进来放到.got表对应处,同时跳转执行该地址指令.以后桩函数从.got取得地址都是真实函数地址了。 - .got(全局偏移表)
5、装载和动态链接
5.1.可执行文件的装载
1)装载方式
1>覆盖装入:直接按照执行文件段大小进行映射即可
2>页映射:
2)链接视图和执行视图
1>链接视图 按可执行文件进行分段,但考虑到执行视图参考的属性不同,故后面会对segment进行section转
化,其中还会涉及到段对齐等问题
2>执行视图
a.可读可执行段:
b.可读可写段:
c.可读段:
3)窥探Linux的进程内存映射
1>可执行文件到VMA的映射: 在创建进程时,即建立了可执行文件的段到VMA的映射,但没有真正映射到
物理地址
2>VMA到物理地址空间的映射: 在Linux进程内存访问时,当没有物理地址页时就会产生页错误,此时调用
操作系统进行物理地址到VMA的映射
5.2 动态链接
1)动态链接与静态链接对比分析:
1>静态链接:
a.内存空间的限制,链接进所有需要的静态文件,会使执行文件过大;
b.会存在链接进去很多重复文件,浪费内存空间;
c.对于程序的部署,每次需要整个重新更新整个执行文件。
2>动态链接;
a.增强了程序的可扩展性;
b.增强了程序的可执行性。
2)地址无关代码:
1>执行视图中分开存储代码和数据,提高代码的使用率;
2>地址重映射时会有问题,多个线程可能对应同意代码段,不能进行地址重映射;
3>基于以上原因,产生了地址无关码,每个线程都有自己对应的数据区,故可以那些需要重映射的地址和数
据保存在一起,这样就产生了地址无关代码
5.3 内存布局
1)程序的内存分配
1>对于Linux而言,内存布局 kernel space stack dynamic libraries heap data code reserved
2)栈
1>定义:一个动态的内存区域,函数执行完了之后释放,遵循规则:FIFO
2>调用惯性:程序在进行调用时,其数据、地址、寄存器变量等均要遵循一定的规则
3>函数返回值:实现接口之间的通信。
3)堆
1>定义:栈会释放,静态区域只能在编译时建立,结合两者的优势,产生了可以动态生成和释放的堆
2>Linux进程堆管理:
a.brk()系统调用
b.mmep()系统调用
3>堆算法
a.链表:链接链接所有的内存,然后按需查询分配
b.位图:内存大小全部固定,按需分配
c.内存池:结合两者优势