Follow Excellent, Success will Chase you

0%

程序员的自我修养

程序员的自我修养 链接、装载与库

温故而知新

内存不够怎么办

任何将计算机上有限的物理内存分配给多个程序使用

如果采用直接使用物理内存空间执行程序,存在一下问题:

  1. 地址空间不隔离

    所有程序直接访问物理地址,程序所使用的内存空间不是相互隔离的,程序容易被有意或者无意的修改,使其崩溃.

  2. 内存使用率低

    整个程序都加载到内存中占用大量空间,执行新的程序空间不足时,也需要换入换出大量数据.

  3. 程序运行地址不确定

    程序在编写时,其实是编译成可执行文件时,它访问数据和指令跳转时的目的地址很多都是固定的,地址不确定会造成很大麻烦.

解决方法: 增加中间层 使用一种间接的地址访问方法 — 虚拟地址(Virtual Address)

把程序给出的地址看作是一种虚拟地址,然后通过某些映射方法,将这个虚拟地址转换成实际的物理地址.

隔离

地址空间: 所谓地址空间是个比较抽象的概念,你可以把它想象成一个很大的数组,每个数组的元素是一个字节,而这个数组大小由地址空间的地址长度(地址线的个数)决定,比如32位地址空间大小为2^32=4 294 967 296字节,即4G

虚拟地址空间:指虚拟的,人们想象出来的地址空间,其实它并不存在,每一个进程都有自己独立的虚拟地址空间,而且每个进程只能访问自己的地址空间,这样就有效的做到了进程的隔离

分段(Segmentation)

基本思路: 把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
                                           +-------------------+
| |
| |
0x0640 0000+----------------XX | |
| | XXXX | |
| | XXXXXXXXXXX-------------------+0x7000 0000
| | | | +
| | | | |
| | | | |
| Virtual Addr | | | |
| Space of B | | Physical | |
| | | Address Space | |
| | map | of B | |
| | | | v
| | | | 100MB
| | | | ^
| | | | |
| | | | |
| | | | |
0x0000 0000+----------------XXX | | |
XXXX | | |
XXXX | | +
0x00A0 0000+----------------XX XXXXXX+-------------------+0x00c0 0000
| Virtual Addr |XXX | |
| Apace of A | XXXXXXXXXXXXX-------------------+0x00B0 0000
| | | Physical Address |
0x0000 0000+----------------X map | Space of A | 10MB
XXXXX | |
XXXXXXXXXXX-------------------+0x0010 0000
| |
| |
+-------------------+0x0000 0000

分段可以解决第一个和第三个问题.

  1. 地址隔离: 程序A和程序被映射到两块不同的物理空间区域,他们之间没有任何重叠.如果程序A访问访问虚拟空间的地址超过了0x00A0 0000这个范围,那么硬件就会判断这是一个非法访问,拒绝这个地址请求,并将这个请求报告给操作系统或监控程序,由它决定处理.
  2. 固定程序运行地址:每个程序而言不需要关心虚拟地址与物理地址之间的映射,相当于其透明的,程序编写只需要按照从地址0x0000 0000到0x00A0 0000来编写程序,放置变量,程序不需要重定位.

分页(Paging)

分段是对整个程序而言,其换入换出将增加大量的磁盘访问操作,从而严重影响速度,因此利用程序的局部性原理使用更小粒度的内存分割和映射方法,就是分页(Paging)

分页的基本方法是把地址空间人为的等分成固定大小的页,每一页的大小由硬件决定,或硬件支持多种大小的页,由操作系统选择决定页的大小

页大小: MIPS 8K

Page Fault

page_fault_flow

进程的虚拟地址空间按进行分割后,把常用的数据和代码页装载到内存中,把不常用的数据和代码页保存在磁盘中,当需要用到的时候再把它从磁盘中取出来即可.

假设进程Process1,Process2,他们进程中的部分虚拟页面被映射到了物理页面,比如VP0,VP1和VP7映射到PP0,PP2和PP3,但是一部分在磁盘中比如DP0,DP1.

  • 如果程序运行时,只是有到了VP0,VP1和VP7的页空间,将不存在任何异常,程序正常运行
  • 如果程序运行是,访问到了VP2和VP3的页空间,由于这两个页不在内存中,在磁盘中DP0和DP1中,因此硬件会捕获到这个信息,这就是段错误(Page Fault)

页映射–数据保护

在页映射时,可以对每个页设置权限属性,谁可以修改,谁可以访问,而只有操作系统有修改页属性的权利

Linux内核中如何实现???

MMU (Memory Management Unit)

CPU内部集成的一个硬件部件

1
2
3
4
5
+-------------+               +-----------+              +----------------+
| | Virtual | | Physical | Physical |
| CPU +---------------> MMU +--------------> Memory |
| | Address | | Address | |
+-------------+ +-----------+ +----------------+
  • 所谓CPU的总线地址大多数情况下就是指物理地址

线程 – Thread

线程: 执行流的最小单元,有时也称轻量级进程,

  • 一个标准的线程由线程ID,当前指令指针(PC),寄存器集合堆栈组成
  • 通常,一个进程由一个到多个线程组成,各个线程之间共享程序的内存空间(包括代码段,数据段,堆等)及一些进程级资源(如打开文件和信号)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
+--------------------------------------------------------------+
| +----------------------------------------------------------+ |
| | 代码 | 数据 | 进程空间 | 打开文件 | |
| +------------+--------------+----------------+-------------+ |
| |
| +-------------+ +-------------+ +--------------+ |
| | +---------+ | | +---------+ | | +----------+ | |
| | | 寄存器 | | | | 寄存器 | | | | 寄存器 | | |
| | +---------+ | | +---------+ | | +----------+ | |
| | | | | | | |
| | +---------+ | | +---------+ | | +----------+ | |
| | | 栈 | | | | 栈 | | | | 栈 | | |
| | +---------+ | | +---------+ | | +----------+ | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | Main Thread | | Thread 1 | | Thread 2 | |
| +-------------+ +-------------+ +--------------+ |
+--------------------------------------------------------------+

线程的访问权限

线程私有线程之间共享(进程所有)
局部变量全局变量
函数参数堆上的数据
TLS数据函数里的静态变量
-程序代码,任何线程都有权利读取并执行任何代码
-打开的文件, A线程打开的文件可以由线程B读写

线程调度

线程的三种状态:

  • 运行(Runing): 此时线程正在执行
  • 就绪(Ready): 此时线程可以立刻运行,但是CPU已经被占用
  • 等待(Wait): 此时线程正在等待某一事件(通常指I/O或同步)发生,无法执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
                 无运行线程,且本线程被选中
+------------------------------------------+
| |
| |
| |
| |
+----v-----+ +-----+-----+
| | 时间片用尽 | |
| Runing +------------------------------> Ready |
| | | |
+----+-----+ +-----^-----+
| |
| |
开始等待 等待结束
| |
| +----------+ |
| | | |
+-------------> Wait +-----------------+
| |
+----------+

线程安全

多线程并发执行时,数据的一致性

竞争与原子操作

  • 原子(Atomic):指单指令操作

锁与同步

  1. 二元信号量: 最简单的一种锁,它只有两种状态:占用非占用.适用只能被唯一一个线程独占访问的资源
  2. 互斥量(Mutex):与二元信号量类似,资源仅同时允许一个线程访问,但是互斥量是哪个线程获取互斥量,必须哪个线程释放互斥量
  3. 临界区:互斥量保护的范围,就是临界区
  4. 读写锁:读写锁两种获取方式共享的(Shared)独占的(Exclusive),适用频繁读取,只是偶尔写入的场景
  5. 条件变量: 类似于一个栅栏,一个条件变量可以被多个线程等待,当时间发生时(条件变量被唤醒),所有线程可以一起恢复执行

可重入(Reentrant)与线程安全

一个函数可重入的特点:

  1. 不使用任何(局部)静态或全局的非const变量
  2. 不使用任何(局部)静态或全局的非const变量的变量
  3. 仅依赖调用函数提供的参数
  4. 不依赖任何单个资源的锁(mutex等)
  5. 不调用任何不可重入函数

过度优化

过度优化带来的问题:

  1. 编译器调整顺序:编译器为提高执行速度,将一些结果保存临时寄存器中
  2. CPU动态调度换序: CPU的动态调度,在执行过程中,为了提高效率,几个互补相关的指令,可能被交换执行,或者同时执行

解决方法:

  1. volatile关键字阻止过度优化
    • 阻止编译器为了提高速度将一个变量缓存到寄存器不写回
    • 阻止编译器调整操作volatile变量的指令顺序
  2. barrier指令, 一条barrier指令会阻止CPU将该指令之前的指令交换到barrier指令之后,也就是说CPU执行到barrier指令时,前面的所有指令已经执行完成

静态链接

GCC编译过程

1
$gcc hello.c

编译的过程可以分解成4个步骤:预处理(Prepressing),编译(Compilation),汇编(Assembly)链接(Linking)

Gcc编译过程

预编译-Prepressing

1
2
3
gcc -E hello.c -o hello.i

cpp hello.c > hello.i

预编译过程主要处理源代码中以"#"开始的预编译指令.比如”#include”, “#define”

处理规则:

  • 将所有的"#define"删除,并且展开所有的宏定义
  • 处理所有条件预编译指令,比如"#if", "ifdef", "#elif", "#else", "#endif"
  • 处理"#include"预编译指令,将包含的文件插入到该预编译指令的位置,注意这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件
  • 删除所有注释"//""/* */"
  • 添加行号和文件标识,比如#2 “hello.c” 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误和警告时能够显示行号
  • 保留所有的"#pragma"编译器指令,因为编译器需要使用它们

编译-Compilation

1
gcc -S hello.i -o hello.s

编译的过程就是把预处理完的文件进行一系列词法分析,语法分析,语义分析及优化后生成相应的汇编代码文件

汇编-Assembly

1
2
3
gcc -c hello.s -o hello.o

as hello.s -o hello.o

链接-Linking

通过ld链接一些必要的文件使其生成一个可执行文件

示例: Gcc 7.3.0

环境编译器及系统: gcc version 7.3.0 (Ubuntu 7.3.0-27ubuntu1~18.04)

1
gcc hello.c -v
  • -v: 显示编译器调用处理的细节
  • -save-temps: 不删除中间临时文件,如*.i, *.s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
=====>$gcc hello.c -v  -save-temps
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 7.3.0-27ubuntu1~18.04' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --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 --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 7.3.0 (Ubuntu 7.3.0-27ubuntu1~18.04)
COLLECT_GCC_OPTIONS='-v' '-save-temps' '-mtune=generic' '-march=x86-64'
/usr/lib/gcc/x86_64-linux-gnu/7/cc1 -E -quiet -v -imultiarch x86_64-linux-gnu hello.c -mtune=generic -march=x86-64 -fpch-preprocess -fstack-protector-strong -Wformat -Wformat-security -o hello.i
ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/7/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/x86_64-linux-gnu/7/include
/usr/local/include
/usr/lib/gcc/x86_64-linux-gnu/7/include-fixed
/usr/include/x86_64-linux-gnu
/usr/include
End of search list.
COLLECT_GCC_OPTIONS='-v' '-save-temps' '-mtune=generic' '-march=x86-64'


/usr/lib/gcc/x86_64-linux-gnu/7/cc1 -fpreprocessed hello.i -quiet -dumpbase hello.c -mtune=generic -march=x86-64 -auxbase hello -version -fstack-protector-strong -Wformat -Wformat-security -o hello.s
GNU C11 (Ubuntu 7.3.0-27ubuntu1~18.04) version 7.3.0 (x86_64-linux-gnu)
compiled by GNU C version 7.3.0, GMP version 6.1.2, MPFR version 4.0.1, MPC version 1.1.0, isl version isl-0.19-GMP

GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
GNU C11 (Ubuntu 7.3.0-27ubuntu1~18.04) version 7.3.0 (x86_64-linux-gnu)
compiled by GNU C version 7.3.0, GMP version 6.1.2, MPFR version 4.0.1, MPC version 1.1.0, isl version isl-0.19-GMP

GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: c8081a99abb72bbfd9129549110a350c
COLLECT_GCC_OPTIONS='-v' '-save-temps' '-mtune=generic' '-march=x86-64'


as -v --64 -o hello.o hello.s
GNU assembler version 2.30 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.30
COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/7/:/usr/lib/gcc/x86_64-linux-gnu/7/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/7/:/usr/lib/gcc/x86_64-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/7/:/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/7/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-save-temps' '-mtune=generic' '-march=x86-64'

/usr/lib/gcc/x86_64-linux-gnu/7/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/7/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper -plugin-opt=-fresolution=hello.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 -pie -z now -z relro /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/7/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/7 -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../../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/7/../../.. hello.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/7/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.o
COLLECT_GCC_OPTIONS='-v' '-save-temps' '-mtune=generic' '-march=x86-64'

注意:在链接时使用的是collect2,不是ld,那么二者有什么关系??

collect2ld链接器的一个封装,最终还是要调用ld来完成链接工作,collect2的作用是在实现main函数的代码前对目标文件中命名的特殊符号进行收集. 这些特殊的符号表明它们是全局构造函数或在main前执行,collect2会生成一个临时的.c文件,将这些符号的地址收集成一个数组,然后放到这个.c文件里面,编译后与其他目标文件一起被链接到最终的输出文件中。在这里我们没有加-nostdlib,所以自然不会调用__main,也就不会链接main函数所需引用的目标文件,也就不会对那些特殊的符号进行收集.

目标文件–Object File

目标文件及可执行文件,主要格式Windows下PE(Portable Executable)和Linux的ELF(Executable Linkable Format),他们都是COFF(Common file format)的变种

可执行文件按照可执行文件格式存储,动态链接库(DLL, Dynamic Linking Library)静态链接库(Static Linking Library)文件都是按照可执行文件格式存储

文件格式-ELF

  • ELF格式文件分类:
ELF文件类型说明示例
可重定位文件(Relocatablr File)包含代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库属于这类Linux的.o, Windows的.obj
可执行文件(Executable File)包含可以直接执行的程序,它的代表就是ELF可执行文件,一般没有扩展名比如/bin/bash文件, Windows的.exe
共享目标文件(Share Object File)包含代码和数据,可以在两种情况下使用,一种链接器使用生成目标文件,另一种动态链接器可以将几个共享目标文件和可执行文件结合,作为进程映像一起运行Linux的.so, Windows的DLL
核心转储文件(Core Dump File)当进程意外终止时,系统可以将该进程的地址空间内容及终止时的一些其他信息转存到核心转储文件Linux下的core dump
  • 查看
1
2
=====>$file a.out
a.out: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=fadad58a4fd9204e015da490f57908c27ba46ccf, not stripped

ELF的格式

目标文件的内容包含代码和数据,还有链接时必要的一些信息,比如符号表,调试信息,字符串等.一般目标文件将这些信息按不同的属性,以"节"(Section)的形式存储,有时候也就"段"(Segment),在一般情况下都表示一个一定长度的区域

SectionSegment的区别:???

  • 在ELF的链接视图(编译)中,不同的属性称为Section
  • 在ELF的装载视图(运行)中,不同的属性称为Segment

ELF文件:

Executable File / Object File说明
File Header包含一个段表(Section Table)
.text section代码段, 机器指令
.data section数据段, 全局变量和局部静态变量数据
.bss section未初始化的全局变量和局部静态变量,默认值为0

:

  • 段表,是一个描述文件中各个段的数组,描述了文件中各个段在文件中的偏移位置及段的属性等
  • .bss, 只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占空间

总体来说,程序源代码被编译后主要分成两种段, 程序指令程序数据,代码段属于程序指令,而数据段和.bss段 属于程序数据

数据和指令分段的好处

  1. 权限控制, 当程序被装载后,数据和指令分别被映射到两个虚存区域.由于数据局域对进程来说是可读写的,指令区域对进程来说是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读.这样可以防止程序被有意或无意的修改.
  2. CPU的设计角度出发,现代CPU都有着缓存(Cache)体系.指令区和数据区的分离有利于提高程序的局部性.现代CPU的缓存一般都设计成数据缓存(D-Cache)和指令缓存(I-Cache)分离,所以程序的指令和数据分开存放对CPU的缓存命中率提高有好处.
  3. 指令共享, 当系统中运行多个该程序的副本时,它们的指令都是一样的,所以内存中只需要保存一份程序的指令部分.

挖掘SimpleSection.o

示例分析ELF格式的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*#############################################################
* File Name : SimpleSection.c
* Author : winddoing
* Created Time : 2018年12月18日 星期二 15时17分55秒
* Description :
* gcc -c SimpleSection.c -o SimpleSection.o
* gcc version 7.3.0 (Ubuntu 7.3.0-27ubuntu1~18.04)
*############################################################*/

int printf(const char* format, ...);

int global_init_var = 84;
int global_uninit_var;

void func1(int i)
{
printf("%d\n", i);
}

int main(int argc, const char *argv[])
{
static int static_var = 85;
static int static_var2;

int a = 1;
int b;

func1(static_var + static_var2 + a + b);

return a;
}

系统环境: ubuntu18.04 64bit, gcc version 7.3.0

1
gcc -c SimpleSection.c -o SimpleSection.o

-c: 表示只编译不链接

Object文件的内部结构:

1
$objdump -h SimpleSection.o
  • -h: 打印ELF文件各个段的基本信息
  • -x: 全部详细打印输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SimpleSection.o:     file format elf64-x86-64

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000005e 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 000000a0 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 000000a8 2**2
ALLOC
3 .rodata 00000004 0000000000000000 0000000000000000 000000a8 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000002b 0000000000000000 0000000000000000 000000ac 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000d7 2**0
CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 000000d8 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
Section Name注释
.text代码段
.data数据段
.bssBSS段
.rodata只读数据段
.comment注释信息段
.note.GNU-stack堆栈提示段
.eh_frame展开堆栈所需的调用帧信息

eh_frame, http://blog.51cto.com/zorro/1034925

ELF的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
+----------------------+
| |
| |
| |
| Other Data |
| |
| |
| |
| |
+----------------------+ 0x0000 00d8
| .eh_frame |
+----------------------+ 0x0000 00d7
| |
| .comment |
| |
+----------------------+ 0x0000 00ac
| .rodata |
| |
+----------------------+ 0x0000 00a8
| .data |
+----------------------+ 0x0000 00a0
| |
| |
| .text |
| |
| |
+----------------------+ 0x0000 0040
| |
| ELF Header |
| |
+----------------------+ 0x0000 0000

查看ELF文件中代码段,数据段,和BSS段的长度,size命令

1
2
3
$size SimpleSection.o
text data bss dec hex filename
186 8 4 198 c6 SimpleSection.o

代码段

1
$objdump -s -d SimpleSection.o
  • -s: 将所有段的内容以十六进制的方式打印出来
  • -d: 将所有包含指令的段反汇编
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
SimpleSection.o:     file format elf64-x86-64

Contents of section .text:
0000 554889e5 4883ec10 897dfc8b 45fc89c6 UH..H....}..E...
0010 488d3d00 000000b8 00000000 e8000000 H.=.............
0020 0090c9c3 554889e5 4883ec20 897dec48 ....UH..H.. .}.H
0030 8975e0c7 45f80100 00008b15 00000000 .u..E...........
0040 8b050000 000001c2 8b45f801 c28b45fc .........E....E.
0050 01d089c7 e8000000 008b45f8 c9c3 ..........E...
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata:
0000 25640a00 %d..
Contents of section .comment:
0000 00474343 3a202855 62756e74 7520372e .GCC: (Ubuntu 7.
0010 332e302d 32377562 756e7475 317e3138 3.0-27ubuntu1~18
0020 2e303429 20372e33 2e3000 .04) 7.3.0.
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 24000000 00410e10 8602430d ....$....A....C.
0030 065f0c07 08000000 1c000000 3c000000 ._..........<...
0040 00000000 3a000000 00410e10 8602430d ....:....A....C.
0050 06750c07 08000000 .u......

Disassembly of section .text:

0000000000000000 <func1>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 8b 45 fc mov -0x4(%rbp),%eax
e: 89 c6 mov %eax,%esi
10: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 17 <func1+0x17>
17: b8 00 00 00 00 mov $0x0,%eax
1c: e8 00 00 00 00 callq 21 <func1+0x21>
21: 90 nop
22: c9 leaveq
23: c3 retq

0000000000000024 <main>:
24: 55 push %rbp
25: 48 89 e5 mov %rsp,%rbp
28: 48 83 ec 20 sub $0x20,%rsp
2c: 89 7d ec mov %edi,-0x14(%rbp)
2f: 48 89 75 e0 mov %rsi,-0x20(%rbp)
33: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
3a: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 40 <main+0x1c>
40: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 46 <main+0x22>
46: 01 c2 add %eax,%edx
48: 8b 45 f8 mov -0x8(%rbp),%eax
4b: 01 c2 add %eax,%edx
4d: 8b 45 fc mov -0x4(%rbp),%eax
50: 01 d0 add %edx,%eax
52: 89 c7 mov %eax,%edi
54: e8 00 00 00 00 callq 59 <main+0x35>
59: 8b 45 f8 mov -0x8(%rbp),%eax
5c: c9 leaveq
5d: c3 retq

数据段和只读数据段

  • 数据段: .data主要存放初始化了的全局静态变量局部静态变量

    在SimpleSection.c示例中, 有这样两个变量global_init_varstatic_var, 一共8字节,所以.data段的大小将是8字节

  • 只读数据段: .rodata存放的只读数据.一般是程序中的只读变量(const修饰的变量)和字符串常量

    好处:

    • 操作系统在加载时,将.rodata段映射成只读后,任何对这个段的操作,将被视为非法操作,保证了程序的安全性
    • 在嵌入式平台中,有些存储器是采用的只读存储器,如ROM,这样将rodata段放在该存储区域就可以保证程序访问存储器的正确性(比如固化在CPU中的bootram的数据段映射)
1
$objdump -s SimpleSection.o
1
2
3
4
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata:
0000 25640a00 %d..

global_init_var=84十六进制表示54,占四字节,由于是小端模式Little Endian, 排列顺序:[54 00 00 00]

BBS段

  • BBS段: .bss存放未初始化全局变量局部静态变量

    在示例中global_uninit_varstatic_var2两个变量将存放在BSS段,准确说就是.bss段为其预留空间,但是我们看到该段大小只有4字节,而这两个变量的大小是8字节.

不同的语言与不同的编译器实现有关,有些编译器会将全局的未初始化变量存放到BSS段,有些则不存放,只是预留一个未定义的全局变量符号,等到最终链接成可执行文件时,再在BSS段分配空间.(弱符号和强符号)

编译单元内部可见的静态变量(static修饰),的确存放在BSS段

1
$objdump -h SimpleSection.o
1
2
3
4
Sections:
Idx Name Size VMA LMA File off Algn
2 .bss 00000004 0000000000000000 0000000000000000 000000a8 2**2
ALLOC

其他段

常用段名说明
.rodata1Read only Data 只存放只读数据,比如字符串常量,全局const变量,和.rodata一样
.comment存放编译器版本信息,比如字符串:”.GCC: (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0.”
.debug调试信息
.dynamic动态链接信息
.hash符号哈希表
.line调试时的行号表,即源代码行号与编译后指令的对应表
.note额外的编译器信息,比如程序的公司名,发布版本号
.strtabString Table字符串表,用于存储ELF文件中用到的各种字符串
.symtabSymbol Table符号表
.shstrtabSection String Table 段名表
.plt .got动态链接的跳转表和全局入口表
.init fini程序初始化和终结代码段
  • 将一个二进制文件,比如图片,音乐作为目标文件中的一个段, 使用objcopy工具
1
$objcopy -I binary -O elf64-x86-64  pic.jpg pic.o
1
$objdump -ht pic.o
1
2
3
4
5
6
7
8
9
10
11
pic.o:     file format elf64-little

Sections:
Idx Name Size VMA LMA File off Algn
0 .data 000799df 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, DATA
SYMBOL TABLE:
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 g .data 0000000000000000 _binary_pic_jpg_start
00000000000799df g .data 0000000000000000 _binary_pic_jpg_end
00000000000799df g *ABS* 0000000000000000 _binary_pic_jpg_size

符号_binary_pic_jpg_start,_binary_pic_jpg_end,_binary_pic_jpg_size表示该图片文件所在内存中的起始地址,结束地址和大小.可以在程序中直接声明并使用它们.

自定义段

GCC提供的一种扩展机制,可以指定变量所处的段.

比如为了满足某些硬件的内存或IO地址布局,将某些变量或代码放到指定的段

在全局变量或函数前加"__attribute__((section("name")))"属性就可以把相应的变量或函数放到以"name"作为段名的段中

1
2
3
4
5
__attribute__((section("FOO"))) int global = 42;
__attribute__((section("BAR"))) void foo()
{

}

ELF文件结构描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+----------------------+
| ELF Header |
+----------------------+
| .text |
+----------------------+
| .data |
+----------------------+
| .bss |
+----------------------+
| ... |
+----------------------+
| other sections |
+----------------------+
| Section header table | <== 段表: 描述ELF文件所有段的信息
+----------------------+
| String Tables |
| |
| Symbol Tables |
| |
| |
+----------------------+

ELF文件分析工具: readelf

头文件

1
$readelf -h SimpleSection.o
  • -h: Display the ELF file header
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
头            ELF Header:
ELF魔数 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版本 ABI Version: 0
ELF重定位类型 Type: REL (Relocatable file)
硬件平台 Machine: Advanced Micro Devices X86-64
硬件平台版本 Version: 0x1
入口地址 Entry point address: 0x0
程序入口 Start of program headers: 0 (bytes into file)
段表位置 Start of section headers: 1112 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 12
  • 数据结构定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;

file: /usr/include/elf.h

ELF魔数: 最开始的4个字节所有的ELF文件 都必须相同,分别是0x7f,0x45,0x4c, 0x46

  • 第一个字节对应ASCII字符: DEL字符
  • 后面3个字符对应ASCII字符: E, L, F

段表

段表:保存各个段的基本属性,描述各个段的信息,如段名,段的长度,在文件中的偏移,读写权限等

1
$readelf -S SimpleSection.o
  • -S: Display the sections’ header,每个段的头信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
There are 13 section headers, starting at offset 0x458:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000005e 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000348
0000000000000078 0000000000000018 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 000000a0
0000000000000008 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 000000a8
0000000000000004 0000000000000000 WA 0 0 4
[ 5] .rodata PROGBITS 0000000000000000 000000a8
0000000000000004 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 000000ac
000000000000002b 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000d7
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 000000d8
0000000000000058 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 000003c0
0000000000000030 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 00000130
0000000000000198 0000000000000018 11 11 8
[11] .strtab STRTAB 0000000000000000 000002c8
000000000000007c 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 000003f0
0000000000000061 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
  • 各段在文件的分布:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    start +------------------+0x0000 0000
    | ELF Header |
    | e_shoff=0x458 +--------------------+
    +------------------+0x0000 0040 |
    | | |
    | .text | |
    | | |
    +------------------+0x0000 00a0 |
    | .data | |
    +------------------+0x0000 00a8 |
    | .rodata | |
    +------------------+0x0000 00ac |
    | | |
    | .comment | |
    +------------------+0x0000 00d7 |
    | .note.GNU|stack | |
    +------------------+0x0000 00d8 |
    | .eh_frame | |
    | | |
    | | |
    | | |
    +------------------+0x0000 0130 |
    | .symtab | |
    | | |
    | | |
    | | |
    +------------------+0x0000 02c8 |
    | .strtab | |
    | | |
    | | |
    | | |
    +------------------+0x0000 0348 |
    | .rela.text | |
    | | |
    | | |
    +------------------+0x0000 03c0 |
    | .rela.eh_frame | |
    | | |
    | | |
    +------------------+0x0000 03f0 |
    | .shstrtab | |
    | | |
    | <--+0x0000 0451 |
    | | |
    +------------------+0x0000 0458 <------+
    | |
    | Section Table |
    | |
    | |
    | |
    | |
    | |
    end +------------------+0x0000 0798

文件大小:SimpleSection.o = 1944 = 0x798 Bit

段的名字只是在编译和链接过程中有意义,不能正真代表段的类型

  • .rela.text: 重定位表(Relocation Table), 链接器处理目标文件时,对目标文件中的某些部位进行重定位,即代码段和数据段中那些绝对地址的引用位置.
  • .strtab: 字符串表(String Table), 用于保存普通的字符串
  • .shstrtab: 段表字符串表(Section Header String Table), 用于保存段表中用到的字符串

字符串表

ELF文件中对字符串的存储,由于不确定其长度,没有固定的结构进行表示.常用的做法是将字符串集中起来存放在一个表中,使用字符串在表中的偏移来引用字符串

  • 字符串表:
偏移+0+1+2+3+4+5+6+7+8+9
+0\0helloworl
+10d\0Myvariab
+20le\0
  • 引用
偏移字符串
0空字符串
1helloworld
6wrold
12Myvariable

字符串表在ELF中以段的形式保存

段名含义
.strtab字符串表(String Table)用于保存普通字符串
.shstrtab段表字符串表(Section Header String Table)用于保存段表中用到的字符串

链接的接口–符号

链接的本质就是把多个不同的目标文件(.o)之间相互”粘”到一起

在链接中,将函数和变量统称为符号(Symbol),函数名和变量名统称为符号名(Symbol Name)

特殊符号

符号作用
__executable_start程序起始地址(注意不是程序入口地址,是程序最开始执行的地址)
__etext_etext\etext代码段结束地址,即代码段最末尾的地址
_edata\edata数据段结束地址
_end\end程序结束地址

以上地址都是程序装载是的虚拟地址

extern “C”

C++为了兼容C,在符号管理上使用extern "C"

1
2
3
4
extern "C" {
int func(int);
int var;
}

弱符号和强符号

名称英文示例
弱符号Weak Symbol编译器默认,未初始化的全局变量
强符号Strong Symbol编译器默认,函数和初始化的全局变量

弱符号的定义

通过GCC的"__attribute__((weak))"定义任何一个强符号为弱符号

1
2
3
4
5
6
7
8
9
10
extern int ext;

int weak;
int strong = 1;
__attribute__((weak)) weak2 = 2;

int main()
{
return 0;
}
  • weakwaek2是弱符号
  • strongmain是强符号
  • ext既非强符号也非弱符号,其是一个外部变量的引用

链接器的处理规则

  • 规则1: 不允许强符号多次定义.
  • 规则2: 如果一个符号在某一个目标文件中定义为强符号,在其他目标文件中定义为弱符号,那么选择强符号.
  • 规则3: 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个.

强引用(Strong Reference)和弱引用(Weak Reference)

在GCC中,可以通过__attribute__((weakref))扩展关键字来声明一个外部函数的引用为弱引用

1
2
3
4
5
6
__attribute__((weakref)) void foo();

int main()
{
return 0
}

弱符合强符号在库的定义使用中,弱符号可以被用户自定义的强符合所覆盖,从而使得程序可以使用自定义版本函数,方便程序扩展模块的裁剪和组合

静态链接

将相同性质的段合并到一起,如所有输入文件的.text段合并到输出文件的.text

链接

链接器一般采用两步链接:

  • 第一步: 空间与地址分配
  • 第二部: 符号解析与定位
1
$ld a.o b.o -e main -o ab
  • -e main: 表示将main函数作为程序入口,ld链接器默认的程序入口为_start

ld链接脚本

控制链接过程,就是控制输入段如何变成输出段

  • 输入段(Input Section): 输入文件中的段
  • 输出段(Output Section):输出文件中的段

ld链接脚本语法

链接脚本由一系列语句构成,语句分两种,一种是命令语句,另一种是赋值语句

链接脚本语法与C语言相似:

  • 语句之间使用分号“”作为分隔符
  • 表达式与运算符
    • +-*/+=-=*=&|>><<
  • 注释和字符引用
    • 注释:/**/

命令语句:

命令语句说明
ENTRY(symbol)指定符号的入口地址。入口地址即进程执行的第一条用户空间的指令所在进程地址空间的地址,它被指定在ELF文件头Elf32_Ehdr中的e_entry成员中。
STARTUP(filename)将文件filename作为链接过程中的第一个输入文件
SEARCH_DIR(path)将路径path加入到ld链接器的库查找目录。与“-Lpath”命令作用相同
INPUT(file, file, …)将指定文件作为链接过程中的输入文件
INCLUDE filename将指定文件包含到链接脚本
PROVIDE(symbol)在链接脚本中定义某个符号

语法格式:

1
2
3
4
5
6
SECTIONS
{
...
secname : {contents}
...
}

secname: 表示输出段段名

示例

1
2
3
4
5
6
7
8
9
ENTRY(nomain)
SECTIONS
{
- = 0x08048000 + SIZEOF_HEADERS;

tinytext : { *(.text) *(.data) *(.rodata)}

/DISCARD/ : { *(.comment)}
}

解析:

  • ENTRY(nomain): 指定程序入口为nomain()函数
  • SECTIONS: 链接脚本主体
  • . = 0x08048000 + SIZEOF_HEADERS:将当前虚拟地址设置成0x08048000+SIZEOF_HEADERS, SIZEOF_HEADERS为输出文件头大小。
  • tinytext : { *(.text) *(.data) *(.rodata)}: 段的转换,即为所以输入文件中的名字为”.text” “.data” “.rodata”的段依次合并到输出文件的”tinytext”。
  • /DISCARD/ : { *(.comment)}: 将所有输入文件中的”.comment”的段丢弃,不保存到输出文件

装载与动态链接

可执行文件的装载与进程

进程的虚拟地址空间

虚拟地址空间(Virtual Address Space)大小,有CPU位数决定。

PAE: (Physical Address Extension)物理地址扩展,主要是为了解决32位CPU中通过增加地址线扩展内存而导致的实际物理内存大小与虚拟地址空间大小之间的不匹配问题

PAE,通过页映射改变不同时机对不同物理空间的访问机制, 在Linux系统中采用mmap()系统调用实现。

装载方式

装载方式有静态装载动态装载,大部分使用动态装载的方式,动态装载分为覆盖装入(Overlay)页映射(Paging)

  • 覆盖装入:需要程序员自己编写覆盖管理器,进行程序的划分和装载管理
  • 页映射:以页大小将程序进行划分后,有操作系统进行装入的管理

页映射

将内存和所有磁盘中的数据和指令按照页(Page)为单位进行划分后,在需要时进行动态映射的过程。

页大小,硬件规定页大小4k、8K、2M、4M等,常用4K大小

程序员的自我修养_page_map

页的置换:

  • 先进先出算法(FIFO):置换最先映射到页
  • 最少使用算法(LUR):将内存中很少访问到的页置换出去

进程的虚拟空间分布

segment:从装载的角度重新划分了ELF的各个段,一个“segment”包含一个或多个属性类似的”Section”

segment的好处,减少页面内部碎片,节省内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
readelf -l a.out

Elf file type is DYN (Shared object file)
Entry point 0x540
There are 9 program headers, starting at offset 64

Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000001f8 0x00000000000001f8 R 0x8
INTERP 0x0000000000000238 0x0000000000000238 0x0000000000000238
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000878 0x0000000000000878 R E 0x200000
LOAD 0x0000000000000db8 0x0000000000200db8 0x0000000000200db8
0x0000000000000258 0x0000000000000260 RW 0x200000
DYNAMIC 0x0000000000000dc8 0x0000000000200dc8 0x0000000000200dc8
0x00000000000001f0 0x00000000000001f0 RW 0x8
NOTE 0x0000000000000254 0x0000000000000254 0x0000000000000254
0x0000000000000044 0x0000000000000044 R 0x4
GNU_EH_FRAME 0x0000000000000730 0x0000000000000730 0x0000000000000730
0x000000000000003c 0x000000000000003c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000000db8 0x0000000000200db8 0x0000000000200db8
0x0000000000000248 0x0000000000000248 R 0x1

Section to Segment mapping:
Segment Sections...
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 .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .dynamic .got .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .dynamic .got
  • section:ELF文件的链接视图
  • segment:ELF文件的执行视图

  • VMA:(Virtual Memory Area)虚拟内存区域

  • AVMA:(Anonvmous Virtual Memory Area)匿名虚拟内存区域,主设备号、次设备号以及文件节点号都是0,表示没有映射到文件中的VMA

随机地址空间分布技术

段地址对齐

物理内存与虚拟进程之间的映射关系,内存空间的长度必须是4096的整数倍,并且这段空间的物理内存和进程虚拟地址空间的起始地址必须是4096的整数倍。

提高内存空间的利用率。

Linux内核装载ELF过程

  1. bash进程调用fork()系统调用创建一个新的进程
  2. 新进程调用execve()系统调用执行指定的ELF文件
    1. execve -> SYSCALL_DEFINE3(execve, …) -> do_execve
    2. do_execve中判断ELF文件的类型,不同的类型(C,Java等)选择不同的装载过程,装载ELF文件,并且do_execve系统调用返回的地址就是被装载的ELF程序的入口地址
    3. execve系统调用从内核态返回用户态时,EIP(PC)寄存器直接跳转到ELF程序的入口地址,执行新程序

动态链接

  • 静态链接好处,使不同的开发者能够独立的开发和测试自己的程序模块,不好之处在于,对计算机的内存和磁盘空间浪费严重, 同时也不利于模块更新。
  • 动态链接,主要是为解决空间浪费更新困难,其思想是将链接过程推迟到运行时进行,好处节省内存,减少物理页面的换入换出,增加CPU缓存的命中率

动态链接模块的装载地址是从地址0x00000000开始,无效的地址,真实的地址是在装载时确定的。

装载时重定位

为了能够使共享对象能够在任意地址装载

  • 静态链接时的重定位,链接时重定位(Link Time Relocation)
  • 动态链接时的重定位,装载时重定位(Load Time Relocation),也叫基址重置(Rebasing)

动态链接模块被装载映射到虚拟内存后,指令部分是在多个进程之间共享的,由于装载时重定位是需要修改指令,所以没有办法做到同一份指令被多进程共享

动态链接库中的可修改数据部分对于不同的进程来说有多个副本,所以他们可以采用装载是重定位的方法解决。

地址无关代码

-fPIC, 选择地址无关代码(Position-independent Code)

数据段GOT表(全局偏移表,Global Offset Table)的引入,就是把地址相关部分放到数据段里。

  • 地址引用方式
指令跳转、调用数据访问
模块内部相对跳转和调用相对地址访问
模块外部间接跳转和调用(GOT)间接访问(GOT)

ELF共享库编译时,默认把定义在模块内部的全局变量当做定义在其他模块的全局变量,通过GOT表实现数据访问

数据段地址无关性

装载时重定位数据段

-fPIC

编译选项:

  • 添加: 表示产生地址无关的代码段,这样生成的动态链接的可执行文件中存在GOT表,代码段共享节省空间
  • 不添加:表示装载时使用重定位共享对象,代码段多份拷贝,不能节省空间,但是运行速度要比使用地址无关代码的共享对象快,因为它省去了地址无关代码中每次访问全局数据和函数时需要做一次计算当前地址以及间接地址的寻址过程。

延迟绑定(PLT)

PLT: Procedure Linkage Table(程序链接表)

ELF将GOT拆分为两个表叫做“.got””.got.plt"

  • .got: 用于保存全局变量引用的地址
  • .got.plt: 用于保存函数引用的地址

.interp

动态链接器的位置既不是系统配置指定,也不是由环境参数决定,而由ELF可执行文件决定,在ELF文件的.interp段中定义(interp是interpreter(解释器)的缩写)

1
2
$readelf -l a.out | grep "interpreter"
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

.dynamic

保存链接器所需的基本信息,如依赖哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址

1
readelf -d libxx.so

动态符号表

动态符号表(Dynamic Symbol Table),为表示动态链接时模块之间的导入导出关系,段名.dynsym

1
readelf -sD libxx.so

动态链接重定位表

PIC模式的共享对象也需要重定位,其代码段是地址无关,不需要重定位,但是数据段其中包含决定地址并且GOT表也在数据段

动态链接器的自举

动态链接器本身是一个共享对象,但是其具有特殊性不可以依赖其他任何对象,其本身所需要的全局变量和静态变量的重定位工作由它本身完成

自举(bootstrap):具有一定限制条件的启动代码

动态链接器的入口地址是自举代码的入口地址。

显石运行时链接

动态库的装载API:#include <dlfcn.h>

  • dlopen:打开动态库
  • dlsym:查找符号
  • dlerror:错误处理
  • dlclose:关闭动态库

Linux共享库的组织

兼容即接口,二进制接口(ABI,Application Binary Interface)

共享库版本命名

libname.so.z.y.z

  • x:主版本号(Major Version Number),表示库的重大升级,不同主版本号之间不兼容
  • y:次版本号(Minor Version Number),表示库的增量升级,高的次版本号的库向后兼容低的次版本号的库
  • z:发布版本号(Release Version Number),表示库的错误修正、性能改进等,不添加如何新的接口也不对任何接口进行改动

SO-NAME

共享库的命名机制,即共享库的文件名去掉次版本号和发布版本号,保留主版本号,本质就是一个软链接

共享库查找过程

动态链接器会在/lib/user/lib和由/etc/ld.so.conf配置文件指定的目录中查找共享库

环境变量

LD_LIBRARY_PATH

改变动态链接器装载共享库路径的方法

1
export LD_LIBRARY_PATH = /xxxx/libname.so.x

LD_PRELOAD

指定预先要装载的共享库路径

LD_DEBUG

打开动态链接器调试功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
LD_DEBUG=files ./a.out
1696:
1696: WARNING: Unsupported flag value(s) of 0x8000000 in DT_FLAGS_1.
1696:
1696: file=libc.so.6 [0]; needed by ./a.out [0]
1696: file=libc.so.6 [0]; generating link map
1696: dynamic: 0x00007f36e1202b80 base: 0x00007f36e0e18000 size: 0x00000000003f0ae0
1696: entry: 0x00007f36e0e39cb0 phdr: 0x00007f36e0e18040 phnum: 10
1696:
1696:
1696: calling init: /lib/x86_64-linux-gnu/libc.so.6
1696:
1696:
1696: initialize program: ./a.out
1696:
1696:
1696: transferring control: ./a.out
1696:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LD_DEBUG=help ./a.out
Valid options for the LD_DEBUG environment variable are:

libs display library search paths
reloc display relocation processing
files display progress for input file
symbols display symbol table processing
bindings display information about symbol binding
versions display version dependencies
scopes display scope information
all all previous options combined
statistics display relocation statistics
unused determined unused DSOs
help display this help message and exit

To direct the debugging output into a file instead of standard output
a filename can be specified using the LD_DEBUG_OUTPUT environment variable.

库与运行库

内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
+-----------------------+ 0xffff ffff
| |
| kernel space |
| |
+-----------------------+ 0xc000 0000
| stack |
+-----------+-----------+
| | |
| v |
| unused |
+-----------------------+
| dynamic libraries |
+-----------------------+ 0x4000 0000
| unused |
| ^ |
| | |
+-----------+-----------+
| heap |
+-----------------------+
| read/write sections |
+-----------------------+
| readonly sections |
| .init .rodata .text |
+-----------------------+ 0x0804 8000
| reserved |
+-----------------------+ 0

Linux进程地址空间分布

Segment fault

原因:

  • 指针初始化为NULL,没有指定空间直接开始使用
  • 没有初始化栈上的指针,直接开始使用,该指针一般是一个随机数

在经典的计算机科学中,是一种特殊的容器,具有先进先出(FIFO)的规则,但是在计算机系统中,栈则是一个具有FIFO属性的内存区域

栈保存了一个函数调用所需的维护信息,通常称为堆栈帧(Stack Frame)或活动记录(Activate Record)

堆栈帧的内容:

  • 函数的返回地址和参数
  • 临时变量,包括函数的非静态变量以及编译器生成的其他临时变量
  • 保存的上下文,包含函数调用前后保持不变的寄存器

mmap

向操作系统申请一段虚拟地址空间,这段空间可以映射到一个文件,也可以不用射文件,不映射文件的时候叫匿名空间

1
2
3
4
5
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);
  • addr: 申请空间的起始地址,如果为0表示Linux系统会自动选择合适的起始地址
  • length: 申请空间的长度
  • prot: 设置申请的空间权限(可读、可写、可执行)
  • flags:设置申请空间的映射类型(文件类型,匿名空间)
  • fd:表示映射的文件描述符
  • offset: 指定文件映射的文件偏移

glibc的malloc函数处理用户空间请求:对于小于128KB的请求,它会在现有堆空间里面,按照堆分配算法为它分配一块空间并返回;对于大于128KB的请求来说,会使用mmap分配一块匿名空间,然后在这块匿名空间中为用户分配空间。

堆分配算法

  • 空闲链表
  • 位图
  • 对象池

运行库

系统调用与API

运行库实现


工具

gcc

-fno-builtin

关闭内置函数优化选项

objdump

objcopy

readelf

nm

strip

清除掉共享库或可执行文件的所有符号和调试信息

1
strip libname.so.x

ar

查看函数库里的详细情况和用多个对象文件生成一个库文件

1
2
3
4
5
6
7
8
9
10
11
12
$ar
Usage: ar [emulation options] [-]{dmpqrstx}[abcDfilMNoPsSTuvV] [--plugin <name>] [member-name] [count] archive-file file...
ar -M [<mri-script]
commands:
d - delete file(s) from the archive
m[ab] - move file(s) in the archive
p - print file(s) found in the archive
q[f] - quick append file(s) to the archive
r[ab][f][u] - replace existing or insert new file(s) into the archive
s - act as ranlib
t - display contents of archive
x[o] - extract file(s) from the archive
1
ar -t libname.a

显示所有对象文件(.o文件)的列表

1
ar -rv libname.a  objfile1.o objfile2.o ... objfilen.o

把objfile1.o–objfilen.o打包成一个库文件

ld

1
2
3
4
5
ld --help
Usage: ld [options] file...
Options:
-e ADDRESS: 指定程序入口
-T FILE:读取链接脚本(*.ld)

ldd

查看一个程序主模块或共享库依赖哪些共享库

ldconfig

为共享库目录下的各个共享库创建、删除或更新相应的SO-NAME

  • 安装
1
ldconfig -n libname.so.x