NJUOS-17-动态链接和加载

本文最后更新于:1 年前

本次课回答的问题

  • Q1: 可执行文件是如何被操作系统加载的?
  • Q2: 什么是动态链接/动态加载?

本次课主要内容

  • 若干真正的静态 ELF 加载器
  • 动态链接和加载

静态 ELF 加载器:实现

image-20230103101853443

/usr/include/elf.h -> 有我们想知道的elf文件的全部信息!!!很好用!!!

Readelf -h /bin/ls -> 可以看到和上面类似的,X86_64的信息,也是可以从elf文件中获取出来的昂!

在操作系统上实现 ELF Loader

image-20230103102817207

可执行文件

  • 一个描述了状态机的初始状态 (迁移) 的数据结构
    • 不同于内存里的数据结构,“指针” 都被 “偏移量” 代替
    • 数据结构各个部分定义:/usr/include/elf.h

加载器 (loader)

  • 解析数据结构 + 复制到内存 + 跳转
  • 创建进程运行时初始状态 (argv, envp, …)
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <elf.h>
#include <fcntl.h>
#include <sys/mman.h>

#define STK_SZ (1 << 20)
#define ROUND(x, align) (void *)(((uintptr_t)x) & ~(align - 1))
#define MOD(x, align) (((uintptr_t)x) & (align - 1))
#define push(sp, T, ...) ({ *((T*)sp) = (T)__VA_ARGS__; sp = (void *)((uintptr_t)(sp) + sizeof(T)); })

void execve_(const char *file, char *argv[], char *envp[]) {
// WARNING: This execve_ does not free process resources.
int fd = open(file, O_RDONLY);
assert(fd > 0);
Elf64_Ehdr *h = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0);
assert(h != (void *)-1);
assert(h->e_type == ET_EXEC && h->e_machine == EM_X86_64);

Elf64_Phdr *pht = (Elf64_Phdr *)((char *)h + h->e_phoff);
for (int i = 0; i < h->e_phnum; i++) {
Elf64_Phdr *p = &pht[i];
if (p->p_type == PT_LOAD) {
int prot = 0;
if (p->p_flags & PF_R) prot |= PROT_READ;
if (p->p_flags & PF_W) prot |= PROT_WRITE;
if (p->p_flags & PF_X) prot |= PROT_EXEC;
void *ret = mmap(
ROUND(p->p_vaddr, p->p_align), // addr, rounded to ALIGN
p->p_memsz + MOD(p->p_vaddr, p->p_align), // length
prot, // protection
MAP_PRIVATE | MAP_FIXED, // flags, private & strict
fd, // file descriptor
(uintptr_t)ROUND(p->p_offset, p->p_align)); // offset
assert(ret != (void *)-1);
memset((void *)(p->p_vaddr + p->p_filesz), 0, p->p_memsz - p->p_filesz);
}
}
close(fd);

static char stack[STK_SZ], rnd[16];
void *sp = ROUND(stack + sizeof(stack) - 4096, 16);
void *sp_exec = sp;
int argc = 0;

// argc
while (argv[argc]) argc++;
push(sp, intptr_t, argc);
// argv[], NULL-terminate
for (int i = 0; i <= argc; i++)
push(sp, intptr_t, argv[i]);
// envp[], NULL-terminate
for (; *envp; envp++) {
if (!strchr(*envp, '_')) // remove some verbose ones
push(sp, intptr_t, *envp);
}
// auxv[], AT_NULL-terminate
push(sp, intptr_t, 0);
push(sp, Elf64_auxv_t, { .a_type = AT_RANDOM, .a_un.a_val = (uintptr_t)rnd } );
push(sp, Elf64_auxv_t, { .a_type = AT_NULL } );

asm volatile(
"mov $0, %%rdx;" // required by ABI
"mov %0, %%rsp;"
"jmp *%1" : : "a"(sp_exec), "b"(h->e_entry));
}

int main(int argc, char *argv[], char *envp[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s file [args...]\n", argv[0]);
exit(1);
}
execve_(argv[1], argv + 1, envp);
}

  • 使用
1
2
3
4
gcc loader-static.c -o loader
./loader minimal
./loader dfs
./loader env

image-20230103113339178

image-20230103113352691

  • Strace ./loader minimal:

不是loader的第一个系统调用是execve,而是shell解释器调用的execve,让loader跑起来了。loader执行过程中,没有调用任何的execve,但是让程序正常跑起来了。

自定义loader,把要加在的,ELF中的代码段,数据段给mmap映射到当前进程虚拟地址空间对应的区域中,并让其跑起来!!!

image-20230103160514441

Loader相当于根据ELF文件的数据结构,把ELF文件放入内存,设置好堆栈之类的,就能跑了!!!

  • 上面的RTFM里面,就有很多的内容:

image-20230103160809776

image-20230103160844902

Loader是可以自己手动实现的!!!

image-20230103161514468

内存按照手册放好了,就可以很简单的实现一个loader。loader无非就是一个,把ELF文件中的数据结构解析,放进内存和寄存器中的对应位置,并让其执行起来的程序嘛!!!

  • 回想execve这个API,不也类似的?底层就是由open, mmap, close调用组合而成的。帮助我们将状态机的位置“set”好,数据结构都放好,然后就能够正常执行啦!!!
  • execve是内核态的,我们的代码还是用户态的呢。用户态就能完成这个操作,不需要进入操作系统内核哇。诶嘿,那我们自己做操作系统的时候,是不是就可以不用写类似的API。Loader写好,所有的程序通过Loader进行加载就行,程序就是先执行loader,再让loader构造好内存空间,执行loader对应的程序内容的加载和放置呢?

Boot Block Loader

加载操作系统内核?

  • 也是一个 ELF 文件
  • 解析数据结构 + 复制到内存 + 跳转

bootmain.c (i386/x86-64 通用)

  • 之前给大家调试过
    • 不花时间调试了
    • 马上有重磅主角登场
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <stdint.h>
#include <elf.h>
#include <x86/x86.h>

#define SECTSIZE 512
#define ARGSIZE 1024

static inline void wait_disk(void) {
while ((inb(0x1f7) & 0xc0) != 0x40);
}

static inline void read_disk(void *buf, int sect) {
wait_disk();
outb(0x1f2, 1);
outb(0x1f3, sect);
outb(0x1f4, sect >> 8);
outb(0x1f5, sect >> 16);
outb(0x1f6, (sect >> 24) | 0xE0);
outb(0x1f7, 0x20);
wait_disk();
for (int i = 0; i < SECTSIZE / 4; i ++) {
((uint32_t *)buf)[i] = inl(0x1f0);
}
}

static inline void copy_from_disk(void *buf, int nbytes, int disk_offset) {
uint32_t cur = (uint32_t)buf & ~(SECTSIZE - 1);
uint32_t ed = (uint32_t)buf + nbytes;
uint32_t sect = (disk_offset / SECTSIZE) + (ARGSIZE / SECTSIZE) + 1;
for(; cur < ed; cur += SECTSIZE, sect ++)
read_disk((void *)cur, sect);
}

static void load_program(uint32_t filesz, uint32_t memsz, uint32_t paddr, uint32_t offset) {
copy_from_disk((void *)paddr, filesz, offset);
char *bss = (void *)(paddr + filesz);
for (uint32_t i = filesz; i != memsz; i++) {
*bss++ = 0;
}
}

static void load_elf64(Elf64_Ehdr *elf) {
Elf64_Phdr *ph = (Elf64_Phdr *)((char *)elf + elf->e_phoff);
for (int i = 0; i < elf->e_phnum; i++, ph++) {
load_program(
(uint32_t)ph->p_filesz,
(uint32_t)ph->p_memsz,
(uint32_t)ph->p_paddr,
(uint32_t)ph->p_offset
);
}
}

static void load_elf32(Elf32_Ehdr *elf) {
Elf32_Phdr *ph = (Elf32_Phdr *)((char *)elf + elf->e_phoff);
for (int i = 0; i < elf->e_phnum; i++, ph++) {
load_program(
(uint32_t)ph->p_filesz,
(uint32_t)ph->p_memsz,
(uint32_t)ph->p_paddr,
(uint32_t)ph->p_offset
);
}
}

void load_kernel(void) {
Elf32_Ehdr *elf32 = (void *)0x8000;
Elf64_Ehdr *elf64 = (void *)0x8000;
int is_ap = boot_record()->is_ap;

if (!is_ap) {
// load argument (string) to memory
copy_from_disk((void *)MAINARG_ADDR, 1024, -1024);
// load elf header to memory
copy_from_disk(elf32, 4096, 0);
if (elf32->e_machine == EM_X86_64) {
load_elf64(elf64);
} else {
load_elf32(elf32);
}
} else {
// everything should be loaded
}

if (elf32->e_machine == EM_X86_64) {
((void(*)())(uint32_t)elf64->e_entry)();
} else {
((void(*)())(uint32_t)elf32->e_entry)();
}
}

image-20230103162607647

操作系统内核的Boot Loader本质上干的事情是一样的

BootLoader启动的时候,将操作系统(Kernel)这个ELF文件,搬到我们的内存里面来,然后把它跑起来。Loader都是一样的,创建一个状态机。但是区别在于:BootLoader的时候,操作系统并不存在!!!从磁盘把内容搬入内容,还需要别的机制(RTFM),调用一些硬件指令,把操作系统代码搬进来。然后一样的,进行ELF解析,把操作系统运行起来!!!

Linux 内核闪亮登场

image-20230103163152028

老师演示了一个能跑的Linux,编译之后,发现没有任何区别昂!!!也就是一个ELF文件而已昂!!!原视频40min开始,建议去看看,视频链接在References里面。

编译好,专门有make debug,甚至可以gdb调试linux的启动…太恐怖了……没有任何的魔法!!!

loader-static.c, bootmain.c 和 Linux 有本质区别吗?没有!

  • 解压缩源码包

  • make menuconfig (生成 .config 文件)

  • ```shell
    make bzImage -j8

    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
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92

    - 顺便给 Kernel 个补丁 (kernel/exit.c)

    ------

    编译结果

    - vmlinux (ELF 格式的内核二进制代码)
    - vmlinuz (压缩的镜像,可以直接被 QEMU 加载)
    - readelf 入口地址 0x1000000 (物理内存 16M 位置)
    - `__startup_64`: [RTFSC](https://elixir.bootlin.com/linux/latest/source/arch/x86/kernel/head64.c#L165); 调试起来!
    - 时刻告诉自己:不要怕,就是状态机 (和你们的 lab 完全一样)

    ```c
    #include <stdint.h>
    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <assert.h>
    #include <elf.h>
    #include <fcntl.h>
    #include <sys/mman.h>

    #define STK_SZ (1 << 20)
    #define ROUND(x, align) (void *)(((uintptr_t)x) & ~(align - 1))
    #define MOD(x, align) (((uintptr_t)x) & (align - 1))
    #define push(sp, T, ...) ({ *((T*)sp) = (T)__VA_ARGS__; sp = (void *)((uintptr_t)(sp) + sizeof(T)); })

    void execve_(const char *file, char *argv[], char *envp[]) {
    // WARNING: This execve_ does not free process resources.
    int fd = open(file, O_RDONLY);
    assert(fd > 0);
    Elf64_Ehdr *h = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0);
    assert(h != (void *)-1);
    assert(h->e_type == ET_EXEC && h->e_machine == EM_X86_64);

    Elf64_Phdr *pht = (Elf64_Phdr *)((char *)h + h->e_phoff);
    for (int i = 0; i < h->e_phnum; i++) {
    Elf64_Phdr *p = &pht[i];
    if (p->p_type == PT_LOAD) {
    int prot = 0;
    if (p->p_flags & PF_R) prot |= PROT_READ;
    if (p->p_flags & PF_W) prot |= PROT_WRITE;
    if (p->p_flags & PF_X) prot |= PROT_EXEC;
    void *ret = mmap(
    ROUND(p->p_vaddr, p->p_align), // addr, rounded to ALIGN
    p->p_memsz + MOD(p->p_vaddr, p->p_align), // length
    prot, // protection
    MAP_PRIVATE | MAP_FIXED, // flags, private & strict
    fd, // file descriptor
    (uintptr_t)ROUND(p->p_offset, p->p_align)); // offset
    assert(ret != (void *)-1);
    memset((void *)(p->p_vaddr + p->p_filesz), 0, p->p_memsz - p->p_filesz);
    }
    }
    close(fd);

    static char stack[STK_SZ], rnd[16];
    void *sp = ROUND(stack + sizeof(stack) - 4096, 16);
    void *sp_exec = sp;
    int argc = 0;

    // argc
    while (argv[argc]) argc++;
    push(sp, intptr_t, argc);
    // argv[], NULL-terminate
    for (int i = 0; i <= argc; i++)
    push(sp, intptr_t, argv[i]);
    // envp[], NULL-terminate
    for (; *envp; envp++) {
    if (!strchr(*envp, '_')) // remove some verbose ones
    push(sp, intptr_t, *envp);
    }
    // auxv[], AT_NULL-terminate
    push(sp, intptr_t, 0);
    push(sp, Elf64_auxv_t, { .a_type = AT_RANDOM, .a_un.a_val = (uintptr_t)rnd } );
    push(sp, Elf64_auxv_t, { .a_type = AT_NULL } );

    asm volatile(
    "mov $0, %%rdx;" // required by ABI
    "mov %0, %%rsp;"
    "jmp *%1" : : "a"(sp_exec), "b"(h->e_entry));
    }

    int main(int argc, char *argv[], char *envp[]) {
    if (argc < 2) {
    fprintf(stderr, "Usage: %s file [args...]\n", argv[0]);
    exit(1);
    }
    execve_(argv[1], argv + 1, envp);
    }

调试 Linux Kernel ELF Loader

fs/binfmt_elf.c: load_elf_binary

  • 这里我们看到了 Linux Kernel 里的面向对象 (同我们的 oslab)

让我们愉快地打个断点……

  • 当然是使用正确的工具了

    • ```shell
      script/gen_compile_commands.py
      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
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151

      - 思考题: 如何实现 “自动” 获得所有编译选项的工具?

      > 上面生成的这个东西,就能够放到vscode里面,作为配置进行使用的。

      - vscode 快捷键

      - ⌃/⌘ + P (`@`, `#`)
      - ⌃/⌘ + ⇧ + P - 任何你不知道按什么键的时候,搜索!

      - Linux Kernel 也不过如此!

      - 你们需要一个 “跨过一道坎” 的过程

      > vscode配置好,就很好用!!!现代化的工具还是必要的!!!!!!
      >
      > 配置好了之后,可以直接在vscode里面启动整个linux内核代码!!!!!!甚至可以打断点,命令行里输入,vscode里面就可以追踪处理昂!!!

      ![image-20230103164057027](https://cdn.jsdelivr.net/gh/alexanderliu-creator/blog_img/img/202301031640072.png)

      > 和我们的操作没有本质区别呀,也是遍历ELF的(PHT, Program Header Table),并进行校验处理!































































      # 动态链接和加载

      ## “拆解应用程序” 的需求

      > 随着库函数越来越大,希望项目能够 “运行时链接”。

      ![image-20230103164703924](https://cdn.jsdelivr.net/gh/alexanderliu-creator/blog_img/img/202301031647970.png)

      > 模块儿化方便更新!!!也减少了很多内存占用!!!

      减少库函数的磁盘和内存拷贝

      - 每个可执行文件里都有所有库函数的拷贝那也太浪费了
      - 只要大家遵守基本约定,不挑库函数的版本
      - “[Semantic Versioning](https://semver.org/)”
      - 否则发布一个新版本就要重新编译全部程序

      ------

      大型项目的分解

      - 编译一部分,不用重新链接
      - libjvm.so, libart.so, ...
      - NEMU: “把 CPU 插上 board”

      ![image-20230103164953442](https://cdn.jsdelivr.net/gh/alexanderliu-creator/blog_img/img/202301031649529.png)

      > 甚至可以动态换CPU...抽象成一个动态链接就好了。

      ## 动态链接:今天不讲 ELF

      和 ELF battle 的每一年:讲着讲着就讲不下去了

      - 其实不是讲不清楚,是大家跟不上
      - 根本原因:概念上紧密相关的东西在数据结构中被强行 “拆散” 了
      - `GOT[0]`, `GOT[1]`, ... ???

      ------

      换一种方法

      - 如果编译器、链接器、加载器都受你控制
      - 你怎么设计、实现一个 “最直观” 的动态链接格式?
      - 再去考虑怎么改进它,你就得到了 ELF!
      - **假设编译器可以为你生成位置无关代码 (PIC)**

      ![image-20230103165509889](https://cdn.jsdelivr.net/gh/alexanderliu-creator/blog_img/img/202301031655933.png)

      ## 设计一个新的二进制文件格式

      > **动态链接的符号查表就行了嘛。**

      ```c
      DL_HEAD

      LOAD("libc.dl") # 加载动态库
      IMPORT(putchar) # 加载外部符号
      EXPORT(hello) # 为动态库导出符号

      DL_CODE

      hello:
      ...
      call DSYM(putchar) # 动态链接符号
      ...

      DL_END
  • 四个需求:

    • 加载动态库
    • 加载外部符号
    • 为动态库导出符号
    • 动态链接符号

用最小代价为 .dl 文件配齐全套工具链

编译器

  • 开局一条狗,出门全靠偷 (GCC, GNU as)

binutils

  • ld = objcopy (偷来的)
  • as = GNU as (偷来的)
  • 剩下的就需要自己动手了
    • readdl (readelf)
    • objdump
    • 你同样可以山寨 addr2line, nm, objcopy, …

和最重要的加载器

  • 这个也得自己动手了

动态链接:实现

头文件

  • dl.h (数据结构定义)
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
#define REC_SZ 32
#define DL_MAGIC "\x01\x14\x05\x14"

#ifdef __ASSEMBLER__
#define DL_HEAD __hdr: \
/* magic */ .ascii DL_MAGIC; \
/* file_sz */ .4byte (__end - __hdr); \
/* code_off */ .4byte (__code - __hdr)
#define DL_CODE .fill REC_SZ - 1, 1, 0; \
.align REC_SZ, 0; \
__code:
#define DL_END __end:

#define RECORD(sym, off, name) \
.align REC_SZ, 0; \
sym .8byte (off); .ascii name

#define IMPORT(sym) RECORD(sym:, 0, "?" #sym "\0")
#define EXPORT(sym) RECORD( , sym - __hdr, "#" #sym "\0")
#define LOAD(lib) RECORD( , 0, "+" lib "\0")
#define DSYM(sym) *sym(%rip)
#else
#include <stdint.h>

struct dl_hdr {
char magic[4];
uint32_t file_sz, code_off;
};

struct symbol {
int64_t offset;
char type, name[REC_SZ - sizeof(int64_t) - 1];
};
#endif


“全家桶” 工具集

  • dlbox.c (gcc, readdl, objdump, interp)

示例代码

  • libc.S - 提供 putchar 和 exit

  • libhello.S - 调用 putchar, 提供 hello

  • main.S

    调用 hello, 提供 main

    • (假装你的高级语言编译器可以生成这样的汇编代码)

使用:

1
2
3
4
gcc dlbox.c -g
./dlbox gcc libc.S
./dlbox gcc libhello.S
./dlbox gcc main.S

image-20230103170121179

image-20230103170207665

工具链可以生成我们自定义的二进制文件的格式。

动态链接过程解析(dl)

image-20230103170617519

image-20230104161345505

动态调用DYSM会被翻译成间接跳转,指向符号表里面对应的位置昂!

  • 翻译后的结果:

image-20230104162020046

动态链接的过程:

  • 如果你想要跳转到一块你不知道的地址上,你就先找一块儿数据填上0
  • 做一个间接跳转的书写,跳转过去,比如call *hello(%rip)
  • 加载器在加载的时候,就会把对应的位置地址填上。(间接调用嗷,static就是直接调用啦!!!)

image-20230104162552179

上面就是导出的函数的格式,加载器就能够知道,putchar方法在哪里,就可以对于上面动态导入的地方,就进行替换了昂!!!就是在 ld 的时候把要动态链接的地址找出来填上。

image-20230104163606275

可以看到我们的二进制文件,对应的格式就是上面咱定义的!!!

image-20230104163311820

readdl无非就是对于我们上面的符号表进行扫描嘛!!!符号表里面+就是要Load,?就是外部链接,#就打印发好地址。

image-20230104163818237

objdump也是遍历符号表,但是还会遍历代码的每一个字节。如果代码的某一个字节,刚好对应了一个符号的时候,我们就扔给反汇编器,帮我们解释出来,就实现了一个dl -> 汇编的东西!!!

image-20230104164543549

image-20230104164626159

image-20230104164642457

interpreter一样的,遍历符号表,如果找到一个是main的话,就赋值给一个函数指针。找到这个符号,直接执行它就可以了昂!!!

image-20230104164802899

加载器最核心部分,dl_open,非常简单!!!解析文件头,知道文件有多大,然后直接mmap映射到内存里。就开始遍历!!!如果符号表里面是+,我们就递归加载dependency,如果是?,我们会解析这个符号(直接给它赋值,注意,就是这一行进行动态的链接更新的,把上面说到的0的地方,改成符号的真实地址昂!!!)

二进制文件就是个头 + 符号表 + 代码,就有动态链接的功能了昂。

重新回到 ELF

解决 dl 文件的设计缺陷

我们设计的dl是最简单的,这个小例子就拥有动态链接的功能了。但是真实的ELF,远远考虑的更多,我们考虑的情况,其实是有很多的缺陷的昂!!!

存储保护和加载位置

  • 允许将 .dl 中的一部分以某个指定的权限映射到内存的某个位置 (program header table)

允许自由指定加载器 (而不是 dlbox)

  • 加入 INTERP

空间浪费

  • 字符串存储在常量池,统一通过 “指针” 访问
    • 这是带来 ELF 文件难读的最根本原因

其他:不那么重要

  • 按需 RTFM/RTFSC

另一个重要的缺陷

1
#define DSYM(sym)   *sym(%rip)

DSYM 是间接内存访问

1
2
extern void foo();
foo();

一种写法,两种情况

  • 来自其他编译单元 (静态链接)
    • 例如我们有a.o和b.o,ld成a.out,链接到同一个binary中,还间接跳转都很不聪明啦!!!(但是ld中,我们必须这么指定昂!!!)
    • 直接 PC 相对跳转即可 -> 这种情况下,间接跳转效率太低了!!!
  • 动态链接库
    • 必须查表 (编译时不能决定) -> 这种情况下,必须间接跳转了,因为我们当前的这个binary中,没有哇orz……

“发明” GOT & PLT

针对上面遇到的这种问题,我们统一编译成,相对于rip简单的call。但是如果链接的时候,我们发现这个是一个动态链接库,外部的符号,我们就必须call PLT对应的Entry -> 发明了GOT和PLT!!!

我们的 “符号表” 就是 Global Offset Table (GOT)

  • 这下你不会理解不了 GOT 的概念了!
    • 概念和名字都不重要,发明的过程才重要

image-20230104170452976

通过自己定义的一个小的二进制文件,我们就明白了ELF同样的,设计的一些初衷!!!


统一静态/动态链接:都用静态!

  • 增加一层 indirection: Procedure Linkage Table (PLT)
  • 所有未解析的符号都统一翻译成 call
    • 现代处理器都对这种跳转做了一定的优化 (e.g., BTB)
1
2
3
4
5
putchar@PLT:
call DSYM(putchar) # in ELF: jmp *GOT[n]

main:
call putchar@PLT

再次回到 printf

你会发现和我们的 “最小” 二进制文件几乎完全一样!

image-20230104170625748

看到PLT对应的,下面这个,其实就类似于call *hello(%rip),哦,就是加载上填上的东西呀!!!

  • ELF 还有一些额外的 hack (比如可以 lazy binding)

1
2
3
4
5
6
7
8
00000000000010c0 <printf@plt>:
10c0: endbr64
10c4: bnd jmpq *0x2efd(%rip) # DSYM(printf)
10cb: nopl 0x0(%rax,%rax,1)

00000000000011c9 <main>:
...
1246: callq 10c0 <printf@plt>

最后还一个问题:数据

如果我们想要引用动态链接库里的数据?

  • 数据不能增加一层 indirection

stdout/errno/environ 的麻烦

  • 多个库都会用;但应该只有一个副本!

image-20230104191900193

由加载器来保证,世界里只有一个副本昂!!!


当然是做实验了!

  • readelf 看看 stdout 有没有不同
  • 再用 gdb 设一个 watch point
    • 原来被特殊对待了
    • 算是某种 “摆烂” (workaround) 了

总结

img

本次课回答的问题

  • Q1: 可执行文件是如何被操作系统加载的?
  • Q2: 什么是动态链接/动态加载?

Take-away messages

  • 加载器

    • 借助底层机制把数据结构按 specification “搬运”
  • 动态链接/加载

    • GOT, PLT 和最小二进制文件
  • 啪的一下!很快啊!

    我们就进入了操作系统内核

    • 没什么难的,就是个普通 C 程序

References

  1. /usr/include/elf.h -> 有我们想知道的elf文件的全部信息
  2. readelf和objdump工具在mac下的安装:https://www.cnblogs.com/zlcxbb/p/6059517.html
  3. file /bin/ls
1
2
3
4
alex ~  $ file /bin/ls
/bin/ls: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64e:Mach-O 64-bit executable arm64e]
/bin/ls (for architecture x86_64): Mach-O 64-bit executable x86_64
/bin/ls (for architecture arm64e): Mach-O 64-bit executable arm64e
  1. 读elf文件:readelf /bin/ls,读elf文件头:readelf -h /bin/ls
  2. :set no wrap
  3. vedio link: https://www.bilibili.com/video/BV1wL4y1L72C/?spm_id_from=333.999.0.0&vd_source=ff957cd8fbaeb55d52afc75fbcc87dfd
  4. .so文件:https://blog.csdn.net/wangquan1992/article/details/113770115
  5. dsym: https://www.jianshu.com/p/ec695ef26891
  6. !xxd main.dl

NJUOS-17-动态链接和加载
https://alexanderliu-creator.github.io/2023/01/03/njuos-17-dong-tai-lian-jie-he-jia-zai/
作者
Alexander Liu
发布于
2023年1月3日
许可协议