这篇笔记做过一次,但是做的不好,现在又重新做一遍,其中有几处借鉴了同学的笔记文档,对此表示感谢。不足之处欢迎交流,我来进行改进,希望能对其他学习这方面东西的同学有所帮助。
linker是Android系统的加载器/链接器,当上层加载.so文件时,具体的工作就是由linker实际完成的。
linker的启动
因为linker本身也是一个.so文件,它也需要链接器的装载和链接,而这个链接器就是它本身。
首先,linker的入口在Android/bionic/linker/arch/arm/begin.S:
_start:
mov r0, sp
mov r1, #0
bl __linker_init
/* linker init returns the _entry address in the main image */
mov pc, r0
.section .ctors, "wa"
.globl __CTOR_LIST__
__CTOR_LIST__:
.long -1
首先将堆栈指针的值传给r0,并将r1置零。接下来跳转到函数__linker_init,传入的参数就是r0。最后返回的值为程序镜像的入口地址,传给pc,开始正式地执行程序。
__linker_init
接下来,看一下函数__linker_init的内容,首先看一下该函数的注释:
/*
* This is the entry point for the linker, called from begin.S. This
* method is responsible for fixing the linker's own relocations, and
* then calling __linker_init_post_relocation().
*
* Because this method is called before the linker has fixed it's own
* relocations, any attempt to reference an extern variable, extern
* function, or other GOT reference will generate a segfault.
*/
这段注释的意思是,该函数是linker的入口,负责对linker本身进行重定位,然后调用函数__linker_init_post_relocation。由于在这个函数调用之前,linker还没有自举,也就没法重定位自身,所以任何试图引用外部变量、函数或者其他GOT的操作都会导致错误,因为这些操作都需要链接器(linker)来完成。然后看一下函数的具体内容。
extern "C" unsigned __linker_init(unsigned **elfdata) {
unsigned linker_addr = find_linker_base(elfdata);
Elf32_Ehdr *elf_hdr = (Elf32_Ehdr *) linker_addr;
Elf32_Phdr *phdr =
(Elf32_Phdr *)((unsigned char *) linker_addr + elf_hdr->e_phoff);
soinfo linker_so;
memset(&linker_so, 0, sizeof(soinfo));
linker_so.base = linker_addr;
linker_so.size = phdr_table_get_load_size(phdr, elf_hdr->e_phnum);
linker_so.load_bias = get_elf_exec_load_bias(elf_hdr);
linker_so.dynamic = (unsigned *) -1;
linker_so.phdr = phdr;
linker_so.phnum = elf_hdr->e_phnum;
linker_so.flags |= FLAG_LINKER;
if (soinfo_link_image(&linker_so)) {
// It would be nice to print an error message, but if the linker
// can't link itself, there's no guarantee that we'll be able to
// call write() (because it involves a GOT reference).
//
// This situation should never occur unless the linker itself
// is corrupt.
exit(-1);
}
// We have successfully fixed our own relocations. It's safe to run
// the main part of the linker now.
return __linker_init_post_relocation(elfdata, linker_addr);
}
首先通过函数find_linker_base()获取linker的地址,因为linker本身是一个Elf文件,所以可以分别获取它的文件header和程序段header(这是在可执行视图下,具体的详见Elf文件格式的相关资料)。创建一个soinfo结构体,获取linker文件的相关信息,填入该soinfo结构体的对应字段。最后调用函数soinfo_link_image()完成linker的重定位,执行函数__linker_init_post_relocation()继续完成linker的主体部分。
find_linker_base
这个函数是用来寻找链接器linker本身的装载地址。
/*
* Find the value of AT_BASE passed to us by the kernel. This is the load
* location of the linker.
*/
static unsigned find_linker_base(unsigned **elfdata) {
int argc = (int) *elfdata;
char **argv = (char**) (elfdata + 1);
unsigned *vecs = (unsigned*) (argv + argc + 1);
while (vecs[0] != 0) {
vecs++;
}
/* The end of the environment block is marked by two NULL pointers */
vecs++;
while(vecs[0]) {
if (vecs[0] == AT_BASE) {
return vecs[1];
}
vecs += 2;
}
return 0; // should never happen
}
当内核启动动态链接器的时候,内核会把一个指针传递给linker,也就是上面提到的r0。这个指针指向一块这样的内容:这块内容包括 argc、argv 数组、环境变量数组和动态链接器所需的信息辅助数组,这些事先保存在进程堆栈的初 始化信息都是由操作系统完成的。上述 find_linker_base 函数的参数 elfdata 便是内核传递给 Linker 的指针, 利用该指针 find_linker_base 首先获取参数个数、参数指针,然后准备跳过环境变量字符串,argv + argc + 1中的 +1 是因为参数字符串地址结束是以0结束的,即有 4 个字节是 0。后面的 while 循环便是跳过环境变量字符串,跳过环境变量字符串之后,便是动态链接器中的辅助信息数组,其结构如下:
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;
前面的a_type表示类型,后面的a_val表示对应的值,这里要找的是AT_BASE类型,其值表示的是链接器的装载地址,然后返回这个地址。
由于链接器本身就是一个Elf文件,知道了它的装载地址,可以根据Elf文件格式获取它的文件头和段表。
现在还有一个需要搞清楚的就是结构体soinfo。
struct soinfo
{
char name[SOINFO_NAME_LEN];
const Elf32_Phdr *phdr;
int phnum;
unsigned entry;
unsigned base;
unsigned size;
int unused; // DO NOT USE, maintained for compatibility.
unsigned *dynamic;
unsigned unused2; // DO NOT USE, maintained for compatibility
unsigned unused3; // DO NOT USE, maintained for compatibility
soinfo *next;
unsigned flags;
const char *strtab;
Elf32_Sym *symtab;
unsigned nbucket;
unsigned nchain;
unsigned *bucket;
unsigned *chain;
unsigned *plt_got;
Elf32_Rel *plt_rel;
unsigned plt_rel_count;
Elf32_Rel *rel;
unsigned rel_count;
unsigned *preinit_array;
unsigned preinit_array_count;
unsigned *init_array;
unsigned init_array_count;
unsigned *fini_array;
unsigned fini_array_count;
void (*init_func)(void);
void (*fini_func)(void);
#if defined(ANDROID_ARM_LINKER)
/* ARM EABI section used for stack unwinding. */
unsigned *ARM_exidx;
unsigned ARM_exidx_count;
#elif defined(ANDROID_MIPS_LINKER)
#if 0
/* not yet */
unsigned *mips_pltgot
#endif
unsigned mips_symtabno;
unsigned mips_local_gotno;
unsigned mips_gotsym;
#endif /* ANDROID_*_LINKER */
unsigned refcount;
struct link_map linkmap;
int constructors_called;
/* When you read a virtual address from the ELF file, add this
* value to get the corresponding address in the process' address space */
Elf32_Addr load_bias;
int has_text_relocations;
};
内容十分多,从函数__linker_init的源码中可以看到它分别填充了soinfo结构体中的几个字段,分别是:base、size、load_bias、dynamic、phdr、phnum和flags。其中,base表示linker的装载地址,size表示所有可加载的段的总大小,load_bias是为了将虚拟地址映射到物理地址的偏移值,dynamic表示.dynamic的地址(初始化为-1),phdr表示段表头,phnum表示段的个数,flags表示装载的类型(FLAG_LINKER表示链接器的自举)。这些字段基本上都可以直接得到,除了size和load_bias两个是需要计算的。
先看如何计算size。
/* Compute the extent of all loadable segments in an ELF program header
* table. This corresponds to the page-aligned size in bytes that needs to be
* reserved in the process' address space
*
* This returns 0 if there are no loadable segments.
*/
Elf32_Addr phdr_table_get_load_size(const Elf32_Phdr* phdr_table,
size_t phdr_count)
{
Elf32_Addr min_vaddr = 0xFFFFFFFFU;
Elf32_Addr max_vaddr = 0x00000000U;
for (size_t i = 0; i < phdr_count; ++i) {
const Elf32_Phdr* phdr = &phdr_table[i];
if (phdr->p_type != PT_LOAD) {
continue;
}
if (phdr->p_vaddr < min_vaddr) {
min_vaddr = phdr->p_vaddr;
}
if (phdr->p_vaddr + phdr->p_memsz > max_vaddr) {
max_vaddr = phdr->p_vaddr + phdr->p_memsz;
}
}
if (min_vaddr > max_vaddr) {
return 0;
}
min_vaddr = PAGE_START(min_vaddr);
max_vaddr = PAGE_END(max_vaddr);
return max_vaddr - min_vaddr;
}
传入的参数是第一个段表头的地址和段表头的数目,从第一个开始便利,若其类型为PT_LOAD,则表示可加载的,min_vaddr最后保存的是这些可加载的段的最小地址,max_vaddr保存的是最大地址。再通过两个宏处理一下,将两者的差作为返回值。这两个宏的具体内容如下:
/* Returns the address of the page starting at address 'x' */
#define PAGE_START(x) ((x) & ~PAGE_MASK)
/* Returns the address of the next page after address 'x', unless 'x' is
* itself at the start of a page. Equivalent to:
*
* (x == PAGE_START(x)) ? x : PAGE_START(x)+PAGE_SIZE
*/
#define PAGE_END(x) PAGE_START((x) + (PAGE_SIZE-1))
PAGE_START(x)通过将虚拟地址与页表的掩码相与,可以得出是从哪一页开始的;PAGE_END(x)就是在当前地址的基础上加上一个页的大小减1,这样做是为了页的对齐,最后返回的大小并不是恰好所占用的虚拟空间大小,而是页的整数倍。
然后计算当前可执行文件的偏移值,这个函数只能用于通过内核加载的可执行文件或者共享目标文件。
/* Compute the load-bias