📄
字号:
the image should be loaded at fixed address, not at a variable address. */
for(i = 0, elf_ppnt = elf_phdata; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
int elf_prot = 0, elf_flags;
unsigned long k, vaddr;
if (elf_ppnt->p_type != PT_LOAD)
continue;
. . . . . .
vaddr = elf_ppnt->p_vaddr;
if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {
elf_flags |= MAP_FIXED;
} else if (loc->elf_ex.e_type == ET_DYN) {
/* Try and get dynamic programs out of the way of the default mmap
base, as well as whatever program they might try to exec. This
is because the brk will follow the loader, and is not movable. */
load_bias = ELF_PAGESTART(ELF_ET_DYN_BASE - vaddr);
}
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags);
. . . . . .
if (!load_addr_set) {
load_addr_set = 1;
load_addr = (elf_ppnt->p_vaddr - elf_ppnt->p_offset);
if (loc->elf_ex.e_type == ET_DYN) {
load_bias += error -
ELF_PAGESTART(load_bias + vaddr);
load_addr += load_bias;
reloc_func_desc = load_bias;
}
}
k = elf_ppnt->p_vaddr;
if (k < start_code) start_code = k;
if (start_data < k) start_data = k;
. . . . . .
k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;
if (k > elf_bss)
elf_bss = k;
if ((elf_ppnt->p_flags & PF_X) && end_code < k)
end_code = k;
if (end_data < k)
end_data = k;
k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz;
if (k > elf_brk)
elf_brk = k;
} //end for() loop
loc->elf_ex.e_entry += load_bias;
elf_bss += load_bias;
elf_brk += load_bias;
start_code += load_bias;
end_code += load_bias;
start_data += load_bias;
end_data += load_bias;
/* Calling set_brk effectively mmaps the pages that we need
* for the bss and break sections. We must do this before
* mapping in the interpreter, to make sure it doesn't wind
* up getting placed where the bss needs to go.
*/
retval = set_brk(elf_bss, elf_brk);
. . . . . .[/code]
还是从目标映像的程序头表中搜索,这一次是寻找类型为PT_LOAD的部(Segment)。在二进制映像中,只有类型为PT_LOAD的部才是需要装入的。
找到一个PT_LOAD片以后,先要确定其装入地址。正如代码前面的注释所述,这里先假定装入地址是固定的,然后再根据映像是否允许浮动而作出调整。具体片头数据结构中的p_vaddr提供了映像在连接时确定的装入地址vaddr。如果映像的类型为ET_EXEC,(或者load_addr_set已经被设置成1,见下)那么装入地址就是固定的。而若类型为ET_DYN、即共享库,那么即使装入地址固定也要加上一个偏移量,代码中给出了计算方法,其中ELF_ET_DYN_BASE对于x86定义为(TASK_SIZE / 3 * 2),所以这是2GB边界,而ELF_PAGESTART表示按页面边界对齐。
确定了装入地址以后,就通过elf_map()、实际上是elf32_map()、建立用户空间虚存区间与目标映像文件中某个连续区间之间的映射。这个函数基本上就是do_mmap(),其返回值就是实际映射的(起始)地址。对于类型为ET_EXEC的可执行程序映像而言,代码中的load_bias是0,所以装入的起点就是映像自己提供的地址vaddr。另一方面,对于ET_EXEC,由于参数中的elf_flags中的MAP_FIXED标志位为1,所以给定的映射地址是刚性的而不容许变通,如果与已经映射的区间有冲突就以失败告终。不过,目标映像的映射是从一片空白开始的,所以实际上不可能失败。顺便提一下,现在又多了一种ELF格式的目标映像,称为FDPIC,其装入地址就是可浮动的。
即使总的装入地址是浮动的,一旦装入了第一个Segment以后,下一个Segment的装入地址就应该是固定的了,所以这里一方面把load_addr_set设置成1,
我们不妨以程序wine为例看一下映像的装入。GNU提供了一个很有用的工具readelf,可以用来观察各种ELF映像的内部结构。我们就用它来看/usr/local/bin/wine 的各种头部。首先是它的ELF头部:
[code]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[/code]
可见,这是EXEC型的映像,其装入地址是固定的、不可浮动的。这个映像有6个程序头、36个section头。我们先看程序头表:
[code]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[/code]
所以需要装入的是两个Segment,从它们在映像中的起始地址和大小可以看出,它们在映像中是连续的。但是,从它们的装入地址却可以看出,装入到用户空间之后它们就分开了。第一个Segment的装入地址是0x08048000,装入以后应该占据0x08048000-0x080491cc,而第二个Segment的装入地址却是0x0804a1cc。再看区段头表:
[code]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
. . . . . .[/code]
前面说装入的第一个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。
现在该装入解释器的映像了,我们再往下看。
[code][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;
}[/code]
这段程序的逻辑很简单:如果需要装入解释器,并且解释器的映像是ELF格式的,就通过load_elf_interp()装入其映像,并把将来进入用户空间时的入口地址设置成load_elf_interp()的返回值,那显然是解释器的程序入口。而若不装入解释器,那么这个地址就是目标映像本身的程序入口。
显然,关键的操作是由load_elf_interp()完成的,所以我们追下去看load_elf_interp()的代码。
[code][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.
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -