ELF装载和动态链接过程

..1. 目录

..1.1. 动态链接过程

..1.1.1. 基础宏定义
1
2
3
4
5
6
7
8
9
10
11
//宏访问link_map成员并计算地址   
# 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

/* Result of the lookup functions and how to retrieve the base address. */
//lookup系列函数的返回值
typedef struct link_map *lookup_t;
#define LOOKUP_VALUE(map) map
#define LOOKUP_VALUE_ADDRESS(map) ((map) ? (map)->l_addr : 0)
..1.1.2. 重定位定义
1
2
3
4
5
6
7
8
9
10
11
// Elf64_Rela
typedef uint64_t Elf64_Addr;
typedef uint64_t Elf64_Xword;
typedef int64_t Elf64_Sxword;

typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;
..1.1.3. 符号表定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/Elf64_Sym
typedef uint32_t Elf64_Word;
typedef uint16_t Elf64_Section;
typedef uint64_t Elf64_Addr;
typedef uint64_t Elf64_Xword;

typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index), 4 bytes */
unsigned char st_info; /* Symbol type and binding, 1 byte */
unsigned char st_other; /* Symbol visibility, 1 byte */
Elf64_Section st_shndx; /* Section index, 2 bytes */
Elf64_Addr st_value; /* Symbol value, 8 bytes */
Elf64_Xword st_size; /* Symbol size, 8 bytes */
} Elf64_Sym;
..1.1.4. 动态段定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct {
elf32_sword d_tag
union{
elf32_word d_val
elf32_addr d_ptr
}d_un;
}elf32_dyn;

d_tag控制d_un的含义
DT_HASH符号散列表地址
DT_STRTAB字符串表的地址
DT_SYMTAB符号表地址
DT_RELA相对地址重定位表的地址
DT_STRSZ字符串表的字节大小
DT_INIT初始化函数的地址
DT_FINI终止函数的地址
DT_SONAME共享目标文件名的字符串表
接近量DT_JMPREL仅用于plt的重定位定位地址
..1.1.5. r_scope_elem 定义
1
2
3
4
5
6
7
8
9
10
11
/* Structure to describe a single list of scope elements.  The lookup
functions get passed an array of pointers to such structures. */
//描述一个特定范围的单链表结构 lookup函数往往需要传递一个保存这种结构的数组作为参数
struct r_scope_elem
{
/* Array of maps for the scope. */
//用于描述范围的maps数组
struct link_map **r_list;
/* 这个范围的入口点个数 */
unsigned int r_nlist;
};
..1.1.6. link_map
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
//link_map用于描述可加载共享目标文件的结构 l_next,l_prev是一个链接了已加载的所有共享目标文件的单链表结构   
//这个单链表结构一般被用于动态链接器

struct link_map
{
/* These first few members are part of the protocol with the debugger.
This is the same format used in SVR4. */
//共享文件加载基地址
ElfW(Addr) l_addr; /* Base address shared object is loaded at. */
//绝对文件名
char *l_name; /* Absolute file name object was found in. */
//动态段加载地址
ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */
//加载项鍊表
struct link_map *l_next, *l_prev; /* Chain of loaded objects. */

/* All following members are internal to the dynamic linker.
They may change without notice. */
//其他成员是对于动态链接器内部的 可能随时改变不受提醒

/* This is an element which is only ever different from a pointer to
the very same copy of this type for ld.so when it is used in more
than one namespace. */
struct link_map *l_real;

/* Number of the namespace this link map belongs to. */
//这link map属于的命名空间个数

ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
+ DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];
//这个数组用于快速访问动态段的信息 在lookup系列函数中会频繁使用
//它的有关定义还包含了一系列用于访问信息的功能宏。

/* Array of DT_NEEDED dependencies and their dependencies, in
dependency order for symbol lookup (with and without
duplicates). There is no entry before the dependencies have
been loaded. */
//依赖项及其依赖项的数组 按符号查找的依赖项顺序排列(有和没有重复)。
//在加载依赖项之前没有条目。
struct r_scope_elem l_searchlist;

/* Dependent object that first caused this object to be loaded. */
//第一次唤起当前模块被加载的模块
struct link_map *l_loader;
/* This is an array defining the lookup scope for this link map.
There are initially at most three different scope lists. */
//这个数组定义了当前模块用于lookup函数搜索的范围 最初最多有三个不同范围的列表
struct r_scope_elem **l_scope;

......

};
..1.1.7. _dl_fixup 函数定义和分析

libc/elf/dl-runtime.c

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
__attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE _dl_fixup (
/* GKM FIXME: Fix trampoline to pass bounds so we can do
without the `__unbounded' qualifier. */
struct link_map *__unbounded l, ElfW(Word) reloc_offset)
{
const ElfW(Sym) *const symtab
= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);

const PLTREL *const reloc
= (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
lookup_t result;
DL_FIXUP_VALUE_TYPE value;

/* Sanity check that we're really looking at a PLT relocation. */
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

/* Look up the target symbol. If the normal lookup rules are not
used don't look in the global scope. */
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 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;
}

/* We need to keep the scope around so do some locking. This is
not necessary for objects which cannot be unloaded or when
we are not using any threads (yet). */
int flags = DL_LOOKUP_ADD_DEPENDENCY;
if (!RTLD_SINGLE_THREAD_P)
{
THREAD_GSCOPE_SET_FLAG ();
flags |= DL_LOOKUP_GSCOPE_LOCK;
}

result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);

/* We are done with the global scope. */
if (!RTLD_SINGLE_THREAD_P)
THREAD_GSCOPE_RESET_FLAG ();

/* Currently result contains the base load address (or link map)
of the object that defines sym. Now add in the symbol
offset. */
value = DL_FIXUP_MAKE_VALUE (result,
sym ? (LOOKUP_VALUE_ADDRESS (result)
+ sym->st_value) : 0);
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
result = l;
}

/* And now perhaps the relocation addend. */
value = elf_machine_plt_value (l, reloc, value);

/* Finally, fix up the plt itself. */
if (__builtin_expect (GLRO(dl_bind_not), 0))
return value;

return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}
字符串表strtab
1
2
3
4
5
6
7
8
9
10

reloc_offset即是传入的参数reloc_arg 其代表在.rela.plt表中的第几项 保存在reloc中
reloc的r_offset表示需要修改的函数地址在GOT表中的地址 加上装载地址l_addr得到的rel_addr就是最终要修改的.got.plt保存该函数地址的项的绝对地址

st_other描述符号的可见性 如果包含STV_PROTECTED、STV_HIDDEN和STV_INTERNAL的其中任何一种 则直接将装载地址加上st_value即得到函数的最终地址value 将其写入rel_addr (相当于作用域不超过当前符号表的范围)
最后调用elf_machine_fixup_plt函数进行修正



fixup plt 回写.got.plt的项

static inline Elf64_Addr
elf_machine_fixup_plt (struct link_map *map, lookup_t t,
const Elf64_Rela *reloc,
Elf64_Addr *reloc_addr, Elf64_Addr value)
{
return *reloc_addr = value;
}

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121



其他情况 会进入if语句
首先获得符号的version信息 然后调用 ```_dl_lookup_symbol_x``` 函数从已装载的共享库中查找最终的符号地址



elf/dl-lookup.c
```C++
/* Search loaded objects' symbol tables for a definition of the symbol
UNDEF_NAME, perhaps with a requested version for the symbol.
We must never have calls to the audit functions inside this function
or in any function which gets called. If this would happen the audit
code might create a thread which can throw off all the scope locking. */
lookup_t
_dl_lookup_symbol_x (const char *undef_name, struct link_map *undef_map,
const ElfW(Sym) **ref,
struct r_scope_elem *symbol_scope[],
const struct r_found_version *version,
int type_class, int flags, struct link_map *skip_map)
{
size_t n = scope->r_nlist;
__asm volatile ("" : "+r" (n), "+m" (scope->r_list));
struct link_map **list = scope->r_list;

do
{
Elf_Symndx symidx;
int num_versions = 0;
const ElfW(Sym) *versioned_sym = NULL;
const struct link_map *map = list[i]->l_real;

...

const ElfW(Sym) *symtab = (const void *) D_PTR (map, l_info[DT_SYMTAB]);
const char *strtab = (const void *) D_PTR (map, l_info[DT_STRTAB]);

const ElfW(Sym) *sym;
const ElfW(Addr) *bitmask = map->l_gnu_bitmask;
if (__builtin_expect (bitmask != NULL, 1))
{
ElfW(Addr) bitmask_word
= bitmask[(new_hash / __ELF_NATIVE_CLASS)
& map->l_gnu_bitmask_idxbits];

unsigned int hashbit1 = new_hash & (__ELF_NATIVE_CLASS - 1);
unsigned int hashbit2 = ((new_hash >> map->l_gnu_shift)
& (__ELF_NATIVE_CLASS - 1));

if (__builtin_expect ((bitmask_word >> hashbit1)
& (bitmask_word >> hashbit2) & 1, 0))
{
Elf32_Word bucket = map->l_gnu_buckets[new_hash
% map->l_nbuckets];
if (bucket != 0)
{
const Elf32_Word *hasharr = &map->l_gnu_chain_zero[bucket];

do
if (((*hasharr ^ new_hash) >> 1) == 0)
{
symidx = hasharr - map->l_gnu_chain_zero;
sym = check_match (&symtab[symidx]);
if (sym != NULL)
goto found_it;
}
while ((*hasharr++ & 1u) == 0);
}
}
symidx = SHN_UNDEF;
}
else
{
if (*old_hash == 0xffffffff)
*old_hash = _dl_elf_hash (undef_name);
for (symidx = map->l_buckets[*old_hash % map->l_nbuckets];
symidx != STN_UNDEF;
symidx = map->l_chain[symidx])
{
sym = check_match (&symtab[symidx]);
if (sym != NULL)
goto found_it;
}
}

sym = num_versions == 1 ? versioned_sym : NULL;
if (sym != NULL)
{
found_it:
switch (__builtin_expect (ELFW(ST_BIND) (sym->st_info), STB_GLOBAL))
{
case STB_WEAK:
if (__builtin_expect (GLRO(dl_dynamic_weak), 0))
{
if (! result->s)
{
result->s = sym;
result->m = (struct link_map *) map;
}
break;
}

case STB_GLOBAL:
success:
result->s = sym;
result->m = (struct link_map *) map;
return 1;

case STB_GNU_UNIQUE:
...

default:
break;
}
}
}
while (++i < n);

return 0;
}

首先获得该scope下的link_map个数r_nlist和数组r_list 然后进行遍历
省略的部分是检查当前link_map是否有符合查找的条件 没有就继续遍历
再往下取出当前link_map的符号表symtab和字符串表strtab
接下来的if和else条件语句部分都是通过哈希值找到符号表中对应符号列表的索引
如果找到 就通过check_match函数比对符号表中的函数名symtab[symidx]和待查找的函数名undef_name是否相等 如果相等 就找到了该符号并跳转到found_it语句 否则返回null

找到了该符号后对符号类型进行判断
如果找到的是弱符号STB_WEAK 则保存第一次找到的结果 然后继续循环查找
如果后面没有找到可以覆盖该结果的符号 则返回的就是该第一次保存的结果。
如果找到的是全局符号STB_GLOBAL 则直接返回该结果。

如果找到的符号是其他类型的符号 则继续循环查找
最后 如果什么都没找到 则返回0

/elf/dl-lookup.c::check_match

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
const ElfW(Sym) *__attribute_noinline__
check_match (const ElfW(Sym) *sym)
{
...

if (sym != ref && strcmp (strtab + sym->st_name, undef_name))
return NULL;

const ElfW(Half) *verstab = map->l_versyms;
if (version != NULL)
{

ElfW(Half) ndx = verstab[symidx] & 0x7fff;
if ( (map->l_versions[ndx].hash != version->hash
|| strcmp (map->l_versions[ndx].name, version->name) )
&& (version->hidden || map->l_versions[ndx].hash || (verstab[symidx] & 0x8000)))
return NULL;
}
else
{
...
}

return sym;
}

这里主要有一个版本检查

..1.1.8. 符号版本

由于符号版本不是C语言的标准用法 所以它使用了汇编器的一个特殊指示 也就是.symver 指示
而GCC中通过内嵌汇编完成 如下:

1
2
3
4
__asm__(".symver original_foo,foo@");  
__asm__(".symver old_foo,foo@VERS_1.1");
__asm__(".symver old_foo1,foo@VERS_1.2");
__asm__(".symver new_foo,foo@@VERS_2.0");

这个例子中定义了foo的四个版本
其中的symver的第一个参数为源代码中真正定义的实现 而之后的foo则为对外公开的版本 也就是可以有不同版本的符号
其中@@则表示这个是一个默认版本(简单来说 如果一个可执行文件链接的时候foo还没有任何版本控制 但是在运行时foo已经引入了多个版本 则此时的可执行文件可以选择这个@@表示的默认版本符号
这里的old_foo就是foo的VERS_1.1版本 如果在so中定义了old_foo 那么这个old_foo就是这个foo的VERS_1.1版本的实现
如果没有定义 可以通过old_foo来引用这个特殊版本的foo符号的定义

..1.1.9. 强弱符号

针对强弱符号的概念 链接器就会按照如下规则处理与选择被多次定义的全局符号

  • 规则1: 不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号)
    • 如果有多个强符号定义 则链接器报符号重复定义错误
  • 规则2: 如果一个符号在某个目标文件中是强符号 在其他文件中都是弱符号 那么选择强符号
  • 规则3: 如果一个符号在所有目标文件中都是弱符号 那么选择其中占用空间最大的一个
    • 比如目标文件A定义全局变量global为int型 占4个字节
    • 目标文件B定义global为doulbe型 占8个字节
    • 那么目标文件A和B链接后 符号global占8个字节(尽量不要使用多个不同类型的弱符号,否则容易导致很难发现的程序错误)
..1.1.10. 强弱引用

我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时 它们必须要被正确决议
如果没有找到该符号的定义 链接器就会报符号未定义错误 这种被称为强引用(Strong Reference)
与之相对应还有一种弱引用(Weak Reference)
在处理弱引用时 如果该符号有定义 则链接器将该符号的引用决议 如果该符号未被定义 则链接器对于该引用不报错
链接器处理强引用和弱引用的过程几乎一样 只是对于未定义的弱引用 链接器不认为它是一个错误
一般对于未定义的弱引用 链接器默认其为0 或者是一个特殊的值 以便于程序代码能够识别

..1.1.11. 符号的作用域

C++中non-member function template模板的代码是具有不同于普通的函数定义的
C++标准里对于在global scope声明对象的链接描述:

[ISO/IEE 14882:2011]A name declared in a namespace scope without a storage-class-specifier has external linkage unless it has internal linkage because of a previous declaration and provided it is not declared const. Objects declared const and not explicitly declared extern have internal linkage.

例如声明int max(int,int)是具有外部链接的符号

C++中非成员函数模板(non-member function template)的链接不同于普通的函数

[ISO/IEE 14882:2014]A template name has linkage (3.5). A non-member function template can have internal linkage; any other template name shall have external linkage. Specializations (explicit or implicit) of a template that has internal linkage are distinct from all specializations in other translation units.

即通过模板实现的max(const T&,const T&)是具有内部链接internal linkage的符号
所以在链接时对于max的调用查找只能够找到自己目标文件中的符号名(内部链接外部不可见) 不会和外部链接产生重定义冲突

..2. 动态库装载过程

..2.1. ELF的辅助向量 AUXV

main函数的第三个参数 char* envp[]

..2.2. ELF的装载有三种方法

函数 描述
load_binary 通过读存放在可执行文件中的信息为当前进程建立一个新的执行环境
load_shlib 用于动态的把一个共享库捆绑到一个已经在运行的进程, 这是由uselib()系统调用激活的
core_dump 在名为core的文件中, 存放当前进程的执行上下文. 这个文件通常是在进程接收到一个缺省操作为”dump”的信号时被创建的, 其格式取决于被执行程序的可执行类型