总是在工作中会遇到符号表,链接等字眼,之前看过《程序员自我修养》这本书,但是基本上都忘记了,这几天再刷一遍,顺便记录一下,加深记忆。
本文会完整的描述程序运行的动态加载及运行的整个流程,会涉及到elf文件,elf中的section表,以及ld.so的相关知识,且其中会夹杂着一些命令,方便工作时的使用。
本文讲述并不深,比较浅显,很多比较烧脑和难理解就美提及,同时也时不时有例子帮助大家理解,篇幅较长所以分为两篇。
elf文件是我们常用到的可执行文件的组织形式,同时还是可重定位文件(也就是.o文件),动态库等组织形式。下图是elf文件的大体结构(有删减):
elf文件是由一些描述信息和存储信息的各个段组成。图中可以看到首先是elf文件头,用来表明该文件的一些基本信息,然后是程序头表,用来运行时使用,接下来就是各种功能的段,后边根据需要一一讲述,然后是节头部表,就是用来描述各个段。
而且各种段我使用两种颜色区分,上边是会载入到内存,下边不会载入到内存,且文件头和程序头都会载入到内存。图中载入到内存的段和我们熟知的进程地址空间一些段有些是一样的(.text, .data, .bss),有一些可能我们并不熟悉。
我们通过命令来看一个可执行文件的文件头:
# readelf -h /bin/bash
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: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x2f630
Start of program headers: 64 (bytes into file)
Start of section headers: 1166920 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 11
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
头信息包含了一些elf文件的基本信息,我们根据源码中的注释来简单介绍几个我们可能需要关注的:
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;
Magic即为魔数,用来标识这是一个elf文件,平台属性(32位/64位)等等。
Type则为elf文件的类型,可以看到bash这个被定义为是Shared object file,elf支持的文件格式有: 可重定位文件,可执行文件,动态库,core文件。
#define ET_NONE 0 /* No file type */
#define ET_REL 1 /* Relocatable file */
#define ET_EXEC 2 /* Executable file */
#define ET_DYN 3 /* Shared object file */
#define ET_CORE 4 /* Core file */
Entry point address表示文件执行的入口虚拟地址,可重定位文件这个字段为0。
然后还有程序头表(program headers)在elf文件的位置及大小,节头部表(section headers)在elf文件的位置及大小。
其他暂且就不展开了
我们通过指令来查看elf的节头部表:
# readelf -S /bin/bash
There are 29 section headers, starting at offset 0x11ce48:
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 00000000000002a8 000002a8
000000000000001c 0000000000000000 A 0 0 1
[ 5] .dynsym DYNSYM 0000000000004db0 00004db0
000000000000e400 0000000000000018 A 6 1 8
[ 9] .rela.dyn RELA 000000000001dca8 0001dca8
000000000000dc80 0000000000000018 A 5 0 8
[10] .rela.plt RELA 000000000002b928 0002b928
0000000000001470 0000000000000018 AI 5 24 8
[11] .init PROGBITS 000000000002d000 0002d000
0000000000000017 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 000000000002d020 0002d020
0000000000000db0 0000000000000010 AX 0 0 16
[13] .plt.got PROGBITS 000000000002ddd0 0002ddd0
0000000000000018 0000000000000008 AX 0 0 8
[14] .text PROGBITS 000000000002ddf0 0002ddf0
00000000000ac991 0000000000000000 AX 0 0 16
[15] .fini PROGBITS 00000000000da784 000da784
0000000000000009 0000000000000000 AX 0 0 4
[16] .rodata PROGBITS 00000000000db000 000db000
0000000000019930 0000000000000000 A 0 0 32
[22] .dynamic DYNAMIC 0000000000114cf0 00113cf0
0000000000000200 0000000000000010 WA 6 0 8
[23] .got PROGBITS 0000000000114ef0 00113ef0
0000000000000100 0000000000000008 WA 0 0 8
[24] .got.plt PROGBITS 0000000000115000 00114000
00000000000006e8 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000115700 00114700
0000000000008604 0000000000000000 WA 0 0 32
[26] .bss NOBITS 000000000011dd20 0011cd04
0000000000009c78 0000000000000000 WA 0 0 32
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)
这个命令会展示出该elf文件中所有的段(section),我这里为了方便展示省略掉一些不需要关注的段。
接下来看下表示段的各个字段,还是使用源码中来看下,注释相对来说比较清晰,
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
Key to Flags
:W就是这个段可写,A就是会在内存中分配空间,X就是这个段是可执行的。然后我们再简单描述下几个相对来说比较简单且常用的段:
其他和我们比较相关的我们下边详细讲述下
涉及到程序的装载,我们就需要用到程序头表。操作系统将程序装载进进程的地址空间中时,往往只需要关注段的权限(可读,可写,可执行等)。为了装载的方便,elf的做法是将相同权限的段合并为1个segment(程序头),那么描述这写segment的信息就是在程序头表中。
# readelf -l /bin/bash
Elf file type is DYN (Shared object file)
Entry point 0x2f630
There are 11 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x0000000000000268 0x0000000000000268 R 0x8
INTERP 0x00000000000002a8 0x00000000000002a8 0x00000000000002a8
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x000000000002cd98 0x000000000002cd98 R 0x1000
LOAD 0x000000000002d000 0x000000000002d000 0x000000000002d000
0x00000000000ad78d 0x00000000000ad78d R E 0x1000
LOAD 0x00000000000db000 0x00000000000db000 0x00000000000db000
0x0000000000035730 0x0000000000035730 R 0x1000
LOAD 0x00000000001113f0 0x00000000001123f0 0x00000000001123f0
0x000000000000b914 0x00000000000155a8 RW 0x1000
DYNAMIC 0x0000000000113cf0 0x0000000000114cf0 0x0000000000114cf0
0x0000000000000200 0x0000000000000200 RW 0x8
...
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
03 .init .plt .plt.got .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .data.rel.ro .dynamic .got .got.plt .data .bss
06 .dynamic
...
也是省略了一些子项,同样也是各个字段看下:
typedef struct
{
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;
可以看到每个字段的含义,type其实我们只需要关注lOAD类型,表示这个segment会加载进内存。flags同样也表示权限相关的,R是可读,E是可执行,W是可写的。我们根据上边的输出可以看到每个segment包含哪些段以及每个segment的权限。
我们在动态加载和重定位的阶段会用到dynamic段,它主要也是包含一些动态加载和动态重定位的一些关键的段信息,很类似节头部表,是的你没看错:
# readelf -d /bin/bash
Dynamic section at offset 0x113cf0 contains 28 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libtinfo.so.6]
0x0000000000000001 (NEEDED) Shared library: [libdl.so.2]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x2d000
0x000000000000000d (FINI) 0xda784
0x000000006ffffef5 (GNU_HASH) 0x308
0x0000000000000005 (STRTAB) 0x131b0
0x0000000000000006 (SYMTAB) 0x4db0
0x000000000000000a (STRSZ) 38696 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x115000
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x2b928
0x0000000000000007 (RELA) 0x1dca8
0x0000000000000008 (RELASZ) 56448 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffb (FLAGS_1) Flags: PIE
0x000000006ffffff9 (RELACOUNT) 2338
0x0000000000000000 (NULL) 0x0
该段以NULL子项结束,我们先来看下这个dynamic在代码中的结构:
typedef struct
{
Elf64_Sxword d_tag; /* Dynamic entry type */
union
{
Elf64_Xword d_val; /* Integer value */
Elf64_Addr d_ptr; /* Address value */
} d_un;
} Elf64_Dyn;
其实它只有两个字段,tag表示子项(entry)的类型,d_un要么是一个int值,要么是一个地址。
简单解读几个:
大家可能会比较意外,不是存放的是数字吗,怎么能知道是哪个库,其实存放的是一个索引,这个索引是.dynstr段的索引,可以从这个段中提取出来字符串,.dynstr存放的全是字符串,.dynstr是动态链接下的字符串表,涉及到字符串的动态链接下的查找都可以在这个字符串中找到。除此之外还有静态字符串表.strtab,在静态链接中使用。
无论是静态链接还是动态链接符号都作为接口的存在,充当这粘合剂的角色。函数和变量统称为符号,函数名和变量名就是符号名。符号表则是记录符号信息的,我们还是先来看下符号表:
# readelf -s /bin/dd
Symbol table '.dynsym' contains 88 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __ctype_toupper_loc@GLIBC_2.3 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getenv@GLIBC_2.2.5 (3)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sigprocmask@GLIBC_2.2.5 (3)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __snprintf_chk@GLIBC_2.3.4 (4)
...
77: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __ctype_b_loc@GLIBC_2.3 (2)
78: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __sprintf_chk@GLIBC_2.3.4 (4)
79: 0000000000013348 8 OBJECT GLOBAL DEFAULT 26 stdout@GLIBC_2.2.5 (3)
80: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (3)
81: 0000000000013340 8 OBJECT GLOBAL DEFAULT 26 __progname@GLIBC_2.2.5 (3)
82: 0000000000013358 8 OBJECT WEAK DEFAULT 26 program_invocation_name@GLIBC_2.2.5 (3)
83: 0000000000013358 8 OBJECT GLOBAL DEFAULT 26 __progname_full@GLIBC_2.2.5 (3)
readelf -s
是展示符号表,readelf --dyn-syms
是展示动态符号表,符号表是包含了该文件的所有符号,动态符号表仅仅包含动态链接中会使用的符号。符号表包含动态符号表。不一定所有的文件都有两个段的。
那么这个段到底记录了什么信息呢,看下各个字段的描述
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
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 */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
vis段暂且忽略
总结下,符号表表示的各个符号的类型信息以及这个符号所在的位置。
链接器链接的过程,需要对文件进行重定位,静态链接针对目标文件,动态链接针对加载到内存的文件。主要是针对代码段或者数据段中引用了一些符号的地址进行重新定位,举例来说,比如目标文件a引用了b文件的符号,但是在链接之前还是不知道地址的,链接时用真实的地址来对之前的地址进行修正。那么重定位表中记录这些信息。
# readelf -r /bin/dd
Relocation section '.rela.dyn' at offset 0x10a8 contains 32 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000012d10 000000000008 R_X86_64_RELATIVE 4410
000000012d18 000000000008 R_X86_64_RELATIVE 43d0
...
000000012ff0 004800000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
000000012ff8 005000000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0
000000013340 005100000005 R_X86_64_COPY 0000000000013340 __progname@GLIBC_2.2.5 + 0
000000013348 004f00000005 R_X86_64_COPY 0000000000013348 stdout@GLIBC_2.2.5 + 0
Relocation section '.rela.plt' at offset 0x13a8 contains 74 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000013018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 __ctype_toupper_loc@GLIBC_2.3 + 0
000000013020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 getenv@GLIBC_2.2.5
000000013028 000300000007 R_X86_64_JUMP_SLO 0000000000000000 sigprocmask@GLIBC_2.2.5
000000013030 000400000007 R_X86_64_JUMP_SLO 0000000000000000 __snprintf_chk@GLIBC_2.3.4
我们使用readelf -r
命令来查询重定位表,因为我们查询的是可执行文件,所以展示的是动态可重定位表,也看出来了有.rela.dyn和.rela.plt段。.rela.dyn 是对数据引用修正,修正的段是代码段和.got段。.rela.plt是对函数引用的修正,修正的段是got.plt。关于got和plt的相关概念下一章节讲述。
然后来看下各个字段:
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;
这个结构体也太过简单了,我们来看下各个字段的含义吧。
通过这里我们就可以知道比较关键的信息是重定位表记录的是各个符号在哪里引用的。符号表表示的是符号具体的位置,这一点要做一下区分。
本文主要是讲述动态链接的过程,所以关于静态链接就简单提到。动态链接是发生在可执行文件和库的加载过程中,由动态链接器完成。(接上一篇)
我们讲述的动态链接是比较常见的编译模式-采用地址无关的形式,也即使用编译命令-fPIC(Position-independent-code<地址无关代码>)。由于加载进来的库可以给其他进程共享(节省内存),且这个库在不同的进程地址空间的虚拟地址是不同的,那么这个库中引用别处的符号地址就需要是相同的,不然各个进程地址不同,库就不可以共享。由于此原因,elf采用了地址无关的方案供使用。
地址无关代码主要是针对于模块间的数据访问和函数调用,模块内的数据或者函数可以使用相对地址来访问到,这样也是地址无关的。那么模块间的数据或者函数调用,使用GOT这个结构来实现。GOT即为全局偏移表,GOT存放了使用的每个数据或者函数的最终地址,GOT可以理解成是一个以4字节(32位下)为一个子项的数组。GOT不会被多个进程共享,每个进程都有一份GOT。这样当代码中想要使用变量或者函数调用,会先找到GOT中,进而再从GOT中找到相应的函数或者变量进行访问。代码找GOT这个操作是相对寻址,所以可以被共享。GOT是在程序的装载时被修改。
当开启了PIC时,默认就有延迟绑定的功能。也就是说在函数第一次调用的时候才进行绑定,所谓的绑定就是符号查找,重定位GOT或者数据段等。而不是加载的时候就一股脑的将所有的符号全部重定位完成。elf是通过PLT(Procedure Linkage Table)的方法来实现的。简单的原理就是会有一个plt的段,当进行模块间的函数调用时,代码段中的调用都是先到plt段中,plt中会继续调用dl_runtime_resolve函数进行符号的解析和重定位进一步到got中的地址,当函数第二次调用到plt段中就能直接找到相应got中的地址实现跳转。
来看下和PLT及GOT相关的段有:
PLT相关:plt,plt.got, plt.sec (存放代码)
GOT相关:got,got.plt (存放地址)
我们通过一个例子来完整的描述下地址无关下延迟绑定的函数调用流程:
# cat hello.c
#include
int main() {
printf("hello: %d\n", 111);
return 0;
}
使用gcc -fPic hello.c -o hello
编译后,来看下hello的各个段:
# readelf -S hello
There are 30 section headers, starting at offset 0x3960:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[12] .plt PROGBITS 0000000000001020 00001020
0000000000000020 0000000000000010 AX 0 0 16
[13] .plt.got PROGBITS 0000000000001040 00001040
0000000000000008 0000000000000008 AX 0 0 8
[14] .text PROGBITS 0000000000001050 00001050
0000000000000171 0000000000000000 AX 0 0 16
[21] .dynamic DYNAMIC 0000000000003df8 00002df8
00000000000001e0 0000000000000010 WA 6 0 8
[22] .got PROGBITS 0000000000003fd8 00002fd8
0000000000000028 0000000000000008 WA 0 0 8
[23] .got.plt PROGBITS 0000000000004000 00003000
0000000000000020 0000000000000008 WA 0 0 8
使用objdump来看反汇编代码,-d就是查看反汇编,–section来指定看哪一个段
# objdump -d --section=text hello
0000000000001135 :
1135: 55 push %rbp
1136: 48 89 e5 mov %rsp,%rbp
1139: be 6f 00 00 00 mov $0x6f,%esi
113e: 48 8d 3d bf 0e 00 00 lea 0xebf(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
1145: b8 00 00 00 00 mov $0x0,%eax
114a: e8 e1 fe ff ff callq 1030
114f: b8 00 00 00 00 mov $0x0,%eax
1154: 5d pop %rbp
1155: c3 retq
1156: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
115d: 00 00 00
我们看main函数的callq 1030
这里,那么我们就去plt段中去找:
# objdump -d --section=.plt hello
hello: file format elf64-x86-64
Disassembly of section .plt:
0000000000001020 <.plt>:
1020: ff 35 e2 2f 00 00 pushq 0x2fe2(%rip) # 4008 <_GLOBAL_OFFSET_TABLE_+0x8>
1026: ff 25 e4 2f 00 00 jmpq *0x2fe4(%rip) # 4010 <_GLOBAL_OFFSET_TABLE_+0x10>
102c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000001030 :
1030: ff 25 e2 2f 00 00 jmpq *0x2fe2(%rip) # 4018
1036: 68 00 00 00 00 pushq $0x0
103b: e9 e0 ff ff ff jmpq 1020 <.plt>
这里也能看到plt中的第一个子项是不是和我们前边说的一致。然后我们看printf@plt的实现:
首先跳转到4018这个位置中存放的地址处,继续看下4018中是啥, 根据上边的段表4018是在.got.plt 中,我们使用 readelf -x 23 hello
查看段表索引是23(.got.plt)的内容:
Hex dump of section '.got.plt':
NOTE: This section has relocations against it, but these have NOT been applied to this dump.
0x00004000 f83d0000 00000000 00000000 00000000 .=..............
0x00004010 00000000 00000000 36100000 00000000 ........6.......
4018处的是1036,那么我们看到1036正好位于printf@plt的第二条指令,继续在plt中执行,pushq $0x0就是printf 的重定位表中索引,然后调转到PLT0,继续push模块ID,然后就是去调用dl_runtime_resolve函数,该函数就会根据重定位表针对.got.plt中进行修复,然后到实际的printf函数执行。
关于dl_runtime_resolve函数调用和重定位我们下一节加载和链接会讲到。
当运行可执行文件时,操作系统会将可执行文件加载到内存中,首先判断是否有.interp段,如果有.interp段,这个段中存放的是动态链接器路径,操作系统然后就会加载动态链接器,并跳转到动态链接器的start处(在最开始中讲到readelf -h
文件头中可以知道代码的开始是在那个位置)。在这之前,操作系统会准备调用栈的环境,除了参数和环境变量外,操作系统还准备了辅助信息数组(Auxiliary Vector),辅助数组中主要存放的是可执行文件的一些信息,包含程序头表,起始地址等等信息。
数据结构是:
typedef struct
{
uint64_t a_type; /* Entry type */
union
{
uint64_t a_val; /* Integer value */
/* We use to have pointer elements added here. We cannot do that,
though, since it does not work when using 32-bit definitions
on 64-bit platforms and vice versa. */
} a_un;
} Elf64_auxv_t;
type类型我们截取部分来看:
#define AT_NULL 0 /* End of vector */
#define AT_IGNORE 1 /* Entry should be ignored */
#define AT_EXECFD 2 /* File descriptor of program */
#define AT_PHDR 3 /* Program headers for program */
#define AT_PHENT 4 /* Size of program header entry */
#define AT_PHNUM 5 /* Number of program headers */
#define AT_PAGESZ 6 /* System page size */
#define AT_BASE 7 /* Base address of interpreter */
#define AT_FLAGS 8 /* Flags */
#define AT_ENTRY 9 /* Entry point of program */
...
然后就是到动态链接器中开始执行了
根据动态链接器的elf文件中的程序入口,我们找到首先是一段汇编代码,这段汇编代码调用到_dl_start
函数
static ElfW(Addr) __attribute_used__
_dl_start (void *arg)
{
//...
/* Figure out the run-time load address of the dynamic linker itself. */
bootstrap_map.l_addr = elf_machine_load_address ();
/* Read our own dynamic section and fill in the info array. */
bootstrap_map.l_ld = (void *) bootstrap_map.l_addr + elf_machine_dynamic ();
elf_get_dynamic_info (&bootstrap_map, NULL);
if (bootstrap_map.l_addr || ! bootstrap_map.l_info[VALIDX(DT_GNU_PRELINKED)])
{
/* Relocate ourselves so we can do normal function calls and
data access using the global offset table. */
ELF_DYNAMIC_RELOCATE (&bootstrap_map, 0, 0, 0);
}
bootstrap_map.l_relocated = 1;
#ifdef DONT_USE_BOOTSTRAP_MAP
ElfW(Addr) entry = _dl_start_final (arg);
#else
ElfW(Addr) entry = _dl_start_final (arg, &info);
#endif
}
大家也不需要全部看懂,代码在glibc/elf/rtld.c中,简单看下流程就可以了。
首先动态链接器计算自己运行时的内存位置,根据内存位置来计算得到.dynamic段的地址,并且读取.dynamic段并将数据填充到上边的info数组。然后就是重定位自己了。我们知道动态链接器首先也是一个共享对象,那么他的重定位是谁来做呢,就是自己对自己做重定位,也被称为自举,除此之外动态链接器并不依赖其他的系统库,自然在自举的过程就无需涉及到其他库的加载和引用。
最后会调用_dl_start_final函数:
static ElfW(Addr) __attribute__ ((noinline))
_dl_start_final()
{
// ...
start_addr = _dl_sysdep_start (arg, &dl_main);
return start_addr;
}
_dl_start_final函数会调用_dl_sysdep_start函数,_dl_sysdep_start这个函数就会去解析上边说到辅助数组,然后传递给dl_main这个函数并调用,dl_main函数比较大,我们分段来解释:
static void
dl_main (const ElfW(Phdr) *phdr,
ElfW(Word) phnum,
ElfW(Addr) *user_entry,
ElfW(auxv_t) *auxv)
{
// ...
}
先来看参数,phdr就是可执行文件的程序头表的地址,phnum就是可执行文件文件的程序头表中子项个数,user_entry是可执行文件入口地址,以上参数都是从辅助数组中解析出来,最后一个参数是辅助数组的指针。
static void dl_main(...) {
// ...
if (*user_entry == (ElfW(Addr)) ENTRY_POINT) {
// ...
} else {
// ...
}
}
这里是动态连接器可以独自当作可执行文件直接运行,这里是判断是运行自己还是去动态链接别人。我们下边也直接看else中的代码:
static void dl_main(...) {
// ...
if (*user_entry == (ElfW(Addr)) ENTRY_POINT) {}
else {
}
}
然后动态链接器就会去解析传进来的可执行文件的程序头表:
static void dl_main(...) {
// ...
if (*user_entry == (ElfW(Addr)) ENTRY_POINT) {}
else {
/* Create a link_map for the executable itself.
This will be what dlopen on "" returns. */
main_map = _dl_new_object ((char *) "", "", lt_executable, NULL,
__RTLD_OPENEXEC, LM_ID_BASE);
assert (main_map != NULL);
main_map->l_phdr = phdr;
main_map->l_phnum = phnum;
main_map->l_entry = *user_entry;
}
//...
for (ph = phdr; ph < &phdr[phnum]; ++ph)
switch (ph->p_type)
{
case PT_PHDR:
main_map->l_addr = (ElfW(Addr)) phdr - ph->p_vaddr;
break;
case PT_DYNAMIC:
/* This tells us where to find the dynamic section,
which tells us everything we need to do. */
main_map->l_ld = (void *) main_map->l_addr + ph->p_vaddr;
break;
case PT_INTERP:
// ...
case PT_LOAD:
{
mapstart = (main_map->l_addr
+ (ph->p_vaddr & ~(GLRO(dl_pagesize) - 1)));
if (main_map->l_map_start > mapstart)
main_map->l_map_start = mapstart;
/* Also where it ends. */
allocend = main_map->l_addr + ph->p_vaddr + ph->p_memsz;
if (main_map->l_map_end < allocend)
main_map->l_map_end = allocend;
if ((ph->p_flags & PF_X) && allocend > main_map->l_text_end)
main_map->l_text_end = allocend;
}
break;
// ...
}
简单顺一下思路,动态连接器读取可执行文件的程序头表将相应数据赋予main_map这个数据结构。
static void dl_main(...) {
// ...
if (! rtld_is_main)
{
/* Extract the contents of the dynamic section for easy access. */
elf_get_dynamic_info (main_map, NULL);
/* Set up our cache of pointers into the hash table. */
_dl_setup_hash (main_map);
}
// ...
}
然后解析可执行文件的.dynamic段,相应的数据也还是填充在main_map中。
static void dl_main(...) {
// ...
/* Initialize the data structures for the search paths for shared
objects. */
_dl_init_paths (library_path);
//...
/* We have two ways to specify objects to preload: via environment
variable and via the file /etc/ld.so.preload. The latter can also
be used when security is enabled. */
npreloads += handle_preload_list (preloadlist, main_map, "LD_PRELOAD");
npreloads += handle_preload_list (preloadarg, main_map, "--preload");
// ...
/* Load all the libraries specified by DT_NEEDED entries. If LD_PRELOAD
specified some libraries to load, these are inserted before the actual
dependencies in the executable's searchlist for symbol resolution. */
{
RTLD_TIMING_VAR (start);
rtld_timer_start (&start);
_dl_map_object_deps (main_map, preloads, npreloads, mode == trace, 0);
rtld_timer_accum (&load_time, start);
}
}
再接下来是设定库的搜索路径,加载preload的库,然后去加载所需要的全部的动态库。
static void dl_main(...) {
// ...
if (l != &GL(dl_rtld_map))
_dl_relocate_object (l, l->l_scope, GLRO(dl_lazy) ? RTLD_LAZY : 0,
consider_profiling);
/* Add object to slot information data if necessasy. */
if (l->l_tls_blocksize != 0 && tls_init_tp_called)
_dl_add_to_slotinfo (l, true);
}
然后就是比较关键的重定位操作了。
根据加载进来的所有的库的符号表可以定位到每一个符号的位置,再根据重定位表找到哪里使用到某个符号,重定位的方式。以此来修改.got
(数据段)和.got.plt
(函数引用)。
需要关注的是,在动态链接器进行重定位时,只是重定位那些不是延迟加载的符号,延迟加载的符号的重定位要到_dl_runtime_resolve函数的调用。
本文到这里结束了,主要讲述了elf文件的文件结构,已经其中各个段的用处。然后就继续讲述关于动态链接的一些情况,希望大家有所收获,篇幅较长,感谢看完。