系统性学习Linux-第七讲-库制作与原理
- [1. 什么是库](#1. 什么是库)
- [2. 静态库](#2. 静态库)
-
- [2-1 静态库生成](#2-1 静态库生成)
- [2-2 静态库使用](#2-2 静态库使用)
- [3. 动态库](#3. 动态库)
-
- [3-1 动态库生成](#3-1 动态库生成)
- [3-2 动态库使用](#3-2 动态库使用)
- [3-3 库运行搜索路径](#3-3 库运行搜索路径)
-
- [3-3-1 问题](#3-3-1 问题)
- [3-3-2 解决⽅案](#3-3-2 解决⽅案)
- [4. 使用外部库-课后尝试](#4. 使用外部库-课后尝试)
- [5. 目标文件](#5. 目标文件)
- [6. ELF 文件](#6. ELF 文件)
- [7. ELF 从形成到加载轮廓](#7. ELF 从形成到加载轮廓)
-
- [7-1 ELF 形成可执行](#7-1 ELF 形成可执行)
- [7-2 ELF 可执行文件加载](#7-2 ELF 可执行文件加载)
- [8. 理解连接与加载](#8. 理解连接与加载)
-
- [8-1 静态链接](#8-1 静态链接)
本节重点:
-
动静态库的制作
-
动静态库的使用
-
动态库的查找
-
可执行程序ELF格式
-
可执行程序的加载过程
-
虚拟地址空间和动态库加载的过程
1. 什么是库
库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,
不可能每个人的代码都从零开始,因此库的存在意义非同寻常。
本质上来说库是⼀种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:
-
静态库 .a[Linux]、.lib[windows]
-
动态库 .so[Linux]、.dll[windows]
bash
// ubuntu 动静态库
// C
$ ls -l /lib/x86_64-linux-gnu/libc-2.31.so
-rwxr-xr-x 1 root root 2029592 May 1 02:20 /lib/x86_64-linux-gnu/libc-2.31.so
$ ls -l /lib/x86_64-linux-gnu/libc.a
-rw-r--r-- 1 root root 5747594 May 1 02:20 /lib/x86_64-linux-gnu/libc.a
//C++
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so -l
lrwxrwxrwx 1 root root 40 Oct 24 2022 /usr/lib/gcc/x86_64-linuxgnu/9/libstdc++.so -> ../../../x86_64-linux-gnu/libstdc++.so.6
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
/usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
// Centos 动静态库
// C
$ ls /lib64/libc-2.17.so -l
-rwxr-xr-x 1 root root 2156592 Jun 4 23:05 /lib64/libc-2.17.so
[chengkl4@ubuntu ~]$ ls /lib64/libc.a -l
-rw-r--r-- 1 root root 5105516 Jun 4 23:05 /lib64/libc.a
// C++
$ ls /lib64/libstdc++.so.6 -l
lrwxrwxrwx 1 root root 19 Sep 18 20:59 /lib64/libstdc++.so.6 ->
libstdc++.so.6.0.19
$ ls /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a -l
-rw-r--r-- 1 root root 2932366 Sep 30 2020 /usr/lib/gcc/x86_64-redhatlinux/4.8.2/libstdc++.a
预备工作,准备好历史封装的 libc 代码,在任意新增 "库文件"
cpp
// my_stdio.h
#pragma once
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
struct IO_FILE
{
int flag; // 刷新⽅式
int fileno; // ⽂件描述符
char outbuffer[SIZE];
int cap;
int size;
// TODO
};
typedef struct IO_FILE mFILE;
mFILE *mfopen(const char *filename, const char *mode);
int mfwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);
// my_stdio.c
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
mFILE *mfopen(const char *filename, const char *mode)
{
int fd = -1;
if (strcmp(mode, "r") == 0)
{
fd = open(filename, O_RDONLY);
}
else if (strcmp(mode, "w") == 0)
{
fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
}
else if (strcmp(mode, "a") == 0)
{
fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
}
if (fd < 0)
return NULL;
mFILE *mf = (mFILE *)malloc(sizeof(mFILE));
if (!mf)
{
close(fd);
return NULL;
}
mf->fileno = fd;
mf->flag = FLUSH_LINE;
mf->size = 0;
mf->cap = SIZE;
return mf;
}
void mfflush(mFILE *stream)
{
if (stream->size > 0)
{
// 写到内核⽂件的⽂件缓冲区中!
write(stream->fileno, stream->outbuffer, stream->size);
// 刷新到外设
fsync(stream->fileno);
stream->size = 0;
}
}
int mfwrite(const void *ptr, int num, mFILE *stream)
{
// 1. 拷贝
memcpy(stream->outbuffer + stream->size, ptr, num);
stream->size += num;
// 2. 检测是否要刷新
if (stream->flag == FLUSH_LINE && stream->size > 0 && stream - > outbuffer[stream->size - 1] == '\n')
{
mfflush(stream);
}
return num;
}
void mfclose(mFILE *stream)
{
if (stream->size > 0)
{
mfflush(stream);
}
close(stream->fileno);
}
// my_string.h
#pragma once
int my_strlen(const char *s);
// my_string.c
#include "my_string.h"
int my_strlen(const char *s)
{
const char *end = s;
while (*end != '\0')
end++;
return end - s;
}
2. 静态库
-
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库。
-
⼀个可执行程序可能用到许多的库,这些库运行有的是静态库,有的是动态库,而我们的编译默认为动态链接库,只有在该库下找不到动态 .so 的时候才会采用同名静态库。我们也可以使用 gcc 的 -static 强转设置链接静态库。
2-1 静态库生成
bash
// Makefile
libmystdio.a:my_stdio.o my_string.o
@ar -rc $@ $^
@echo "build $^ to $@ ... done"
%.o:%.c
@gcc -c $<
@echo "compling $< to $@ ... done"
.PHONY:clean
clean:
@rm -rf *.a *.o stdc*
@echo "clean ... done"
.PHONY:output
output:
@mkdir -p stdc/include
@mkdir -p stdc/lib
@cp -f *.h stdc/include
@cp -f *.a stdc/lib
@tar -czf stdc.tgz stdc
@echo "output stdc ... done"
- ar 是 gnu 归档工具, rc 表示 (replace and create)
bash
$ ar -tv libmystdio.a
rw-rw-r-- 1000/1000 2848 Oct 29 14:35 2024 my_stdio.o
rw-rw-r-- 1000/1000 1272 Oct 29 14:35 2024 my_string.o
2-2 静态库使用
cpp
// 任意⽬录下,新建
// main.c,引⼊库头⽂件
#include "my_stdio.h"
#include "my_string.h"
#include <stdio.h>
int main()
{
const char *s = "abcdefg";
printf("%s: %d\n", s, my_strlen(s));
mFILE *fp = mfopen("./log.txt", "a");
if (fp == NULL)
return 1;
mfwrite(s, my_strlen(s), fp);
mfwrite(s, my_strlen(s), fp);
mfwrite(s, my_strlen(s), fp);
mfclose(fp);
return 0;
}
// 场景1:头⽂件和库⽂件安装到系统路径下
$ gcc main.c -lmystdio
// 场景2:头⽂件和库⽂件和我们⾃⼰的源⽂件在同⼀个路径下
$ gcc main.c -L. -lmymath
// 场景3:头⽂件和库⽂件有⾃⼰的独⽴路径
$ gcc main.c -I头⽂件路径 -L库⽂件路径 -lmymath
-
-L :指定库路径
-
-I : 指定头文件搜索路径
-
-l :指定库名
-
测试目标文件生成后,静态库删掉,程序照样可以运行
-
关于 -static 选项,稍后介绍
-
库文件名称和引入库的名称:去掉前缀 lib ,去掉后缀 .so , .a ,如:libc.so -> c
3. 动态库
-
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
-
⼀个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的⼀个表,而不是外部函数所在目标文件的整个机器码
-
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
-
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间
3-1 动态库生成
bash
// Makefile
libmystdio.so:my_stdio.o my_string.o
gcc -o $@ $^ -shared
%.o:%.c
gcc -fPIC -c $<
.PHONY:clean
clean:
@rm -rf *.so *.o stdc*
@echo "clean ... done"
.PHONY:output
output:
@mkdir -p stdc/include
@mkdir -p stdc/lib
@cp -f *.h stdc/include
@cp -f *.so stdc/lib
@tar -czf stdc.tgz stdc
@echo "output stdc ... done"
-
shared: 表示生成共享库格式
-
fPIC:产生位置无关码(position independent code)
-
库名规则:libxxx.so
3-2 动态库使用
bash
// 场景1:头⽂件和库⽂件安装到系统路径下
$ gcc main.c -lmystdio
// 场景2:头⽂件和库⽂件和我们⾃⼰的源⽂件在同⼀个路径下
$ gcc main.c -L. -lmymath // 从左到右搜索-L指定的⽬录
// 场景3:头⽂件和库⽂件有⾃⼰的独⽴路径
$ gcc main.c -I头⽂件路径 -L库⽂件路径 -lmymath
$ ldd libmystdio.so // 查看库或者可执⾏程序的依赖
linux-vdso.so.1 => (0x00007fffacbbf000)
libc.so.6 => /lib64/libc.so.6 (0x00007f8917335000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8917905000)
// 以场景2为例
$ ll
total 24
-rwxrwxr-x 1 whb whb 8592 Oct 29 14:50 libmystdio.so
-rw-rw-r-- 1 whb whb 359 Oct 19 16:07 main.c
-rw-rw-r-- 1 whb whb 447 Oct 29 14:50 my_stdio.h
-rw-rw-r-- 1 whb whb 447 Oct 29 14:50 my_string.h
$ gcc main.c -L. -lmystdio
$ ll
total 36
-rwxrwxr-x 1 whb whb 8600 Oct 29 14:51 a.out
-rwxrwxr-x 1 whb whb 8592 Oct 29 14:50 libmystdio.so
-rw-rw-r-- 1 whb whb 359 Oct 19 16:07 main.c
-rw-rw-r-- 1 whb whb 447 Oct 29 14:50 my_stdio.h
-rw-rw-r-- 1 whb whb 447 Oct 29 14:50 my_string.h
[whb@bite-alicloud other]$ ./a.out
...
3-3 库运行搜索路径
3-3-1 问题
bash
$ ldd a.out
linux-vdso.so.1 => (0x00007fff4d396000)
libmystdio.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007fa2aef30000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa2af2fe000)
3-3-2 解决⽅案
-
拷贝 .so 文件到系统共享库路径下, ---般指 /usr/lib、/usr/local/lib、/lib64 或者开篇指明的库路径等
-
向系统共享库路径下建立同名软连接
-
更改环境变量: LD_LIBRARY_PATH
-
ldconfig 方案:配置/ etc/ld.so.conf.d/ ,ldconfig 更新
bash
[root@localhost linux]# cat /etc/ld.so.conf.d/bit.conf
/root/tools/linux
[root@localhost linux]# ldconfig // 要⽣效,这⾥要执⾏ldconfig,重新加载库搜索路径
4. 使用外部库-课后尝试
我们现在没接触过太多的库,唯一接触过的就是 C、C++ 标准库,这里我们可以推荐一个好玩的图形库:ncurses
bash
// 安装
// Centos
$ sudo yum install -y ncurses-devel
// ubuntu
$ sudo apt install -y libncurses-dev
系统中其实有很多库,它们通常由一组互相关联的用来完成某项常见工作的函数构成。
比如用来处理屏幕显示情况的函数( ncurses 库)
演示代码(大模型生成,读者可以自行尝试一下):
cpp
#include <stdio.h>
#include <string.h>
#include <ncurses.h>
#include <unistd.h>
#define PROGRESS_BAR_WIDTH 30
#define BORDER_PADDING 2
#define WINDOW_WIDTH (PROGRESS_BAR_WIDTH + 2 * BORDER_PADDING + 2) // 加边框的
宽度
#define WINDOW_HEIGHT 5
#define PROGRESS_INCREMENT 3
#define DELAY 300000 // 微秒(300毫秒)
int main()
{
initscr();
start_color();
init_pair(1, COLOR_GREEN, COLOR_BLACK); // 已完成部分:绿⾊前景,⿊⾊背景
init_pair(2, COLOR_RED, COLOR_BLACK); // 剩余部分(虽然⽤红⾊可能不太合适,但为演⽰⽬的):红⾊背景
cbreak();
noecho();
curs_set(FALSE);
int max_y, max_x;
getmaxyx(stdscr, max_y, max_x);
int start_y = (max_y - WINDOW_HEIGHT) / 2;
int start_x = (max_x - WINDOW_WIDTH) / 2;
WINDOW *win = newwin(WINDOW_HEIGHT, WINDOW_WIDTH, start_y, start_x);
box(win, 0, 0); // 加边框
wrefresh(win);
int progress = 0;
int max_progress = PROGRESS_BAR_WIDTH;
while (progress <= max_progress)
{
werase(win); // 清除窗⼝内容
// 计算已完成的进度和剩余的进度
int completed = progress;
int remaining = max_progress - progress;
// 显⽰进度条
int bar_x = BORDER_PADDING + 1; // 进度条在窗⼝中的x坐标
int bar_y = 1; // 进度条在窗⼝中的y坐标(居中)
// 已完成部分
attron(COLOR_PAIR(1));
for (int i = 0; i < completed; i++)
{
mvwprintw(win, bar_y, bar_x + i, "#");
}
attroff(COLOR_PAIR(1));
// 剩余部分(⽤背景⾊填充)
attron(A_BOLD | COLOR_PAIR(2)); // 加粗并设置背景⾊为红⾊(仅⽤于演⽰)
for (int i = completed; i < max_progress; i++)
{
mvwprintw(win, bar_y, bar_x + i, " ");
}
attroff(A_BOLD | COLOR_PAIR(2));
// 显⽰百分⽐
char percent_str[10];
snprintf(percent_str, sizeof(percent_str), "%d%%", (progress * 100) / max_progress);
int percent_x = (WINDOW_WIDTH - strlen(percent_str)) / 2; // 居中显⽰
mvwprintw(win, WINDOW_HEIGHT - 1, percent_x, percent_str);
wrefresh(win); // 刷新窗⼝以显⽰更新
// 增加进度
progress += PROGRESS_INCREMENT;
// 延迟⼀段时间
usleep(DELAY);
}
// 清理并退出ncurses模式
delwin(win);
endwin();
return 0;
}
演示效果:

推荐一篇不错的使用指南:https://blog.csdn.net/bdn_nbd/article/details/134019142
5. 目标文件
编译和链接这两个步骤,在 Windows 下被我们的 IDE 封装的很完美,我们一般都是一键构建非常方便,
但一旦遇到错误的时候呢,尤其是链接相关的错误,很多人就束手无策了。
在 Linux 下,我们之前也学过如何通过 gcc 编译器来完成这一系列操作。

接下来我们深入探讨一下编译和链接的整个过程,来更好的理解动静态库的使用原理。
先来回顾下什么是编译呢?编译的过程其实就是将我们程序的源代码翻译成 CPU 能够直接运行的机器代码。
比如:在⼀个源文件 hello.c 里便简单输出 "hello world!" ,并且调用一个 run 函数,
而这个函数被定义在另一个原文件 code.c 中。这里我们就可以调用 gcc -c 来分别编译这两个原文件。
cpp
// hello.c
#include <stdio.h>
void run();
int main()
{
printf("hello world!\n");
run();
return 0;
}
// code.c
#include <stdio.h>
void run()
{
printf("running...\n");
}
可以看到,在编译之后会生成两个扩展名为 .o 的文件,它们被称作目标文件。要注意的是如果我们修改了⼀个原文件,
那么只需要单独编译它这⼀个,而不需要浪费时间重新编译整个工程。目标文件是⼀个二进制的文件,文件的格式是 ELF ,
是对、二进制代码的⼀种封装。
bash
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
## file命令⽤于辨识⽂件类型。
6. ELF 文件
要理解编译链链接的细节,我们不得不了解一下 ELF 文件。其实有以下四种文件其实都是 ELF 文件:
-
可重定位文件(Relocatable File) :即 xxx.o 文件。包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
-
可执行文件(Executable File) :即可执行程序。
-
共享目标文件(Shared Object File) :即 xxx.so 文件。
-
内核转储(core dumps) ,存放当前进程的执行上下文,用于 dump 信号触发。一个 ELF 文件由以下四部分组成:
- ELF头 (ELF header) :描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位件的其他部分。
- 程序头表(Program header table) :列举了所有有效的段 (segments) 和他们的属性。表里记着每个段的开始的位置和位移(offset)、长度,毕竟这些段,都是紧密的放在二进制文件中,需要段表的描述信息,才能把他们每个段分割开。
- 节头表 (Section header table) :包含对节(sections)的描述。
- 节(Section):ELF 文件中的基本组成单位,包含了特定类型的数据。ELF文件的各种信息和数据都存储在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。
最常见的节:
-
代码节(.text):用于保存机器指令,是程序的主要执行部分。
-
数据节(.data):保存已初始化的全局变量和局部静态变量。

7. ELF 从形成到加载轮廓
7-1 ELF 形成可执行
-
step-1:将多份 C/C++ 源代码,翻译成为目标 .o 文件 + 动静态库 (ELF)
-
step-2:将多份 .o 文件 section 进行合并

📌 注意:
- 实际合并是在链接时进行的,但是并不是这么简单的合并,也会涉及对库合并,此处不做过多追究
7-2 ELF 可执行文件加载
-
⼀个 ELF 会有多种不同的 Section ,在加载到内存的时候,也会进行 Section 合并,形成 segment
-
合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等.
-
这样,即便是不同的 Section ,在加载到内存中,可能会以 segment 的形式,加载到⼀起
-
很显然,这个合并工作也已经在形成 ELF 的时候,合并方式已经确定了,具体合并原则被记录在了 ELF 的 程序头表(Program header table) 中



📌 为什么要将 section 合并成为 segment:
- Section 合并的主要原因是为了减少页面碎片,提高内存使用效率。如果不进行合并,假设页面大小为 4096 字节(内存块基本大小,加载,管理的基本单位),如果 .text 部分为 4097 字节,.init 部分为 512 字节,那么它们将占用 3 个页面,而合并后,它们只需 2 个页面。
- 此外,操作系统在加载程序时,会将具有相同属性的 section 合并成一个大的 segment,这样就可以实现不同的访问权限,从而优化内存管理和权限访问控制。
对于 程序头表 和 节头表 又有什么用呢,其实 ELF 文件提供 2 个不同的视图/视角来让我们理解这两个部分:
-
链接视图(Linking view) - 对应节头表 Section header table
-
文件结构的粒度更细,将文件按功能模块的差异进行划分,静态链接分析的时候⼀般关注的是链接视图,能够理解 ELF 文件中包含的各个部分的信息。
-
为了空间布局上的效率,将来在链接目标文件时,链接器会把很多节(section)合并,规整成可执行的段(segment)、可读写的段、只读段等。合并了后,空间利用率就高了,否则,很小的很小的⼀段,未来物理内存页浪费太大(物理内存页分配⼀般都是整数倍⼀块给你,比如 4k ),所以,链接器趁着链接就把小块们都合并了。
-
-
执行视图 (execution view) - 对应程序头表 Program header table
- 告诉操作系统,如何加载可执行文件,完成进程内存的初始化。---个可执行程序的格式中,---定有 program header table。
-
说白了就是:一个在链接时作用,---个在运行加载时作用。

从 链接视图 来看:
-
命令 readelf -S hello.o 可以帮助查看 ELF 文件的 节头表。
-
.text 节 :是保存了程序代码指令的代码节。
-
.data 节 :保存了初始化的全局变量和局部静态变量等数据。
-
.rodata 节 :保存了只读的数据,如一行 C 语言代码中的字符串。由于 .rodata 节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能是在 text 段(不是 data 段)中找到 .rodata 节。
-
.BSS 节 :为未初始化的全局变量和局部静态变量预留位置
-
.symtab 节 : Symbol Table 符号表,就是源码里面那些函数名、变量名和代码的对应关系。
-
.got.plt 节 (全局偏移表 - 过程链接表):.got 节保存了全局偏移表。.got 节和 .plt 节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。对于 GOT 的理解,我们后面会说。
- 使用 readelf 命令查看 .so 文件可以看到该节。
从 执行视图 来看:
-
告诉操作系统哪些模块可以被加载进内存。
-
加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执行的。我们可以在 ELF 头 中找到文件的基本信息,以及可以看到 ELF 头是如何定位程序头表和节头表的。例如我们查看下 hello.o 这个可重定位文件的主要信息:



对于 ELF HEADER 这部分来说,我们只用知道其作用即可,它的主要目的是定位文件的其他部分。
8. 理解连接与加载
8-1 静态链接
-
无论是自己的 .o , 还是静态库中的 .o ,本质都是把 .o 文件进行连接的过程
-
所以:研究静态链接,本质就是研究 .o 是如何链接的
bash
$ ll
-rw-rw-r-- 1 whb whb 62 Oct 31 15:36 code.c
-rw-rw-r-- 1 whb whb 103 Oct 31 15:36 hello.c
whb@bite:~/test/test/test$ gcc -c *.c
whb@bite:~/test/test/test$ gcc *.o -o main.exe
$ ll
-rw-rw-r-- 1 whb whb 62 Oct 31 15:36 code.c
-rw-rw-r-- 1 whb whb 1672 Oct 31 15:46 code.o
-rw-rw-r-- 1 whb whb 103 Oct 31 15:36 hello.c
-rw-rw-r-- 1 whb whb 1744 Oct 31 15:46 hello.o
-rwxrwxr-x 1 whb whb 16752 Oct 31 15:46 main.exe*
查看编译后的 .o 目标文件

-
objdump -d 命令:将代码段(.text)进行反汇编查看
-
hello.o 中的 main 函数不认识 printf
c
$ cat hello.c
#include<stdio.h>
void run();
int main()
{
printf("hello world!\n");
run();
return 0;
}
- code.o 不认识 printf 函数
c
$ cat code.c
#include<stdio.h>
void run()
{
printf("running...\n");
}
我们可以看到这里的 call 指令,它们分别对应之前调用的 printf 和 run 函数,但是你会发现他们的跳转地址都被设成了 0 。
那这是为什么呢?
其实就是在编译 hello.c 的时候,编译器是完全不知道 printf 和 run 函数的存在的,比如他们位于内存的哪个区块,
代码长什么样都是不知道的。因此,编译器只能将这两个函数的跳转地址先暂时设为 0 。
这个地址会在哪个时候被修正?链接的时候!为了让链接器将来在链接时能够正确定位到这些被修正的地址,
在代码块(.data)中还存在⼀个重定位表,这张表将来在链接的时候,就会根据表里记录的地址将其修正。
📌注意:
- printf涉及到动态库,这⾥暂不做说明