前言
主要描述的是动态库的映射。
动态库映射
动态库加载时会根据动态库的program header table进行内存映射,主要是对具有LOAD属性的segment进行映射,以一例说明:
通过readelf得到的其program header的内容为:
Elf file type is DYN (Shared object file)
Entry point 0x1ab19
There are 10 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
EXIDX 0x0dcf90 0x000dcf90 0x000dcf90 0x04a30 0x04a30 R 0x4
PHDR 0x000034 0x00000034 0x00000034 0x00140 0x00140 R 0x4
INTERP 0x0dbcb8 0x000dbcb8 0x000dbcb8 0x00019 0x00019 R 0x4
[Requesting program interpreter: /lib/ld-linux-armhf.so.3]
LOAD 0x000000 0x00000000 0x00000000 0xe19c4 0xe19c4 R E 0x10000
LOAD 0x0e1b90 0x000f1b90 0x000f1b90 0x026d8 0x04b64 RW 0x10000
DYNAMIC 0x0e2d44 0x000f2d44 0x000f2d44 0x000f8 0x000f8 RW 0x4
NOTE 0x000174 0x00000174 0x00000174 0x00044 0x00044 R 0x4
TLS 0x0e1b90 0x000f1b90 0x000f1b90 0x00008 0x00054 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
GNU_RELRO 0x0e1b90 0x000f1b90 0x000f1b90 0x01470 0x01470 R 0x1
Section to Segment mapping:
Segment Sections...
00 .ARM.exidx
01
02 .interp
03 .note.gnu.build-id .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_d .gnu.version_r .rel.dyn .rel.plt .plt
.text __libc_freeres_fn .rodata .interp .ARM.extab .ARM.exidx .eh_frame
04 .tdata .init_array .data.rel.ro .dynamic .got .data __libc_subfreeres __libc_IO_vtables __libc_atexit .bss __libc_freeres_ptrs
05 .dynamic
06 .note.gnu.build-id .note.ABI-tag
07 .tdata .tbss
08
09 .tdata .init_array .data.rel.ro .dynamic .got
由上可知,其具有两个需要映射的segment:
LOAD 0x000000 0x00000000 0x00000000 0xe19c4 0xe19c4 R E 0x10000
LOAD 0x0e1b90 0x000f1b90 0x000f1b90 0x026d8 0x04b64 RW 0x10000
但是通过cat /proc/pid/maps查看到其占有4个VM area:
f747c000-f755e000 r-xp 00000000 b3:13 915 /lib/libc-2.31.so
f755e000-f756d000 ---p 000e2000 b3:13 915 /lib/libc-2.31.so
f756d000-f756f000 r--p 000e1000 b3:13 915 /lib/libc-2.31.so
f756f000-f7571000 rw-p 000e3000 b3:13 915 /lib/libc-2.31.so
f7571000-f7573000 rw-p 00000000 00:00 0
根据属性判断可知,第一个f747c000-f755e000区间的vm area对应于动态库的代码段,即第一个需要load的segment。而第二个vm area,即f755e000-f756d000区间的来源是第一个load segment 和第二个load segment的虚拟地址之间形成的空洞:
第一个load的虚拟地址结尾(4k对齐向上取整):0xe2000 (0xe19c4得来)
第二个load的虚拟地址其实(4k对齐向下取整):0xf1000(0xf1b90的来)
两者之间的空洞为:0xf1000 - 0xe2000 = f756d000 - f755e000
该空洞内没有任何内容,可以认为没有内容。对于第二个load segment,其映射地址应该是:
f756d000 - f757000 (f756d000 + 0x026d8 4k对齐向上取整)
其中,映射地址的结尾按照file_size而不是mem_size计算,mem_size比file_size大的原因是包含bss段,而bss段不用映射,可以在此后发生读写操作的时候产生pagefault再建立映射关系。所以其实从maps中可以看到其后面还跟了一个rw-p的vm area。而在加载动态库时,动态库解释器还对GNU_RELRO segment进行了判断,该segment为只读区域,所以对第二个load segment又划分成了两段,其中一段为GNU_RELRO segment,其映射的vm area应该为:
f756f000-f7571000(mem_size为0x01470 4k对齐向上取整)
第二段即为第二个load segment剩余没有映射的区间,为:
0x026d8 - 0x01470 ,4k对齐向上取整为0x2000
所以才得到最终的四个vm area。解释器代码路径如果不清楚可以通过objdump解释器获取,对共享库进行映射主要集中在函数_dl_map_object_from_fd中。
此外,文件的映射是指将ELF文件load segment中的virtual_addr映射到进程的虚拟地址空间(并且还要page页对齐,向下取整),而load segment中的virtul_addr和offset并不匹配,此时如果已知一个符号在进程中的虚拟地址为p,假设该地址所属动态库在进程中的地址区间(可执行段,即text段所在的区间)为:
f747c000-f755e000 r-xp 00000000 b3:13 915 /lib/lib_random.so
该段的加载地址为:
LOAD 0x000000 0x00001100 0x00000000 0xe19c4 0xe19c4 R E 0x10000
那么该地址在ELF文件中的实际偏移为:
(p - 0xf747c000) + (0x00001100 & ~(PAGE_SIZE - 1))
即,要0xf747c000是在对load segment的virtual_addr进行page对齐,向下取整之后得到的地址值,在计算的时候需要将这部分地址空间加回来。