ELF链接原理

26 篇文章 0 订阅
本文详细介绍了静态链接和动态链接的区别,包括它们的链接过程、内存布局以及在程序执行中的作用。静态链接将多个模块合并成单一可执行文件,而动态链接则在运行时加载库,节省内存。文中还深入探讨了位置无关代码(PIC)、全局偏移表(GOT)、过程链接表(PLT)和延迟绑定的概念,展示了如何通过GDB进行调试和理解这些机制。
摘要由CSDN通过智能技术生成

1. 示例代码

// main
extern int g_nShared;
extern void func(int *a, int *b) ;

void main(void) {
    int n = 2;

    func(&n, &g_nShared);
}



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

int g_nShared = 1;
int g_nTmp = 0;

void func(int *a, int *b) {
    if(!a || !b) return;
    g_nTmp = *a;
    *a = *b;
    *b = g_nTmp;
}

2. 静态链接

按时间分类:

  • compile time linking
  • load time linking
  • run time linking

多个模块合成一个可执行文件,方法有两种:

  • 按顺序叠加,显然浪费空间(因为要按页对齐);
  • 相似节合并,比如两个模块的text合成一个。这是现在的linker所采纳的。

链接过程

编译:

gcc -static -fno-stack-protector main.c func.c -save-temps --verbose -o func.out 

-save-temps可以保留中间的编译文件。

看下main.o的section header,VMA(虚拟内存地址)和LMA(加载内存地址)都是0,因为还没有链接。

$ objdump -h main.o

main.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000025  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  0000000000000000  0000000000000000  00000065  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000065  2**0
                  ALLOC
  3 .comment      0000002a  0000000000000000  0000000000000000  00000065  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  0000008f  2**0
                  CONTENTS, READONLY
  5 .eh_frame     00000038  0000000000000000  0000000000000000  00000090  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

而链接后的func.out,相似节已经被合并,VMA和LMA都被填充:

$ objdump -h func.out

func.out:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
...
  5 .text         0008f490  00000000004004d0  00000000004004d0  000004d0  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
...
 18 .got          000000f8  00000000006b8ef8  00000000006b8ef8  000b8ef8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 19 .got.plt      000000d0  00000000006b9000  00000000006b9000  000b9000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 20 .data         00001af0  00000000006b90e0  00000000006b90e0  000b90e0  2**5
                  CONTENTS, ALLOC, LOAD, DATA
...
 25 .bss          000016f8  00000000006bb2e0  00000000006bb2e0  000bb2d8  2**5
                  ALLOC

反汇编看下main.o, func地址偏移是0:

$ objdump -d -j .text main.o

main.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	48 83 ec 10          	sub    $0x10,%rsp
   8:	c7 45 fc 02 00 00 00 	movl   $0x2,-0x4(%rbp)
   f:	48 8d 45 fc          	lea    -0x4(%rbp),%rax
  13:	48 8d 35 00 00 00 00 	lea    0x0(%rip),%rsi        # 1a <main+0x1a>
  1a:	48 89 c7             	mov    %rax,%rdi
  1d:	e8 00 00 00 00       	callq  22 <main+0x22>
  22:	90                   	nop
  23:	c9                   	leaveq 
  24:	c3                   	retq 

相比,func.out已经获取了func函数的偏移:

$ objdump -d -j .text func.out | grep -A 20 "<main>"
0000000000400b6d <main>:
  400b6d:	55                   	push   %rbp
  400b6e:	48 89 e5             	mov    %rsp,%rbp
  400b71:	48 83 ec 10          	sub    $0x10,%rsp
  400b75:	c7 45 fc 02 00 00 00 	movl   $0x2,-0x4(%rbp)
  400b7c:	48 8d 45 fc          	lea    -0x4(%rbp),%rax
  400b80:	48 8d 35 69 85 2b 00 	lea    0x2b8569(%rip),%rsi        # 6b90f0 <g_nShared>
  400b87:	48 89 c7             	mov    %rax,%rdi
  400b8a:	e8 03 00 00 00       	callq  400b92 <func>	# 400b8f+3==func
  400b8f:	90                   	nop
  400b90:	c9                   	leaveq 
  400b91:	c3                   	retq   

0000000000400b92 <func>:
  400b92:	55                   	push   %rbp
  400b93:	48 89 e5             	mov    %rsp,%rbp
  400b96:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)

另外对于main.o重定位文件,最重要的是重定位表:

$ objdump -r main.o

main.o:     file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
0000000000000016 R_X86_64_PC32     g_nShared-0x0000000000000004
000000000000001e R_X86_64_PLT32    func-0x0000000000000004

RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE 
0000000000000020 R_X86_64_PC32     .text

0x16,是指objdump -d -j .text main.o那里,偏移0x16的地方需要重定位。

R_X86_64_PC32,是相对偏移类型。

-0x4是Elf32_Rela.r_addend的值。

静态链接库

后缀.a就是静态链接库,根据上面静态编译的func.out也能想到,.a是一组目标文件经过了压缩打包,比如printf.o, scanf.o, malloc.o。 压缩打包的工具是ar,可以看下libc.a的内容:

$ ar -t /usr/lib32/libc.a
init-first.o
libc-start.o
sysdep.o
version.o
check_fds.o
libc-tls.o
elf-init.o
...

3. 动态链接

目的:多个程序使用内存中的一个库,节省内存。

编译示例代码func.c为动态库:

gcc -shared -fpic func.c -m32 -o func.so

pic放到后面解释。

主程序修改一下,调用两次动态库的func函数, 并按注释的gcc命令编译:

// gcc main.c ./func.so -no-pie -m32 -o main.out
// -no-pie:  Don't produce a position independent executable.
// 注意要带上这个参数,不然没有用.got.plt section

extern int g_nShared;
extern void func(int *a, int *b) ;

void main(void) {
    int n = 2;

    func(&n, &g_nShared);
    func(&n, &g_nShared);
}

PIC

PIC: Position Independent Code, 位置无关代码,代码和数据的引用与地址无关,程序可以被加载到地址空间的任意位置。它是共享库必有的属性,这样才能被多个进程使用。

PIC 使用 GOT 来引用变量和函数的绝对地址,把位置独立的引用重定向到绝对位置。

  • 对于模块外部引用的全局变量和全局函数,用 GOT 表的表项内容作为地址来间接寻址;
  • 对于本模块内的静态变量和静态函数,用相对于GOT 表的首地址的偏移量来引用。
$ objdump -h func.so

func.so:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .note.gnu.build-id 00000024  00000000000001c8  00000000000001c8  000001c8  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
...
 15 .got          00000028  0000000000200fd8  0000000000200fd8  00000fd8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 16 .got.plt      00000018  0000000000201000  0000000000201000  00001000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
...
$ readelf -r func.so

Relocation section '.rela.dyn' at offset 0x400 contains 8 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000200e78  000000000008 R_X86_64_RELATIVE                    5d0
...
000000200ff0  000600000006 R_X86_64_GLOB_DAT 0000000000201028 g_nTmp + 0
...

可以看到这个全局变量g_nTmp位于.got section内。

.got可以保存全局变量引用,那么反汇编看下func():

$ objdump -M intel -d func.so | grep -A 20 "<func>"
00000000000005da <func>:
 5da:	55                   	push   rbp
 5db:	48 89 e5             	mov    rbp,rsp
 5de:	48 89 7d f8          	mov    QWORD PTR [rbp-0x8],rdi
 5e2:	48 89 75 f0          	mov    QWORD PTR [rbp-0x10],rsi
 5e6:	48 83 7d f8 00       	cmp    QWORD PTR [rbp-0x8],0x0
 5eb:	74 33                	je     620 <func+0x46>
 5ed:	48 83 7d f0 00       	cmp    QWORD PTR [rbp-0x10],0x0
 5f2:	74 2c                	je     620 <func+0x46>
 5f4:	48 8b 45 f8          	mov    rax,QWORD PTR [rbp-0x8]
 5f8:	8b 10                	mov    edx,DWORD PTR [rax]
 5fa:	48 8b 05 ef 09 20 00 	mov    rax,QWORD PTR [rip+0x2009ef]        # 200ff0 <g_nTmp-0x38>
 601:	89 10                	mov    DWORD PTR [rax],edx
 603:	48 8b 45 f0          	mov    rax,QWORD PTR [rbp-0x10]
 607:	8b 10                	mov    edx,DWORD PTR [rax]
 609:	48 8b 45 f8          	mov    rax,QWORD PTR [rbp-0x8]
 60d:	89 10                	mov    DWORD PTR [rax],edx
 60f:	48 8b 05 da 09 20 00 	mov    rax,QWORD PTR [rip+0x2009da]        # 200ff0 <g_nTmp-0x38>
 616:	8b 10                	mov    edx,DWORD PTR [rax]
 618:	48 8b 45 f0          	mov    rax,QWORD PTR [rbp-0x10]
 61c:	89 10                	mov    DWORD PTR [rax],edx

rip+0x2009ef == 0x601+0x2009ef == 0x200ff0, 这就是重定位表里的offset。

GOT与PLT

一个程序的数据段和代码段的相对距离是不变的常量,GOT全局偏移表可以保存全局变量和库函数的引用。

每个条目占8字节,加载时会进行重定位并填入符号的绝对地址。

为了引入RELRO保护机制,GOT被拆分为两部分:

  • .got,不需要要延迟绑定,用于保存全局变量引用,运行时只读;
  • .got.plt,需要延迟绑定,用来保存函数引用,运行时可读可写;

还有一个.plt.got,存放 __cxa_finalize 函数对应的 PLT 条目,可先忽略。

.got.plt section包含一个GOT数组:

  • GOT[0]存放.dynamic section地址;
  • GOT[1]存放reloc entries;
  • GOT[2]存放ld-linux.so的_dl_runtime_resolve动态解析函数地址;
  • GOT[3]存放__stack_chk_fail, 用来检查canary安全防护;
  • GOT[4]存放__libc_start_main, 它会调用main();
  • GOT[5]开始是其它延迟加载的函数地址,本例则是func().

可以用010editor看一下main.out section header,里面有内存偏移,条目大小(0x4,数量0x18/0x46):
请添加图片描述

另外是.plt section,Procedure Linkage Table,虽说翻译过来是表,但它是可执行的(不是函数指针):

  • PLT[0]:跳转到动态链接器ld.so;
  • PLT[1]:__libc_start_main;
  • PLT[2]开始则是被调用的各个函数条目。

延迟绑定Lazy Binding

静态链接要比动态链接的程序稍微快点,两个原因:

  • 动态链接程序对于全局和静态的数据访问、都要进行GOT定位,,然后间接寻址;对于模块间的调用,也要先定位GOT,然后再进行间接跳转。
  • 链接器会寻找并装载所需要的共享对象,然后进行符号査找地址重定位等工作。

延迟绑定(Lazy Binding)的基本思想就是,当函数第一次被用到时才进行绑定(符号査找、重定位等),如果没有用到则不进行绑定。

和windows的延迟加载模块不一样

ELF通过PLT和GOT的配合来实现延迟加载。每个库函数都有一组PLT和GOT。

看下编译好的main.out:

$ readelf -S main.out
...
  [ 9] .rel.dyn          REL             08048390 000390 000010 08   A  5   0  4
  [10] .rel.plt          REL             080483a0 0003a0 000018 08  AI  5  23  4
  [11] .init             PROGBITS        080483b8 0003b8 000023 00  AX  0   0  4
  [12] .plt              PROGBITS        080483e0 0003e0 000040 04  AX  0   0 16
  [13] .plt.got          PROGBITS        08048420 000420 000008 08  AX  0   0  8
...
  [21] .dynamic          DYNAMIC         08049f08 000f08 0000f0 08  WA  6   0  4
  [22] .got              PROGBITS        08049ff8 000ff8 000008 04  WA  0   0  4
  [23] .got.plt          PROGBITS        0804a000 001000 000018 04  WA  0   0  4
...

.rel.dyn 记录了加载时需要重定位的变量,.rel.plt 记录的是需要重定位的函数。

gdb调试,第一次调用func:

→  0x8048583 <main+61>        call   0x8048410 <func@plt>
    ↳   0x8048410 <func@plt+0>     jmp    DWORD PTR ds:0x804a014
        0x8048416 <func@plt+6>     push   0x10
        0x804841b <func@plt+11>    jmp    0x80483e0
        0x8048420 <__gmon_start__@plt+0> jmp    DWORD PTR ds:0x8049ff8
        0x8048426 <__gmon_start__@plt+6> xchg   ax, ax
        0x8048428                  add    BYTE PTR [eax], al
gef➤  x/4x 0x804a014
0x804a014:	0x08048416	0x00000000	0x00000000	0x00000000
gef➤  si
...
    0x8048400 <__libc_start_main@plt+0> jmp    DWORD PTR ds:0x804a010
    0x8048406 <__libc_start_main@plt+6> push   0x8
    0x804840b <__libc_start_main@plt+11> jmp    0x80483e0
 →  0x8048410 <func@plt+0>     jmp    DWORD PTR ds:0x804a014
    0x8048416 <func@plt+6>     push   0x10
    0x804841b <func@plt+11>    jmp    0x80483e0
gef➤  si
...
    0x8048406 <__libc_start_main@plt+6> push   0x8
    0x804840b <__libc_start_main@plt+11> jmp    0x80483e0
    0x8048410 <func@plt+0>     jmp    DWORD PTR ds:0x804a014
 →  0x8048416 <func@plt+6>     push   0x10
    0x804841b <func@plt+11>    jmp    0x80483e0
    0x8048420 <__gmon_start__@plt+0> jmp    DWORD PTR ds:0x8049ff8
    0x8048426 <__gmon_start__@plt+6> xchg   ax, ax

0x804a014, 正位于.got.plt。也就是说func@plt要进入.got.plt去寻找func的地址。现在func还没有加载,jmp里保存的就是jmp前的下一条指令(0x8048416),

 →  0x8048416 <func@plt+6>     push   0x10
    0x804841b <func@plt+11>    jmp    0x80483e0

这个0x10,是指func函数在.rel.plt中的偏移,在010editor里也可以看一下,下图中有3个函数数据项,依次要填入GOT[3:6],本次延迟加载实验关注点是,执行一次func后0x804a014要填入func的地址:

请添加图片描述

继续si跟进,进入了.plt:

 →  0x80483e0                  push   DWORD PTR ds:0x804a004
    0x80483e6                  jmp    DWORD PTR ds:0x804a008
...
gef➤  x /10wx 0x804a000
0x804a000:	0x08049f08	0xf7ffd940	0xf7fead40	0x080483f6
0x804a010:	0xf7df6eb0	0x08048416	0x00000000	0x00000000
...
gef➤  x /10x 0x804a008
0x804a008:	0xf7fead40  ...

上面输出了.got.plt的内容:

  • GOT[0] – 0x08049f08 – .dynamic
  • GOT[1] – 0xf7ffd940 – reloc entries
  • GOT[2] – 0xf7fead40 –_dl_runtime_resolve
  • GOT[3] – 0x080483f6–__stack_chk_fail
  • GOT[4] – 0xf7df6eb0 – __libc_start_main
  • GOT[5] – 待填充真实的func地址GOT[1]

也就是说,PLT[0]这里先压栈func函数在.rel.plt中的偏移0x10,再压栈GOT[1]。

跟进这个jmp _dl_runtime_resolve,就调用了链接器:

 → 0xf7fead40                  push   eax
   0xf7fead41                  push   ecx
   0xf7fead42                  push   edx
...
0xf7fead40 in ?? () from /lib/ld-linux.so.2

多单步几次,就进入了func:

 → 0xf7fcd45d <func+0>         push   ebp
   0xf7fcd45e <func+1>         mov    ebp, esp
   0xf7fcd460 <func+3>         call   0xf7fcd49f <__x86.get_pc_thunk.ax>
   0xf7fcd465 <func+8>         add    eax, 0x1b9b

回到main,第二次调用func:

 →  0x8048599 <main+83>        call   0x8048410 <func@plt>
   ↳   0x8048410 <func@plt+0>     jmp    DWORD PTR ds:0x804a014
       0x8048416 <func@plt+6>     push   0x10
       0x804841b <func@plt+11>    jmp    0x80483e0
gef➤  x /10x 0x804a014
0x804a014:	0xf7fcd45d

可以看到,0x804a014,也就是GOT[5]里已经填充了func的函数地址。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值