动态链接和加载(1)

本次课要回答的问题

  1. 可执行文件是如何被操作系统加载的
  2. 什么是动态链接/动态加载

静态ELF加载器:实现

加载器

  1. 解析数据结构 + 复制到内存 + 跳转
  2. 创建进程运行时的初始状态(argv,envp,…)

loader-static.c

  1. 可以加载任何静态链接的代码, minimal.S, dfs-fork.c
  2. 并可以正确处理参数/环境变量 env.c
    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
    #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);
    }

    解释一下我们这里的loader(它是动态链接的): 给我们的loader传入一个参数,它执行到某一个状态的时候,把我们的ELF文件(argv[1])搬到我们的loader程序的状态上,相当于我们的程序被替换了,但这个过程并没有执行execve,只是简单地用mmap系统调用(当然操作系统加载loader这个程序的时候会用execve)。

初始化堆栈

状态机是well-defined的。

表头 表头 长度(字节)
其他信息 未知
Null auxiliary vector entry 1 eightbyte each
Auxiliary vector entries 2 eightbytes each
0 8
Environment pointers 8 bytes each
0 8 + 8*argc + %rsp 8
Argument pointers 8 + %rsp argc 8
Argument count %rsp 8
Undefined Low Address

有趣之处

这是在操作系统上实现的。用 open , mmap, close实现了一个 execve。


动态链接和加载

为什么要动态加载

  1. 减少库函数的磁盘和内存拷贝
  • 每个可执行文件里面都有所有的库函数拷贝那也太浪费了
  • 只要遵守约定,不挑战库函数的版本(否则发布一个新版本就要重新编译全部程序)

这就有了”拆解应用程序”的需求

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

动态链接,但不讲ELF,换一种方法。

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

来看一下蒋神的设计(main part)

头文件

  • dl.h(数据结构定义)

全家桶工具集

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

示例代码

  • libc.S - 提供 putchar 和 exit
  • libhello.S - 调用 putchar, 提供 hello
  • main.S - 调用 hello, 提供 main

使用说明

1
2
3
4
5
gcc -o dlbox dlbox.c
./dlbox gcc libc.S
./dlbox gcc libhello.S
./dlbox gcc main.S
./dlbox readdl libc.dl //readelf

会生成.dl格式的自定义可执行文件。这个可执行文件是不可以在操作系统上执行,需要自己的加载器。
并且我们的加载器是在当前目录中动态加载.dl文件(根据)的,如果先前没有生成所需要的.dl文件的话,我们的加载器会出现错误。

演示一下下

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
#include "dl.h"

DL_HEAD

LOAD("libc.dl")
IMPORT(putchar)
EXPORT(hello)

DL_CODE

hello:
lea str(%rip), %rdi
mov count(%rip), %eax
push %rbx
mov %rdi, %rbx
inc %eax
mov %eax, count(%rip)
add $0x30, %eax
movb %al, 0x6(%rdi)
loop:
movsbl (%rbx),%edi
test %dil,%dil
je out
call DSYM(putchar)
inc %rbx
jmp loop
out:
pop %rbx
ret

str:
.asciz "Hello X\n"

count:
.int 0

DL_END

这个文件需要使用外部的putchar函数,所以需要call DSYM(putchar), DSYM表示动态链接的,也需要手动指明putchar函数所在的库libc.dl。它定义有一个hello函数,所以需要导出。也就是EXPORT(hello).

代码解析

  1. 首先来看一下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

    1. __ASSEMBLER__是一个内置的宏,它由编译器预定义,用于判断当前代码是否为汇编代码。在编写汇编代码时,编译器会自动定义这个宏。
    2. 怎么用汇编语言定义结构体呢? 为什么变量名前面要加.呢?这表示这是在当前偏移量下定义的。
    3. RECORD宏定义:.align REC_SZ, 0 表示将当前位置对齐到 REC_SZ 字节边界。
      这行代码定义了一个标签 sym,并将 off 表示的偏移量存储到该标签处。.8byte 指令告诉汇编器为该标签分配一个 8 字节的存储空间,即使用一个 64 位无符号整数来存储偏移量。
      .ascii name 表示将 name 参数表示的记录名称作为 ASCII 字符串嵌入到汇编代码中。.ascii 指令用于将一个字符串常量嵌入到汇编代码中。
    4. #define DSYM(sym) 这是间接跳转,先将 %rip 寄存器中存储的当前指令地址加上 hello 符号相对于当前指令的偏移量,得到函数地址,然后再根据这个地址的值进行跳转,而符号表结构体前八个字节就是函数的地址。
  2. dlbox.c文件

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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>
#include "dl.h"

#define SIZE 4096
#define LENGTH(arr) (sizeof(arr) / sizeof(arr[0]))

struct dlib {
struct dl_hdr hdr;
struct symbol *symtab; // borrowed spaces from header
const char *path;
};

static struct dlib *dlopen(const char *path);

struct dlib *dlopen_chk(const char *path) {
struct dlib *lib = dlopen(path);
if (!lib) {
fprintf(stderr, "Not a valid dlib file: %s.\n", path);
exit(1);
}
return lib;
}

// Implementation of binutils

void dl_gcc(const char *path) {
char buf[256], *dot = strrchr(path, '.');
if (dot) {
*dot = '\0';
sprintf(buf, "gcc -m64 -fPIC -c %s.S && "
"objcopy -S -j .text -O binary %s.o %s.dl", path, path, path);
system(buf);
}
}


void dl_readdl(const char *path) {
struct dlib *h = dlopen_chk(path);
printf("DLIB file %s:\n\n", h->path);
for (struct symbol *sym = h->symtab; sym->type; sym++) {
switch (sym->type) {
case '+': printf(" LOAD %s\n", sym->name); break;
case '?': printf(" EXTERN %s\n", sym->name); break;
case '#': printf( "%08lx %s\n", sym->offset, sym->name); break;
}
}
}

void dl_objdump(const char *path) {
struct dlib *h = dlopen_chk(path);
char *hc = (char *)h, cmd[64];
FILE *fp = NULL;

printf("Disassembly of binary %s:\n", h->path);

for (char *code = hc + h->hdr.code_off; code < hc + h->hdr.file_sz; code++) {
for (struct symbol *sym = h->symtab; sym->type; sym++) {
if (hc + sym->offset == code) {
int off = code - hc - h->hdr.code_off;
if (fp) pclose(fp);
sprintf(cmd, "ndisasm - -b 64 -o 0x%08x\n", off);
fp = popen(cmd, "w");
printf("\n%016x <%s>:\n", off, sym->name);
fflush(stdout);
}
}
if (fp) fputc(*code, fp);
}
if (fp) pclose(fp);
}

// binutils: interpreter
void dl_interp(const char *path) {
struct dlib *h = dlopen_chk(path);
int (*entry)() = NULL;
for (struct symbol *sym = h->symtab; sym->type; sym++)
if (strcmp(sym->name, "main") == 0)
entry = (void *)((char *)h + sym->offset);
if (entry) {
exit(entry());
}
}

struct cmd {
const char *cmd;
void (*handler)(const char *path);
} commands[] = {
{ "gcc", dl_gcc },
{ "readdl", dl_readdl },
{ "objdump", dl_objdump },
{ "interp", dl_interp },
{ "", NULL },
};

int main(int argc, char *argv[]) {
if (argc < 3) {
fprintf(stderr, "Usage: %s {gcc|readdl|objdump|interp} FILE...\n", argv[0]);
return 1;
}

for (struct cmd *cmd = &commands[0]; cmd->handler; cmd++) {
for (char **path = &argv[2]; *path && strcmp(argv[1], cmd->cmd) == 0; path++) {
if (path != argv + 2) printf("\n");
cmd->handler(*path);
}
}
}

// Implementation of dlopen()

static struct symbol *libs[16], syms[128];

static void *dlsym(const char *name);
static void dlexport(const char *name, void *addr);
static void dlload(struct symbol *sym);

static struct dlib *dlopen(const char *path) {
struct dl_hdr hdr;
struct dlib *h;

int fd = open(path, O_RDONLY);
if (fd < 0) goto bad;
if (read(fd, &hdr, sizeof(hdr)) < sizeof(hdr)) goto bad;
if (strncmp(hdr.magic, DL_MAGIC, strlen(DL_MAGIC)) != 0) goto bad;

h = mmap(NULL, hdr.file_sz, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
if (h == (void *)-1) goto bad;

h->symtab = (struct symbol *)((char *)h + REC_SZ);
h->path = path;

for (struct symbol *sym = h->symtab; sym->type; sym++) {
switch (sym->type) {
case '+': dlload(sym); break; // (recursively) load
case '?': sym->offset = (uintptr_t)dlsym(sym->name); break; // resolve
case '#': dlexport(sym->name, (char *)h + sym->offset); break; // export
}
}

return h;

bad:
if (fd > 0) close(fd);
return NULL;
}

static void *dlsym(const char *name) {
for (int i = 0; i < LENGTH(syms); i++)
if (strcmp(syms[i].name, name) == 0)
return (void *)syms[i].offset;
assert(0);
}

static void dlexport(const char *name, void *addr) {
for (int i = 0; i < LENGTH(syms); i++)
if (!syms[i].name[0]) {
syms[i].offset = (uintptr_t)addr; // load-time offset
strcpy(syms[i].name, name);
return;
}
assert(0);
}

static void dlload(struct symbol *sym) {
for (int i = 0; i < LENGTH(libs); i++) {
if (libs[i] && strcmp(libs[i]->name, sym->name) == 0) return; // already loaded
if (!libs[i]) {
libs[i] = sym;
dlopen(sym->name); // load recursively
return;
}
}
assert(0);
}

来解释一下(按顺序):

  1. 首先需要dl_gcc各.S文件得到.dl。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void dl_gcc(const char *path) {
    char buf[256], *dot = strrchr(path, '.');
    if (dot) {
    *dot = '\0';
    sprintf(buf, "gcc -m64 -fPIC -c %s.S && "
    "objcopy -S -j .text -O binary %s.o %s.dl", path, path, path);
    system(buf);
    }
    }

    原来命令行还可以这么写! 前一句生成64位的位置无关代码,然后再把代码段拷贝成.dl文件。这里主要是一些宏替换,并且由于我们的汇编代码格式是很严格地按照dl_hdr的形式写的,所以我们得到的其实是一个dl_lib的结构体。更直观地来感受一下,我们使用命令gcc -E main.S,得到宏替换展开的文件(如下):

    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
    # 0 "main.S"
    # 0 "<built-in>"
    # 0 "<command-line>"
    # 1 "/usr/include/stdc-predef.h" 1 3 4
    # 0 "<command-line>" 2
    # 1 "main.S"
    # 1 "dl.h" 1
    # 2 "main.S" 2

    __hdr: .ascii "\x01\x14\x05\x14"; .4byte (__end - __hdr); .4byte (__code - __hdr)

    .align 32, 0; .8byte (0); .ascii "+" "libc.dl" "\0"
    .align 32, 0; .8byte (0); .ascii "+" "libhello.dl" "\0"
    .align 32, 0; hello: .8byte (0); .ascii "?" "hello" "\0"
    .align 32, 0; .8byte (main - __hdr); .ascii "#" "main" "\0"

    .fill 32 - 1, 1, 0; .align 32, 0; __code:

    main:
    call *hello(%rip)
    call *hello(%rip)
    call *hello(%rip)
    call *hello(%rip)
    movq $0, %rax
    ret

    __end:

    xxd main.dl命令得到二进制文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    00000000: 0114 0514 e000 0000 c000 0000 0000 0000  ................
    00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
    00000020: 0000 0000 0000 0000 2b6c 6962 632e 646c ........+libc.dl
    00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
    00000040: 0000 0000 0000 0000 2b6c 6962 6865 6c6c ........+libhell
    00000050: 6f2e 646c 0000 0000 0000 0000 0000 0000 o.dl............
    00000060: 0000 0000 0000 0000 3f68 656c 6c6f 0000 ........?hello..
    00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
    00000080: c000 0000 0000 0000 236d 6169 6e00 0000 ........#main...
    00000090: 0000 0000 0000 0000 0000 0000 0000 0000 ................
    000000a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
    000000b0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
    000000c0: ff15 9aff ffff ff15 94ff ffff ff15 8eff ................
    000000d0: ffff ff15 88ff ffff 48c7 c000 0000 00c3 ........H.......

    libc.dl、libhello.dl、hello这三个符号都是填零的。只有main函数已经填上了正确的偏移。
    可以反推,我们得到的.dl文件的格式是这样的:首先是__hdr头,这里有模数,文件的大小,以及代码段的偏移。 然后是符号表;符号表结束后,再32个字节填0作为分界线,然后是代码段。 妙哇妙哇!原来宏定义还可以这么用!

  2. 然后是dl_interp函数来解释执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void dl_interp(const char *path) {
    struct dlib *h = dlopen_chk(path);
    int (*entry)() = NULL;
    for (struct symbol *sym = h->symtab; sym->type; sym++)
    if (strcmp(sym->name, "main") == 0)
    entry = (void *)((char *)h + sym->offset);
    if (entry) {
    exit(entry());
    }
    }

    找到main函数,exit(entry()) 的作用就是在程序结束时执行 main 函数,并将其返回值作为程序的退出码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct dlib *dlopen_chk(const char *path) {
    struct dlib *lib = dlopen(path);
    if (!lib) {
    fprintf(stderr, "Not a valid dlib file: %s.\n", path);
    exit(1);
    }
    return lib;
    }

    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
    static struct dlib *dlopen(const char *path) {
    struct dl_hdr hdr;
    struct dlib *h;

    int fd = open(path, O_RDONLY);
    if (fd < 0) goto bad;
    if (read(fd, &hdr, sizeof(hdr)) < sizeof(hdr)) goto bad;
    if (strncmp(hdr.magic, DL_MAGIC, strlen(DL_MAGIC)) != 0) goto bad;

    h = mmap(NULL, hdr.file_sz, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
    if (h == (void *)-1) goto bad;

    h->symtab = (struct symbol *)((char *)h + REC_SZ);
    h->path = path;

    for (struct symbol *sym = h->symtab; sym->type; sym++) {
    switch (sym->type) {
    case '+': dlload(sym); break; // (recursively) load
    case '?': sym->offset = (uintptr_t)dlsym(sym->name); break; // resolve
    case '#': dlexport(sym->name, (char *)h + sym->offset); break; // export
    }
    }

    return h;

    bad:
    if (fd > 0) close(fd);
    return NULL;
    }
    1. 这里要打开.dl文件,并且把这个文件用mmap函数映射到dlbox进程的地址空间,此时其他.dl文件的代码段就会映射到dlbox进程的地址空间中,也就可以确定函数的地址了,这也就和动态链接的链接时绑定思想不谋而合。打开这个可执行程序时,h->symtab = (struct symbol *)((char *)h + REC_SZ);这里是初始化符号表。
    2. 如果遇到.dl作为符号表项(‘+’),则用dlload递归加载,dlload则是调用dlopen实现的。
    3. 如果遇到符号表项的某一项标记位’?’,表示引用外部符号,我们通过查表dlsym函数来填表。
    4. 可以想象,这是一个递归的过程,递归地填表。如果变量是这个main程序的函数(‘#’),我们直接可以确定该函数的地址。也就是表头的地址加上偏移。
    5. 如果这个符号是个.dl文件,则需要调用dlopen把这个文件整体映射进进程的地址空间(递归),映射完后,所有符号的地址都会被确定,然后我们就可以遍历来填sym表了。这个sym表记录了所有符号,也是一个结构体,一开始所有的项的name字段初始化为NULL。
    1
    case '#': dlexport(sym->name, (char *)h + sym->offset); break;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    static void dlexport(const char *name, void *addr) {
    for (int i = 0; i < LENGTH(syms); i++)
    if (!syms[i].name[0]) {
    syms[i].offset = (uintptr_t)addr; // load-time offset
    strcpy(syms[i].name, name);
    return;
    }
    assert(0);
    }
    1. 如果这个符号是外部的函数,由于我们先包含.dl的库文件,所以外部符号这时候都会解析完毕,我们就可以直接填入正确的地址。
    2. 上面的思想主要是:我们在装载动态库的时候,我们不像静态链接那样可以知道这个模块是装载在哪个位置的。我们的解决方法是通过间接跳转在本模块的某个位置(这是可以确定的),这个位置就有这个函数的地址的信息。由于我们都是按模块装载的,所以这一点并不难实现,通过添加一个sym的全局变量结构数组,在每装载一个模块(.dl)时,就把该模块的所有这个模块的export类型的变量全部填入这个sym数组中。
      然后,有了这个sym数组,就可以开始回填到每个模块的符号表中带有(‘?’)的符号offset字段了。

反思与改进(最精彩的部分)

一些小缺陷
  • 存储保护和加载位置。允许将.dl中的一部分以某个指定的权限映射到内存的某个位置—>程序头表
  • 允许自由指定加载器—>加入INTERP
  • 空间浪费 —>字符串存储在常量池,统一通过“指针”访问(这也是ELF难读的原因)
另一个大缺陷
1
2
// a.c
extern void foo();

一种写法,两种情况

  • 来自于其他编译单元(静态链接)
  • 动态链接库

例如,有a.o和b.o静态链接再和lib.so动态链接,如果a.o中引用了一个外部符号foo,那么该如何判断这个符号究竟是属于哪个单元呢?如果只是简单地宏替换, call *foo(%rip),但其实这样是效率很低的。

“发明”PLT & GOT

先编译为相对于%rip的简单的call调用,在链接的时候,如果发现这是一个本单元的符号,直接相对于rip寻址;如果发现这是一个外部(动态链接)库的话,就需要plt这条entry,再把地址填上去。

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

1
2
3
4
00000000000011e0 <printf@plt>:
11e0: f3 0f 1e fa endbr64
11e4: f2 ff 25 7d 2d 00 00 bnd jmp *0x2d7d(%rip) # 3f68 <printf@GLIBC_2.2.5>
11eb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)

咦,这条jmp指令不是有点熟悉吗?和我们的DSYM很相似。这不印证了我们的猜想吗?

最后一个问题:数据

不管多少个静态库动态库,但我们的程序只有一个errno,environ,stdout。


动态链接和加载(1)
http://example.com/2023/08/02/操作系统/jyy操作系统/动态链接和加载(1)/
作者
LiuZhaocheng
发布于
2023年8月2日
许可协议