Linux下的ELF文件、链接、加载

ELF文件

ELF文件的三种形式

ELF的全称为:Executable and Linkable Format,即 ”可执行、可链接格式“。

在Linux下,可执行文件、动态库文件、目标文件(可重定向文件)都是同一种文件格式,称之为ELF文件格式。虽然都是ELF文件格式但不相同,可以通过file filename查看文件的格式信息

  • 可重定向文件(relocatable)目标文件:通常是.o文件,包含二进制代码和数据,但它的代码及数据都没有指定绝对地址,可以在编译时与其他可重定向目标文件合并起来创建一个可执行目标文件
  • 可执行(executable)目标文件:是完全链接的可执行文件,即静态链接的可执行文件。包含二进制代码和数据,其代码和数据都有固定的地址 (或相对于基地址的偏移 ),可以被直接复制到内存并执行
  • 共享(shared)目标文件:通常是.so动态链接库文件或者动态链接生成的可执行文件。一种特殊的可重定向目标文件,可以在加载或者运行时被动态地加载进内存并链接。动态库文件和动态链接生成的文件都属于这一类。

显然这里的三个ELF文件形式要么是可执行的、要么是可链接的。

还有一种core文件也属于ELF文件,在core dumped时可以得到。

在Linux中并不以后缀名作为区分文件格式的绝对标准

文件内部构成

ELF header在文件开始处描述了整个文件的组织,节头部表(Section Tables)提供了目标文件的各项信息(如指令、数据、符号表、重定位信息等),程序头表(Program Tables)指出怎样创建进程映像,含有每个program header的入口,section header table包含每一个section的入口,给出名字、大小等信息。可以通过readelf -l [fileName]readelf -S [fileName]来查看。

但是并不是所有以上的三种ELF形式都有这两种表

  • 如果用于编译和链接(可重定向目标文件),则编译器和链接器将把ELF文件看作是节头表描述的节的集合,程序头表可选
  • 如果用于加载执行(可执行目标文件),则加载器将把ELF文件看作是程序头表描述的段的集合,一个段可能包含多个节,节头部表可选。
  • 如果是共享目标文件,则两者都有。 因为链接器在链接的时候需要节头部表来查看目标文件各个section的信息然后对各个目标文件进行链接;而加载器在加载可执行程序的时候需要程序头表,它需要根据这个表把相应的段加载到进程自己的虚拟内存中

ELF头

ELF头描述了整个文件的基本信息,它位于文件的最开始的部分,大小一般为64个字节。首先是64字节的ELF头Elf64_Ehdr,其中包含了很多重要的信息,这些信息中有一个很关键的信息叫做Start of section headers,它指明了节头部表,Section Headers Elf64_Shdr的位置。段表中储存了ELF文件中各个的偏移量以记录其位置。可以使用readelf -h [filename]查看。ELF中的各个段可以通过readelf -S [fileName]来查看。

可重定向文件的文件头信息如下:

hno@hno-virtual-machine:~/Desktop/OTHER/111$ readelf -h main.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: 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: 1080 (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: 15
Section header string table index: 14

可执行文件的文件头信息如下:

hno@hno-virtual-machine:~/Desktop/OTHER/111$ readelf -h a.out
ELF Header:
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - GNU
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x401bc0
Start of program headers: 64 (bytes into file)
Start of section headers: 869912 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 10
Size of section headers: 64 (bytes)
Number of section headers: 32
Section header string table index: 31

ELF文件头的结构信息的数据结构定义在/usr/include/elf.h文件中, 例如64位版本的如下:

#define EI_NIDENT (16)
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;

32位版本如下:

#define EI_NIDENT       16   
typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type; /* file type */
Elf32_Half e_machine; /* architecture */
Elf32_Word e_version;
Elf32_Addr e_entry; /* entry point */
Elf32_Off e_phoff; /* PH table offset */
Elf32_Off e_shoff; /* SH table offset */
Elf32_Word e_flags;
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* PH size */
Elf32_Half e_phnum; /* PH number */
Elf32_Half e_shentsize; /* SH size */
Elf32_Half e_shnum; /* SH number */
Elf32_Half e_shstrndx; /* SH name string table index */
} Elf32_Ehdr;

程序头表

程序头表存在于可执行文件中,位于ELF文件头后面,在程序加载的时候会使用到程序头表。对于ELF文件,可以从链接角度加载角度两方面来看,对应了链接视图与加载视图。加载视图关心的是程序头表,链接视图关心节头部表。在链接生成可执行文件时,会把多个节(section)合并对应一个段(segment)。所以节头部表中的数目少于程序头表的数目的。程序头的作用是方便程序的加载,多个节可能有相同的读写属性,把相同属性的节合并成一个段,一次性加载入内存中。

读取可执行文件的程序头表时,输出了不同的section和对应segment的对应关系

hno@hno-virtual-machine:~/Desktop/OTHER/111$ readelf -l -W a.out

Elf file type is EXEC (Executable file)
Entry point 0x401bc0
There are 10 program headers, starting at offset 64

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x000518 0x000518 R 0x1000
LOAD 0x001000 0x0000000000401000 0x0000000000401000 0x093791 0x093791 R E 0x1000
LOAD 0x095000 0x0000000000495000 0x0000000000495000 0x02663d 0x02663d R 0x1000
LOAD 0x0bc0c0 0x00000000004bd0c0 0x00000000004bd0c0 0x005150 0x0068c0 RW 0x1000
NOTE 0x000270 0x0000000000400270 0x0000000000400270 0x000020 0x000020 R 0x8
NOTE 0x000290 0x0000000000400290 0x0000000000400290 0x000044 0x000044 R 0x4
TLS 0x0bc0c0 0x00000000004bd0c0 0x00000000004bd0c0 0x000020 0x000060 R 0x8
GNU_PROPERTY 0x000270 0x0000000000400270 0x0000000000400270 0x000020 0x000020 R 0x8
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x0bc0c0 0x00000000004bd0c0 0x00000000004bd0c0 0x002f40 0x002f40 R 0x1

Section to Segment mapping:
Segment Sections...
00 .note.gnu.property .note.gnu.build-id .note.ABI-tag .rela.plt
01 .init .plt .text __libc_freeres_fn .fini
02 .rodata .stapsdt.base .eh_frame .gcc_except_table
03 .tdata .init_array .fini_array .data.rel.ro .got .got.plt .data __libc_subfreeres __libc_IO_vtables __libc_atexit .bss __libc_freeres_ptrs
04 .note.gnu.property
05 .note.gnu.build-id .note.ABI-tag
06 .tdata .tbss
07 .note.gnu.property
08
09 .tdata .init_array .fini_array .data.rel.ro .got

节头部表

节头部表(section header table),它保存了文件所有节的信息。节头部表就是一个数组,第一个元素都是一个描述节信息的数据结构,使用objdump -hreadelf -S命令可以看到,节头部表位于文件的后半部分,在所有的section的后面。在ELF文件头信息中给出节头部表在文件中的偏移位置(Start of section headers)

使用readelf工具显示的重定位文件中的节头部表信息如下所示

hno@hno-virtual-machine:~/Desktop/OTHER/111$ readelf -S main.o
There are 15 section headers, starting at offset 0x438:

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
0000000000000000 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 00000040
0000000000000000 0000000000000000 WA 0 0 1
[ 3] .bss NOBITS 0000000000000000 00000040
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .rodata.str1.1 PROGBITS 0000000000000000 00000040
000000000000000e 0000000000000001 AMS 0 0 1
[ 5] .text.startup PROGBITS 0000000000000000 0000004e
0000000000000036 0000000000000000 AX 0 0 1
[ 6] .rela.text.startu RELA 0000000000000000 000002e8
00000000000000a8 0000000000000018 I 12 5 8
[ 7] .comment PROGBITS 0000000000000000 00000084
000000000000002c 0000000000000001 MS 0 0 1
[ 8] .note.GNU-stack PROGBITS 0000000000000000 000000b0
0000000000000000 0000000000000000 0 0 1
[ 9] .note.gnu.propert NOTE 0000000000000000 000000b0
0000000000000020 0000000000000000 A 0 0 8
[10] .eh_frame PROGBITS 0000000000000000 000000d0
0000000000000030 0000000000000000 A 0 0 8
[11] .rela.eh_frame RELA 0000000000000000 00000390
0000000000000018 0000000000000018 I 12 10 8
[12] .symtab SYMTAB 0000000000000000 00000100
00000000000001b0 0000000000000018 13 12 8
[13] .strtab STRTAB 0000000000000000 000002b0
0000000000000037 0000000000000000 0 0 1
[14] .shstrtab STRTAB 0000000000000000 000003a8
0000000000000089 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)

section header table是一个数组结构,这个数组的位置在e_shoff处,共有e_shnum个元素(即section),每个元素的大小为e_shentsize字节。每个元素的结构如下:

typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to another section */
Elf32_Word sh_info; /* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Entry size if section holds table */
} Elf32_Shdr;

.data节

保存全局变量、全局静态变量、局部静态变量等可变数据

.rodata节

保存只读的全局变量、只读的全局静态、只读的局部静态变量等只读数据

.text节

保存程序指令,使用objdump -d可以反汇编代码信息

使用size命令可以查看ELF文件的代码段、数据段、BSS段的长度

hno@hno-virtual-machine:~/Desktop/OTHER/111$ size main.o
text data bss dec hex filename
148 0 0 148 94 main.o
hno@hno-virtual-machine:~/Desktop/OTHER/111$ size a.out
text data bss dec hex filename
761962 20772 6048 788782 c092e a.out

.bss节

在链接后的可执行文件中,该节保存:未初始化的全局变量,初始化为0的全局变量,未初始化的全局静态变量,初始化为0的全局静态变量,未初始化的局部静态变量,初始化为0的局部静态变量。总之,只要初始值为0(变量如果不初始化默认为0),无论时全局变量还是静态变量,都保存在该节内。

在可重定位文件中,与可执行文件中有一处不同:未初始化的全局变量保存在.common节中。原因是:未初始化的全局变量是弱符号,链接器允许多个同名的弱符号存在,并且链接的时候决定使用哪一个弱符号。在编译阶段,编译器在编译成可重定向文件时,不能确定未初始化的全局变量是否会在链接成可执行文件时使用,因为可能其他可重定位文件也存在同名的弱符号。所以就把所有未初始化的全局变量都放到.common节中,让链接器决定使用哪一个弱符号。当链接器确定了以后,链接成可执行文件时,未初始化的全局变量又最终还是放到了.bss节中。、

未初始化的全局静态变量,因为它们的作用域在当前文件内,所以不可能与其他文件的未初始化的全局静态变量冲突,所以保存在.bss节就可以了

.bss节在文件中不占空间,关于它的信息保存在节头部表中,当被加载到内存中时,操作系统会为它分配一块内存,并且分配这块内存初始化为0值,它的这种特性也就决定了它保存的变量的类型

.rel.text和.rel.data节

重定位表,保存对目标文件的重定位信息,也就是对代码段和数据段的绝对地址引用的位置描述。在进行重定位时,链接器会读取定位表来决定对给定符号周期什么在哪里进行重定位

.symtab节

符号表中保存了本文件中定义的所有符号信息,符号是链接的窗口,在一个文件既可能定义了一些符号,也可能引用了其他文件的符号,它们的信息都会保存到符号表中,函数和变量名都是符号。符号表入口结构定义如下:

typedef struct elf32_sym{  
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;

其中st_name包含指向符号表字符串表(strtab)中的索引,从而可以获得符号名。st_value指出符号的值,可能是一个绝对值、地址等。st_size指出符号相关的内存大小,比如一个数据结构包含的字节数等。st_info规定了符号的类型和绑定属性,指出这个符号是一个数据名、函数名、section名还是源文件名,并且指出该符号的绑定属性时local、global、还是weak。

全局符号:包含非静态函数、全局变量,对于链接过程,它只关心全局符号,全局符号是对文件外可见的,它们会被重定位。

局部符号:包含静态函数、全局静态函数、局部静态变量,这类符号只在文件内部可见,调试器可以使用这些局部符号来分析程序或崩溃时的核心转储文件,这些符号对链接过程没有作用,链接器会忽略它们。

extern c 关键字 :C++为了与C兼容,在符号管理上,为了不按照C++的符号签名的方式对符号进行扩展(C++的名称修饰机制},C++编译器会将在”exterrn c“的大括号内的代码当作C语言处理

强符号与弱符号:强符号与弱符号的概念一般都是对全局符号才有用,因为全局符号是对文件外可见的,多个文件之间相同的符号名可能冲突。局部符号对文件外不可见,只在文件内部可见,链接的时候不可能冲突。

对于C/C++来说,编译器默认数名与初始化的全局变量为强符号,未初始化的全局变量是弱符号, 也可以通过GCC的 “attribute((weak))”来定义弱符号。链接器按以下规则处理全局符号:

  1. 不允许强符号被多次定义
  2. 如果一个符号在某个目标文件中是强符号,在其它文件中是弱符号,那么链接器选择弱符号
  3. 如果一个符号在所有目标文件中都是弱符号,选择其中占用空间最大的那个符号

强引用与弱引用

强引用:当没有在其它文件中找到对应符号的定义时,链接器报符号未定义的错误
弱引用:在处理弱引用时,如果该符号未定义,链接器不会对该引用报错,而是默认值为0。在GCC中,通过” atrribute((weakref))”扩展关键字来声明对一个符号的弱引用

.strtab节

字符串表,保存ELF文件中所有的字符串。ELF文件中用到了很多字符串,比如段名/变量名/字符串变量等。因为字符串的长度往往不定,一种常见的做法是把字符串集中起来存放到一个表中,然后使用字符串在表中的偏移来引用字符串。

静态链接

编译与链接

为了节省空间和时间,一般不会将所有的代码写在同一个文件内。所以C语言允许引用其他文件里定义的符号。

假如现在有三个C文件,分别是a.c,b.c,main.c

// a.c
int foo(int a, int b){
return a + b;
}
// b.c
int x = 100, y = 200;
// main.c
extern int x, y;
int foo(int a, int b);
int main(){
printf("%d + %d = %d\n", x, y, foo(x, y));
}

在mian.c文件中声明了外部变量x、y和函数foo,C语言不会禁止并且在声明时也不会做什么类型检查,当然,在编译main.c时,是看不到这些外部变量和函数的定义的

编译、链接这些代码,Makefile如下:

CFLAGS := -Os

a.out: a.o b.o main.o
gcc -static -Wl,--verbose a.o b.o main.o

a.o: a.c
gcc $(CFLAGS) -c a.c

b.o: b.c
gcc $(CFLAGS) -c b.c

main.o: main.c
gcc $(CFLAGS) -c main.c

clean:
rm -f *.o a.out

结果生成的可执行文件能够正常地输出我们想要的内容

foo是一个函数名,在代码区。但是如果将main.c中的foo声明为一个整型,并且直接打印出这个整型,然后尝试对齐加一,即改写为如下代码:

// main.c (changed)
#include <stdio.h>
extern int x, y;
// int foo(int a, int b);
extern int foo;
int main(){
printf("%x\n", foo);
foo += 1;
// printf("%d + %d = %d\n", x, y, foo(x, y));
}

代码执行输出:

fa1e0ff3
Segmentation fault (core dumped)

能够打印出四个字节(整型为四个字节)

C语言中,可以理解为没有类型,在C语言中只有内存和指针,也就是内存地址,而类型其实就是对地址的一个解读。比如有符号整型,就按照补码解读接下来的四个字节地址;而浮点型就是按照IEEE754的浮点数规定来解读接下来的四个字节地址。

而之前我们将符号foo定义为了整型,那么编译器也会按照整型4个字节来解读,而这个地址指针指向的其实还是函数foo地址,那这四个字节应该就是函数foo在代码段的前四个字节,使用objdump -d a.out反汇编来验证:

输出:

0000000000401cd5 <foo>:
401cd5: f3 0f 1e fa endbr64
401cd9: 8d 04 37 lea (%rdi,%rsi,1),%eax
401cdc: c3 retq
401cdd: 0f 1f 00 nopl (%rax)

foo函数在代码段的前四个字节的地址就是上面打印输出的fa 1e 0f f3(小端序)。

那接下来试图对foo进行加一操作相当于是对代码段的写操作,而内存中的代码段是可读可执行不可写的,所以就会输出Segmentation fault (core dumped)

总结:

  • 编译链接的需求:允许引用其他文件(C标准称为编译单元,Compilation Unit)里定义的符号。C语言中不禁止随便声明符号的类型,但是类型不匹配是Undefined Behavior
  • C语言中类型的概念,C语言中的可以理解为没有类型,在C语言中眼中只有内存和指针,也就是内存地址,而所谓的C语言中的类型,其实就是对这个地址的一个解读

程序的编译 - 可重定向文件

使用file命令来查看main.c编译生成的main.o文件的属性

main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

可以看到,main.o文件是可重定向文件( relocatable)的ELF文件,这里的重定向指的就是链接过程中对外部符号的引用,也就是说,编译过的main.o文件对于其中声明的外部符号如foox,y,是不知道的

既然外部的符号是在链接时才会被main程序知道,那在编译main程序,生成可重定向文件时这些外部的符号是怎么处理的?同样使用objdump来查看编译出的main.o文件

0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 50 push %rax
5: 8b 35 00 00 00 00 mov 0x0(%rip),%esi # b <main+0xb>
b: 8b 3d 00 00 00 00 mov 0x0(%rip),%edi # 11 <main+0x11>
11: e8 00 00 00 00 callq 16 <main+0x16>
16: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 1c <main+0x1c>
1c: 8b 35 00 00 00 00 mov 0x0(%rip),%esi # 22 <main+0x22>
22: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 29 <main+0x29>
29: 89 c1 mov %eax,%ecx
2b: 31 c0 xor %eax,%eax
2d: e8 00 00 00 00 callq 32 <main+0x32>
32: 31 c0 xor %eax,%eax
34: 5a pop %rdx
35: c3 retq

image-20230727153722064

main在编译的时候,引用的外部符号都留空了

可以看到,在编译但还未链接的main.o文件中,对于引用的外界符号的部分是用留空的方式用0暂时填充的,即上图中用红框框出来的位置,%rip相对寻址的偏移量都是0,在静态链接完成后,它们的偏移量会被填上正确的数值

使用readelf工具readelf -r main.o查看ELF文件的重定位信息

image-20230727160242268

可以将readelf的结果图和上面objdump的结果图结合起来看,能够发现前两个外部符号的偏移量:objdump的结果 + 2 = readelf的结果,其他的以此类推。注意%rip寄存器指向了当前寄存器的末尾,也就是下一条指令的开关,所以上图中最后的偏移量要减4(y-4)

程序的静态链接

简单来说,程序的静态链接是会把所需要的文件链接起来生成可执行的二进制文件,将相应的外部符号,填入正确的位置。

  • 段合并

    首先会把相同的段识别出来放在一起。

  • 重定位

    重定位表,可以用objdump -r [fileName] 查看

    简单来说,就是当某个文件引用了外部符号,在编译时编译器不会阻止,编译器会认为你在链接时告诉他这些外部符号是什么,但是在编译时,他也不清楚这些符号在什么地址,因此这些符号的地址会在编译时被留空为0。此时的重定位就是链接器将这些留空为0的外部符号填上正确的地址。

    链接过程可以通过ld --verbose来查看默认链接脚本,在需要的时候修改链接脚本。

    可以通过使用gcc的-Wl,--verbose--verbose传递给链接器ld,从而观察到静态链接的过程

    • ldscript里面的各个section是按何种顺序“粘贴”
    • ctors / dtors (constructors / destructores) 的实现
    • 只读数据和读写数据之间的padding

    可以通过objdump来查看静态链接完成以后生成的可执行文件a.out的内容

    image-20230728192842496

    和前面的main.o的objdump输出对比来看,之前填0留空的地方都被填充上了正确的数值,%rip相对寻址的偏移量以被填上了正确的数值

静态链接库的构建与使用

如果要制作一个关于向量的静态链接库libvector.a,它包含两个源代码addvec.cmultvec。c

// addvec.c
int addcnt = 0;

void addvec(int *x, int *y, int*z, int n){
int i;
addcnt++;

for (i=0; i<n; i++) z[i] = x[i] + y[i];
}
// multvec.v
int multcnt = 0;

void multvec(int *x, int *y, int*z, int n){
int i;
multcnt++;

for (i=0; i<n; i++) z[i] = x[i] * y[i];
}

编译:

gcc -c addvec.c multvec.c
ar rcs libvector.a addvec.o multvec.o

如果有个程序main.c要调用这个静态库libvector.a

// main.c
#include <stdio.h>
#include "vector.h"

int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];

int main(){
addvec(x, y, z, 2);
printf("z = [%d %d]\n", z[0], z[1]);
return 0;
}
// vector.h
void addvec(int*, int*, int*, int);
void multvec(int*, int*, int*, int);

编译链接

gcc -c main.c
gcc -static main.o ./libvector.a

静态链接过程

可执行文件的装载

进程和装载

程序(可执行文件)和进程

  • 程序是静态概念
  • 进程是动态概念,是跑起来的程序

现代操作系统如何装载可执行文件

  • 给进程分配独立的虚拟空间
  • 将可执行文件映射到进程的虚拟空间(mmap)
  • 将cpu指令寄存器设置到程序的入口地址

可执行文件在装载过程中实际上是映射的虚拟空间,所以可执行文件通常被叫做映像文件(Image文件)

可执行ELF文件的两种视角

可执行ELF格式具有双重特性,编译器、汇编器和链接器将这个文件看成是被区段(section)头部描述的一系列逻辑区段的集合,而系统加载器将文件看成是由程序头部表描述的一系列段(segment)的集合。一个段通常会由多个区段组成。例如,一个“可加载只读”段可以由可执行代码区段、只读数据区段和动态链接器需要的符号区段组成

区段(section)是从链接器的视角来看ELF文件,对应段表Section Headers,而段(Segment)是从执行的视角来看ELF文件,也就是它会被映射到内存中,对应程序头表Program Headers。

使用命令readelf -a [fileName] 中的Section to Segment mapping部分可以看到可执行文件中的段的映射关系。

可执行文件的程序头表

readelf -h [fileName]命令查看一个可执行ELF文件的ELF头时,会发现与可重定位ELF文件的ELF头有一个重大不同:可重定位文件ELF头中Start of program headers为0,因为它是没有程序头表,Program Headers,ELF64_Phdr的;而在可执行文件中,Start of program headers是有值的,为64,也就是说,在可执行ELF文件中程序头表会紧接着ELF头(因为ELF头的大小为64字节)

通过readelf -l [filename]可以直接查看到程序头表

可执行ELF文件各个进程虚拟地址空间的映射关系

可以通过cat /proc/[pid]/maps 来查看某个进程的虚拟地址空间

该虚拟文件有6列,分别为:

名称 含义
地址 虚拟内存区域的起始和终止地址
权限 虚拟内存的权限,r=读,w=写,x=执行,s=共享,p=私有
偏移量 虚拟内存区域在被映射文件中的偏移量
设备 映像文件的主设备号和次设备号;
节点 映像文件的节点号;
路径 映像文件的路径

vdso的全称是虚拟动态共享库(virtual dynamic shared library),vsyscall的全称是虚拟系统调用(virtual system call)

总体来说,在程序加载过程中,磁盘上的可执行文件,进程的虚拟地址空间,还有机器的物理内存的映射关系如下:

Linux下的装载过程

ELF文件的识别和装载涉及到Linux内核,这里只涉及到ELF文件处理相关的代码,实际上涉及到的更多

在bash输入命令执行某一个ELF文件时,首先bash进程调用fork()系统调用创建一个新的进程,然后新的进程调用execve()系统调用执行指定ELF文件,内核开始真正的装载工作

下图是Linux内核代码中与ELF文件的装载相关的一些代码:

/fs/binfmt_elf.cLoad_elf_binary的代码解读:

  1. 检查ELF文件头部信息(一致性检查)
  2. 加载程序头表(可以看到一个可执行程序必须至少有一个段(segment),而所有段的大小之和不能超过64K(65536u))
  3. 寻找和处理解释器段
  4. 装入目标程序的段(elf_map)
  5. 填写目标程序的入口地址
  6. 填写目标程序的参数,环境变量等信息(create_elf_tables)
  7. start_thread会将 eip 和 esp 改成新的地址,就使得CPU在返回用户空间时就进入新的程序入口

例子:静态ELF加载器,加载 a.out 执行

用前面的a.cb.cmain.c的例子来看一下静态链接的可执行文件的加载

静态ELF文件的加载:将磁盘上静态链接的可执行文件按照ELF program header,正确地搬运到内存中执行。

操作系统在execve时完成:

  • 操作系统在内核态调用mmap
    • 进程还未准备好,由内核直接执行”系统调用“
    • 映射好a.out 的代码、数据、堆区、堆栈、vvar、vdso、vsyscall
  • 更简单的实现:直接读入进程的地址空间

加载完成后,静态链接的程序就开始从ELF entry开始执行,之后就变成状态机,唯一的行为就是取指执行

通过readelf -h a.out来查看a.out文件的信息

image-20230729114759987

程序入口地址为:Entry point address: 0x401bc0,使用gdb进行验证

image-20230729115042327

image-20230729142012211

  1. 使用starti来使得程序在第一条指令就停下,可以看到,程序确实是从0x401bc0开始的,与我们上面查到的入口地址一致

  2. cat /proc/[PID]/maps 来查看这个程序中内存的内容,看到我们之前提到的代码、数据、堆区、堆栈、vvar、vdso、vsyscall都已经被映射进了内存中

符合我们对静态程序加载时操作系统的行为的预期

动态链接

什么是动态链接以及为什么需要动态链接

链接是将各种代码和数据片段收集并组合为单一文件的过程,这个文件可以被加载到内存中并执行。链接可以在编译时执行,也就是源代码被翻译成机器代码时;也可以在加载时执行,也就是被加载器加载到内存中执行;甚至在运行时执行,也就是由应用程序来执行。

实际上,链接程序在链接时一般是优先链接动态库的,除非指明使用-static参数指定链接静态库

gcc -static hello.c

静态链接和动态链接的可执行文件的大小差距比较显著,因为静态库被链接后库就直接嵌入可执行文件中了

这样会带来两个弊端

  1. 首先会浪费系统空间,如果多个程序链接了同一个库,则每一个生成的可执行文件就都会有一个库的副本,必然会浪费系统空间
  2. 并且一旦发现库中有问题或者需要升级,必须将链接该库的程序全部找出来,全部重新编译

libc.so中有300K条指令,2M大小,如果每个程序都采用静态链接,浪费的空间会很大,解决的办法是整个系统只有一个libc的副本,而每个用到libc的程序在运行时都可以用到libc中的代码

动态库的出现正是为了弥补静态库的弊端,因为动态库是在程序运行时被链接的,所以磁盘上和内存中只需要保留一份副本,因此节约了磁盘空间,如果发现了bug或者要升级也很简单,只需要更换原来的动态库即可

Linux环境下的动态链接对象都是以.so为拓展名的共享对象(Shared Object)

动态链接的实现机制

  1. 程序头表

    可以使用 readelf -l [fileName]来查看动态链接的可执行ELF文件的程序头表,编译完成的地址是从 0x00000000 开始的,即编译完成之后最终的装载地址是不确定的。

  2. 关键技术

    之前在静态链接的过程中提到过重定位的过程,那个时候其实属于链接时重定位,现在需要装载时重定位,只要使用了以下技术:

    • PIC位置无关代码
    • GOT全局偏移表
    • GOT配合PLT实现的延迟绑定技术

    引入动态链接之后,实际上在操作系统开始运行应用程序之前,首先会把控制权交给动态链接器,它完成动态链接的工作后再把控制权交给应用程序

    动态链接器的路径在.interp这个段中体现,并且通常它是个软链接,最终链接在像ld-2.27.so这样的共享库上。

  3. .dynamic段

    和动态链接相关的.dynamic段和它的结构,.dynamic段其实就是全局偏移表的第一项,即GOT[0]

    可以通过readelf -d [fileName]来查看。

    它对应的是elf.h中的Elf64_Dyn这个结构体。

  4. 动态链接器ld

    对于动态链接的可执行文件,内核会分析它的动态链接器地址,把动态链接器映射到进程的地址空间,把控制权交给动态链接器。动态链接器本身也是.so文件,但是它是静态链接的。本身不依赖任何其他的共享对象,也不能使用全局和静态变量。这是合理的,因为动态链接器不能是动态链接的。

    Linux的动态链接器是glibc的一部分,入口地址是sysdeps/x86_64/dl-machine.h中的_start,然后调用 elf/rtld.c _dl_start函数,最终调用 dl_main(动态链接器的主函数)。

动态链接图示

动态链接库的构建与使用

创建好一个动态链接库之后,肯定是不可能只在当前目录使用它,为了能够全局使用动态链接库,可以将字节的动态链接库移动到/usr/lib

sudo mv libvector.so /usr/lib

之后只需要在使用时加上-l[linName]选项即可,如

gcc main.c -lvector

上面的库都要用到管理员权限,因为是系统级的动态链接目录。如果要创建自己的第三方库,最好创建一个自己的动态链接库目录,并将这个目录添加到环境变量LD_LIBRARY_PATH

mkdir /home/song/dynlib
mv libvector.so /home/song/dynlib
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/song/dynlib

动态链接库最好命名为:lib[libName].so 的形式

动态链接的具体实现

基础知识

动态链接

​ 在动态链接方式实现以前,普遍采用静态链接的方式来生成可执行文件。 如果一个程序使用了外部的库函数,那么整个库都会被直接编译到可执行文件中。ELF 支持动态链接,这在处理共享库的时候就会非常高效。 当一个程序被加载进内存时,动态链接器会把需要的共享库加载并绑定到该进程的地址空间中。随后在调用某个函数时,对该函数地址进行解析,以达到对该函数调用的目的。

PLT表和GOT表

PLT表(Procedure Linkage Table)

PLT表是过程连接表,在程序中以 .plt 节表示,该表处于代码段,每一个表项表示了一个与要重定位的函数相关的若干条指令,每个表项长度为 16 个字节,存储的是用于做延迟绑定的代码。

结构如下:

PLT[0]  --> 与每个函数第一次链接相关指令
例:
0x4004c0:
0x4004c0: ff 35 42 0b 20 00 push QWORD PTR [rip+0x200b42] // push [GOT[1]]
0x4004c6: ff 25 44 0b 20 00 jmp QWORD PTR [rip+0x200b44] // jmp [GOT[2]]
0x4004cc: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
即:
第一条指令为 push 一个值,该值为 GOT[1] 处存放的地址,
第二条指令为 jmp 到一个地址执行,该值为 GOT[2] 处存放的地址

PLT[1] --> 某个函数链接时所需要的指令,与 got 表一一对应
例:
0x4004d0 <__stack_chk_fail@plt>:
0x4004d0: ff 25 42 0b 20 00 jmp QWORD PTR [rip+0x200b42] // jmp GOT[3]
0x4004d6: 68 00 00 00 00 push 0x0 // push reloc_arg
0x4004db: e9 e0 ff ff ff jmp 0x4004c0 <_init+0x20> // jmp PLT[0]
即:
第一条指令为: jmp 到一个地址执行,该地址为对应 GOT 表项处存放的地址,在下文中会具体讨论这种结构
第二条指令为: push 一个值,该值作用在下文提到
第三个指令为: jmp 一个地址执行,其实该地址就是上边提到的 PLT[0] 的地址,
也就是说接下来要执行 PLT[0] 中保存的两条指令
GOT表(Global Offset Table)

GOT表是全局偏移表,在 ELF 文件中分为两个部分,.got存储全局变量的引用,.got.plt存储函数的引用。该表处于数据段,每一个表项存储的都是一个地址,每个表项长度是当前程序的对应需要寻址长度(32位程序:4字节,64位程序:8字节)。d_tag = DT_PLTGOT

结构如下:

GOT[0]  --> 此处存放的是 .dynamic 的地址;该节(段)的作用会在下文讨论
GOT[1] --> 此处存放的是 link_map 的地址;该结构也会在下文讨论
GOT[2] --> 此处存放的是 dl_runtime_resolve 函数的地址
GOT[3] --> 与 PLT[1] 对应,存放的是与该表项 (PLT[1]) 要解析的函数相关地址,
由于延迟绑定的原因,开始未调用对应函数时该项存的是 PLT[1] 中第二条指令的地址,
当进行完一次延迟绑定之后存放的才是所要解析的函数的真实地址
GOT[4] --> 与 PLT[2] 对应,所以存放的是与 PLT[2] 所解析的函数相关的地址
.
.
.

两个表之间的关系

GOT[0]: .dynamic 地址                    PLT[0]: 与每个函数第一次链接相关指令
GOT[1]: link_map 地址
GOT[2]: dl_runtime_resolve 函数地址
GOT[3] --> PLT[1] // 一一对应
GOT[4] --> PLT[2] // 相互协同,作用于一个函数
GOT[5] --> PLT[3] // 一个保存的是该函数所需要的延迟绑定的指令
GOT[6] --> PLT[4] // 一个是保存个该函数链接所需要的地址
. .
. .
. .

一个段三个节

.dynamic

因为在加载过程中,.dynamic 节整个以一个段的形式加载进内存,所以说在程序中的 .dynamic 节也就是运行后的 .dynamic 段。该段主要与动态链接的整个过程有关,所以保存的是与动态链接相关信息,只需要关心DT_STRTAB, DT_SYMTAB, DT_JMPREL这三项,这三项分别包含了指向.dynstr, .dynsym, .rel.plt这3个section的指针,此处主要用于寻找与动态链接相关的其他节( .dynsym .dynstr .rela.plt 等节)。该段保存了许多 Elf64_Dyn 结构,该数据结构保存了一些其他节的信息。下面展示该段所保存的数据结构。p_type = PT_DYNAMIC(值为 0x2)的段。

结构如下:

// 该结构都有 64 位程序和 32 位程序的区别,不过大致结构相似,此处只讨论 64 位程序中的
// /usr/include/elf.h

typedef struct
{
Elf64_Sxword d_tag; /* Dynamic entry type */
// d_tag 识别该结构体表示的哪一个节,通过以此字段不同来寻找不同的节
union
{
Elf64_Xword d_val; /* Integer value */
// 对应节的地址,用于存储该结构体表示下的节所在的地址
Elf64_Addr d_ptr; /* Address value */
// 一般与上一个字段表示的值相同,区别暂时不了解
} d_un;
} Elf64_Dyn;

其中Tag对应着每个节(readelf -d 命令将列出文件的动态节信息,包括它所依赖的共享库):

Dynamic section at offset 0x908 contains 24 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x4004b0
0x000000000000000d (FINI) 0x400784
0x0000000000000019 (INIT_ARRAY) 0x6008f8
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x600900
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x400260
0x0000000000000005 (STRTAB) 0x400360
0x0000000000000006 (SYMTAB) 0x400288
0x000000000000000a (STRSZ) 94 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x600ae8
0x0000000000000002 (PLTRELSZ) 96 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x400450
0x0000000000000007 (RELA) 0x4003f0
0x0000000000000008 (RELASZ) 96 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x4003d0
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x4003be

下图列出了该文件的所有节区,其中类型为REL的节区包含重定位表项:

hno@hno-virtual-machine:~/Desktop/OTHER/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/64/no-relro$ readelf -S main_no_relro_64
There are 29 section headers, starting at offset 0x14e0:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400200 00000200
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 000000000040021c 0000021c
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 000000000040023c 0000023c
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400260 00000260
0000000000000028 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 0000000000400288 00000288
00000000000000d8 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400360 00000360
000000000000005e 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 00000000004003be 000003be
0000000000000012 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 00000000004003d0 000003d0
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 00000000004003f0 000003f0
0000000000000060 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000400450 00000450
0000000000000060 0000000000000018 AI 5 22 8
[11] .init PROGBITS 00000000004004b0 000004b0
0000000000000017 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 00000000004004d0 000004d0
0000000000000050 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400520 00000520
0000000000000262 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 0000000000400784 00000784
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 0000000000400790 00000790
0000000000000004 0000000000000004 AM 0 0 4
[16] .eh_frame_hdr PROGBITS 0000000000400794 00000794
0000000000000044 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 00000000004007d8 000007d8
0000000000000120 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 00000000006008f8 000008f8
0000000000000008 0000000000000008 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000600900 00000900
0000000000000008 0000000000000008 WA 0 0 8
[20] .dynamic DYNAMIC 0000000000600908 00000908
00000000000001d0 0000000000000010 WA 6 0 8
[21] .got PROGBITS 0000000000600ad8 00000ad8
0000000000000010 0000000000000008 WA 0 0 8
[22] .got.plt PROGBITS 0000000000600ae8 00000ae8
0000000000000038 0000000000000008 WA 0 0 8
[23] .data PROGBITS 0000000000600b20 00000b20
0000000000000010 0000000000000000 WA 0 0 8
[24] .bss NOBITS 0000000000600b30 00000b30
0000000000000020 0000000000000000 WA 0 0 16
[25] .comment PROGBITS 0000000000000000 00000b30
0000000000000029 0000000000000001 MS 0 0 1
[26] .symtab SYMTAB 0000000000000000 00000b60
0000000000000648 0000000000000018 27 43 8
[27] .strtab STRTAB 0000000000000000 000011a8
000000000000022f 0000000000000000 0 0 1
[28] .shstrtab STRTAB 0000000000000000 000013d7
0000000000000103 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)
.dynsym

动态符号表,存储着在动态链接中所需要的每个函数所对应的符号信息,每个结构体分别对应一个符号 (函数) 。结构体数组。 d_tag = DT_SYMTAB(值为 0x6) 的节。Elf32_Sym[num]中的num对应着ELF32_R_SYM(Elf32_Rel->r_info)。根据定义:

ELF32_R_SYM(Elf32_Rel->r_info) = (Elf32_Rel->r_info) >> 8

结构如下:

typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
// 保存着该函数函数名在 .dynstr 中的偏移,可以结合 .dynstr 找到准确函数名。
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
// 如果这个符号被导出,则存有这个导出函数的虚拟地址,否则为NULL.
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
.dynstr

动态字符串表,表中存放了一系列字符串,这些字符串代表了符号的名称,在此处可以看成函数名。 这个节以\x00作为开始和结尾,中间每个字符串也以\x00间隔。该结构是一个字符串数组。d_tag = DT_STRTAB(值为 0x5) 的节。

Elf32_Sym[6]->st_name=0x4c(.dynsym + Elf32_Sym_size * num)

相关数据结构引用一个字符串时,用的是相对这个section头的偏移

.rel.plt (.rela.plt)

重定位节,保存了重定位相关的信息,这些信息描述了如何在链接或者运行时,对 ELF 目标文件的某部分内容或者进程镜像进行补充或修改。每个结构体也与某一个重定位的函数相关。结构体数组。.rel.plt节是用于函数重定位,.rel.dyn节是用于变量重定位。d_tag = DT_REL(值为 0x11) / d_tag = DT_RELA(值为 0x7) 的节。

结构如下:

typedef struct
{
Elf64_Addr r_offset; /* Address */
// 此处表示的是解析完的函数真实地址存放的位置,
// 即对应解析函数的 GOT 表项地址
Elf64_Xword r_info; /* Relocation type and symbol index */
// 该结构主要用到高某位,表示索引,低位表示类型
// 例如:0x10000007 此处 1 表示索引,7 代表类型,主要用到 1 值
//上边在 PLT 中的指令,每一个表项的第二条指令, PUSH 了一个索引,所 PUSH 的索引与此相关,也就是通过 PLT 中 PUSH 的索引找到当时解析的函数对应的此结构体的
} Elf64_Rel;

//与上一结构体类似,只是不同编译环境下产生的不同结构,作用相同,就不再次讨论
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
//一些关于导入符号的信息,我们只关心从第二个字节开始的值((val)>>8),忽略那个07
//1和3是这个导入函数的符号在.dynsym中的下标,
//如果往回看的话会发现1和3刚好和.dynsym的puts和__libc_start_main对应
Elf32_Sword r_addend; /* Addend */
} Elf32_Rela;
hno@hno-virtual-machine:~/Desktop/OTHER/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/64/no-relro$ readelf -r main_no_relro_64

Relocation section '.rela.dyn' at offset 0x3f0 contains 4 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000600ad8 000500000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000600ae0 000600000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000600b30 000700000005 R_X86_64_COPY 0000000000600b30 stdout@GLIBC_2.2.5 + 0
000000600b40 000800000005 R_X86_64_COPY 0000000000600b40 stdin@GLIBC_2.2.5 + 0

Relocation section '.rela.plt' at offset 0x450 contains 4 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000600b00 000100000007 R_X86_64_JUMP_SLO 0000000000000000 write@GLIBC_2.2.5 + 0
000000600b08 000200000007 R_X86_64_JUMP_SLO 0000000000000000 strlen@GLIBC_2.2.5 + 0
000000600b10 000300000007 R_X86_64_JUMP_SLO 0000000000000000 setbuf@GLIBC_2.2.5 + 0
000000600b18 000400000007 R_X86_64_JUMP_SLO 0000000000000000 read@GLIBC_2.2.5 + 0

如图,在.rel.plt中列出了链接的C库函数,一下均以write函数为例,write函数的r_offset =600b00,r_info=100000007。

扩充结构体(在 Full RELRO 用到)

struct r_debug{  //由于并没有找到该结构体的定义,所以没有声明类型
r_version
r_map //指向 link_map
r_brk
r_state
r_ldbase
}

保存着 Binary 里面所有信息的一个结构体,该结构体很大,内容丰富。

主要字段:

l_next:链接着该程序所有用到的 libary
上边提到的 GOT[1] 中保存的地址是第一层 link_map 中所表示的 libary,此时是指向的程序本身,
不过可以用 l_next 结构寻找下一层表示的 libary,以此来遍历程序中所用到的 libary,
并利用下边所提到的字段找到该层 libary 的名字、基地址、以及所有的 section 等信息。
l_name:表示 libary 的名字
l_addr:表示 libary 的基地址
l_info[x]:指向该 libary 下的 .dynamic。
l_info[1] 指向 d_tag = 1 时所表示的 section ,所以可以改变 x 的值找到每个相关 section 的地址。
在链接过程中 binary 中的 section 地址,以及 libary 中的地址都是通过此方法确定的。

_dl_fixup源码

以下就是_dl_fixup用于函数重定向的代码,展示了函数重定向的流程。

#ifdef DL_RO_DYN_SECTION
# define D_PTR(map, i) ((map)->i->d_un.d_ptr + (map)->l_addr)
#else
# define D_PTR(map, i) (map)->i->d_un.d_ptr
#endif

#define ELF32_R_TYPE(val) ((val) & 0xff)
#define ELF32_R_SYM(val) ((val) >> 8)
#define ELF32_ST_VISIBILITY(o) ((o) & 0x03)

const ElfW(Sym) *const symtab
= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
//通过link_map找到DT_SYMTAB地址,进而得到.dynsym的指针,记作symtab

const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
//通过link_map找到DT_STRTAB地址,进而得到.dynstr的指针,记作strtab

const PLTREL *const reloc
= (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
//reloc_offset就是reloc_arg
//将.rel.plt地址与reloc_offset相加,得到函数所对应的Elf32_Rel指针,记作reloc

const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
//将(reloc->r_info)>>8作为.dynsym下标,得到函数所对应的Elf32_Sym指针,记作sym

const ElfW(Sym) *refsym = sym;

void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
//l->l_addr 加载共享对象的基本地址
//l->l_addr + reloc->r_offset即为需要修改的got表地址。

lookup_t result;
DL_FIXUP_VALUE_TYPE value;

assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
//检查r_info最低为是不是R_386_JMP_SLOT=7

if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0) //判断(sym->st_other)&0x03是否为0
{
const struct r_found_version *version = NULL;

if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
const ElfW(Half) *vernum =(const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}

int flags = DL_LOOKUP_ADD_DEPENDENCY;
if (!RTLD_SINGLE_THREAD_P)
{
THREAD_GSCOPE_SET_FLAG ();
flags |= DL_LOOKUP_GSCOPE_LOCK;
}

#ifdef RTLD_ENABLE_FOREIGN_CALL
RTLD_ENABLE_FOREIGN_CALL;
#endif

//通过strtab + sym->st_name找到函数字符串,result为libc基地址
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);

if (!RTLD_SINGLE_THREAD_P)
THREAD_GSCOPE_RESET_FLAG ();

#ifdef RTLD_FINALIZE_FOREIGN_CALL
RTLD_FINALIZE_FOREIGN_CALL;
#endif

//libc基地址+解析函数的偏移地址,即函数的真实地址
value = DL_FIXUP_MAKE_VALUE (result,sym ? (LOOKUP_VALUE_ADDRESS (result)+ sym->st_value) : 0);
}
else
{
value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
result = l;
}

value = elf_machine_plt_value (l, reloc, value);

if (sym != NULL
&& __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));

if (__glibc_unlikely (GLRO(dl_bind_not)))
return value;

//将got表中的数据修改为函数的真实地址
//value为函数真实地址,rel_addr为需要修改的got表地址。
return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);

链接过程

概括描述

​ 完成延迟绑定的函数主要是 dl_runtime__resolve(link_map_obj, reloc_arg) ,该函数的第一个参数是一个 link_map 结构,第二个参数是一个重定位参数,即运行 PLT 中的代码时 PUSH 进栈中的参数。该函数主要是调用一个 dl_fixup(link_map_obj, reloc_arg) 完成了主要功能。参数一的主要作用是:获得重定位函数所在了的libary 的基地址,以及获取在 libary 中寻找需要定位函数时所需要的 Section (.dynstr .dynsym 等)。第二个函数主要是确定需要解析的函数名,以及解析完之后写回的地址。
  该过程可以先大概理解为,dl_fixup 函数通过 reloc_arg 参数确定当前正在解析的函数名。之后,拿着这个函数名,再利用 link_map 结构找到 libary 中的 .dynsym .dynstr 。利用 .dynsym .dynstr 进行匹配。若匹配成功,则从 .dynsym 中获取该函数的函数地址。

//上边的详细过程
reloc_arg --> 函数名 A

利用 link_map --> l_info[x] 通过改变 x 的值,确定 .dynsym .dynstr
再用 .dynsym 与 .dynstr 对整个动态符号表 .dynstym 进行遍历,去匹配函数名 A
若 某一个 Elf64_Sym(符号) 的 st_name + .dynstr == A
则 该 Elf64_Sym 表示的符号即为函数 A

// 整个过程可以这样理解,不过真实情况使用的 Hash方法去寻找的这个 Elf64_Sym(符号)

具体过程

  • 调用某个函数后进入该函数的 PLT[x] ,在 PLT[x] 中 push 一个参数 reloc_arg

​ 拿到这个 reloc_arg 后,链接器会通过该值找到对应函数的 Elf_Rel 结构,通过该结构的 r_info 变量中的偏移量找到对应函数的 Elf_Sym 结构,然后再通过 Elf_Sym 结构的 st_name 结合之前已经确定的 .dynstr 地址,通过 st_name + .dynstr 获得对应函数的函数名。这就是拿到 reloc_arg 参数后链接器获得的信息,即知道了本次链接中的函数的函数名。(注:此处用到的 binary 中的 Elf_Rel Elf_Sym .dynstr 等地址都是通过 link_map->l_info[x] 的方式寻找的。)

  • 在链接过程中 PLT[0] 会 push dl_runtime_resolve 函数的第二个参数 link_map

​ 拿到这个变量后链接器会获得所要解析的函数的函数库(通过 link_map 的 l_next 字段),然后拿到这个外部库之后 link_map 的 l_addr 字段会记录该库的基地址,然后链接器通过 new_hash 函数求出要链接函数的 hash(new_hash(st_name + .dynstr)),然后通过该 hash 和之前的保存值进行匹配,如果匹配上就获得了该函数在外部库的 Elf64_Sym 结构,然后通过该结构的 st_value 获取该函数在外部库里面的偏移,最后通过 st_value + l_addr 获取该函数的真实地址,最后通过 Elf64_Rel 的 r_offset 定位该函数在 GOT 中对应的地址,然后将最后结果写入该地址中。(其中有通过这两个参数共同获得的东西,不过为了便于理解就不再分开讨论。)

攻击

详见:高级ROP-ret2dlslove

参考