[CS:APP-ch7] Linker

Linker

SUMMARY

1.链接器的任务

2.链接方式

3.PIC



7.1 Complier Driver - 编译器驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int referenced_sum ( int *a, int n );

void defined_fun () {}

int array[ 2 ] = {1, 2};

int zero_global = 0;
static int zero_static = 0;

static int ini_static = 7;
int ini_global = 8;

int uninitialize_global;
static int uninitialize_static;

const char const_string[ 6 ] = "Hello";

int main () {
        defined_fun ();
        int local_val = referenced_sum ( array, 2 );
        printf ( "%s\n", const_string );
        return local_val;
}

1.预处理器(cpp)

  • 将源程序翻译成一个ASCII码的中间文件 main.i

2.C编译器(cc1)

  • 将main.i翻译成一个ASCII汇编语言文件 main.s

3.汇编器(as)

  • 将main.s翻译成一个可重定位目标文件 main.o

4.链接器(ld)

  • 将多个.o文件以及一些必要的系统目标文件组合起来,创建一个可执行目标文件

(ps…链接出来的程序并不能正常运行)

1
ld: warning: cannot find entry symbol _start; defaulting to 00000000004000e8

Compiler Driver



7.2 Static Library - 静态链接

为什么要使用链接器?

1.模块化角度考虑

  • 我们可以把程序分散到不同的小的源代码中,而不是一个巨大的类中。这样带来的好处是可以复用常见的功能/库,比方说 Math library, standard C library.

2.效率角度考虑

  • 改动代码时只需要重新编译改动的文件,其他不受影响。而常用的函数和功能可以封装成库,提供给程序进行调用(节省空间)

链接器的任务主要有两个

1.Symbol Resolution 符号解析

  • 将每个符号的引用和唯一的定义相关联 (符号包括变量名和函数名)

2.Relocation 重定位

  • 将每个符号的定义和一个内存地址相关联


7.3 Object Files - 目标文件

3种目标文件

  • Relocatable 可重定位
1
2
//gcc -c main.c
  Type:                              REL (Relocatable file)
  • Executable 可执行
1
2
//ld -o prog main.o sum.o 
  Type:                              EXEC (Executable file)
  • Shared 可共享
1
2
//gcc main.c 默认
  Type:                              DYN (Shared object file)

目标文件的格式:这里主要讨论ELF格式


7.4 Relocatable Object Files - 可重定位

格式:

组成 解释
ELF header 生成此文件的系统等
.text 已编译程序的机器码
.radata 只读数据
.data 已初始化的全局/静态变量
.bss 未初始化的静态,初始化为0的全局/静态
(未初始化全局是COM)
.symtab 所有 定义或引用 的 函数 和 全局变量
.rel.text 要重定位的函数在.text的位置
.rel.data 全局变量的重定位信息
.debug
.line
.strtab
Section header table 记录每个section的位置和大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          1144 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         13
  Section header string table index: 12

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Section Headers:
 Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000032  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000398
       0000000000000048  0000000000000018   I      10     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000078
       0000000000000010  0000000000000000  WA       0     0     8
  [ 4] .bss              NOBITS           0000000000000000  00000088
       000000000000000c  0000000000000000  WA       0     0     4
  [ 5] .rodata           PROGBITS         0000000000000000  00000088
       0000000000000006  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  0000008e
       0000000000000012  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000a0
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  000000a0
       0000000000000058  0000000000000000   A       0     0     8
  [ 9] .rela.eh_frame    RELA             0000000000000000  000003e0
       0000000000000030  0000000000000018   I      10     8     8
  [10] .symtab           SYMTAB           0000000000000000  000000f8
       00000000000001f8  0000000000000018          11    12     8
  [11] .strtab           STRTAB           0000000000000000  000002f0
       00000000000000a7  0000000000000000           0     0     1
  [12] .shstrtab         STRTAB           0000000000000000  00000410
       0000000000000061  0000000000000000           0     0     1


7.5 Symbol and Symbol Tables - 符号和符号表

每一个symbol都代表一个 函数 / 全局变量 / 静态变量

symbol的种类有3个

1.Global

  • 本模块定义的符号。包括非静态的函数和全局变量

2.Global(Extern)

  • 其他模块定义,本模块引用的符号。包括其他文件里的非静态的函数和全局变量

3.Local

  • 本模块定义且只能被本模块引用的。包括静态属性的函数和全局变量。

其他

  • 1.普通的局部变量在运行时由栈管理
  • 2.静态属性的局部变量在.data/.bss中,且拥有唯一的名字
    如书上的例子:
1
2
5: 0000000000000000 4 OBJECT  LOCAL  DEFAULT    4 x.1794
6: 0000000000000000 4 OBJECT  LOCAL  DEFAULT    3 x.1797

每个symbol的结构

1
2
3
4
5
6
7
8
9
typedef struct {
        int name;            //在strtab里的偏移量
        char type : 4,       //是函数还是数据   
             binding : 4;    //全局还是局部
        char reserved;          
        short section;       //所在节的索引,三个伪节ABS,UNDEF,COM   (下面的Ndx)
        long value;          //对Reloctable OBJ File:所在节到symbol的偏移(相对地址),Executable: 会变成绝对地址(见7.7)
        long size;            //目标(符号)的大小(字节为单位)  
} ELf64_Symbol;

ABS: 不应被重定位的symbol
UNDEF: 未定义的
COMMON: 未初始化的全局


Num:.symtab表的标号,符号的个数。
一个SymbolTable的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Symbol table '.symtab' contains 22 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    4 zero_static
     6: 0000000000000008     4 OBJECT  LOCAL  DEFAULT    3 ini_static
     7: 0000000000000008     4 OBJECT  LOCAL  DEFAULT    4 uninitialize_static
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
    11: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
    12: 0000000000000000     7 FUNC    GLOBAL DEFAULT    1 defined_fun
    13: 0000000000000000     8 OBJECT  GLOBAL DEFAULT    3 array
    14: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 zero_global
    15: 000000000000000c     4 OBJECT  GLOBAL DEFAULT    3 ini_global
    16: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM uninitialize_global
    17: 0000000000000000     6 OBJECT  GLOBAL DEFAULT    5 const_string
    18: 0000000000000007    55 FUNC    GLOBAL DEFAULT    1 main
    19: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    20: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND referenced_sum
    21: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

rt
.text 里有 defined_fun,main
.data 里有 array, ini_static, ini_global
.bss 里有 uninitialize_static, zero_static, zero_global
.rodata 里有 const_string
COMuninitialize_global
UNDFreferenced_sum , puts , GOT表

Type:
ini_static , uninitialize_static , zero_static 都是 Local
其他都是 Global



7.6 Symbol Resolution - 符号解析

让所有的引用和符号表中唯一的符号定义相关联

  1. Local Symbol: 给予唯一名字和定义

  2. Global Symbol: 非本模块定义,留给链接器处理


7.6.1 Resolve Multiply Defined Global Symbols

定义符号的强/弱

  • 函数/已初始化的变量

  • 未初始化的变量

规则

  1. 不能有多个同名的强符号
  2. 同名的强符号和弱符号,选择强的符号
  3. 多个同名的弱符号任意选一个

遇到多重定义的符号时警告

1
gcc -fno-common main.c func.c -o t

7.6.2 Linking with Static Libraries - 链接静态库

静态库在程序链接的时候使用
链接器会将程序中使用到函数的代码从库文件中拷贝到应用程序中。

静态库包含多个可重定位目标文件和描述各成员的头部,以archive存档的形式保存

优点

  • 编译后的执行程序不需要外部的函数库支持

缺点

  1. 体积较大
  2. 静态函数库改变了,那么程序必须重新链接
  3. 多个可执行文件链接在一个库上时,运行会使库里的代码重复复制到内存,造成浪费

建立,链接方法

1
2
ar rcs libvector.a addvec.o multvec.o
gcc -static -o prog2C main2.o ./libvector.a

此外,链接的时候,拷贝到最终可执行文件的基本单位还是模块
比如只用到了addvec.o,就不会同时打包libvector.a里面的multvec.o


7.6.3 Use static library resolve reference

集合E

  • 所有重定位的目标文件

集合U

  • 未解析的符号引用

集合D

  • 已经定义的符号

最后U为空,则成功

按顺序扫描,所以,一般将库放在命令行的结尾。
若是有特殊的需求,比如循环引用,也可以在命令行上重复导入某个库。
(出现这种情况,更好的办法应该是,这两个相互依赖的模块放在同一个.a存档文件中)。


7.7 Relocation - 重定位

合并输入模块,为每个符号分配运行时地址

步骤
1.重定位(section)节和(symbol)符号定义

  • 合并各文件里同名的节,运行地址赋给每个指令
    这一步完成时,程序中的每一个指令和全局变量都有唯一的运行时存储器地址了。

2.重定位符号引用

  • 链接器修改引用指向运行时地址,依靠relocation entry(重定位条目)

重定位节(section)
可以看到Relocate里的ADDR是0
但Exec里的ADDR是4000e8
(动态链接又是相对的了,7.9)

1
2
3
4
5
6
7
8
9
10
11
12
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
# Relocate:
[ 1] .text             PROGBITS         0000000000000000  00000040
       000000000000003e  0000000000000000  AX       0     0     1
# Executable:
[ 1] .text             PROGBITS         00000000004000e8  000000e8
       0000000000000061  0000000000000000  AX       0     0     1
# Dynamic:
[11] .text             PROGBITS         00000000000004f0  000004f0
       0000000000000202  0000000000000000  AX       0     0     16

重定位符号(Symbol)
Value不同

1
2
3
4
5
6
7
8
Symbol Table '.symtab'
Num:    Value          Size Type    Bind   Vis      Ndx Name
# Relocate:
      15: 000000000000000c     4 OBJECT  GLOBAL DEFAULT    3 ini_global
# Executable:
      12: 0000000000601018     4 OBJECT  GLOBAL DEFAULT    5 ini_global
# Dynamic:
      50: 0000000000201034     4 OBJECT  GLOBAL DEFAULT   21 ini_global

7.7.1 Relocation Entry - 重定位条目

汇编器遇到未知的引用生成重定位条目
函数放到.rel.text
未初始化数据放到.rel.data

每个条目的格式

1
2
3
4
5
6
struct{
    long offset;          //在所在节的偏移量
    long type: 32,            
         symbol: 32;      //引用指向的symbol
    long addend;          
}

两种类型:
1.R_X86_64_PC32 相对寻址
2.R_X86_64_32 直接寻址

1
2
3
4
5
Relocation section '.rela.text' at offset 0x398 contains 3 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000015  000c00000002 R_X86_64_PC32     0000000000000000 defined_fun - 4
000000000021  000d00000002 R_X86_64_PC32     0000000000000000 array - 4
000000000026  001400000004 R_X86_64_PLT32    0000000000000000 referenced_sum - 4

7.7.2 Relocating Symbol References

还是书上的例子比较详细…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// s : section
// r: relocation entry
for_each section s {
        for_each relocation entry r {
                refptr = s + r.offset;

                if ( r.type == R_X86_64_PC32 ) {
                        refaddr = ADDR ( s ) + r.offset;
                        *refptr = (unsigned)( ADDR ( r.symbol ) + r.addend - refaddr );
                }

                if ( r.type == R_X86_64_32 ) {
                        *refptr = (unsigned)( ADDR ( r.symbol ) + r.addend );
                }
        }
}

PC-relative 相当于 s[r.offset] = r.symbol - ( s + r.offset ) + r.addend r.symbol: symbol所在的地址(call发生时候的地址) s + r.offset: 函数真正的运行地址 r.addend: 运行call时PC的变化 - 1

也就是说0x08 = 62c - 620 + (-4)

1
2
3
4
5
6
7
# RELO OBJ
  25:	e8 00 00 00 00       	callq  2a <main+0x23>

# EXEC OBJ
 61f:	e8 08 00 00 00       	callq  62c <referenced_sum>
 
 000000000000062c <referenced_sum>

然后运行时候的call的过程就是刚好相反:

61f + 5(下一个指令的位置,也是为什么addend是-4)+ 8 = 62c


Absolute

1
4004d9: bf 18 10 60 00 mov $0x601018, %edi = &array

array 就直接定位到了0x601018
(我这里没重定位绝对引用的就抄书了qwqqwqqwq)



7.8 Executable Object File - 可执行

格式:

组成 解释
ELF header 只读
segment header table
.init
.text
.radata
.data 读写
.bss
.symtab 不加载进内存
.debug
.line
.strtab
Section header table

Program header table程序头部表描述了文件里的每一片映射到的连续内存段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 部分 Program Headers:
# 静态链接出来的 ld -o prog main.o sum.o, 代码段地址是0x400000
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000001b8 0x00000000000001b8  R E    0x200000
  LOAD           0x0000000000001000 0x0000000000601000 0x0000000000601000
                 0x0000000000000028 0x0000000000000030  RW     0x200000

# 动态(默认)链接出来的 gcc main.c sum.c,代码段地址是相对地址0
 Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000000008d0 0x00000000000008d0  R E    0x200000
  LOAD           0x0000000000000de0 0x0000000000200de0 0x0000000000200de0
                 0x0000000000000260 0x0000000000000270  RW     0x200000


7.9 Loading - 加载可执行文件

输入./prog后

加载Load

elelelelel

代码段总是从地址0x0400000处开始,它保存编译程序的机器代码

data段在接下来的一个4KB对齐的地址处,保存已初始化的全局C变量和静态变量

bss段记录的是未初始化的全局C变量,事实上它并不占据目标文件的任何空间,只是一个占位符

运行时堆在接下来的第一个4KB对齐的地址处,通过调用malloc库向上增长,用于程序的动态内存管理

共享库段,用于加载共享库、映射共享内存和文件I/O,使用mmap和unmap函数申请和释放新的内存区

用户栈占据进程地址空间的最高部分,并向下增长,用于存放调用过程中的局部变量、函数返回地址、参数

内核代码和数据、物理存储器,它们均被映射到所有进程共享的物理页面,这就为内核提供一个便利的方法来访问内存中任何特定的位置。对于每个进程来说他们均是一样的

最顶层的内核地址空间包括了与进程有关的数据结构,如页表、内核在进程的上下文结构task_struct和mm结构,内核栈

hahahahahallllll



7.10 Shared Library - 共享库

加载时的链接

优点

  1. 与共享库连接的可执行文件只包含它需要的函数的引用表,而不是所有的函数代码,只有在程序执行时, 那些需要的函数代码才被拷贝到内存中。
  2. 操作系统使用虚拟内存,使得一份共享库驻留在内存中被多个程序使用,也同时节约了内存。
  3. 修改后只需要重新编译库,而不用编译使用库的程序

缺点

  • 运行时要去链接库会花费一定的时间,执行速度相对会慢一些
1
2
gcc -shared -fpic -o libvector.so addvec.c multvec.c #创建
gcc -o prog21 main2.c ./libvector.so                 #链接

qaq

ppppps
没加-fpic会报错…

1
2
3
gcc -shared -o nofpicvec.so addvec.c multvec.c

/usr/bin/ld: /tmp/ccasjaEP.o: relocation R_X86_64_PC32 against symbol `addcnt' can not be used when making a shared object; recompile with -fPIC


运行时的链接

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <dlfcn.h>

/* 打开共享库,成功返回指向句柄的指针,失败返回null */
void *dlopen(const char *filename, int flag);

/* 解析符号,输入是共享库句柄指针和符号名,成功返回符号地址,失败返回null */
void *dlsym(void *handle, char *symbol);

/* 卸载共享库,如果此时没有其他共享库正在使用这个共享库的话 */
int dlclose(void *handle)

/* 返回上面三个api发生的错误,需要手动调用,没有错误返回null */
const char *dlerror(void)
1
gcc -rdynamic -o prog2r dll.c -ldl


7.12 Position Independent Code - 位置无关

编译GNU/Linux共享库, 为什么要用PIC编译?(转)
PLT and GOT - the key to code sharing and dynamic libraries

实现多进程共享内存中唯一一个的库代码的方法

  • 如果固定每个库分配到的内存片
    1. 不使用的库也会被分配空间
    2. 必须保证每个库更新之后片也不会重叠,就会变得很难管理

可以在任意位置加载而无需链接器进行修改,这样的代码被称作位置无关代码(PIC)


运行时数据段和代码段的偏移量不变

读写数据节总是放在到这个库代码节的一个已知的偏移位置
即 我希望对象的地址 = 当前地址 + 已知固定的偏移。

为了利用这一特点,编译器在数据段的开头创建了一个全局偏移表(GOT)。在GOT中,目标模块所引用的每个全局数据对象都对应一个表项。编译器同时为GOT中的每个表项生成了一个重定位记录。在加载时,动态链接器重定位GOT中的每个表项,使其包含正确的绝对地址。每个包含全局数据引用的目标模块都有其自己的GOT。

7.12.1 数据

此处输入图片的描述

1
2
3
4
5
# objdump -d libvec.so
000000000000064a <addvec>:
 65d:	48 8b 05 84 09 20 00 	mov    0x200984(%rip),%rax        # 200fe8 <[email protected]@Base-0x3c>
 664:	8b 00                	mov    (%rax),%eax
 666:	8d 50 01             	lea    0x1(%rax),%edx
1
2
3
4
# readelf --relocs libvector.so 
Relocation section '.rela.dyn' at offset 0x478 contains 9 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000200fe8  000900000006 R_X86_64_GLOB_DAT 0000000000201024 addcnt + 0

这里是%rip + 0x200984 = 0x200fe8 ( GOT 表里的位置 )
这样,加载时候的重定位只需要找到addcnt的真实地址,然后放到GOT表里就行了,不需要修改代码的任何值


7.12.2 函数

延迟绑定 lazy binding

在这里使用的间接性被称为一个过程连接表或PLT。代码不会直接调用一个外部函数,只会通过一个PLT存根。

此处输入图片的描述

1
2
3
# objdump -d prog21
000000000000075a <main>:
778:	e8 c3 fe ff ff       	callq  640 <[email protected]>
  • 首先main函数跳到plt表里addvec的位置0x640
1
2
3
4
5
6
7
# objdump -D prog21
Disc of section .plt:

0000000000000640 <[email protected]>:
 640:	ff 25 da 09 20 00    	jmpq   *0x2009da(%rip)        # 201020 <addvec>
 646:	68 01 00 00 00       	pushq  $0x1
 64b:	e9 d0 ff ff ff       	jmpq   620 <.plt>
1
2
3
4
# readelf --relocs prog21
Relocation section '.rela.plt' at offset 0x5d8 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000201020  000400000007 R_X86_64_JUMP_SLO 0000000000000000 addvec + 0
  • plt跳到addvec重定位的位置0x201020
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Disc of section .got.plt:

0000000000201000 <_GLOBAL_OFFSET_TABLE_>:
  201000:	e0 0d                	loopne 20100f <_GLOBAL_OFFSET_TABLE_+0xf>
  201002:	20 00                	and    %al,(%rax)
	...
  201018:	36 06                	ss (bad) 
  20101a:	00 00                	add    %al,(%rax)
  20101c:	00 00                	add    %al,(%rax)
  20101e:	00 00                	add    %al,(%rax)
  201020:	46 06                	rex.RX (bad) 
  201022:	00 00                	add    %al,(%rax)
  201024:	00 00                	add    %al,(%rax)
	...
  • 初始状态下,可以看到上面0x201020的位置是…emmmm空的…
  • 所以返回去运行下一条指令 pushq 1, 然后跳去.plt
1
2
3
4
0000000000000620 <.plt>:
 620:	ff 35 e2 09 20 00    	pushq  0x2009e2(%rip)        # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
 626:	ff 25 e4 09 20 00    	jmpq   *0x2009e4(%rip)        # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>
 62c:	0f 1f 40 00          	nopl   0x0(%rax)
  • plt[0]就把GOT[1](不知道是什么参数)push进栈
  • 然后往下召唤GOT[2]里的动态链接器,确定addvec运行时的真正地址,再把地址赋给0x201020
  • 然后以后再找的时候就可以从GOT里取了

这就是延迟绑定(lazy binding)
推迟解析实际用到的函数,避免不需要的重定位。



7.13 Library Interpositioning - 库打桩

用自己的代码取代库
编译,链接,运行三种

基本思想:给定一个目标函数,创建一个原型与目标函数完全一样的包装函数,使用某种特殊的打桩机制,欺骗系统调用包装函数而不是打桩函数。


7.13.1 编译时打桩 (Compile-Time Interpositioning)

1
2
3
4
5
6
7
8
9
10
// Example program int.c
#include <malloc.h>
#include <stdio.h>
int main ()

{
        int *p = malloc ( 32 );
        free ( p );
        return ( 0 );
}
1
2
3
4
5
6
//Local malloc.h file
#define malloc(size) mymalloc(size)
#define free(ptr) myfree(ptr)

void *mymalloc(size_t size);
void myfree(void *ptr);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// wrapper functions in mymalloc.c
#ifdef COMPILETIME //编译时传入参数-DCOMPILETIME可编译

#include <malloc.h>
#include <stdio.h>

void *mymalloc ( size_t size ) {

        void *ptr = malloc ( size );
        printf ( "malloc(%d)=%p\n", (int)size, ptr );
        return ptr;
}

void myfree ( void *ptr )

{
        free ( ptr );
        printf ( "free(%p)\n", ptr );
}
#endif
1
2
3
# 编译和链接命令
gcc -DCOMPILETIME -c mymalloc.c
gcc -I. -o intc int.c mymalloc.o

-I.参数告诉C预处理器在搜索系统目录之前,现在当前目录中查找malloc.h,通过malloc.h文件中

1
2
#define malloc(size) mymalloc(size)
#define free(ptr) myfree(ptr)

指示预处理器用对相应包装函数的调用替换掉对目标函数的调用。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifdef LINKTIME
#include <stdio.h>

void *__real_malloc ( size_t size );
void __real_free ( void *prt );

void *__wrap_malloc ( size_t size ) {
        void *ptr = __real_malloc ( size );
        printf ( "malloc(%d)=%p\n", (int)size, ptr );
        return ptr;
}

void __wrap_free ( void *ptr ) {
        __real_free ( ptr );
        printf ( "free(%p)\n", ptr );
}

#endif

Linux 静态链接器支持用 --wrap f 标志进行链接时打桩。这个标志告诉链接器将 f 解析成 __wrap_f,讲 __real_ 解析成 f

1
2
3
gcc -DLINKTIME -c mymalloc.c
gcc -c int.c
gcc -Wl,--wrap,malloc -Wl,--wrap,free -o intl int.o mymalloc.o

-Wl,option标志把 option传递给链接器,并把 option 中的 替换成空格
-Wl,--wrap,malloc--wrap malloc传递给链接器
-Wl,--wrap,free--wrap free传递给链接器


7.13.3 运行时打桩 (Run-Time Interpositioning)

编译时打桩需要能够访问程序的源代码
链接时打桩需要能够访问程序的可重定位文件
运行时打桩只需要能够访问可执行目标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#ifdef RUNTIME
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>

// malloc wrapper function
void *malloc ( size_t size ) {

        void *( *mallocp ) ( size_t size );

        // get address of libc malloc
        mallocp = dlsym ( RTLD_NEXT, "malloc" );

        if ( ( error = dlerror () ) != NULL ) {
                fputs ( error, stderr );
                exit ( 1 );
        }

        char *ptr = mallocp ( size );
        printf ( "malloc %p size %p\n", ptr, (int)size );
        return ptr;
}

// free wrapper function
void free ( void *ptr ) {
        void ( *freep ) ( void *ptr );
        char *error;

        if ( !ptr )
                return;

        // get address of libc free
        freep = dlsym ( RTLD_NEXT, "free" );
        if ( ( error = dlerror () ) != NULL ) {
                fputs ( error, stderr );
                exit ( 1 );
        }

        freep ( ptr );
        printf ( "free %p\n", ptr );
}
#endif
1
2
3
gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl
gcc -o intr int.c
LD_PRELOAD="./mymalloc.so" ./intr

如果 LD_PRELOAD 环境变量被设置为一个共享库路径名的列表(以空格键或分号分隔),那么当加载和执行一个程序,需要解析未定义的引用时,动态链接器(LD_LINUX.SO)会先搜索LD_PRELAOD库,然后才搜索其他库。