链接与装载

Table of Contents

1 简介

结合ELF文件的结构,以实例的形式介绍编译、链接及装载的技术细节。

ELF类型的文件包括:编译产生的目标文件、动态链接库、可执行文件等。静态链接库是对目标文件的一个打包。

2 编译

2.1 目标文件

#include <stdio.h>

int global_init_var = 82;
int global_uninit_var;

int sbar(){
  static int static_var = 54;
  static int static_var2;
  int sum;

  printf("&static_var: %p, &static_var2(%d): %p\n",
   (void*)&static_var, static_var2, (void*)&static_var2);
  sum = static_var + static_var2;
  return sum;
}

void sfoo(){
  int  s = sbar()-54;
  printf("value returned by sbar(): %d\n", s);
}

编译成目标文件 $gcc -m32 -c s1.c -o s1.o

2.1.1 文件头

$ readelf -h s1.o
ELF 头:
  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:                              REL (可重定位文件)
  Machine:                           Intel 80386
  Version:                           0x1
  入口点地址:               0x0
  程序头起点:          0 (bytes into file)
  Start of section headers:          464 (bytes into file)
  标志:             0x0
  本头的大小:       52 (字节)
  程序头大小:       0 (字节)
  Number of program headers:         0
  节头大小:         40 (字节)
  节头数量:         13
  字符串表索引节头: 10

本文件头(Size of this header)的大小,这与 .text 在文件中的偏移量一致。

ELF文件的类型(Type)

类型 含义
REL 可重定位文件
EXEC 可执行文件
DYN 共享文件
CORE 核心转储文件

入口点地址(Entry Point Address)是程序入口的虚拟地址,可重定位文件一般没有该地址,故值为0。

字符串表索引节点(Section headers string table index),指出与段名表相应的段表描述符的索引。

段表在文件中的起始地址(Start of section headers)。

节头大小(Size of section headers),段表描述符的大小。

节头数量(Number of section headers),段表描述符的数量。

2.1.2 段表

$ readelf -S s1.o
共有 13 个节头,从偏移量 0x1d0 开始:
节头:
  [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 000066 00  AX  0   0  1
  [ 2] .rel.text         REL             00000000 000534 000050 08     11   1  4
  [ 3] .data             PROGBITS        00000000 00009c 000008 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 0000a4 000004 00  WA  0   0  4
  [ 5] .rodata           PROGBITS        00000000 0000a4 000045 00   A  0   0  4
  [ 6] .comment          PROGBITS        00000000 0000e9 00002c 01  MS  0   0  1
  [ 7] .note.GNU-stack   PROGBITS        00000000 000115 000000 00      0   0  1
  [ 8] .eh_frame         PROGBITS        00000000 000118 000058 00   A  0   0  4
  [ 9] .rel.eh_frame     REL             00000000 000584 000010 08     11   8  4
  [10] .shstrtab         STRTAB          00000000 000170 00005f 00      0   0  1
  [11] .symtab           SYMTAB          00000000 0003d8 000100 10     12  11  4
  [12] .strtab           STRTAB          00000000 0004d8 00005a 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)

段描述符包含的信息有,段名、段类型、段虚拟地址、段在文件中的偏移、段大小、段中所含项的长 度(ES)、段的标志、段的链接信息(Link, Info)及对齐要求。

常见段

段名 含义
.text 代码段
.rel.text 代码段重定位表
.data 数据段
.rel.data 数据段重定位表
.bss 未初始化的数据段
.rodata1 同.rodata一样,存放只读数据,如字符串常量、全局const变量
.symtab 符号表
.hash 符号哈希表
.comment 存放编译器版本信息
.shstrtab 段名表
.strtab 字符串表,存放ELF文件中用到的字符串

某些段包含了一些固定大小的项,比如符号表,对于这样的段,ES用于给出这些项的大小。

段类型

段类型 含义
PROGBITS 程序段,包括代码段,数据段
RELA 重定位表,有补齐字段
REL 重定位表,没补齐字段
NOBITS 该段在文件中没内容
STRTAB 字符串表
SYMTAB 符号表
HASH 符号表的哈希表

由于没有进行链接,所以段的虚拟地址都为0。

数据段 .data 中存放的是已初始化了的全局变量 global_init_var 和已初始化的局部静态变量 static_var ,正好8个字节。

只读数据段 .rodata 中存放的是字符串常量, "&static_var: %p, &static_var2(%d): %p\n" 和“value returned by sbar(): %d\n”加起来正好69字节。

未初始化数据段 .bss 存放的是未初始化的全局变量和未初始化的局部静态变量,该段在文件中不 占空间,且在执行时其所占的空间大小是4字节,而非8字节,原因是这里只计算了未初始化的局部静 态变量 static_var2 的大小,而对于未初始化的全局变量 global_uninit_var (典型的弱符号),由于可能在其他编译单元定义的大小 不一样,所以此时无法决断,需要等到链接时再在 .bss 中考虑它,在这里,该变量只是一个未定义 的“COMMON 符号”。

段的标志 A(Alloc) 表示需要在进程空间中分配内存。

段的链接信息

段类型 Link Info
HASH 该段所使用的符号表在段表中的下标 0
REL 该段所使用的符号表在段表中的下标 该重定位表所作用的段在段表中的下标
RELA 同REL 同REL
SYMTAB 操作系统相关 操作系统相关

强符号,就是函数和初始化了的全局变量。弱符号,就是未初始化的全局变量。强符号、弱符号都是针对符号的定义而言的,不针对符号的引用。可通过 __attribute__((weak)) 来讲一个强符号变为弱符号。对于强符号、弱符号,链接器的处理规则

  • 不允许强符号的重复定义
  • 在一个目标文件为强符号,在其他目标文件为弱符号,则选择强符号
  • 一个符号在所有目标文件中都是弱符号,则选择占用空间最大的那个

2.1.3 符号表

符号是变量和函数的统称。每一个目标文件都有一个符号表。符号表中的符号记录着本目标文件中定义 的(导出符号)和使用的全局符号(导入符号)。从ELF文件头,可知每个符号在符号表在16个字节。

$ readelf -s s1.o
Symbol table '.symtab' contains 16 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS s1.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    1
     3: 00000000     0 SECTION LOCAL  DEFAULT    3
     4: 00000000     0 SECTION LOCAL  DEFAULT    4
     5: 00000000     0 SECTION LOCAL  DEFAULT    5
     6: 00000000     4 OBJECT  LOCAL  DEFAULT    4 static_var2.1828
     7: 00000004     4 OBJECT  LOCAL  DEFAULT    3 static_var.1827
     8: 00000000     0 SECTION LOCAL  DEFAULT    7
     9: 00000000     0 SECTION LOCAL  DEFAULT    8
    10: 00000000     0 SECTION LOCAL  DEFAULT    6
    11: 00000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var
    12: 00000004     4 OBJECT  GLOBAL DEFAULT  COM global_uninit_var
    13: 00000000    64 FUNC    GLOBAL DEFAULT    1 sbar
    14: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    15: 00000040    38 FUNC    GLOBAL DEFAULT    1 sfoo

符号信息包括:符号值Value、符号大小Size、符号类型Type、符号绑定信息Bind、符号所在段Ndx、 符号名Name。

符号类型

符号类型 含义
NOTYPE 未知类型
OBJECT 数据对象
FUNC 函数及其他可执行代码
SECTION
FILE 文件名

符号绑定信息

绑定信息 含义
LOCAL 局部符号
GLOBAL 全局符号
WEAK 弱引用

对于定义在本目标文件中的符号,符号所在段是这个符号所在段在段表中的下标; 对于未定义在本目标文件中的符号,或一些特殊符号,见下表。

符号所在段 含义
ABS 该符号包含一个绝对的值, 如文件名符号
COM 该符号是一个“COMMON” 类型的符号,如未初始化的全局符号定义
UND 未在本目标文件中定义

符号值

  • 该目标文件不是可执行文件
    • 符号的定义在该目标文件
      • 该符号不是“COMMON”类型的

        该符号在段(Ndx)中的偏移

      • 该符号是“COMMON”类型的

        该符号的对齐属性

  • 该目标文件是可执行文件或共享对象(动态链接库)

    该符号的虚拟地址

2.1.4 重定位表

该重定位表用于链接时重定位。

$ readelf -r s1.o
重定位节 '.rel.text' 位于偏移量 0x534 含有 10 个条目:
 Offset     Info    Type            Sym.Value  Sym. Name
00000007  00000401 R_386_32          00000000   .bss
0000000f  00000401 R_386_32          00000000   .bss
0000001b  00000301 R_386_32          00000000   .data
00000022  00000501 R_386_32          00000000   .rodata
00000027  00000e02 R_386_PC32        00000000   printf
0000002d  00000301 R_386_32          00000000   .data
00000032  00000401 R_386_32          00000000   .bss
00000047  00000d02 R_386_PC32        00000000   sbar
0000005b  00000501 R_386_32          00000000   .rodata
00000060  00000e02 R_386_PC32        00000000   printf

重定位节 '.rel.eh_frame' 位于偏移量 0x584 含有 2 个条目:
 Offset     Info    Type            Sym.Value  Sym. Name
00000020  00000202 R_386_PC32        00000000   .text
00000040  00000202 R_386_PC32        00000000   .text

该表中的每个条目都包含着:需要修正的地址在ELF文件中的偏移、修正的方式(Type)、需要修正的符号。

比如,在链接的过程中,.bss的虚拟地址确定后,就需要修正ELF文件中偏移为7,0xd,0x35 这三处的指令。

2.2 静态链接库

Linux下,使用 ar 工具来查看静态库中包含了那些目标文件;使用 objdump 或 readelf 可查看静态库的符号表。

$ar -t /usr/lib32/libc.a
init-first.o
libc-start.o
sysdep.o
version.o
...
$ readelf -s /usr/lib32/libc.a | grep " printf"
    23: 00000000  1879 FUNC    GLOBAL DEFAULT    1 printf_size
    24: 00000760    42 FUNC    GLOBAL DEFAULT    1 printf_size_info
    10: 00000000    36 FUNC    GLOBAL DEFAULT    1 printf

通常,静态库中的一个目标文件只包含一个函数,如 /usr/lib32/printf.o 中只有 printf 函数。原因是,链接器在链接静态库时, 是以目标文件为单位的,若很多函数位于同一个目标文件,可能使得没用到的函数也一起被连接到输出的结果中。

2.3 动态链接库

假如程序P1和程序P2都用到了某个模块,则在链接输出的可执行文件P1和P2中将各有该模块的 一个副本,同时运行这两个程序时,该模块在内存中也有两个副本,这就造成了磁盘和内存的浪费。而且, 当该模块需要更新时,两个程序都需要重新编译、链接、发布。动态链接解决了这两个问题。

动态链接库的基本思想是,不把程序所依赖的库编译进可执行文件,在程序加载时,也只将可执行文件和动态连接器 加载到进程空间,然后,在程序运行前,动态连接器负责加载所依赖的库,并执行装载时重定位,即把库的指令和数据 中对绝对地址的引用进行重定位,这可以解决动态链接库中可修改的数据部分的寻址问题,因为各个进程都有可修改数 据部分的备份,但这却做不到同一份指令在不同的进程间共享,为此,引出了地址无关代码(PIC),它指的是把指令中 需要重定位的部分分离出来,放在GOT(Global Offset Table)(包括.got及.got.plt),并把GOT放在数据段。 显然,动态链接会在程序执行时引入延时,为了缓解该问题,又引入了借助于PLT实现的延迟加载,通过该方法, 只在对函数符号(不包括数据符号)的引用实际发生时,才去加载该函数符号。

// d2.c
#include <stdio.h>

int d2_global_uninit;

void ext(){
  printf("&d2_global_uninit: %p\n", (void *)&d2_global_uninit);
}

// d1.c       o
#include <stdio.h>

static int d1_global_static_uninit;
extern int d2_global_uninit;
extern void ext();

void bar(){
  d1_global_static_uninit = 1;
  d2_global_uninit = 2;
  printf("&d1_global_static_uninit: %p, &d2_global_uninit: %p\n",
    (void *)&d1_global_static_uninit, (void *)&d2_global_uninit);
}

void foo(){
  ext();
  bar();
}

编译,并查看共享库的依赖关系

$ gcc -m32 -fPIC -shared d2.c -o d2.so
$ gcc -m32 -fPIC -shared d1.c d2.so -o d1.so

$ file d2.so
d2.so: ELF 32-bit LSB  shared object, Intel 80386, version 1 (SYSV), dynamically linked, BuildID[sha1]=0c4f9b94d5c3e746f332954beb0bb09198e6c8ff, not stripped
$ file d1.so
d1.so: ELF 32-bit LSB  shared object, Intel 80386, version 1 (SYSV), dynamically linked, BuildID[sha1]=9eccbd9b2c09c11943e355f6cf63c7cc77fa5b6d, not stripped

$ ldd d2.so
        linux-gate.so.1 =>  (0xf77c4000)
        libc.so.6 => /lib32/libc.so.6 (0xf75fc000)
        /lib/ld-linux.so.2 (0xf77c5000)
$ ldd d1.so
        linux-gate.so.1 =>  (0xf7709000)
        d2.so => not found
        libc.so.6 => /lib32/libc.so.6 (0xf7541000)
        /lib/ld-linux.so.2 (0xf770a000)

参数 fPIC 指使用地址无关代码。参数 shared 指使用装载时重定位产生共享对象。

2.3.1 动态链接库的文件头

$ readelf -h d1.so
ELF 头:
  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:                              DYN (共享目标文件)
  Machine:                           Intel 80386
  Version:                           0x1
  入口点地址:               0x4a0
  程序头起点:          52 (bytes into file)
  Start of section headers:          4416 (bytes into file)
  标志:             0x0
  本头的大小:       52 (字节)
  程序头大小:       32 (字节)
  Number of program headers:         7
  节头大小:         40 (字节)
  节头数量:         28
  字符串表索引节头: 25

共享模块的文件头还包含了:入口点地址、程序头起点(start of program headers)、 程序头大小(size of program header)、程序头数量(number of program headers)。

可执行文件的文件头同共享模块的文件头一样包含了这些信息。

2.3.2 动态链接库的程序头

该段表用于链接,称为ELF文件的链接视图;程序头用于装载,称为ELF文件的装载视图。 从下面的示例,也可以看到它们之间的映射关系。

$ readelf -l d1.so
Elf 文件类型为 DYN (共享目标文件)
入口点 0x4a0
共有 7 个程序头,开始于偏移量 52
程序头:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0x00000000 0x00000000 0x00738 0x00738 R E 0x1000
  LOAD           0x000ef4 0x00001ef4 0x00001ef4 0x00130 0x00138 RW  0x1000
  DYNAMIC        0x000f00 0x00001f00 0x00001f00 0x000e8 0x000e8 RW  0x4
  NOTE           0x000114 0x00000114 0x00000114 0x00024 0x00024 R   0x4
  GNU_EH_FRAME   0x00068c 0x0000068c 0x0000068c 0x00024 0x00024 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
  GNU_RELRO      0x000ef4 0x00001ef4 0x00001ef4 0x0010c 0x0010c R   0x1

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

每个程序头包含的信息有:类型、在ELF文件中的偏移、虚拟地址、物理地址、在ELF文件中所占大小、在内存中所占大小、标志、对齐属性。

从装载的角度,只需关心 LOAD 类型的。编译时不知共享对象的最终装载位置,所以起始地址为0。它们都是页对齐的。

2.3.3 动态链接信息表

尽管在编译命令中输入了文件(d2.so),但符号表中显示的 ext 及 d2_global_uninit 都是 UND, 且值是0,这与静态编译时不同。其实,编译器是记录了这些符号所在的文件的,可以查看动态链接信息表。

$ readelf -d d1.so
Dynamic section at offset 0xf00 contains 25 entries:
  标记        类型                         名称/值
 0x00000001 (NEEDED)                     共享库:[d2.so]
 0x00000001 (NEEDED)                     共享库:[libc.so.6]
 0x0000000c (INIT)                       0x410
 0x0000000d (FINI)                       0x640
 0x00000019 (INIT_ARRAY)                 0x1ef4
 0x0000001b (INIT_ARRAYSZ)               4 (bytes)
 0x0000001a (FINI_ARRAY)                 0x1ef8
 0x0000001c (FINI_ARRAYSZ)               4 (bytes)
 0x6ffffef5 (GNU_HASH)                   0x138
 0x00000005 (STRTAB)                     0x278
 0x00000006 (SYMTAB)                     0x178
 0x0000000a (STRSZ)                      215 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000003 (PLTGOT)                     0x2000
 0x00000002 (PLTRELSZ)                   40 (bytes)
 0x00000014 (PLTREL)                     REL
 0x00000017 (JMPREL)                     0x3e8
 0x00000011 (REL)                        0x3a0
 0x00000012 (RELSZ)                      72 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffe (VERNEED)                    0x370
 0x6fffffff (VERNEEDNUM)                 1
 0x6ffffff0 (VERSYM)                     0x350
 0x6ffffffa (RELCOUNT)                   3
 0x00000000 (NULL)                       0x0

各种类型的含义

类型 含义
NEEDED 所依赖的共享对象
RPATH 共享对象的搜索路径
INIT 初始化代码地址
FINI 结束代码地址
SYMTAB 动态链接符号表位置
STRTAB 动态链接字符串表位置
STRSE 动态链接字符串表大小
REL .rel.dyn 地址
RELSZ .rel.dyn 大小
RELENT .rel.dyn 中每项的大小
JMPREL .rel.plt 地址
PLTRELSZ .rel.plt 大小
PLTGOT .got.plt 地址

2.3.4 动态链接库的段表

$ readelf -S d1.so
共有 28 个节头,从偏移量 0x1140 开始:
节头:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .note.gnu.build-i NOTE            00000114 000114 000024 00   A  0   0  4
  [ 2] .gnu.hash         GNU_HASH        00000138 000138 000040 04   A  3   0  4
  [ 3] .dynsym           DYNSYM          00000178 000178 000100 10   A  4   1  4
  [ 4] .dynstr           STRTAB          00000278 000278 0000d7 00   A  0   0  1
  [ 5] .gnu.version      VERSYM          00000350 000350 000020 02   A  3   0  2
  [ 6] .gnu.version_r    VERNEED         00000370 000370 000030 00   A  4   1  4
  [ 7] .rel.dyn          REL             000003a0 0003a0 000048 08   A  3   0  4
  [ 8] .rel.plt          REL             000003e8 0003e8 000028 08   A  3  10  4
  [ 9] .init             PROGBITS        00000410 000410 000023 00  AX  0   0  4
  [10] .plt              PROGBITS        00000440 000440 000060 04  AX  0   0 16
  [11] .text             PROGBITS        000004a0 0004a0 00019d 00  AX  0   0 16
  [12] .fini             PROGBITS        00000640 000640 000014 00  AX  0   0  4
  [13] .rodata           PROGBITS        00000654 000654 000035 00   A  0   0  4
  [14] .eh_frame_hdr     PROGBITS        0000068c 00068c 000024 00   A  0   0  4
  [15] .eh_frame         PROGBITS        000006b0 0006b0 000088 00   A  0   0  4
  [16] .init_array       INIT_ARRAY      00001ef4 000ef4 000004 00  WA  0   0  4
  [17] .fini_array       FINI_ARRAY      00001ef8 000ef8 000004 00  WA  0   0  4
  [18] .jcr              PROGBITS        00001efc 000efc 000004 00  WA  0   0  4
  [19] .dynamic          DYNAMIC         00001f00 000f00 0000e8 08  WA  4   0  4
  [20] .got              PROGBITS        00001fe8 000fe8 000018 04  WA  0   0  4
  [21] .got.plt          PROGBITS        00002000 001000 000020 04  WA  0   0  4
  [22] .data             PROGBITS        00002020 001020 000004 00  WA  0   0  4
  [23] .bss              NOBITS          00002024 001024 000008 00  WA  0   0  4
  [24] .comment          PROGBITS        00000000 001024 00002b 01  MS  0   0  1
  [25] .shstrtab         STRTAB          00000000 00104f 0000f0 00      0   0  1
  [26] .symtab           SYMTAB          00000000 0015a0 0003c0 10     27  45  4
  [27] .strtab           STRTAB          00000000 001960 000205 00      0   0  1

段名 含义
.init 程序初始化
.fini 程序终结时执行的代码段
.dynsym 动态符号表
.dynstr 动态字符串表
.rel.dyn 动态链接文件的重定位表。修正数据引用,所修正位置位于 .got 及数据段
.rel.plt 动态链接文件的重定位表。修正函数引用,所修正位置位于 .got.plt
.plt 用于延迟绑定的表
.dynamic 动态链接信息表
.got 全局偏移表的一部分,用于数据的重定位
.got.plt 全局偏移表的另一部分,用于函数的重定位

段类型

段类型 含义
DNYSYM 动态链接的符号表
DYNAMIC 动态链接信息

段的链接信息

段类型 Link Info
DYNAMIC 该段所使用的字符串表在段表的下标 0
DNYSYM 操作系统相关 操作系统相关

.data 占用了4字节,.bss 占用了8字节,它们都存放的是什么?

$ objdump -S -j .data d1.so
d1.so:     文件格式 elf32-i386
Disassembly of section .data:
00002020 <__dso_handle>:
    2020:       20 20 00 00                                           ..

$ objdump -S -j .bss d1.so
d1.so:     文件格式 elf32-i386
Disassembly of section .bss:
00002024 <__bss_start>:
    2024:       00 00                   add    %al,(%eax)
        ...
00002028 <d1_global_static_uninit>:
    2028:       00 00 00 00                                         ....

可见,.data 存放了变量 __dso_handle; .bss 存放了 __bss_start 和变量 d1_global_static_uninit

2.3.5 动态链接库的符号表

$ readelf -s d1.so
Symbol table '.dynsym' contains 16 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     2: 00000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.0 (2)
     3: 00000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.1.3 (3)
     4: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     5: 00000000     0 FUNC    GLOBAL DEFAULT  UND ext
     6: 00000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
     7: 00000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     8: 00000000     0 OBJECT  GLOBAL DEFAULT  UND d2_global_uninit
     9: 00002024     0 NOTYPE  GLOBAL DEFAULT   22 _edata
    10: 000005cb    82 FUNC    GLOBAL DEFAULT   11 bar
    11: 0000061d    34 FUNC    GLOBAL DEFAULT   11 foo
    12: 0000202c     0 NOTYPE  GLOBAL DEFAULT   23 _end
    13: 00002024     0 NOTYPE  GLOBAL DEFAULT   23 __bss_start
    14: 00000410     0 FUNC    GLOBAL DEFAULT    9 _init
    15: 00000640     0 FUNC    GLOBAL DEFAULT   12 _fini

Symbol table '.symtab' contains 60 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000114     0 SECTION LOCAL  DEFAULT    1
     2: 00000138     0 SECTION LOCAL  DEFAULT    2
     3: 00000178     0 SECTION LOCAL  DEFAULT    3
     4: 00000278     0 SECTION LOCAL  DEFAULT    4
     5: 00000350     0 SECTION LOCAL  DEFAULT    5
     6: 00000370     0 SECTION LOCAL  DEFAULT    6
     7: 000003a0     0 SECTION LOCAL  DEFAULT    7
     8: 000003e8     0 SECTION LOCAL  DEFAULT    8
     9: 00000410     0 SECTION LOCAL  DEFAULT    9
    10: 00000440     0 SECTION LOCAL  DEFAULT   10
    11: 000004a0     0 SECTION LOCAL  DEFAULT   11
    12: 00000640     0 SECTION LOCAL  DEFAULT   12
    13: 00000654     0 SECTION LOCAL  DEFAULT   13
    14: 00000690     0 SECTION LOCAL  DEFAULT   14
    15: 000006b4     0 SECTION LOCAL  DEFAULT   15
    16: 00001ef4     0 SECTION LOCAL  DEFAULT   16
    17: 00001ef8     0 SECTION LOCAL  DEFAULT   17
    18: 00001efc     0 SECTION LOCAL  DEFAULT   18
    19: 00001f00     0 SECTION LOCAL  DEFAULT   19
    20: 00001fe8     0 SECTION LOCAL  DEFAULT   20
    21: 00002000     0 SECTION LOCAL  DEFAULT   21
    22: 00002020     0 SECTION LOCAL  DEFAULT   22
    23: 00002024     0 SECTION LOCAL  DEFAULT   23
    24: 00000000     0 SECTION LOCAL  DEFAULT   24
    25: 00000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    26: 00001efc     0 OBJECT  LOCAL  DEFAULT   18 __JCR_LIST__
    27: 000004b0     0 FUNC    LOCAL  DEFAULT   11 deregister_tm_clones
    28: 000004f0     0 FUNC    LOCAL  DEFAULT   11 register_tm_clones
    29: 00000540     0 FUNC    LOCAL  DEFAULT   11 __do_global_dtors_aux
    30: 00002024     1 OBJECT  LOCAL  DEFAULT   23 completed.6591
    31: 00001ef8     0 OBJECT  LOCAL  DEFAULT   17 __do_global_dtors_aux_fin
    32: 00000590     0 FUNC    LOCAL  DEFAULT   11 frame_dummy
    33: 00001ef4     0 OBJECT  LOCAL  DEFAULT   16 __frame_dummy_init_array_
    34: 00000000     0 FILE    LOCAL  DEFAULT  ABS d1.c
    35: 00002028     4 OBJECT  LOCAL  DEFAULT   23 d1_global_static_uninit
    36: 00000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    37: 00000738     0 OBJECT  LOCAL  DEFAULT   15 __FRAME_END__
    38: 00001efc     0 OBJECT  LOCAL  DEFAULT   18 __JCR_END__
    39: 00000000     0 FILE    LOCAL  DEFAULT  ABS
    40: 000004a0     4 FUNC    LOCAL  DEFAULT   11 __x86.get_pc_thunk.bx
    41: 00002020     0 OBJECT  LOCAL  DEFAULT   22 __dso_handle
    42: 00001f00     0 OBJECT  LOCAL  DEFAULT   19 _DYNAMIC
    43: 00002024     0 OBJECT  LOCAL  DEFAULT   22 __TMC_END__
    44: 00002000     0 OBJECT  LOCAL  DEFAULT   21 _GLOBAL_OFFSET_TABLE_
    45: 00000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
    46: 00000000     0 FUNC    GLOBAL DEFAULT  UND printf@@GLIBC_2.0
    47: 00002024     0 NOTYPE  GLOBAL DEFAULT   22 _edata
    48: 000005cb    82 FUNC    GLOBAL DEFAULT   11 bar
    49: 00000640     0 FUNC    GLOBAL DEFAULT   12 _fini
    50: 00000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@@GLIBC_2.1
    51: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    52: 0000061d    34 FUNC    GLOBAL DEFAULT   11 foo
    53: 00000000     0 FUNC    GLOBAL DEFAULT  UND ext
    54: 0000202c     0 NOTYPE  GLOBAL DEFAULT   23 _end
    55: 00002024     0 NOTYPE  GLOBAL DEFAULT   23 __bss_start
    56: 00000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
    57: 00000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
    58: 00000000     0 OBJECT  GLOBAL DEFAULT  UND d2_global_uninit
    59: 00000410     0 FUNC    GLOBAL DEFAULT    9 _init

输出包含了两个符号表:.dynsym 和 .symtab ,这两个表中的每一项都是用同一个 ELF文件的数据结构描述的,输出的每个属性的含义参见 2.1.3 一节。

.dynsym是与动态链接相关的符号表,其中的符号有

  • 导入的符号:d2.so中的符号

    如 ext, d2_global_uninit

  • 导出的符号: d1.so中定义的的符号

    如 bar, foo, 但不包含文件的局部变量 d1_global_static_uninit,因其作用域仅限于 d1.c

  • 导入的符号: 引用glibc的符号

    如 printf@GLIBC_2.0

  • 导出的符号: 链接器生成的符号

    如 _end, _edata, _bss_start,这些符号可在程序中声明并使用。

.symtab 不仅涵盖 .dynsym 中的全部符号,还包括了24个 section 及 变量 d1_global_static_uninit (作用域仅限于 d1.c)

2.3.6 动态链接重定位相关信息

这些信息用于装载时重定位。

$ readelf -r d1.so
重定位节 '.rel.dyn' 位于偏移量 0x3a0 含有 9 个条目:
 Offset     Info    Type            Sym.Value  Sym. Name
00001ef4  00000008 R_386_RELATIVE
00001ef8  00000008 R_386_RELATIVE
00002020  00000008 R_386_RELATIVE
00001fe8  00000106 R_386_GLOB_DAT    00000000   _ITM_deregisterTMClone
00001fec  00000306 R_386_GLOB_DAT    00000000   __cxa_finalize
00001ff0  00000406 R_386_GLOB_DAT    00000000   __gmon_start__
00001ff4  00000606 R_386_GLOB_DAT    00000000   _Jv_RegisterClasses
00001ff8  00000706 R_386_GLOB_DAT    00000000   _ITM_registerTMCloneTa
00001ffc  00000806 R_386_GLOB_DAT    00000000   d2_global_uninit

重定位节 '.rel.plt' 位于偏移量 0x3e8 含有 5 个条目:
 Offset     Info    Type            Sym.Value  Sym. Name
0000200c  00000207 R_386_JUMP_SLOT   00000000   printf
00002010  00000a07 R_386_JUMP_SLOT   000005cb   bar
00002014  00000307 R_386_JUMP_SLOT   00000000   __cxa_finalize
00002018  00000407 R_386_JUMP_SLOT   00000000   __gmon_start__
0000201c  00000507 R_386_JUMP_SLOT   00000000   ext

$ objdump -S -j .plt d1.so
Disassembly of section .plt:

00000440 <printf@plt-0x10>:
 440:   ff b3 04 00 00 00       pushl  0x4(%ebx)
 446:   ff a3 08 00 00 00       jmp    *0x8(%ebx)
 44c:   00 00                   add    %al,(%eax)
        ...

00000450 <printf@plt>:
 450:   ff a3 0c 00 00 00       jmp    *0xc(%ebx)
 456:   68 00 00 00 00          push   $0x0
 45b:   e9 e0 ff ff ff          jmp    440 <_init+0x30>

00000460 <bar@plt>:
 460:   ff a3 10 00 00 00       jmp    *0x10(%ebx)
 466:   68 08 00 00 00          push   $0x8
 46b:   e9 d0 ff ff ff          jmp    440 <_init+0x30>

00000470 <__cxa_finalize@plt>:
 470:   ff a3 14 00 00 00       jmp    *0x14(%ebx)
 476:   68 10 00 00 00          push   $0x10
 47b:   e9 c0 ff ff ff          jmp    440 <_init+0x30>

00000480 <__gmon_start__@plt>:
 480:   ff a3 18 00 00 00       jmp    *0x18(%ebx)
 486:   68 18 00 00 00          push   $0x18
 48b:   e9 b0 ff ff ff          jmp    440 <_init+0x30>

00000490 <ext@plt>:
 490:   ff a3 1c 00 00 00       jmp    *0x1c(%ebx)
 496:   68 20 00 00 00          push   $0x20
 49b:   e9 a0 ff ff ff          jmp    440 <_init+0x30>

$ objdump -s d1.so
...
Contents of section .got:
 1fe8 00000000 00000000 00000000 00000000  ................
 1ff8 00000000 00000000                    ........
Contents of section .got.plt:
 2000 001f0000 00000000 00000000 56040000  ............V...
 2010 66040000 76040000 86040000 96040000  f...v...........
...

2.3.2 一节可知,.rel.dyn, .rel.plt 及 .plt 都位于代码段,而 .got 及 .got.plt 都我位于数据段。

可见,需要重定位的数据有: 定义在d2.c中的d2_global_uninit 。 需要重定位的函数有:GLIBC的库函数;定义在d2.c中的ext;定义在本文件的bar;但没有定义在本文件的foo, 原因是,foo 没有被引用。

需要注意的是,尽管 bar 与 foo 定义在同一文件,连接后,它们之间的相对地址是确定的,但还是需要重定位bar, 原因是,在装载该共享对象时,如果bar已经存在于全局符号表,也即相同的符号已经存在于 全局符号表,则该共享对象中的bar就会被忽略,而且,调用了bar的指令(如foo)就需要修正,如果直接修正该指令 就会使得foo的代码不再是地址无关的了,进而,不能在多个进程间共享,所以,编译器将foo对bar的调用,当成是 对其他模块中符号的调用,这样,当同名函数覆盖的问题发生时,只需要修正 .got.plt 中的相应位置,不再影响 共享对象的代码段。

上述各个表的关系图

410 +-----------+            +------------+ 2020        +------------+ 4a0
    |         --+----------->| 496        |<------------+--          | ext@plt <--- ext( )
408 +-----------+            +------------+ 201c        +------------+ 490
    |         --+----------->| 486        |<------------+--          | __gmon_start@plt
400 +-----------+            +------------+ 2018        +------------+ 480
    |         --+----------->| 476        |<------------+--          | __cxa_finalize@plt
3f8 +-----------+            +------------+ 2014        +------------+ 470
    |         --+----------->| 466        |<------------+--          | bar@plt <--- bar( )
3f0 +-----------+            +------------+ 2010        +------------+ 460
    |         --+----------->| 456        |<------------+--          | printf@plt <--- printf( )
3e8 +-----------+            +------------+ 200c        +------------+ 450
      .rel.plt               | 0          |                  .plt
			     +------------+ 2008
			     | 0          |
			     +------------+ 2004
			     |1f00 .dynsym|
3e8 +-----------+            +------------+ 2000 (.got.plt)
    |         --+----------->|            |
3e0 +-----------+            +------------+ 1ffc
    |         --+----------->|            |
3d8 +-----------+            +------------+ 1ff8
    |         --+----------->|            |
3d0 +-----------+            +------------+ 1ff4
    |         --+----------->|            |
3c8 +-----------+            +------------+ 1ff0
    |         --+----------->|            |
3c0 +-----------+            +------------+ 1fec
    |         --+----------->|            |
3b8 +-----------+            +------------+ 1fe8
    | 2020 .data|               .got
3b0 +-----------+
    | 1ef8      |
3a8 +-----------+
    | 1ef4      |
3a0 +-----------+
      .rel.dyn

可以看到,.got.plt 的前三项值是 0x1f00, 0x0, 0x0。其中,0x1f00是段.dynamic的地址,其余两项,分别是 本模块的ID、用于解决函数符号绑定的函数(位于 /lib/ld-linux.so ),这两项是动态链接器装载共享模块时负责将 它们初始化的,这里暂时都为0。

结合本图,下面讲解:

2.3.6.1 动态链接库内部的数据访问

函数 bar 中, 对 d1_global_static_uninit 的赋值。

000004a0 <__x86.get_pc_thunk.bx>:
 4a0:   8b 1c 24                mov    (%esp),%ebx
 4a3:   c3                      ret

....

000005cb <bar>:
 ...
 5d2:   e8 c9 fe ff ff          call   4a0 <__x86.get_pc_thunk.bx>
 5d7:   81 c3 29 1a 00 00       add    $0x1a29,%ebx
 5dd:   c7 83 28 00 00 00 01    movl   $0x1,0x28(%ebx)
 ...

call 4a0 时,会将之后的下一条指令的地址(即5d7)压栈,所以,执行 mov (%esp),%ebx 后, 寄存器ebx的值是5d7, 那么, movl $0x1,0x28(%ebx) 就是将 1 放入内存单元 0x5d7+0x1a29+0x28 = 0x2028。 查询符号表,得知 d1_global_static_uninit 的地址正好是 0x2028。这表明共享模块内部 的数据访问是不需要重定位的。

2.3.6.2 动态链接库间的数据访问

函数 bar 中, 对 d2_global_uninit 的赋值。

000005cb <bar>:
 ...
 5d2:   e8 c9 fe ff ff          call   4a0 <__x86.get_pc_thunk.bx>
 5d7:   81 c3 29 1a 00 00       add    $0x1a29,%ebx
 5dd:   c7 83 28 00 00 00 01    movl   $0x1,0x28(%ebx)
 5e4:   00 00 00
 5e7:   8b 83 fc ff ff ff       mov    -0x4(%ebx),%eax
 5ed:   c7 00 02 00 00 00       movl   $0x2,(%eax)

在上一节,得知寄存器ebx中的值是0x2000,执行 mov -0x4(%ebx),%eax 的效果是,将内存0x2000-0x4=0x1ffc 中的值存入寄存器eax, 执行 movl $0x2,(%eax) 的效果是,将2存入eax指向的内存单元,这说明变量 d2_global_uninit 的地址是存放在0x1ffc中的,查看段表,可知0x1ffc于.got。

再查看重定位表.rel.dyn,可知,装载重定位过程中,待 d2_global_uninit 的地址确定后, 需要修正0x1ffc处。

2.3.6.3 动态链接库内部的函数调用

函数foo中对函数bar的调用。

00000460 <bar@plt>:
 460:   ff a3 10 00 00 00       jmp    *0x10(%ebx)
 466:   68 08 00 00 00          push   $0x8
 46b:   e9 d0 ff ff ff          jmp    440 <_init+0x30>

...

0000061b <foo>:
 ...
 622:   e8 79 fe ff ff          call   4a0 <__x86.get_pc_thunk.bx>
 627:   81 c3 d9 19 00 00       add    $0x19d9,%ebx
 62d:   e8 5e fe ff ff          call   490 <ext@plt>
 632:   e8 29 fe ff ff          call   460 <bar@plt>

call 460 是调用函数bar,这时,寄存器ebx的值是0x19d9+0x627=0x2000, jmp *0x10(%ebx) 程序将跳转到0x466,原因是内存0x2000+0x10=0x2010中存放的值是0x466, 即跳转指令的下一条指令的地址,这是编译器放入的。

接着,将立即数8压栈。延迟加载时,动态链接器找到 bar 符号后,取出存在内存 0x3e8+8=0x3f0 (0x3e8 是 .rel.plt 的地址) 中的值(该值是 0x2010,由编译器存入的,是位于 .got.plt 中的一个单元),然后,去修正地址 0x2010 这个内存单元,使得它 指向 bar 函数的实际地址。

当再一次调用bar函数时,由于.got.plt的0x2010处存放的是修正过的真实的bar地址,0x460处的代码 将直接跳转到bar函数执行,从bar函数返回时,弹出栈的PC寄存器的值是调用bar函数地址处的下一个 指令地址的值,也即0x466将不再执行。

2.3.6.4 动态链接库间的函数调用

函数foo中对函数ext的调用,同共享模块内部的函数调用一样。

2.3.6.5 动态链接库内的全局变量

文件d2.c中对变量 d2_global_uninit 的使用。

0000056b <ext>:
 ...
 572:   e8 c9 fe ff ff          call   440 <__x86.get_pc_thunk.bx>
 577:   81 c3 89 1a 00 00       add    $0x1a89,%ebx
 57d:   8b 83 fc ff ff ff       mov    -0x4(%ebx),%eax
 583:   89 44 24 04             mov    %eax,0x4(%esp)
 587:   8d 83 b0 e5 ff ff       lea    -0x1a50(%ebx),%eax
 58d:   89 04 24                mov    %eax,(%esp)
 590:   e8 7b fe ff ff          call   410 <printf@plt>

call 440 的结果是将0x577放入寄存器ebx, mov %eax,0x4(%esp) 结果是将0x1ffc中存放的值放入栈中, 可见,变量 d2_global_uninit 的地址是位于在0x1ffc中的,而0x1ffc是位于.got的,所以, 对共享模块内全局变量的访问,也是需要重定位的。

原因是,全局变量可能会在主程序中使用,但主程序并不是地址无关的代码(PIC),主程序中可能存在 mov $0x1, addrOfGlobalVar ,这样,为确定该全局变量的地址,链接器就需要在可执行文件中预留一个 位置(一般是.bss中)给该全局变量,这就要求共享模块也可以访问到主程序中该全局变量的副本,所以,共享模块对 全局变量的访问必须是可重定位的。

3 链接

将多个目标文件、库(静态库、动态库)、命令参数、链接控制文件作为输入,产生一个可执行文件的过程,称链接。 它会合并各个目标文件中类型相同的段、并执行链接时重定位。

#include <stdio.h>

extern int global_uninit_var;
extern int global_init_var;
extern int d2_global_uninit;

int main(){
  printf("&global_init_var: %p, &global_uninit_var(%d): %p\n",
   (void*)&global_init_var, global_uninit_var, (void*)&global_uninit_var);

  sfoo();
  printf("&d2_global_uninit: %p\n", (void *)&d2_global_uninit);
  foo();
  fflush(stdout);
  sleep(-1);

  return 0;
}

编译命令 gcc -m32 -g m.c s1.c d1.so d2.so -o main -Xlinker -rpath ./ , 参数Xlinker及rpath表示链接器在当前路径搜寻共享对象。

具体而言,分两步

  • 空间与地址分配
    • 将输入目标文件中所有的符号定义及符号引用收集起来,放到全局符号表
    • 合并所有输入目标文件中的相似段,计算各段合并后的长度及位置,并建立到虚拟地址空间的映射关系

      Linux下, ELF可执行文件默认从0x08048000开始分配虚拟地址空间,ELF文件头及程序头将映射到该 虚拟地址0x8048000。

  • 符号解析

    链接器计算出所有目标文件中符号的地址,更新到全局符号表(.symtab),对于在目标文件中没能找到 的符号,若在动态链接库中可以找到,同样添加到全局符号表(.symtab),同时,也需要添加到动态链 接符号表(.dynsym),若在动态链接库中也找不到,则链接报错。

    C++源码编译后的目标文件中所使用的符号名是相应的函数名(变量名)经过修饰后的名称。对于函数而言, 它包含了函数名、参数类型、所在类、名称空间等信息。可以使用命令 c++filt 来解析被修饰过的名称, 如 c++filt __ZN1N1C4funcEi 。这里引出了一个问题,如果C++源文件中使用了C代 码的符号(比如 memset),那么链接时链接器将无法找到C代码中的这个符号,原因是C++目标文件中使用 的是经过修饰后的符号名(如 __Z6memsetPvii),不同于C目标文件中的符号名 (如 memset)。解决办法是使用 extern "C" (C代码不支持该关键字),对于供C和C++一起使用的头文 件,最好这样声明

    #ifdef __cplusplus
    extern "C" {
    #endif
    void *memset(void*, int, size);
    #ifdef __cplusplus
    }
    #endif
    

    强引用,链接时不能找到符号的定义则报错。弱引用,链接时不能找到符号的定义,不报错。 使用 __attribute__((weakref)) 来声明对 一个外部函数的引用为弱引用。弱引用的例子

    __attribute__((weakref)) void foo();
    
    int main(){
      if(foo) // check if defination of foo is found.
        foo();
    }
    
  • 链接时重定位

    空间与地址分配之后,就需要更新程序中一些指令参数的地址,需要更新的位置参见 2.1.4,可与下 面程序的对比结果结合起来理解。

    // s1.o
    00000000 <sbar>:
     0:   55                      push   %ebp
     1:   89 e5                   mov    %esp,%ebp
     3:   83 ec 28                sub    $0x28,%esp
     6:   a1 00 00 00 00          mov    0x0,%eax
     b:   c7 44 24 0c 00 00 00    movl   $0x0,0xc(%esp)
    12:   00
    13:   89 44 24 08             mov    %eax,0x8(%esp)
    17:   c7 44 24 04 04 00 00    movl   $0x4,0x4(%esp)
    1e:   00
    1f:   c7 04 24 00 00 00 00    movl   $0x0,(%esp)
    26:   e8 fc ff ff ff          call   27 <sbar+0x27>
    2b:   8b 15 04 00 00 00       mov    0x4,%edx
    31:   a1 00 00 00 00          mov    0x0,%eax
    36:   01 d0                   add    %edx,%eax
    38:   89 45 f4                mov    %eax,-0xc(%ebp)
    3b:   8b 45 f4                mov    -0xc(%ebp),%eax
    3e:   c9                      leave
    3f:   c3                      ret
    // main
    08048699 <sbar>:
    8048699:       55                      push   %ebp
    804869a:       89 e5                   mov    %esp,%ebp
    804869c:       83 ec 28                sub    $0x28,%esp
    804869f:       a1 4c a0 04 08          mov    0x804a04c,%eax
    80486a4:       c7 44 24 0c 4c a0 04    movl   $0x804a04c,0xc(%esp)
    80486ab:       08
    80486ac:       89 44 24 08             mov    %eax,0x8(%esp)
    80486b0:       c7 44 24 04 30 a0 04    movl   $0x804a030,0x4(%esp)
    80486b7:       08
    80486b8:       c7 04 24 d8 87 04 08    movl   $0x80487d8,(%esp)
    80486bf:       e8 0c fe ff ff          call   80484d0 <printf@plt>
    80486c4:       8b 15 30 a0 04 08       mov    0x804a030,%edx
    80486ca:       a1 4c a0 04 08          mov    0x804a04c,%eax
    80486cf:       01 d0                   add    %edx,%eax
    80486d1:       89 45 f4                mov    %eax,-0xc(%ebp)
    80486d4:       8b 45 f4                mov    -0xc(%ebp),%eax
    80486d7:       c9                      leave
    80486d8:       c3                      ret
    

3.0.1 可执行文件的文件头

2.3.1 的主要区别是,可执行文件的文件头中的 TYPE 是 EXEC(可执行文件);可执行文件 中的入口点地址是 x8048530。

3.0.2 可执行文件的程序头

$ readelf -l main
Elf 文件类型为 EXEC (可执行文件)
入口点 0x8048530
共有 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 0x0094c 0x0094c R E 0x1000
  LOAD           0x000ef0 0x08049ef0 0x08049ef0 0x00144 0x00164 RW  0x1000
  DYNAMIC        0x000efc 0x08049efc 0x08049efc 0x00100 0x00100 RW  0x4
  NOTE           0x000168 0x08048168 0x08048168 0x00044 0x00044 R   0x4
  GNU_EH_FRAME   0x000820 0x08048820 0x08048820 0x0003c 0x0003c R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
  GNU_RELRO      0x000ef0 0x08049ef0 0x08049ef0 0x00110 0x00110 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 .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

类型为 PHDR 的程序头用于指出程序头的位置及大小。程序头正好是位于ELF文件头之后,ELF文件头同样将映射到内存中, 且查看ELF文件头可知,文件头的大小是0x34字节,这也是程序头在ELF文件的偏移,程序头共有9项,每项32字节,正好是 0x120字节。

类型为 INTERP 的程序头指出了所使用的动态连接器的路径,所占大小正好是路径字符串的大小。

类型为 DYNAMIC 的程序头指出了动态链接信息 .dynamic 段的虚拟地址及大小。

两个类型为 LOAD 类型的程序头分别对应着程序运行的代码段和程序运行的数据段。

加载时,所分配的虚拟地址空间和物理内存都要求是页对齐的,但是,实际中每个LOAD的segment大小并不一定正好是页对齐的, 如果将每个segment独立开来分配物理内存,就会造成物理内存的浪费,为解决此问题,可以让前一个segment的后面部分和 后一个segment的前一部分共享一个物理页面,但在到虚拟地址空间的映射时,分别根据segment的属性,采取不同的映射方式, 对于只读的segment,采取只读的方式映射该共享物理页面到虚拟地址;对于可写的segment,采取COW的方式映射该共享物理页 到虚拟地址。比如,这里第二个LOAD类型的segment就与第一个LOAD类型的segment共享了一个物理页,该共享物理页偏移量0xef0 到页结尾的部分被映射到了0x8049ef0开始的虚拟地址,且虚拟地址0x8049000的页框属性应该是COW的。

3.0.3 可执行文件的段表

$ readelf -S main
共有 30 个节头,从偏移量 0x118c 开始:
节头:
  [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] .gnu.hash         GNU_HASH        080481ac 0001ac 000044 04   A  5   0  4
  [ 5] .dynsym           DYNSYM          080481f0 0001f0 000120 10   A  6   1  4
  [ 6] .dynstr           STRTAB          08048310 000310 0000f2 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          08048402 000402 000024 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         08048428 000428 000020 00   A  6   1  4
  [ 9] .rel.dyn          REL             08048448 000448 000018 08   A  5   0  4
  [10] .rel.plt          REL             08048460 000460 000030 08   A  5  12  4
  [11] .init             PROGBITS        08048490 000490 000023 00  AX  0   0  4
  [12] .plt              PROGBITS        080484c0 0004c0 000070 04  AX  0   0 16
  [13] .text             PROGBITS        08048530 000530 000242 00  AX  0   0 16
  [14] .fini             PROGBITS        08048774 000774 000014 00  AX  0   0  4
  [15] .rodata           PROGBITS        08048788 000788 000095 00   A  0   0  4
  [16] .eh_frame_hdr     PROGBITS        08048820 000820 00003c 00   A  0   0  4
  [17] .eh_frame         PROGBITS        0804885c 00085c 0000f0 00   A  0   0  4
  [18] .init_array       INIT_ARRAY      08049ef0 000ef0 000004 00  WA  0   0  4
  [19] .fini_array       FINI_ARRAY      08049ef4 000ef4 000004 00  WA  0   0  4
  [20] .jcr              PROGBITS        08049ef8 000ef8 000004 00  WA  0   0  4
  [21] .dynamic          DYNAMIC         08049efc 000efc 000100 08  WA  6   0  4
  [22] .got              PROGBITS        08049ffc 000ffc 000004 04  WA  0   0  4
  [23] .got.plt          PROGBITS        0804a000 001000 000024 04  WA  0   0  4
  [24] .data             PROGBITS        0804a024 001024 000010 00  WA  0   0  4
  [25] .bss              NOBITS          0804a040 001034 000014 00  WA  0   0 32
  [26] .comment          PROGBITS        00000000 001034 00004f 01  MS  0   0  1
  [27] .shstrtab         STRTAB          00000000 001083 000106 00      0   0  1
  [28] .symtab           SYMTAB          00000000 00163c 0004f0 10     29  48  4
  [29] .strtab           STRTAB          00000000 001b2c 0002ea 00      0   0  1

这里不再有 .rel.text 及 .rel.data,因为已经在链接时重定位中将它们解决掉了。

段 .interp 包含着动态连接器的路径,Linux操作系统在加载可执行文件时,会去加载该动态连接器, 然后使用该动态连接器加载其它共享对象。

$ objdump -s main
main:     文件格式 elf32-i386
Contents of section .interp:
 8048154 2f6c6962 2f6c642d 6c696e75 782e736f  /lib/ld-linux.so
 8048164 2e3200                               .2.

.bss 本应该是从0x804a034起始,但由于其要求32字节对齐,故起始地址是0x804a040, 该起始地址映射了ELF文件 的偏移是0x1034,而下面的 3.0.4 一节,显示的 _bss_start 的虚拟地址是0x804a034, 且指出它属于段25(即bss段)。

位于目标文件的 .comment 段的未初始化的全局变量 global_uninit_var 回到了 .bss 段。.bss 中,也包含了来自动态链接库中的全局变量 d2_global_uninit , 原因参见 2.3.6.5

$ objdump -dj .bss main
main:     文件格式 elf32-i386
Disassembly of section .bss:
0804a040 <stdout@@GLIBC_2.0>:
 804a040:       00 00 00 00                                         ....
0804a044 <d2_global_uninit>:
 804a044:       00 00 00 00                                         ....
0804a048 <completed.6591>:
 804a048:       00 00 00 00                                         ....
0804a04c <static_var2.1828>:
 804a04c:       00 00 00 00                                         ....
0804a050 <global_uninit_var>:
 804a050:       00 00 00 00                                         ....

.data 中,除了目标文件中的已初始化的全局变量及已初始化的局部静态变量外,还包含了 _dso_handle, 但该变量不是 2.3.4 一节中提到的同名变量,因为它们各自位于各自的数据段,且不在动态重定位表中。

$ objdump -dj .data main
main:     文件格式 elf32-i386
Disassembly of section .data:
0804a024 <__data_start>:
 804a024:       00 00                   add    %al,(%eax)
        ...
0804a028 <__dso_handle>:
 804a028:       00 00 00 00                                         ....
0804a02c <global_init_var>:
 804a02c:       52 00 00 00                                         R...
0804a030 <static_var.1827>:
 804a030:       36 00 00 00                                         6...

3.0.4 可执行文件的符号表

$ readelf -s main
Symbol table '.dynsym' contains 18 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     2: 00000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.0 (2)
     3: 00000000     0 FUNC    GLOBAL DEFAULT  UND fflush@GLIBC_2.0 (2)
     4: 00000000     0 FUNC    GLOBAL DEFAULT  UND sleep@GLIBC_2.0 (2)
     5: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     6: 00000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.0 (2)
     7: 00000000     0 FUNC    GLOBAL DEFAULT  UND foo
     8: 00000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
     9: 00000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
    10: 0804a034     0 NOTYPE  GLOBAL DEFAULT   24 _edata
    11: 0804a054     0 NOTYPE  GLOBAL DEFAULT   25 _end
    12: 0804a040     4 OBJECT  GLOBAL DEFAULT   25 stdout@GLIBC_2.0 (2)
    13: 0804878c     4 OBJECT  GLOBAL DEFAULT   15 _IO_stdin_used
    14: 0804a034     0 NOTYPE  GLOBAL DEFAULT   25 __bss_start
    15: 0804a044     4 OBJECT  GLOBAL DEFAULT   25 d2_global_uninit
    16: 08048490     0 FUNC    GLOBAL DEFAULT   11 _init
    17: 08048774     0 FUNC    GLOBAL DEFAULT   14 _fini

Symbol table '.symtab' contains 79 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     ...

这里省略了 .symtab 。 目标文件中定义的各个符号在 .symtab 中的 Value 属性也不再是某个段的段内偏移了,而是实际运行时的虚拟地址。

foo 作为动态链接符号表中的导入符号(其Value值为0)是可以理解的,因为它是在m.c中引用的 动态链接库中的符号,而奇怪的是 d2_global_uninit 明明是在m.c中引用的 动态链接库中的符号,怎么成了main的导出符号呢?原因回见 2.3.6.5

此处.dynsym中的 _bss_start 应该与 2.3.5 的.dynsym 中的同名 符号,没有关系,因为它们都是导出符号。

.symtab 中UND符号与 .dynsym 中UND符号一致,这些UND的符号都是要在装载时重定位解决掉的。

3.0.5 可执行文件的动态链接信息表

2.3.3 类似。

3.0.6 可执行文件的重定位表

$ readelf -r main
重定位节 '.rel.dyn' 位于偏移量 0x448 含有 3 个条目:
 Offset     Info    Type            Sym.Value  Sym. Name
08049ffc  00000506 R_386_GLOB_DAT    00000000   __gmon_start__
0804a040  00000c05 R_386_COPY        0804a040   stdout
0804a044  00000f05 R_386_COPY        0804a044   d2_global_uninit

重定位节 '.rel.plt' 位于偏移量 0x460 含有 6 个条目:
 Offset     Info    Type            Sym.Value  Sym. Name
0804a00c  00000207 R_386_JUMP_SLOT   00000000   printf
0804a010  00000307 R_386_JUMP_SLOT   00000000   fflush
0804a014  00000407 R_386_JUMP_SLOT   00000000   sleep
0804a018  00000507 R_386_JUMP_SLOT   00000000   __gmon_start__
0804a01c  00000607 R_386_JUMP_SLOT   00000000   __libc_start_main
0804a020  00000707 R_386_JUMP_SLOT   00000000   foo

$ objdump -dj .plt main
Disassembly of section .plt:

080484c0 <printf@plt-0x10>:
 80484c0:       ff 35 04 a0 04 08       pushl  0x804a004
 80484c6:       ff 25 08 a0 04 08       jmp    *0x804a008         o
 80484cc:       00 00                   add    %al,(%eax)
        ...

080484d0 <printf@plt>:
 80484d0:       ff 25 0c a0 04 08       jmp    *0x804a00c
 80484d6:       68 00 00 00 00          push   $0x0
 80484db:       e9 e0 ff ff ff          jmp    80484c0 <_init+0x30>

080484e0 <fflush@plt>:
 80484e0:       ff 25 10 a0 04 08       jmp    *0x804a010
 80484e6:       68 08 00 00 00          push   $0x8
 80484eb:       e9 d0 ff ff ff          jmp    80484c0 <_init+0x30>

080484f0 <sleep@plt>:
 80484f0:       ff 25 14 a0 04 08       jmp    *0x804a014
 80484f6:       68 10 00 00 00          push   $0x10
 80484fb:       e9 c0 ff ff ff          jmp    80484c0 <_init+0x30>

08048500 <__gmon_start__@plt>:
 8048500:       ff 25 18 a0 04 08       jmp    *0x804a018
 8048506:       68 18 00 00 00          push   $0x18
 804850b:       e9 b0 ff ff ff          jmp    80484c0 <_init+0x30>

08048510 <__libc_start_main@plt>:
 8048510:       ff 25 1c a0 04 08       jmp    *0x804a01c
 8048516:       68 20 00 00 00          push   $0x20
 804851b:       e9 a0 ff ff ff          jmp    80484c0 <_init+0x30>

08048520 <foo@plt>:
 8048520:       ff 25 20 a0 04 08       jmp    *0x804a020
 8048526:       68 28 00 00 00          push   $0x28
 804852b:       e9 90 ff ff ff          jmp    80484c0 <_init+0x30>

$ objdump -s main
Contents of section .got:
 8049ffc 00000000                             ....
Contents of section .got.plt:
 804a000 fc9e0408 00000000 00000000 d6840408  ................
 804a010 e6840408 f6840408 06850408 16850408  ................
 804a020 26850408                             &...

上述各表的关系示意图

	    490 +-----------+            +------------+ 804a024     +------------+ 8048530
		|         --+----------->| 8048526    |<------------+--          | foo@plt <--- foo( )
	    488 +-----------+            +------------+ 804a020     +------------+ 8048520
		|         --+----------->| 8048516    |<------------+--          | __libc_start_main@plt
	    480 +-----------+            +------------+ 804a01c     +------------+ 8048510
		|         --+----------->| 8048506    |<------------+--          | __gmon_start__@plt
	    478 +-----------+            +------------+ 804a018     +------------+ 8048500
		|         --+----------->| 80484f6    |<------------+--          | sleep@plt
	    470 +-----------+            +------------+ 804a014     +------------+ 80484f0
		|         --+----------->| 80484e6    |<------------+--          | fflush@plt
	    468 +-----------+            +------------+ 804a010     +------------+ 80484e0
		|         --+----------->| 80484d6    |<------------+--          | printf@plt
  (.rel.plt)460 +-----------+            +------------+ 804a00c     +------------+ 80484d0
d2_global_uninit| 804a044   |            | 0          |                 .plt
	    458 +-----------+            +------------+ 804a008
	 stdout | 804a040   |            | 0          |
	    450 +-----------+            +------------+ 804a004
		|         --+---         | 8049efc    | .dynamic
  (.rel.dyn)448 +-----------+   \----    +------------+ 804a000(.got.plt)
		  .rel.dyn           \-->|            | __gmon_start__
					 +------------+ 8049ffc
					    .got

该图的解释可参考 动态链接重定位相关信息

这里,将位于 .bss 段的、虚拟地址已经确定的、定义在 d2.so 中的变量 d2_global_uninit 也放在了 重定位表中,且在 .got 没有相应的表项,是为何?

4 装载

在GDB中,将 main 运行起来,通过 info files 可以查看到 main,ld-linux.so.2,d1.so,d2.so 及 libc.so.6 五个ELF文件的各个段分别在内存中所占的虚拟地址范围。

(gdb) b main 
Breakpoint 1 at 0x8048636: file m.c, line 9.
(gdb) r
Starting program: /home/ubuntu/linkerAndLoader/main
Breakpoint 1, main () at m.c:9
9         printf("&global_init_var: %p, &global_uninit_var(%d): %p\n",
(gdb) info files
Symbols from "/home/ubuntu/linkerAndLoader/main".
Unix child process:
  Using the running image of child process 25518.
  While running this, GDB does not access memory from...
Local exec file:
  `/home/ubuntu/linkerAndLoader/main', file type elf32-i386.
  Entry point: 0x8048530
  0x08048154 - 0x08048167 is .interp
  0x08048168 - 0x08048188 is .note.ABI-tag
  0x08048188 - 0x080481ac is .note.gnu.build-id
  0x080481ac - 0x080481f0 is .gnu.hash
  0x080481f0 - 0x08048310 is .dynsym
  0x08048310 - 0x08048402 is .dynstr
  0x08048402 - 0x08048426 is .gnu.version
  0x08048428 - 0x08048448 is .gnu.version_r
  0x08048448 - 0x08048460 is .rel.dyn
  0x08048460 - 0x08048490 is .rel.plt
  0x08048490 - 0x080484b3 is .init
  0x080484c0 - 0x08048530 is .plt
  0x08048530 - 0x08048772 is .text
  0x08048774 - 0x08048788 is .fini
  0x08048788 - 0x08048821 is .rodata
  0x08048824 - 0x08048860 is .eh_frame_hdr
  0x08048860 - 0x08048950 is .eh_frame
  0x08049ef0 - 0x08049ef4 is .init_array
  0x08049ef4 - 0x08049ef8 is .fini_array
  0x08049ef8 - 0x08049efc is .jcr
  0x08049efc - 0x08049ffc is .dynamic
  0x08049ffc - 0x0804a000 is .got
  0x0804a000 - 0x0804a024 is .got.plt
  0x0804a024 - 0x0804a034 is .data
  0x0804a040 - 0x0804a054 is .bss
  0xf7fdc114 - 0xf7fdc138 is .note.gnu.build-id in /lib/ld-linux.so.2
  0xf7fdc138 - 0xf7fdc1f8 is .hash in /lib/ld-linux.so.2
  0xf7fdc1f8 - 0xf7fdc2dc is .gnu.hash in /lib/ld-linux.so.2
  0xf7fdc2dc - 0xf7fdc4ac is .dynsym in /lib/ld-linux.so.2
  0xf7fdc4ac - 0xf7fdc642 is .dynstr in /lib/ld-linux.so.2
  0xf7fdc642 - 0xf7fdc67c is .gnu.version in /lib/ld-linux.so.2
  0xf7fdc67c - 0xf7fdc744 is .gnu.version_d in /lib/ld-linux.so.2
  0xf7fdc744 - 0xf7fdc7b4 is .rel.dyn in /lib/ld-linux.so.2
  0xf7fdc7b4 - 0xf7fdc7e4 is .rel.plt in /lib/ld-linux.so.2
  0xf7fdc7f0 - 0xf7fdc860 is .plt in /lib/ld-linux.so.2
  0xf7fdc860 - 0xf7ff47ac is .text in /lib/ld-linux.so.2
  0xf7ff47c0 - 0xf7ff8780 is .rodata in /lib/ld-linux.so.2
  0xf7ff8780 - 0xf7ff8e04 is .eh_frame_hdr in /lib/ld-linux.so.2
  0xf7ff8e04 - 0xf7ffb6fc is .eh_frame in /lib/ld-linux.so.2
  0xf7ffccc0 - 0xf7ffcf34 is .data.rel.ro in /lib/ld-linux.so.2
  0xf7ffcf34 - 0xf7ffcfec is .dynamic in /lib/ld-linux.so.2
  0xf7ffcfec - 0xf7ffcff8 is .got in /lib/ld-linux.so.2
  0xf7ffd000 - 0xf7ffd024 is .got.plt in /lib/ld-linux.so.2
  0xf7ffd040 - 0xf7ffd878 is .data in /lib/ld-linux.so.2
  0xf7ffd878 - 0xf7ffd938 is .bss in /lib/ld-linux.so.2
  0xf7fd6114 - 0xf7fd6138 is .note.gnu.build-id in ./d1.so
  0xf7fd6138 - 0xf7fd6178 is .gnu.hash in ./d1.so
  0xf7fd6178 - 0xf7fd6278 is .dynsym in ./d1.so
  0xf7fd6278 - 0xf7fd634f is .dynstr in ./d1.so
  0xf7fd6350 - 0xf7fd6370 is .gnu.version in ./d1.so
  0xf7fd6370 - 0xf7fd63a0 is .gnu.version_r in ./d1.so
  0xf7fd63a0 - 0xf7fd63e8 is .rel.dyn in ./d1.so
  0xf7fd63e8 - 0xf7fd6410 is .rel.plt in ./d1.so
  0xf7fd6410 - 0xf7fd6433 is .init in ./d1.so
  0xf7fd6440 - 0xf7fd64a0 is .plt in ./d1.so
  0xf7fd64a0 - 0xf7fd663d is .text in ./d1.so
  0xf7fd6640 - 0xf7fd6654 is .fini in ./d1.so
  0xf7fd6654 - 0xf7fd6689 is .rodata in ./d1.so
  0xf7fd668c - 0xf7fd66b0 is .eh_frame_hdr in ./d1.so
  0xf7fd66b0 - 0xf7fd6738 is .eh_frame in ./d1.so
  0xf7fd7ef4 - 0xf7fd7ef8 is .init_array in ./d1.so
  0xf7fd7ef8 - 0xf7fd7efc is .fini_array in ./d1.so
  0xf7fd7efc - 0xf7fd7f00 is .jcr in ./d1.so
  0xf7fd7f00 - 0xf7fd7fe8 is .dynamic in ./d1.so
  0xf7fd7fe8 - 0xf7fd8000 is .got in ./d1.so
  0xf7fd8000 - 0xf7fd8020 is .got.plt in ./d1.so
  0xf7fd8020 - 0xf7fd8024 is .data in ./d1.so
  0xf7fd8024 - 0xf7fd802c is .bss in ./d1.so
  0xf7fd3114 - 0xf7fd3138 is .note.gnu.build-id in ./d2.so
  0xf7fd3138 - 0xf7fd3178 is .gnu.hash in ./d2.so
  0xf7fd3178 - 0xf7fd3258 is .dynsym in ./d2.so
  0xf7fd3258 - 0xf7fd3321 is .dynstr in ./d2.so
  0xf7fd3322 - 0xf7fd333e is .gnu.version in ./d2.so
  0xf7fd3340 - 0xf7fd3370 is .gnu.version_r in ./d2.so
  0xf7fd3370 - 0xf7fd33b8 is .rel.dyn in ./d2.so
  0xf7fd33b8 - 0xf7fd33d0 is .rel.plt in ./d2.so
  0xf7fd33d0 - 0xf7fd33f3 is .init in ./d2.so
  0xf7fd3400 - 0xf7fd3440 is .plt in ./d2.so
  0xf7fd3440 - 0xf7fd359b is .text in ./d2.so
  0xf7fd359c - 0xf7fd35b0 is .fini in ./d2.so
  0xf7fd35b0 - 0xf7fd35c7 is .rodata in ./d2.so
  0xf7fd35c8 - 0xf7fd35e4 is .eh_frame_hdr in ./d2.so
  0xf7fd35e4 - 0xf7fd3648 is .eh_frame in ./d2.so
  0xf7fd4efc - 0xf7fd4f00 is .init_array in ./d2.so
  0xf7fd4f00 - 0xf7fd4f04 is .fini_array in ./d2.so
  0xf7fd4f04 - 0xf7fd4f08 is .jcr in ./d2.so
  0xf7fd4f08 - 0xf7fd4fe8 is .dynamic in ./d2.so
  0xf7fd4fe8 - 0xf7fd5000 is .got in ./d2.so
  0xf7fd5000 - 0xf7fd5018 is .got.plt in ./d2.so
  0xf7fd5018 - 0xf7fd501c is .data in ./d2.so
  0xf7fd501c - 0xf7fd5024 is .bss in ./d2.so
  0xf7e10174 - 0xf7e10198 is .note.gnu.build-id in /lib32/libc.so.6
  0xf7e10198 - 0xf7e101b8 is .note.ABI-tag in /lib32/libc.so.6
  0xf7e101b8 - 0xf7e13ec8 is .gnu.hash in /lib32/libc.so.6
  0xf7e13ec8 - 0xf7e1d438 is .dynsym in /lib32/libc.so.6
  0xf7e1d438 - 0xf7e2315e is .dynstr in /lib32/libc.so.6
  0xf7e2315e - 0xf7e2440c is .gnu.version in /lib32/libc.so.6
  0xf7e2440c - 0xf7e24898 is .gnu.version_d in /lib32/libc.so.6
  0xf7e24898 - 0xf7e248d8 is .gnu.version_r in /lib32/libc.so.6
  0xf7e248d8 - 0xf7e272e8 is .rel.dyn in /lib32/libc.so.6
  0xf7e272e8 - 0xf7e27348 is .rel.plt in /lib32/libc.so.6
  0xf7e27350 - 0xf7e27420 is .plt in /lib32/libc.so.6
  0xf7e27420 - 0xf7f55bfe is .text in /lib32/libc.so.6
  0xf7f55c00 - 0xf7f56b7b is __libc_freeres_fn in /lib32/libc.so.6
  0xf7f56b80 - 0xf7f56d57 is __libc_thread_freeres_fn in /lib32/libc.so.6
  0xf7f56d60 - 0xf7f787b4 is .rodata in /lib32/libc.so.6
  0xf7f787b4 - 0xf7f787c7 is .interp in /lib32/libc.so.6
  0xf7f787c8 - 0xf7f7fc6c is .eh_frame_hdr in /lib32/libc.so.6
  0xf7f7fc6c - 0xf7fb0e48 is .eh_frame in /lib32/libc.so.6
  0xf7fb0e48 - 0xf7fb12a4 is .gcc_except_table in /lib32/libc.so.6
  0xf7fb12a4 - 0xf7fb4804 is .hash in /lib32/libc.so.6
  0xf7fb51d0 - 0xf7fb51d8 is .tdata in /lib32/libc.so.6
  0xf7fb51d8 - 0xf7fb521c is .tbss in /lib32/libc.so.6
  0xf7fb51d8 - 0xf7fb51e4 is .init_array in /lib32/libc.so.6
  0xf7fb51e4 - 0xf7fb525c is __libc_subfreeres in /lib32/libc.so.6
  0xf7fb525c - 0xf7fb5260 is __libc_atexit in /lib32/libc.so.6
  0xf7fb5260 - 0xf7fb5270 is __libc_thread_subfreeres in /lib32/libc.so.6
  0xf7fb5280 - 0xf7fb6da8 is .data.rel.ro in /lib32/libc.so.6
  0xf7fb6da8 - 0xf7fb6e98 is .dynamic in /lib32/libc.so.6
  0xf7fb6e98 - 0xf7fb6ff4 is .got in /lib32/libc.so.6
  0xf7fb7000 - 0xf7fb703c is .got.plt in /lib32/libc.so.6
  0xf7fb7040 - 0xf7fb7ebc is .data in /lib32/libc.so.6
  0xf7fb7ec0 - 0xf7fbaa7c is .bss in /lib32/libc.so.6

4.1 动态链接库的全局变量

从前面输出的信息可知,加载后,d1.so 的 .rel.dyn 段的起始地址是 f7fd63a0,而变量 d2_global_uninit 在 该段的地址偏移是 0x3e0-0x3a0=0x40,故内存 f7fd63e0 存放的值(1ffc,因为该段在d1.so的代码段,而共享对象的代码段不会在加载时被改变) 是该变量在文件 d1.so 中的偏移(位于 .got 表中),鉴于 .got 的起始地址是 f7fd7fe8,所以,该变量在 .got 中的地址是 f7fd7ffc 。

同理,变量 d2_global_uninit 在 d2.so 的 .got 中的地址是 f7fd4ffc 。

变量 d2_global_uninit 在可执行文件 main 中的地址是确定的(804a044),不需要修改的。

下面利用GDB查看该变量在 d1.so 和 d2.so 中何时被修改的。

(gdb) watch *0xf7fd4ffc
Hardware watchpoint 1: *0xf7fd4ffc
(gdb) watch *0xf7fd7ffc
Hardware watchpoint 2: *0xf7fd7ffc
(gdb) watch *0x804a008
Hardware watchpoint 3: *0x804a008

(gdb) r
Starting program: /home/ubuntu/linkerAndLoader/main
Hardware watchpoint 1: *0xf7fd4ffc
Old value = <unreadable>
New value = 134520900
0xf7fe7475 in ?? () from /lib/ld-linux.so.2
(gdb) p/x *0xf7fd4ffc
$1 = 0x804a044
(gdb) bt
\#0  0xf7fe7475 in ?? () from /lib/ld-linux.so.2
\#1  0xf7fdf3e0 in ?? () from /lib/ld-linux.so.2
\#2  0xf7ff0f9b in ?? () from /lib/ld-linux.so.2
\#3  0xf7fe0a0b in ?? () from /lib/ld-linux.so.2
\#4  0xf7fdd0d7 in ?? () from /lib/ld-linux.so.2

(gdb) c
Continuing.
Hardware watchpoint 2: *0xf7fd7ffc
Old value = <unreadable>
New value = 134520900
0xf7fe7475 in ?? () from /lib/ld-linux.so.2

(gdb) c
Continuing.
Hardware watchpoint 3: *0x804a008
Old value = 0
New value = -134282000
0xf7fe7177 in ?? () from /lib/ld-linux.so.2
(gdb) bt
\#0  0xf7fe7177 in ?? () from /lib/ld-linux.so.2
\#1  0xf7fdf3e0 in ?? () from /lib/ld-linux.so.2
\#2  0xf7ff0f9b in ?? () from /lib/ld-linux.so.2
\#3  0xf7fe0a0b in ?? () from /lib/ld-linux.so.2
\#4  0xf7fdd0d7 in ?? () from /lib/ld-linux.so.2

可以看到,该变量最终是放在 804a044 这个内存单元的(位于 .bss ),d1.so 及 d2.so 中相应的 .got 表项都被作了修改以指向该内存单元。从 backtrace 可以看到,该变量的重定位是由动态连接器作出的。

4.2 对动态链接库中函数的调用

 1: (gdb) l 15
 2: 15        foo();
 3: 16        fflush(stdout);
 4: (gdb) b 15 
 5: Breakpoint 1 at 0x8048674: file m.c, line 15.
 6: (gdb) b 16
 7: Breakpoint 2 at 0x8048679: file m.c, line 16.
 8: (gdb) r
 9: ....
10: Breakpoint 1, main () at m.c:15
11: 15        foo();
12: (gdb) b foo@plt
13: Breakpoint 3 at 0x8048520
14: (gdb) b ext@plt
15: Breakpoint 4 at 0xf7fd6490
16: 
17: (gdb) c
18: Continuing.
19: Breakpoint 3, 0x08048520 in foo@plt ()
20: (gdb) disassemble
21: Dump of assembler code for function foo@plt:
22: => 0x08048520 <+0>:     jmp    *0x804a020
23:    0x08048526 <+6>:     push   $0x28
24:    0x0804852b <+11>:    jmp    0x80484c0
25: End of assembler dump.
26: (gdb) p/x *0x804a020
27: $1 = 0x8048526
28: (gdb) bt
29: #0  0x08048520 in foo@plt ()
30: #1  0x08048679 in main () at m.c:15
31: 
32: (gdb) c
33: Continuing.
34: Breakpoint 4, 0xf7fd6490 in ext@plt () from ./d1.so
35: (gdb) p/x *0x804a020
36: $2 = 0xf7fd661b
37: (gdb) info symbol 0xf7fd661b
38: foo in section .text of ./d1.so
39: (gdb) disassemble
40: Dump of assembler code for function ext@plt:
41: => 0xf7fd6490 <+0>:     jmp    *0x1c(%ebx)
42:    0xf7fd6496 <+6>:     push   $0x20
43:    0xf7fd649b <+11>:    jmp    0xf7fd6440
44: End of assembler dump.
45: (gdb) p/x $ebx
46: $4 = 0xf7fd8000
47: (gdb) p/x *0xf7fd801c
48: $5 = 0xf7fd6496
49: (gdb) bt
50: #0  0xf7fd6490 in ext@plt () from ./d1.so
51: #1  0xf7fd6632 in foo () from ./d1.so
52: #2  0x08048679 in main () at m.c:15
53: 
54: (gdb) c
55: ...
56: Breakpoint 2, main () at m.c:16
57: 16        fflush(stdout);
58: (gdb) p/x *0xf7fd801c
59: $6 = 0xf7fd356b
60: (gdb) info symbol 0xf7fd356b
61: ext in section .text of ./d2.so
62: (gdb)

GDB调试中,分别在调用 foo 之前和之后设置了断点,并在调用 foo 之前,在将要调用到的例程 foo@plt(位于 main 的.plt 段) 及 例程 ext@plt(位于 d1.so 的.plt 段)中设置了断点,同时,也看到了这两个例程的虚拟地址。

程序运行后,首先暂停在了 foo@plt ,从26行的 backtrace 发现程序还没有进入 foo 。从28行的打印信息知道相应的 .got.plt 表项 还没有被修正。

继续执行程序后,暂停在了 ext@plt ,从49行的 backtrace 看到程序是在 foo 内部,但还没有进入 ext 。从47行的打印信息知道相应的 .got.plt 表项还没有被修正。另外,从35行及37行的打印信息,知道这时位于 main 的相应的 .got.plt 表项已经被修正,正确地指出了 foo 的地址。

再次继续程序的执行,暂停在 main 的16行,这时已经调用完毕 foo 。从58行及60行的打印信息,知道这时位于 d1.so 的相应的 .got.plt 表项已经被修正,正确地指出了 ext 的地址。

4.3 可执行文件的运行

$./main &
[1] 26980
&global_init_var: 0x804a02c, &global_uninit_var(0): 0x804a050
&static_var: 0x804a030, &static_var2(0): 0x804a04c
value returned by sbar(): 0
&d2_global_uninit: 0x804a044
&d2_global_uninit: 0x804a044
&d1_global_static_uninit: 0xf777b028, &d2_global_uninit: 0x804a044

$ cat /proc/26980/maps
08048000-08049000 r-xp 00000000 ca:01 398021                             /home/ubuntu/linkerAndLoader/main
08049000-0804a000 r--p 00000000 ca:01 398021                             /home/ubuntu/linkerAndLoader/main
0804a000-0804b000 rw-p 00001000 ca:01 398021                             /home/ubuntu/linkerAndLoader/main
f75b2000-f75b3000 rw-p 00000000 00:00 0
f75b3000-f7758000 r-xp 00000000 ca:01 1441811                            /lib32/libc-2.19.so
f7758000-f775a000 r--p 001a5000 ca:01 1441811                            /lib32/libc-2.19.so
f775a000-f775b000 rw-p 001a7000 ca:01 1441811                            /lib32/libc-2.19.so
f775b000-f775e000 rw-p 00000000 00:00 0
f7775000-f7776000 rw-p 00000000 00:00 0
f7776000-f7777000 r-xp 00000000 ca:01 398008                             /home/ubuntu/linkerAndLoader/d2.so
f7777000-f7778000 r--p 00000000 ca:01 398008                             /home/ubuntu/linkerAndLoader/d2.so
f7778000-f7779000 rw-p 00001000 ca:01 398008                             /home/ubuntu/linkerAndLoader/d2.so
f7779000-f777a000 r-xp 00000000 ca:01 397991                             /home/ubuntu/linkerAndLoader/d1.so
f777a000-f777b000 r--p 00000000 ca:01 397991                             /home/ubuntu/linkerAndLoader/d1.so
f777b000-f777c000 rw-p 00001000 ca:01 397991                             /home/ubuntu/linkerAndLoader/d1.so
f777c000-f777e000 rw-p 00000000 00:00 0
f777e000-f777f000 r-xp 00000000 00:00 0                                  [vdso]
f777f000-f779f000 r-xp 00000000 ca:01 1441804                            /lib32/ld-2.19.so
f779f000-f77a0000 r--p 0001f000 ca:01 1441804                            /lib32/ld-2.19.so
f77a0000-f77a1000 rw-p 00020000 ca:01 1441804                            /lib32/ld-2.19.so
ffca7000-ffcc8000 rw-p 00000000 00:00 0                                  [stack]

Last Updated 2016-03-16 Wed 13:22.

Created by Howard Hou with Emacs 24.5.1 (Org mode 8.2.10)