当一个程序被加载进内存时,动态链接器会把需要的共享库加载并绑定到该进程的地址空间中。

共享库在被编译到可执行文件中时是位置独立的,因此很容易被重定位到进程的地址空间中。一个共享库就是一个动态的ELF目标文件。在终端输入readelf -h libc.so命令,会看到e_type(ELF文件类型)是ET_DYN。动态目标文件与可执行文件非常类似,是由程序解释器加载的,通常没有PT_INTERP段,因而不会触发程序解释器。

当一个共享库被加载进一个进程的地址空间中时,一定有指向其他共享库的重定位。动态链接器会修改可执行文件中的GOT(Global Offset Table全局偏移表)。GOT位于数据段中(.got.plt节),因为GOT必须是可写的(至少最初是可写的,可以将只读重定位看做是一种安全特性),因而位于数据段中。动态链接器会使用解析好的共享库地址来修改GOT。

0x01 辅助向量

通过系统调用sys_execve()将程序加载到内存中时,对应的可执行文件会被映射到内存的地址空间,并为该进程的地址空间分配一个栈。这个栈会用特定的方式向动态链接器传递信息。这种特定的对信息的设置和安排即为辅助向量(auxv)。

如下x86结构中,占地存放了以下信息:

辅助向量是一系列ElfN_auxv_t的结构:

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
#ifndef HAVE_ELF32_AUXV_T 
typedef struct
{
uint32_t a_type; /* Entry type */
union
{
uint32_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;
} Elf32_auxv_t;
#endif

#ifndef HAVE_ELF64_AUXV_T
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;
#endif

a_type指定了辅助向量的条目类型,a_val为辅助向量的值。

下面是动态链接器所需要的一些最重要的条目类型:

1
2
3
4
5
6
7
#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_ENTRY 9 /* Entry point of program */
#define AT_UID 11 /* Real uid */

动态链接器从栈中检索可执行程序相关的信息,如程序头、程序入口地址等。上面列出的只是从/usr/include/elf.h中挑选出的几个辅助向量条目类型。

辅助向量是由内核函数create_elf_tables()设定的,该函数在Linux的源码/usr/src/linux/fs/binfmt_elf.c中。

内核的执行过程和如下描述类似:

  1. sys_execve()
  2. 调用do_execve_common()
  3. 调用search_binary_handler()
  4. 调用load_elf_binary()
  5. 调用create_elf_tables()

下面是/usr/src/linux/fs/binfmt_elf.c中的函数create_elf_tables()的代码,这段代码会添加辅助向量条目:

1
2
3
4
5
6
NEW_AUX_ENT(AT_PAGESZ, ELF_EXEC_PAGESIZE);
NEW_AUX_ENT(AT_PHDR, load_addr + exec->e_phoff);
NEW_AUX_ENT(AT_PHENT, sizeof (struct elf_phdr));
NEW_AUX_ENT(AT_PHNUM, exec->e_phnum);
NEW_AUX_ENT(AT_BASE, interp_load_addr);
NEW_AUX_ENT(AT_ENTRY, exec->e_entry);

可以看到,ELF的入口点和程序头地址,以及其他的值,是与内核中的NEW_AUX_ENT()宏一起入栈的。

程序被加载进内存,辅助向量被填充好之后,控制权就交给了动态链接器。动态链接器会解析要链接到进程地址空间的用于共享库的符号和重定位。默认情况下,可执行文件会动态链接GNU C库libc.so。

ldd命令能显示出一个给定的可执行文件所依赖的共享库列表。

0x02 GOT表和PLT表

0x03 参考