- 论坛徽章:
- 0
|
我们不妨以程序wine为例看一下映像的装入。GNU提供了一个很有用的工具readelf,可以用来观察各种ELF映像的内部结构。我们就用它来看/usr/local/bin/wine 的各种头部。首先是它的ELF头部:
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048750
Start of program headers: 52 (bytes into file)
Start of section headers: 114904 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 6
Size of section headers: 40 (bytes)
Number of section headers: 36
Section header string table index: 33
可见,这是EXEC型的映像,其装入地址是固定的、不可浮动的。这个映像有6个程序头、36个section头。我们先看程序头表:
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x000c0 0x000c0 R E 0x4
INTERP 0x0000f4 0x080480f4 0x080480f4 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x011cc 0x011cc R E 0x1000
LOAD 0x0011cc 0x0804a1cc 0x0804a1cc 0x00158 0x00160 RW 0x1000
DYNAMIC 0x0011d8 0x0804a1d8 0x0804a1d8 0x000d8 0x000d8 RW 0x4
NOTE 0x000108 0x08048108 0x08048108 0x00020 0x00020 R 0x4
所以需要装入的是两个Segment,从它们在映像中的起始地址和大小可以看出,它们在映像中是连续的。但是,从它们的装入地址却可以看出,装入到用户空间之后它们就分开了。第一个Segment的装入地址是0x08048000,装入以后应该占据0x08048000-0x080491cc,而第二个Segment的装入地址却是0x0804a1cc。再看区段头表:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 080480f4 0000f4 000013 00 A 0 0 1
. . . . . .
[10] .init PROGBITS 080485e8 0005e8 000017 00 AX 0 0 4
[11] .plt PROGBITS 08048600 000600 000150 04 AX 0 0 4
[12] .text PROGBITS 08048750 000750 0008d8 00 AX 0 0 4
[13] .fini PROGBITS 08049028 001028 00001b 00 AX 0 0 4
[14] .rodata PROGBITS 08049060 001060 000166 00 A 0 0 32
[15] .eh_frame PROGBITS 080491c8 0011c8 000004 00 A 0 0 4
[16] .data PROGBITS 0804a1cc 0011cc 00000c 00 WA 0 0 4
. . . . . .
[21] .got PROGBITS 0804a2c4 0012c4 000060 04 WA 0 0 4
[22] .bss NOBITS 0804a324 001324 000008 00 WA 0 0 4
. . . . . .
[34] .symtab SYMTAB 00000000 01c678 000890 10 35 5c 4
. . . . . .
前面说装入的第一个Segment在映像中的位置是0x0,长度是0x0011cc。跟区段头表中的信息一对照,就可以知道在第16项.data以前的所有区段都是要装入用户空间的。这里面包括了大家所熟知的.text即“代码段”。此外,.init、.fini两个区段也有着特殊的重要性,因为映像的程序入口就在.init段中,实际上在进入main()之前的代码都在这里。而从main()返回之后的代码,包括对exit()的调用,则在.fini中。还有一个区段.plt也十分重要,plt是“Procedure Linkage Table”的缩写,这就是用来为目标映像跟共享库建立动态连接的。再看第二个Segment,这是从.data、即“数据段”开始的。第二个Segment的长度是0x00160,所以应该包括.got和.bss。这里的.got又是个重要的区段,got是“Global Offset Table”的缩写,里面纪录着供动态连接的函数在映像中的位置。显然,这对于共享库是必不可少的。所以,除大家所熟知的.text、.data、.bss等区段以外,映像中还有许多信息都是要装入到用户空间的。这么多的信息给谁用呢?这主要是给“解释器”用的,下一片漫谈我将为读者介绍解释器ld-linux.so.2。另一方面,映像中还有包括符号表.symtab在内的许多别的信息,但是因为不在类型为LOAD的Segment中而不会被装入用户空间。
回到load_elf_binary()的代码。当程序中的for循环结束时,目标映像本身需要装入的内容都已经映射到了用户空间合适的位置上。如果是类型为ET_DYN的映像,则elf_bss等等变量以及映像的程序入口地址都还需要加上偏移量load_bias。
现在该装入解释器的映像了,我们再往下看。
[sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]
if (elf_interpreter) {
if (interpreter_type == INTERPRETER_AOUT)
elf_entry = load_aout_interp(&loc->interp_ex, interpreter);
else
elf_entry = load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_load_addr);
. . . . . .
reloc_func_desc = interp_load_addr;
allow_write_access(interpreter);
fput(interpreter);
kfree(elf_interpreter);
} else {
elf_entry = loc->elf_ex.e_entry;
}
.这段程序的逻辑很简单:如果需要装入解释器,并且解释器的映像是ELF格式的,就通过load_elf_interp()装入其映像,并把将来进入用户空间时的入口地址设置成load_elf_interp()的返回值,那显然是解释器的程序入口。而若不装入解释器,那么这个地址就是目标映像本身的程序入口。
显然,关键的操作是由load_elf_interp()完成的,所以我们追下去看load_elf_interp()的代码。
[sys_execve() > do_execve() > search_binary_handler() > load_elf_binary() > load_elf_interp()]
static unsigned long load_elf_interp(struct elfhdr * interp_elf_ex,
struct file * interpreter, unsigned long *interp_load_addr)
{
struct elf_phdr *elf_phdata;
struct elf_phdr *eppnt;
unsigned long load_addr = 0;
int load_addr_set = 0;
unsigned long last_bss = 0, elf_bss = 0;
unsigned long error = ~0UL;
int retval, i, size;
/* First of all, some simple consistency checks */
if (interp_elf_ex->e_type != ET_EXEC && interp_elf_ex->e_type != ET_DYN)
goto out;
. . . . . .
size = sizeof(struct elf_phdr) * interp_elf_ex->e_phnum;
. . . . . .
elf_phdata = (struct elf_phdr *) kmalloc(size, GFP_KERNEL);
. . . . . .
retval = kernel_read(interpreter,interp_elf_ex->e_phoff,(char *)elf_phdata,size);
. . . . . .
eppnt = elf_phdata;
for (i=0; i<interp_elf_ex->e_phnum; i++, eppnt++) {
if (eppnt->p_type == PT_LOAD) {
. . . . . .
vaddr = eppnt->p_vaddr;
if (interp_elf_ex->e_type == ET_EXEC || load_addr_set)
elf_type |= MAP_FIXED;
map_addr = elf_map(interpreter, load_addr + vaddr, eppnt, elf_prot, elf_type);
error = map_addr;
if (BAD_ADDR(map_addr))
goto out_close;
if (!load_addr_set && interp_elf_ex->e_type == ET_DYN) {
load_addr = map_addr - ELF_PAGESTART(vaddr);
load_addr_set = 1;
}
/*
* Check to see if the section's size will overflow the
* allowed task size. Note that p_filesz must always be
* <= p_memsize so it is only necessary to check p_memsz.
*/
k = load_addr + eppnt->p_vaddr;
if (k > TASK_SIZE || eppnt->p_filesz > eppnt->p_memsz ||
eppnt->p_memsz > TASK_SIZE || TASK_SIZE - eppnt->p_memsz < k) {
error = -ENOMEM;
goto out_close;
}
/*
* Find the end of the file mapping for this phdr, and keep
* track of the largest address we see for this.
*/
k = load_addr + eppnt->p_vaddr + eppnt->p_filesz;
if (k > elf_bss)
elf_bss = k;
/*
* Do the same thing for the memory mapping - between
* elf_bss and last_bss is the bss section.
*/
k = load_addr + eppnt->p_memsz + eppnt->p_vaddr;
if (k > last_bss)
last_bss = k;
} //end if
} //end for
/*
* Now fill out the bss section. First pad the last page up
* to the page boundary, and then perform a mmap to make sure
* that there are zero-mapped pages up to and including the
* last bss page.
*/
if (padzero(elf_bss)) {
error = -EFAULT;
goto out_close;
}
elf_bss = ELF_PAGESTART(elf_bss + ELF_MIN_ALIGN - 1);
* What we have mapped so far */
/* Map the last of the bss segment */
if (last_bss > elf_bss) {
down_write(¤t->mm->mmap_sem);
error = do_brk(elf_bss, last_bss - elf_bss);
up_write(¤t->mm->mmap_sem);
if (BAD_ADDR(error))
goto out_close;
}
*interp_load_addr = load_addr;
error = ((unsigned long) interp_elf_ex->e_entry) + load_addr;
out_close:
kfree(elf_phdata);
out:
return error;
}
代码中的do_brk()从用户空间分配一段空间。这段代码总体上与前面映射目标映像的那一段相似,就把它留给读者细细研究吧。注意解释器映像的类型一般都是ET_DYN,所以load_addr可能不等于0。
回到load_elf_binary()的代码中。
[sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]
compute_creds(bprm);
current->flags &= ~PF_FORKNOEXEC;
create_elf_tables(bprm, &loc->elf_ex, (interpreter_type == INTERPRETER_AOUT),
load_addr, interp_load_addr);
/* N.B. passed_fileno might not be initialized? */
if (interpreter_type == INTERPRETER_AOUT)
current->mm->arg_start += strlen(passed_fileno) + 1;
current->mm->end_code = end_code;
current->mm->start_code = start_code;
current->mm->start_data = start_data;
current->mm->end_data = end_data;
current->mm->start_stack = bprm->p;
在完成装入,启动用户空间的映像运行之前,还需要为目标映像和解释器准备好一些有关的信息,这些信息包括常规的argc、argv[]、envc、envp[]、还有一些所谓的“辅助向量(Auxiliary Vector)”。这些信息已经存在于内核中,但是需要把它们复制到用户空间,使它们在CPU进入解释器或目标映像的程序入口时出现在用户空间堆栈上。这里的create_elf_tables()就起着这个作用。
[sys_execve() > do_execve() > search_binary_handler() > load_elf_binary() > create_elf_tables()]
static int create_elf_tables(struct linux_binprm *bprm, struct elfhdr * exec, int interp_aout,
unsigned long load_addr, unsigned long interp_load_addr)
{
unsigned long p = bprm->p;
int argc = bprm->argc;
int envc = bprm->envc;
elf_addr_t __user *argv;
elf_addr_t __user *envp;
elf_addr_t __user *sp;
elf_addr_t __user *u_platform;
const char *k_platform = ELF_PLATFORM;
int items;
elf_addr_t *elf_info;
int ei_index = 0;
struct task_struct *tsk = current;
. . . . . .
/* Create the ELF interpreter info */
elf_info = (elf_addr_t *) current->mm->saved_auxv;
#define NEW_AUX_ENT(id, val) \
do { elf_info[ei_index++] = id; elf_info[ei_index++] = val; } while (0)
NEW_AUX_ENT(AT_HWCAP, ELF_HWCAP);
NEW_AUX_ENT(AT_PAGESZ, ELF_EXEC_PAGESIZE);
NEW_AUX_ENT(AT_CLKTCK, CLOCKS_PER_SEC);
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_FLAGS, 0);
NEW_AUX_ENT(AT_ENTRY, exec->e_entry);
NEW_AUX_ENT(AT_UID, (elf_addr_t) tsk->uid);
NEW_AUX_ENT(AT_EUID, (elf_addr_t) tsk->euid);
NEW_AUX_ENT(AT_GID, (elf_addr_t) tsk->gid);
NEW_AUX_ENT(AT_EGID, (elf_addr_t) tsk->egid);
NEW_AUX_ENT(AT_SECURE, (elf_addr_t) security_bprm_secureexec(bprm));
. . . . . .
if (bprm->interp_flags & BINPRM_FLAGS_EXECFD) {
NEW_AUX_ENT(AT_EXECFD, (elf_addr_t) bprm->interp_data);
}
#undef NEW_AUX_ENT
/* AT_NULL is zero; clear the rest too */
memset(&elf_info[ei_index], 0,
sizeof current->mm->saved_auxv - ei_index * sizeof elf_info[0]);
/* And advance past the AT_NULL entry. */
ei_index += 2;
sp = STACK_ADD(p, ei_index); //实际上是(p - ei_index),因为堆栈向下伸展。
items = (argc + 1) + (envc + 1);
if (interp_aout) {
items += 3; /* a.out interpreters require argv & envp too */
} else {
items += 1; /* ELF interpreters only put argc on the stack */
}
bprm->p = STACK_ROUND(sp, items); //计算(sp - items)并与16字节边界对齐。
/* Point sp at the lowest address on the stack */
#ifdef CONFIG_STACK_GROWSUP
. . . . . .
#else
sp = (elf_addr_t __user *)bprm->p;
#endif
/* Now, let's put argc (and argv, envp if appropriate) on the stack */
if (__put_user(argc, sp++))
return -EFAULT;
if (interp_aout) {
. . . . . .
} else {
argv = sp; //用户空间堆栈上的argv[]从这里开始
envp = argv + argc + 1; //用户空间堆栈上的envp[]从这里开始
}
/* Populate argv and envp */
p = current->mm->arg_end = current->mm->arg_start;
while (argc-- > 0) {
size_t len;
__put_user((elf_addr_t)p, argv++);
len = strnlen_user((void __user *)p, PAGE_SIZE*MAX_ARG_PAGES);
if (!len || len > PAGE_SIZE*MAX_ARG_PAGES)
return 0;
p += len;
}
if (__put_user(0, argv))
return -EFAULT;
current->mm->arg_end = current->mm->env_start = p;
while (envc-- > 0) {
size_t len;
__put_user((elf_addr_t)p, envp++);
len = strnlen_user((void __user *)p, PAGE_SIZE*MAX_ARG_PAGES);
if (!len || len > PAGE_SIZE*MAX_ARG_PAGES)
return 0;
p += len;
}
if (__put_user(0, envp))
return -EFAULT;
current->mm->env_end = p;
/* Put the elf_info on the stack in the right place. */
sp = (elf_addr_t __user *)envp + 1; //用户空间堆栈上的elf_info[]从这里开始
if (copy_to_user(sp, elf_info, ei_index * sizeof(elf_addr_t)))
return -EFAULT;
return 0;
}
这个函数的代码大体上可以分成前后两半。
前一半是准备阶段,特别是对诸多辅助向量的准备。辅助向量是以编号加值的形式成对出现的。例如,AT_PHDR是个编号,表示目标映像中程序头数组在用户空间的位置(可想而知这是解释器需要的信息),而(load_addr + exec->e_phoff)是它的值,二者占据相继的两个32位长字。同样,AT_PHNUM是个编号,而exec->e_phnum是它的值,余类推。当然,这些编号对于内核和解释器都有着相同的意义。这里先把这些向量准备好在一个数组elf_info[]中,最后以编号AT_NULL即0作为数组的结尾。
后一半则是复制阶段,从代码中的注释行“/* Now, let's put argc (and argv, envp if appropriate) on the stack */”开始。这个阶段的目的是把这些信息复制到用户空间,把它们“种”在堆栈上,为解释器和目标映像的运行做好准备。代码中的变量bprm->p实质上是个指针,它代表着用户空间的堆栈指针。进入create_elf_tables()以后,就把bprm->p的值赋给了这里的变量p,所以在这里p也代表着用户空间的堆栈指针。注意这里通过__put_user()写入用户空间堆栈上的argv[]和envp[]中的只是一些指针,而相应的字符串则已经由do_execve()通过copy_strings()从用户空间拷贝到内核空间的某些页面中,后来这些页面又被映射到了用户空间(新的地址上),这里写入用户空间argv[]和envp[]中的那些指针就是指向各个字符串在用户空间的新的起点。函数strnlen_user()的作用是获取用户空间字符串的长度。
再回到load_elf_binary()的代码,剩下的只是“临门一脚”了。
[sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]
. . . . . .
start_thread(regs, elf_entry, bprm->p);
retval = 0;
. . . . . .
}
最后的start_thread()是个宏操作,其定义如下:
#define start_thread(regs, new_eip, new_esp) do { \
__asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0)); \
set_fs(USER_DS); \
regs->xds = __USER_DS; \
regs->xes = __USER_DS; \
regs->xss = __USER_DS; \
regs->xcs = __USER_CS; \
regs->eip = new_eip; \
regs->esp = new_esp; \
} while (0)
这几条指令把作为参数传下来的用户空间程序入口和堆栈指针设置到regs数据结构中,这个数据结构实际上在系统堆栈中,是在当前进程通过系统调用进入内核时由SAVE_ALL形成的,而指向所保存现场的指针regs则作为参数传给了sys_execve(),并逐层传了下来。把所保存现场中的eip和esp改成了新的地址,就使得CPU在返回用户空间时进入新的程序入口。如果有解释器映像存在,那么这就是解释器映像的程序入口,否则就是目标映像的程序入口。那么什么情况下有解释器映像存在,什么情况下没有呢?如果目标映像与各种库的连接是静态连接,因而无需依靠共享库、即动态连接库,那就不需要解释器映像,启动目标映像运行的条件已经具备;否则就一定要有解释器映像存在。现代的二进制映像一般都使用共享库,所以一般都需要有解释器映像。
现在,对于需要动态连接的目标映像,目标映像和解释器映像都已映射到了当前进程的用户空间,并且“井水不犯河水”、同时并存。但是要启动目标映像的运行则条件还不具备。因为还需要装入(映射)某些共享库的映像,并使目标映像与这些共享库映像之间建立起动态连接,而这需要由解释器在用户空间完成,好在启动解释器运行的条件已经具备了。 |
|