CSAPP笔记(七) 链接

深入理解计算机系统

//main.c
void swap(); // UND
int data[] = {11,22}; // .data
int main() { // .text
    swap();
    static int a = 1; // .data
    return a;
}
//swap.c
extern int data[];  // UND
int * bufp0 = &data[0]; // .data
int * bufp1; // .bss
void swap() { // .text
    int temp; // stack
    bufp1 = &data[0];
    temp = * bufp0;
    * bufp0 = * bufp1;
    * bufp1 = temp;
}

编译

gcc -v main.c swap.c -o b&& ./b

编译器

main.c -> /tmp/ccVSPuud.s

/usr/lib/gcc/x86_64-linux-gnu/5/cc1
-quiet # Do not display functions compiled or elapsed time
-v # Enable verbose output
-imultiarch x86_64-linux-gnu # Set x86_64-linux-gnu to be the multiarch include subdirectory
main.c
-quiet
-dumpbase main.c # Set the file basename to be used for dumps
-mtune=generic # Schedule code for given CPU
-march=x86-64 # Generate code for given CPU
-auxbase main
-version # Display the compiler's version
-fstack-protector-strong #  Use a smart stack protection method for certain functions
-Wformat # Warn about printf/scanf/strftime/strfmon format string anomalies
-Wformat-security # Warn about possible security problems with format functions
-o /tmp/ccVSPuud.s # Place output into /tmp/ccVSPuud.s

汇编器

ccVSPuud.s -> cc8urR1R.o

as -v --64 -o /tmp/cc8urR1R.o /tmp/ccVSPuud.s

链接器

cc8urR1R.o (main.c)
ccxPHKDw.o (swap.c)
-> b

/usr/lib/gcc/x86_64-linux-gnu/5/collect2
-plugin /usr/lib/gcc/x86_64-linux-gnu/5/liblto_plugin.so # 加载插件程序
-plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper # 发送参数给最后加载的插件程序
-plugin-opt=-fresolution=/tmp/ccAO3sgb.res
-plugin-opt=-pass-through=-lgcc
-plugin-opt=-pass-through=-lgcc_s
-plugin-opt=-pass-through=-lc
-plugin-opt=-pass-through=-lgcc
-plugin-opt=-pass-through=-lgcc_s
--sysroot=/ # 强制覆写缺省的 sysroot 位置
--build-id # Generate build ID note
--eh-frame-hdr # Create .eh_frame_hdr section
-m elf_x86_64 # 设定仿真
--hash-style=gnu #  Set hash style to sysv, gnu or both
--as-needed # 如果使用的话,只有设置 DT_NEEDED 于下述的动态函数库
-dynamic-linker /lib64/ld-linux-x86-64.so.2 # 将/lib64/ld-linux-x86-64.so.2设为要使用的动态链接器
-z relro # Create RELRO program header
-o b
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o
-L/usr/lib/gcc/x86_64-linux-gnu/5 # 目录
-L/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu # 目录
-L/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib # 目录
-L/lib/x86_64-linux-gnu # 目录
-L/lib/../lib # 目录
-L/usr/lib/x86_64-linux-gnu # 目录
-L/usr/lib/../lib # 目录
-L/usr/lib/gcc/x86_64-linux-gnu/5/../../.. # 目录
/tmp/cc8urR1R.o #main.c
/tmp/ccxPHKDw.o #swap.c
-lgcc --as-needed # gcc库 main函数开始时自动会调用__main,而__main在gcc标准库中
-lgcc_s --no-as-needed # gcc_s库
-lc # c库
-lgcc --as-needed
-lgcc_s --no-as-needed
/usr/lib/gcc/x86_64-linux-gnu/5/crtend.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o

Collect2在编译器安装目录中,当作ld。

  • 符号解析:文件间变量名对应
  • 重定位:把链接文件的相对地址变成绝对地址
  • 可重定位目标文件:ccVSPuud.s cc8urR1R.o
  • 可执行目标文件:b

ELF

Acronyms relevant to Executable and Linkable Format (ELF)
ELF

ELF Header

readelf main.o -h

ELF 头:
  Magic   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  类别:                              ELF32
  数据:                              2 补码,小端序 (little endian)
  版本:                              1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              REL (可重定位文件)
  系统架构:                          Intel 80386
  版本:                              0x1
  入口点地址:               0x0
  程序头起点:          0 (bytes into file)
  节头起点:          560 (bytes into file)
  标志:             0x0
  本头的大小:       52 (字节)
  程序头大小:       0 (字节)
  程序头数量:         0
  节头大小:         40 (字节)
  节头数量:         12
  字符串表索引节头: 9

Section Header Table

readelf main.o -S
ELF头0x34字节,故[1]节头偏移0x34
[9]节尾偏移量 0x22f,故table从0x230开始

共有 12 个节头,从偏移量 0x230 开始:

节头:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000034 000024 00  AX  0   0  1
  [ 2] .rel.text         REL             00000000 0001c0 000010 08   I 10   1  4
  [ 3] .data             PROGBITS        00000000 000058 00000c 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 000064 000000 00  WA  0   0  1
  [ 5] .comment          PROGBITS        00000000 000064 000035 01  MS  0   0  1
  [ 6] .note.GNU-stack   PROGBITS        00000000 000099 000000 00      0   0  1
  [ 7] .eh_frame         PROGBITS        00000000 00009c 000044 00   A  0   0  4
  [ 8] .rel.eh_frame     REL             00000000 0001d0 000008 08   I 10   7  4
  [ 9] .shstrtab         STRTAB          00000000 0001d8 000057 00      0   0  1
  [10] .symtab           SYMTAB          00000000 0000e0 0000c0 10     11   9  4
  [11] .strtab           STRTAB          00000000 0001a0 00001e 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

.symtab

readelf main.o -s
main函数在[1].text,data在[3].data偏移0,a在[3]偏移8
main.c不被重定位,swap引用外部

Symbol table '.symtab' contains 12 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    1
     3: 00000000     0 SECTION LOCAL  DEFAULT    3
     4: 00000000     0 SECTION LOCAL  DEFAULT    4
     5: 00000008     4 OBJECT  LOCAL  DEFAULT    3 a.1482
     6: 00000000     0 SECTION LOCAL  DEFAULT    6
     7: 00000000     0 SECTION LOCAL  DEFAULT    7
     8: 00000000     0 SECTION LOCAL  DEFAULT    5
     9: 00000000     8 OBJECT  GLOBAL DEFAULT    3 data
    10: 00000000    36 FUNC    GLOBAL DEFAULT    1 main
    11: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND swap

符号解析

链接器

  • Rule 1: Multiple strong symbols are not allowed.
  • Rule 2: Given a strong symbol and multiple weak symbols, choose the strong symbol.
  • Rule 3: Given multiple weak symbols, choose any of the weak symbols.

静态库

  • 把addvec.o和multvec.o打包成libvector.a,ibvector.a称为静态库
    ar rcs libvector.a addvec.o multvec.o
  • 链接静态库要放在尾部
    gcc -static main.o ./libvector.a
  • 链接器会从左到右扫描,先main,再到libvector,维持E D U集合
    E: 需要用到的模块集
    D: 已经定义的符号集
    U: 未定义的符号集
    一直扫描 libvector至EUD不再变化
  • 结束时U非空就报错,否则链接E
  • 库内可以相互依赖,因为一直扫描,库间依赖要重复库
    gcc -static main.o x.a y.a x.a
  • p依赖x,x依赖y,y依赖x,x依赖a
    gcc p.o libx.a liby.a libx.a
    尾部无需再链p,因为p是个块,而xy是集合。块是全入,集合是按需入。
    EDU是根据新来的 来更新,即使y依赖a,但a已定义的符号集早已全部入D
    而x仅仅把被p依赖的块的符号集入D,并不能确保y依赖的那个块有没有入

重定位

objdump -D main.o

00000000 <main>:
   0:	8d 4c 24 04          	lea    0x4(%esp),%ecx
   4:	83 e4 f0             	and    $0xfffffff0,%esp
   7:	ff 71 fc             	pushl  -0x4(%ecx)
   a:	55                   	push   %ebp
   b:	89 e5                	mov    %esp,%ebp
   d:	51                   	push   %ecx
   e:	83 ec 04             	sub    $0x4,%esp
  11:	e8 fc ff ff ff       	call   12 <main+0x12>
  16:	a1 08 00 00 00       	mov    0x8,%eax
  1b:	83 c4 04             	add    $0x4,%esp
  1e:	59                   	pop    %ecx
  1f:	5d                   	pop    %ebp
  20:	8d 61 fc             	lea    -0x4(%ecx),%esp
  23:	c3                   	ret    

objdump -D b

080483db <main>:
 80483db:	8d 4c 24 04          	lea    0x4(%esp),%ecx
 80483df:	83 e4 f0             	and    $0xfffffff0,%esp
 80483e2:	ff 71 fc             	pushl  -0x4(%ecx)
 80483e5:	55                   	push   %ebp
 80483e6:	89 e5                	mov    %esp,%ebp
 80483e8:	51                   	push   %ecx
 80483e9:	83 ec 04             	sub    $0x4,%esp
 80483ec:	e8 0e 00 00 00       	call   80483ff <swap>
 80483f1:	a1 20 a0 04 08       	mov    0x804a020,%eax
 80483f6:	83 c4 04             	add    $0x4,%esp
 80483f9:	59                   	pop    %ecx
 80483fa:	5d                   	pop    %ebp
 80483fb:	8d 61 fc             	lea    -0x4(%ecx),%esp
 80483fe:	c3                   	ret    

080483ff <swap>:
 80483ff:	55                   	push   %ebp

节和符号

同类节合并,比如main.o和swap.o的[1].text节。 合并后就有新的符号表,也定下[1].text存储地址,根据新符号表的main的偏移量就能确定其存储地址。
同样.data节也会合并再确定存储地址

节中符号

main.o地址0x11处
11: e8 fc ff ff ff call 12 <main+0x12>
这条指令执行后PC会指向0x16,加上0xfffffffc就是0x12,也就是CPU会去执行fc ff ff ff。
fc ff ff ff显然这不对,因为main.o没有链接swap.o,这里fc ff ff ff仅是汇编器临时设的,方便日后链接器操作。
实际上,main中的swap在.rel.text而并.text,objdump为了方便才拼在一起。
swap就是节中符号

重定位条目

readelf main.o -R 2
“.rel.text”节的十六进制输出: 0x00000000 12000000 020b0000 17000000 01030000 …
Section Header Table中可以看出“.rel.text”的size为16
0x00000000是节内偏移,也就是节内地址。
“.rel.text”节实际为0x12000000 020b0000 17000000 01030000
main+0x12中的0x12就是第一个字段

链接器根据swap存储地址就能算出call后面具体要跳多远。
但为什么是-4呢?很容易想到这里地址长度是4字节,32位,如果是64位,应该是-8。
取指call后,pc就变了,指明-4有利于链接器判断PC+4还是+8

swap.c中
int * bufp0 = &data[0];
从swap.o的.symbol表中看出bufp0在.data中,偏移0。
查看下swap.o的.data节: readelf swap.o -R 3
“.data”节的十六进制输出:
0x00000000 00000000 ….
bufp0的值就是00000000
如果int * bufp0 = &data[1];
“.data”节的十六进制输出:
0x00000000 40000000 ….
链接器合并.data节后找到data地址,根据swap.o的.data的偏移给bufp0值

前面swap的重定位是.text中,是相对定位

可执行目标文件

readelf b -l

Elf 文件类型为 EXEC (可执行文件)
入口点 0x80482e0
共有 9 个程序头,开始于偏移量 52

程序头:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
  INTERP         0x000154 0x08048154 0x08048154 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x005e0 0x005e0 R E 0x1000
  LOAD           0x000f08 0x08049f08 0x08049f08 0x0011c 0x00124 RW  0x1000
  DYNAMIC        0x000f14 0x08049f14 0x08049f14 0x000e8 0x000e8 RW  0x4
  NOTE           0x000168 0x08048168 0x08048168 0x00044 0x00044 R   0x4
  GNU_EH_FRAME   0x0004c0 0x080484c0 0x080484c0 0x00034 0x00034 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
  GNU_RELRO      0x000f08 0x08049f08 0x08049f08 0x000f8 0x000f8 R   0x1

 Section to Segment mapping:
  段节...
   00     
   01     .interp
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
   04     .dynamic
   05     .note.ABI-tag .note.gnu.build-id
   06     .eh_frame_hdr
   07     
   08     .init_array .fini_array .jcr .dynamic .got

Linux run-time memory image
LOAD第二段size为0x0011c,实际内存大小0x00124,多出来的是给.bss留的。
实际上.bss为空,其中的变量仅在.symtab中保留着。

动态链接

共享库

gcc -shared -fPIC -o libvector.so addvec.c multvec.c -m32

加载时链接

gcc -o p main.c ./libvector.so -m32
动态链接
加载时(也就是刚执行P时)遇到.interp节会去调用动态链接器,完成以下操作:

  • Relocating the text and data of libc.so into some memory segment.
  • Relocating the text and data of libvector.so into another memory segment.
  • Relocating any references in p to symbols defined by libc.so and libvector.so.

运行时链接

gcc -rdynamic -O2 -o p3 dll.c -ldl -m32
全局变量偏移表(Global Offset Table, GOT),位于.data前的.got.plt,纯数据
过程链接表(Procedure Linkage Table, PLT),位于.text前的.plt,实际为代码
运行(到某个地方)时,调用某个外部函数,这些函数在plt中有记录。
第一次会先调用_dl_runtime_resolve进行重定位,把重定位的地址写到got中。
第二次运行到此时就直接用got中的记录。
got

//dll.c
include <dlfcn.h>
void (* addvec)(...);
handle = dlopen("./libvector.so", RTLD_LAZY);
addvec = dlsym(handle, "addvec");
addvec(x, y, z, 2);
dlclose(handle)
dlerror();

延迟绑定

"""PLT"""  
Disassembly of section .plt:
 # plt[0]
08048690 <dlclose@plt-0x10>:
 8048690:	ff 35 04 a0 04 08    	pushl  0x804a004
 8048696:	ff 25 08 a0 04 08    	jmp    * 0x804a008
 804869c:	00 00                	add    %al,(%eax)
	...
 # plt[1]
080486a0 <dlclose@plt>:
 80486a0:	ff 25 0c a0 04 08    	jmp    * 0x804a00c
 80486a6:	68 00 00 00 00       	push   $0x0
 80486ab:	e9 e0 ff ff ff       	jmp    8048690
 # plt[2]
080486b0 <dlerror@plt>:
 80486b0:	ff 25 10 a0 04 08    	jmp    * 0x804a010
 80486b6:	68 08 00 00 00       	push   $0x8
 80486bb:	e9 d0 ff ff ff       	jmp    8048690
 ...
".got.plt"节的十六进制输出:
  0x0804a000 0c9f0408 00000000 00000000 a6860408 ................
  0x0804a010 b6860408 c6860408 d6860408 e6860408 ................
  0x0804a020 f6860408 06870408 16870408          ............

  0c9f0408 # .dynamic 地址
  00000000 # 链接器标识
  00000000 # 动态链接器入口
  a6860408 # plt[1] dlclose
  b6860408 # plt[2] dlerror
  c6860408 # plt[3]
  d6860408 # plt[4]
  e6860408 # plt[5]
  f6860408 # plt[6]
  06870408 # plt[7]
  16870408 # plt[8]

动态链接器根据push的两个标识确定实际位置,改got并执行函数。