linker and loader 读书笔记
Posted threepigs
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linker and loader 读书笔记相关的知识,希望对你有一定的参考价值。
linker and loader这本书是目前介绍连接器跟加载器最全面的书了。毕业之前在学校的时候就看过一遍,不过由于这本书过于理论、晦涩,当时并没有理解透,如今结合实际来读,来彻底的完成对这本书的学习。
这篇笔记并不是想全面介绍什么是连接器和加载器,只是记下自己读这本书的理解过程,结合一些实际来加深理解。
对广大程序员来说,连接器跟加载器也并不是什么特别陌生的东西,虽然并不是很多人都会非常了解编译连接技术,不过没见过猪跑,难道还没吃过猪肉吗,写程序写多了总会对连接器有点了解。下面从一个简单的“hello world”程序开始,一步一步的了解整个连接过程。
ex01.cpp
#include "iostream"
int times = 9;
int main()
const char* str = "hello world";
for(int i = 0; i < times; i++)
std::cout<<str<<std::endl;
g++ ex01.cpp -c 生成可重定位的ex01.o ELF目标文件。先用objdump -D -x ex01.o来查看目标文件的内容。
区信息:
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 000000af 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000004 00000000 00000000 000000e4 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000001 00000000 00000000 000000e8 2**2
ALLOC
3 .rodata 0000000c 00000000 00000000 000000e8 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .ctors 00000004 00000000 00000000 000000f4 2**2
CONTENTS, ALLOC, LOAD, RELOC, DATAi
5 .comment 00000024 00000000 00000000 000000f8 2**0
CONTENTS, READONLY
6 .note.GNU-stack 00000000 00000000 00000000 0000011c 2**0
CONTENTS, READONLY
7 .eh_frame 00000070 00000000 00000000 0000011c 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
该可重定位的目标文件有8个区。.text代码区,大小0xaf,VMA,LMA表示虚拟内存地址,线性内存地址,由于这是可重定义文件,所以这两个域一般都为0,File off文件偏移量是Ox34,前面还有52个字节的ELF头,用readelf -a ex01.o就能很详细的看见。Algn=4,.text区是4字节对齐的。.data只有4个字节,就一个times。.bss是未初始化数据区,有1个size,main里面并没有用到,是libc的静态初始化函数里用的,以后再介绍。由于是全0,所以并不占文件的空间,所以下面的.rodata只读数据区的文件偏移量跟.bss相同,大小0x0c,就是存放“hello world”的段。
接下来是符号表
SYMBOL TABLE:
00000000 l df *ABS* 00000000 ex01.cpp
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000000 l O .bss 00000001 _ZStL8__ioinit
00000000 l d .rodata 00000000 .rodata
00000053 l F .text 00000040 _Z41__static_initialization_and_destruction_0ii
00000093 l F .text 0000001c _GLOBAL__I_times
00000000 l d .ctors 00000000 .ctors
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .eh_frame 00000000 .eh_frame
00000000 l d .comment 00000000 .comment
00000000 g O .data 00000004 times
00000000 g F .text 00000053 main
00000000 *UND* 00000000 __gxx_personality_v0
00000000 *UND* 00000000 _ZSt4cout
00000000 *UND* 00000000 _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
00000000 *UND* 00000000 _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
00000000 *UND* 00000000 _ZNSolsEPFRSoS_E
00000000 *UND* 00000000 _ZNSt8ios_base4InitC1Ev
00000000 *UND* 00000000 _ZNSt8ios_base4InitD1Ev
00000000 *UND* 00000000 __dso_handle
00000000 *UND* 00000000 __cxa_atexit
第一列是表示符号的值,可以看到name为_GLOBAL__I_times的value等于93,根据Objdump的结果可以看到,这是.text区偏移为93的位置的一个函数类型符号。第二列表示符号的bind方式l=local,g=global,w=weak,第三列是类型,第四列是所属的区,第五列是size,最后就是符号名。
Disassembly of section .text:
00000000 <main>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 e4 f0 and $0xfffffff0,%esp
6: 83 ec 20 sub $0x20,%esp
9: c7 44 24 1c 00 00 00 movl $0x0,0x1c(%esp)
10: 00
d: R_386_32 .rodata
11: eb 29 jmp 3c <main+0x3c>
13: 8b 44 24 1c mov 0x1c(%esp),%eax
17: 89 44 24 04 mov %eax,0x4(%esp)
1b: c7 04 24 00 00 00 00 movl $0x0,(%esp)
1e: R_386_32 _ZSt4cout
22: e8 fc ff ff ff call 23 <main+0x23>
23: R_386_PC32 _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
27: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
2e: 00
2b: R_386_32 _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
2f: 89 04 24 mov %eax,(%esp)
32: e8 fc ff ff ff call 33 <main+0x33>
33: R_386_PC32 _ZNSolsEPFRSoS_E
37: 83 44 24 18 01 addl $0x1,0x18(%esp)
3c: a1 00 00 00 00 mov 0x0,%eax
3d: R_386_32 times
41: 39 44 24 18 cmp %eax,0x18(%esp)
45: 0f 9c c0 setl %al
48: 84 c0 test %al,%al
4a: 75 c7 jne 13 <main+0x13>
4c: b8 00 00 00 00 mov $0x0,%eax
51: c9 leave
52: c3 ret
加红部分就是需要重定位的符号,这是用-c生成的可重载目标文件,这些符号都没有被连接过。这就是代码区的反汇编内容。其中offset 0x09开始的指令包含了一个可重定位符号,就是从offset 0x0d的str,它在只读数据区里,在这里还没有被重定位,所以是全0来表示。这个符号在重定位段.rel.text里有一个对应的重定位项,一般之后的连接过程可以对这些未重定位的符号进行重定位。这个可以用readelf -r列出所有的重定位段。
Offset Info Type Sym.Value Sym. Name
0000000d 00000601 R_386_32 00000000 .rodata
offset就是改符号出现的位置的偏移,就是我们之前.text看到的str的偏移。Info由两部分组成,低8位是重定位类型,01就是 R_386_32,
是指S+A,S是该符号在符号表的value项,A表示Addend,对于REL型的重定位,Addend嵌入在代码的符号位置中,而 RELA型则是重定位项有个Addend字段。R_386_PC32的重定位类型是PC相对重定位,是S+A-P,P就是符号所在的位置偏移,如果是同区跳转的话S就等于P,_ZNSolsEPFRSoS_E就保持不变(x86是REL类型的A就是fc ff ff ff = -4)。我们的全局变量times就是一直.data区的符号,重定位类型R_386_32,为符号表的value值跟Addend(这里等于 0)的和。
这就是一个没有连接过的可重载文件的格式解析,连接过程并没有开始,因此基本上也就是目标文件的格式介绍,那么我们下面要看 g++ ex01.cpp的可执行目标文件了。老规矩readelf -a a.out查看目标文件格式,objdump -D -x a.out反汇编。看ELF header的Type字段,已经变成可执行文件EXEC了,增加了程序段(把各区整合成一个很大的段)。
Program Header:
PHDR off 0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
filesz 0x00000120 memsz 0x00000120 flags r-x
INTERP off 0x00000154 vaddr 0x08048154 paddr 0x08048154 align 2**0
filesz 0x00000013 memsz 0x00000013 flags r--
LOAD off 0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
filesz 0x000008f4 memsz 0x000008f4 flags r-x
LOAD off 0x00000ef0 vaddr 0x08049ef0 paddr 0x08049ef0 align 2**12
filesz 0x00000140 memsz 0x000001e8 flags rw-
DYNAMIC off 0x00000f08 vaddr 0x08049f08 paddr 0x08049f08 align 2**2
filesz 0x000000e8 memsz 0x000000e8 flags rw-
NOTE off 0x00000168 vaddr 0x08048168 paddr 0x08048168 align 2**2
filesz 0x00000044 memsz 0x00000044 flags r--
EH_FRAME off 0x0000085c vaddr 0x0804885c paddr 0x0804885c align 2**2
filesz 0x00000024 memsz 0x00000024 flags r--
STACK off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2
filesz 0x00000000 memsz 0x00000000 flags rw-
RELRO off 0x00000ef0 vaddr 0x08049ef0 paddr 0x08049ef0 align 2**0
filesz 0x00000110 memsz 0x00000110 flags r--
一共有9个program header,每个大小是32字节。位于在仅次于ELF文件头的位置,而section header则在ELF文件的尾部。这9个header就表示需要载入内存的段,第一个是从偏移量0x34(52)开始的程序头
本身,加载到 vaddr 0x08048034的虚拟地址,4字节对齐,一共120字节。第二个是INTERP解释器部分,只读。下面两个LOAD段,前面的那个表示只读的可执行代码段,后面的则是可读写的数据段,包含了.data,.bss等。下面就是所有的section到这9个segment的的映射。
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .hash .gnu.hash .dynsym .dyns
tr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata
.eh_frame_hdr .eh_frame
03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .ctors .dtors .jcr .dynamic .got
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 08048154 000154 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4
[ 3] .note.gnu.build-i NOTE 08048188 000188 000024 00 A 0 0 4
[ 4] .hash HASH 080481ac 0001ac 000048 04 A 6 0 4
[ 5] .gnu.hash GNU_HASH 080481f4 0001f4 000034 04 A 6 0 4
[ 6] .dynsym DYNSYM 08048228 000228 0000d0 10 A 7 1 4
[ 7] .dynstr STRTAB 080482f8 0002f8 000183 00 A 0 0 1
[ 8] .gnu.version VERSYM 0804847c 00047c 00001a 02 A 6 0 2
[ 9] .gnu.version_r VERNEED 08048498 000498 000060 00 A 7 2 4
[10] .rel.dyn REL 080484f8 0004f8 000010 08 A 6 0 4
[11] .rel.plt REL 08048508 000508 000048 08 A 6 13 4
[12] .init PROGBITS 08048550 000550 000030 00 AX 0 0 4
[13] .plt PROGBITS 08048580 000580 0000a0 04 AX 0 0 4
[14] .text PROGBITS 08048620 000620 00020c 00 AX 0 0 16
[15] .fini PROGBITS 0804882c 00082c 00001c 00 AX 0 0 4
[16] .rodata PROGBITS 08048848 000848 000014 00 A 0 0 4
[17] .eh_frame_hdr PROGBITS 0804885c 00085c 000024 00 A 0 0 4
[18] .eh_frame PROGBITS 08048880 000880 000074 00 A 0 0 4
[19] .ctors PROGBITS 08049ef0 000ef0 00000c 00 WA 0 0 4
[20] .dtors PROGBITS 08049efc 000efc 000008 00 WA 0 0 4
[21] .jcr PROGBITS 08049f04 000f04 000004 00 WA 0 0 4
[22] .dynamic DYNAMIC 08049f08 000f08 0000e8 08 WA 7 0 4
[23] .got PROGBITS 08049ff0 000ff0 000004 04 WA 0 0 4
[24] .got.plt PROGBITS 08049ff4 000ff4 000030 04 WA 0 0 4
[25] .data PROGBITS 0804a024 001024 00000c 00 WA 0 0 4
[26] .bss NOBITS 0804a040 001030 000098 00 WA 0 0 32
[27] .comment PROGBITS 00000000 001030 000023 01 MS 0 0 1
[28] .shstrtab STRTAB 00000000 001053 0000fc 00 0 0 1
[29] .symtab SYMTAB 00000000 001628 0004d0 10 30 49 4
[30] .strtab STRTAB 00000000 001af8 000389 00 0 0 1
这就是所有区的列表,从每个区的Addr这列就可以看出哪些sections被映射到哪个 segment了,segment就是区的集合而已,只是把他们归类整在一起便于加载,最后还是回到对section的理解。
.interp 有0x13个字节,内容是2f 6c 69 62 2f 6c 64 2d 6c 69 6e 75 78 2e 73 6f 2e 32 00,就是连接器的路径“/lib/ld-linux.so.2”。.hash用来查找符号信息。.dynsym是动态符号区,包含了文件的所有导入导出符号,从08048228开始,一共是0xd0字节,ES这列的0x10表示每个符号表项占16字节,它跟.symtab的结构是一样的,不过放的是动态符号,这个等详细介绍连接过程的时候再展开。.dynstr是动态字符串表区,大小为0。.rel.dyn是.dynsym的重定向区,.rel.plt 则是.plt的重定向区。
Relocation section '.rel.dyn' at offset 0x4f8 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
08049ff0 00000206 R_386_GLOB_DAT 00000000 __gmon_start__
0804a040 00000b05 R_386_COPY 0804a040 _ZSt4cout
__gmon_start__就是在.dynsym符号表02位置的重定位符号。rel.plt也有一个__gmon_start__符号。
Relocation section '.rel.plt' at offset 0x508 contains 9 entries:
Offset Info Type Sym.Value Sym. Name
0804a000 00000107 R_386_JUMP_SLOT 00000000 __cxa_atexit
0804a004 00000207 R_386_JUMP_SLOT 00000000 __gmon_start__
.rel.plt里面的__gmon_start__是指向一个plt项(0x084a004的位置),.rel.dyn里面的是指向0x08049ff0,从偏移来看是.got项。
.rel.dyn中__gmon_start__的类型是R_386_GLOB_DAT,偏移量是0x08049ff0,定义如下:
This relocation type is used to set a global offset table entry to the address of the specified symbol. The special relocation type allows one to determine the correspondence between symbols and global offset table entries.
也就是说它将会被重定位到一个.got区的一个GOT项。
.rel.plt中的__gmon_start__的类型是R_386_JUMP_SLOT,偏移量为0x084a004(GOT+16),定义如下:
The link editor creates this relocation type for dynamic linking. Its offset member gives the location of a procedure linkage table entry. The dynamic linker modifies the procedure linkage table entry to transfer control to the designated symbol’s address.
在.plt区中
Disassembly of section .plt:
08048580 <__cxa_atexit@plt-0x10>:
8048580: ff 35 f8 9f 04 08 pushl 0x8049ff8
8048586: ff 25 fc 9f 04 08 jmp *0x8049ffc
804858c: 00 00 add %al,(%eax)
...
08048590 <__cxa_atexit@plt>:
8048590: ff 25 00 a0 04 08 jmp *0x804a000
8048596: 68 00 00 00 00 push $0x0
804859b: e9 e0 ff ff ff jmp 8048580 <_init+0x30>
080485a0 <__gmon_start__@plt>:
80485a0: ff 25 04 a0 04 08 jmp *0x804a004
80485a6: 68 08 00 00 00 push $0x8
80485ab: e9 d0 ff ff ff jmp 8048580 <_init+0x30>
080485a0 开始的这项就是__gmon_start__的跳转表,调用该例程就是直接jmp到这里,然后jmp *0x804a004,在0x084a004位置(GOT+16)就是__gmon_start__实际GOT项。plt的目的只是迟加载,把加载的过程放到了第一次使用的时候,这时候Got+16的这项是指回到80485a6的push $0x8,然后跳到8048580 <_init+0x30>,来初始化Got+16项,这实际就是.plt的第一项,08048580 <__cxa_atexit@plt-0x10>,首先pushl 0x8049ff8(GOT+4),里面放了特定的库标识来用于库的符号解析,接着jmp *0x8049ffc跳转到GOT+8的地方,这里放这动态连接器的解析例程的地址,来重定位
0804a004(GOT+16)项,下次使用__gmon_start__的时候就能直接jmp到实际地址。
以上主要是分析了ELF目标格式的文件的各个项在连接过程中所起的作用,这对于理解整个连接过程是很关键的,初次接触linker and loader的时候就很容易会被这些区啊,符号呀什么的弄晕。只要弄清了这些区,表在连接过程所起的作用,下一步理解整个连接的详细细节就容易了。
以上是关于linker and loader 读书笔记的主要内容,如果未能解决你的问题,请参考以下文章
大教堂与集市(The Cathedral and the Bazaar)读书笔记
《DISTRIBUTED SYSTEMS Concepts and Design》读书笔记 一
读书笔记之《head first Servlet and Jsp》 第五章 属性和监听者
《Distributed Programming With Ruby》读书笔记一Drb:Hellowold and Pass by Reference
In-memory Computing with SAP HANA读书笔记 - 第七章:Business continuity and resiliency for SAP HANA