深度拆解 Linux 程序编译与链接:从静态库到 ELF 运行时全流程

目录

一、什么是库

二、静态库

2.1静态库的生成

2.2静态库的使用

三、动态库

3.1动态库的生成

3.2动态库的使用

3.3库运行搜索路径

四、使用外部库

五、目标文件

六、ELF文件

七、ELF从形成到加载轮廓

7.1ELF从形成到加载轮廓

7.2ELF可执行文件加载

八、理解链接与加载

8.1静态链接

8.2ELF加载与进程地址空间

8.2.1虚拟地址/逻辑地址

8.2.2重新理解进程虚拟地址空间

8.3动态链接与动态库加载

8.3.1进程如何看到动态库

8.3.2进程间如何共享库的

8.3.3动态链接

1.概要

2.可执行程序被编译器动了手脚

3.动态库中的相对地址

4.我们的程序,怎么和库具体映射起来的

5.我们的程序,怎么进行库函数调用

[6.全局偏移量表GOT(global offset table)](#6.全局偏移量表GOT(global offset table))

7.库间依赖

8.3.4总结


一、什么是库

库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。

本质上来说库是⼀种可执行代码的二进制形式,可以被操作系统载入内存执行。

库有两种:

  • 静态库 .a[Linux]、.lib[windows]
  • 动态库 .so[Linux]、.dll[windows]

我们来看一下,ubuntu动静态库和Centos动静态库

复制代码
// 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
[whb@bite-alicloud ~]$ 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

需要使用的程序

复制代码
// mystdio.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;
}

二、静态库

静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库。

⼀个可执行程序可能用到许多的库,这些库运行有的是静态库,有的是动态库,而我们的编译默 认为动态链接库,只有在该库下找不到动态.so的时候才会采用同名静态库。我们也可以使用gcc 的-static 强转设置链接静态库。

2.1静态库的生成

我们用一个小故事来理解静态库的生成

.a静态库,本质是一直归档文件,不需要使用者解压,而用gcc/g++直接进行链接即可

ar 是 gnu 归档⼯具, rc 表示(replace and create)

  • t: 列出静态库中的文件
  • v:verbose 详细信息

2.2静态库的使用

  • -L:指定库路径
  • -I :指定头文件搜索路径
  • -l : 指定库名
  • 测试目标文件生成后,静态库删掉,程序照样可以运行

如果头文件和库文件安装到系统路径下

复制代码
 gcc main.c -cmyc

头文件和库文件和我们直接的源文件在同⼀个路径下

复制代码
gcc main.c -L. -cmyc

头文件和库文件有自己的独立路径

复制代码
gcc main.c -I头文件路径 -L库文件路径 -cmyc

我们还可以直接这样做

三、动态库

动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。

⼀个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的⼀个表,而不是外部函数所在目标文件的整个机器码

在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中, 这个过程称为动态链接(dynamic linking)

动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采 用虚拟内存机制允许物理内存中的⼀份动态库被要用到该库的所有进程共用,节省了内存和磁盘空 间。

3.1动态库的生成

  • shared:表示生成共享库格式
  • fPIC:产生位置无关码(position independent code)
  • 库名规则:libxxx.so

3.2动态库的使用

头文件和库文件安装到系统路径下

复制代码
gcc main.c -myc

头文件和库文件和我们自己的源文件在同⼀个路径下

复制代码
gcc main.c -L. -myc // 从左到右搜索-L指定的目录 

头文件和库文件有自己的独立路径

复制代码
gcc main.c -I头文件路径 -L库文件路径 -myc

3.3库运行搜索路径

解决方法:

  • 拷贝 .so 文件到系统共享库路径下,⼀般指 /usr/lib、/usr/local/lib、/lib64 或者开篇指明的库路径等
  • 向系统共享库路径下建立同名软连接
  • 更改环境变量: LD_LIBRARY_PATH
  • ldconfig方案:配置/ etc/ld.so.conf.d/ ,ldconfig更新

静态库和动态库可不可以同时存在?

可以,但默认使用的是动态库。如果非要使用静态库要加上-static,但如果使用-static就必须存在对应的静态库。如果只存在静态库就只能进行静态链接。

在Linux系统下默认安装的大部分库都优先安装的是动态库。

库 :应用程序 = 1 : n

vs不仅可以

四、使用外部库

我们现在没接触过太多的库,唯⼀接触过的就是C、C++标准库,这⾥我们可以推荐⼀个好玩的图形库:ncurses

安装:

复制代码
sudo yum install -y ncurses-devel

系统中其实有很多库,它们通常由⼀组互相关联的用来完成某项常见⼯作的函数构成。比如用来处理屏幕显示情况的函数(ncurses库)

五、目标文件

编译和链接这两个步骤,在Windows下被我们的IDE封装的很完美,我们⼀般都是⼀键构建非常方便, 但⼀旦遇到错误的时候呢,尤其是链接相关的错误,很多⼈就束手无策了。在Linux下,我们之前也学习过如何通过gcc编译器来完成这⼀系列操作。

接下来我们深入探讨⼀下编译和链接的整个过程,来更好的理解动静态库的使用原理。 先来回顾下什么是编译呢?编译的过程其实就是将我们程序的源代码翻译成CPU能够直接运行的机器代码。 比如:在⼀个源文件hello.c 里便简单输出"hello world!",而且调用⼀个run函数,而这个函数被定义在另⼀个原文件 code.c 中。这里我们就可以调用 gcc -c 来分别编译这两个原文件。

复制代码
// 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 ,是对二进制代码的⼀种封装。

六、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):保存已初始化的全局变量和局部静态变量。

七、ELF从形成到加载轮廓

7.1ELF从形成到加载轮廓

step-1:将多份 C/C++ 源代码,翻译成为目标.o 文件+动静态库(ELF)

step-2:将多份 .o 文件section进行合并

7.2ELF可执行文件加载

  • ⼀个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment
  • 合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等
  • 这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到⼀起
  • 很显然,这个合并工作也已经在形成 ELF 的时候,合并方式已经确定了,具体合并原则被记录在了 ELF 的 程序头表(Program header table) 中

查看可执行程序的section:

查看section合并的segment

为什么要将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文件可以看到该节。

从执行视图来看:

  • 告诉操作系统哪些模块可以被加载进内存。
  • 加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执行的。

查看目标文件:

查看可执行程序:

八、理解链接与加载

8.1静态链接

无论是自己的 .o ,还是静态库中的 .o ,本质都是把.o文件进行连接的过程。所以:研究静态链接,本质就是研究 .o 是如何链接的。

查看编译后的.o目标文件

objdump -d 命令:将代码段(.text)进行反汇编查看

hello.o 中的 main 函数不认识 printf和run 函数

code.o 不认识 printf 函数

我们可以看到这里的call指令,它们分别对应之前调用的printf和run函数,但是你会发现他们的跳转地址都被设成了0。

**就是在编译 hello.c 的时候,编译器是完全不知道 printf 和 run 函数的存在的,比如他们位于内存的哪个区块,代码长什么样都是不知道的。因此,编译器只能将这两个函数的跳转地址先暂时设为0。**这个地址会在哪个时候被修正?链接的时候!为了让链接器将来在链接时能够正确定位到这些被修正的地址,在代码块(.data)中还存在⼀个重定位表,这张表将来在链接的时候,就会根据表里记录的地址将其修正。

结论:多个.o彼此不知道对方

puts:就是printf的实现

UND就是:undefine,表示未定义说白了就是本.o文件找不到

puts:就是printf的实现, run就是我们自己的方法在hello.o中未定义(因为在code.o中)

UND就是:undefine,表示未定义说白了就是本.o文件找不到

两个.o进行合并之后,在最终的可执行程序中,就找到了run

000000000040054c:其实是地址,后面说

FUNC:表示run符号类型是个函数

13:就是run函数所在的section被合并最终的那⼀个section中了,13就是下标

hello.o和code.o的.text被合并了,是hello的第16个section,关于hello.o或者code.o call后⾯的00 00 00 00有没有被修改成为具体的最终函数地址,这些问题我们可以一起来看看反汇编

所以,两个.o的代码段合并到了⼀起,并进行了统⼀的编址,链接的时候,会修改.o中没有确定的函数地址,在合并完成之后,进行相关call地址,完成代码调用。

静态链接就是把库中的.o进行合并,和上述过程⼀样

所以链接其实就是将编译之后的所有目标文件连同用到的⼀些静态库运行时库组合,拼装成⼀个独立的可执行文件。其中就包括我们之前提到的地址修正,当所有模块组合在⼀起之后,链接器会根据我们的.o文件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从而修正它们的地址。这其实就是静态链接的过程

链接过程中会涉及到对.o中外部符号进行地址重定位。

8.2ELF加载与进程地址空间

8.2.1虚拟地址/逻辑地址

⼀个ELF程序,在没有被加载到内存的时候,有没有地址呢?进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?

⼀个ELF程序,在没有被加载到内存的时候,本来就有地址,当代计算机工作的时候,都采用**"平坦 模式"**进行工作。所以也要求ELF对自己的代码和数据进行统⼀编址

最左侧的就是ELF的虚拟地址,其实,严格意义上应该叫做逻辑地址(起始地址+偏移量),但是我们 认为起始地址是0。也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执行程序进行统⼀编址了。

进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?从ELF各个 segment来,每个segment有自己的起始地址和自己的长度,用来初始化内核结构中的[start,end] 等范围数据,另外在用详细地址,填充页表。

所以:虚拟地址机制,不光光OS要支持,编译器也要支持

8.2.2重新理解进程虚拟地址空间

ELF在被编译好之后,会把自己未来程序的入口地址记录在ELF header的Entry字段中:

一张图帮助大家理解:

8.3动态链接与动态库加载

8.3.1进程如何看到动态库

库函数调用分两个阶段:1,被进程看到:动态库映射到进程的物理空间。2,被进程调用:在进程的地址空间中进行跳转

8.3.2进程间如何共享库的

动态库中的代码不会出现重复!

8.3.3动态链接

1.概要

动态链接其实远比静态链接要常用得多。比如我们查看下 hello 这个可执行程序依赖的动态库,会发现它就用到了⼀个c动态链接库:

这里的 libc.so 是C语⾔的运行时库,里面提供了常用的标准输入输出文件字符串处理等等这些功 能。

**那为什么编译器默认不使用静态链接呢?**静态链接会将编译产生的所有目标文件,连同用到的各种 库,合并形成⼀个独立的可执行文件,它不需要额外的依赖就可以运行。照理来说应该更加方便才对是吧?

**静态链接最大的问题在于生成的文件体积大,并且相当耗费内存资源。**随着软件复杂度的提升,我们的操作系统也越来越臃肿,不同的软件就有可能都包含了相同的功能和代码,显然会浪费大量的硬盘空间。

这个时候,动态链接的优势就体现出来了,我们可以将需要共享的代码单独提取出来,保存成⼀个独立的动态链接库,等到程序运行的时候再将它们加载到内存,这样不但可以节省空间,因为同⼀个模块在内存中只需要保留⼀份副本,可以被不同的进程所共享。

动态链接到底是如何工作的?

首先要交代⼀个结论,**动态链接实际上将链接的整个过程推迟到了程序加载的时候。**比如我们去运行⼀个程序,操作系统会首先将程序的数据代码连同它用到的⼀系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使用情况为它们动态分配⼀段内存。 当动态库被加载到内存以后,⼀旦它的内存地址被确定,我们就可以去修正动态库中的那些函数跳转地址了。

2.可执行程序被编译器动了手脚

有一个很奇怪的现象,所有的C程序处理需要依赖对应的动态库,还都依赖这个库

在C/C++程序中,当程序开始执行时,它首先并不会直接跳转到 main 函数。实际上,程序的入口点是 _start ,这是⼀个由C运行时库(通常是glibc)或链接器(如ld)提供的特殊函数。

在 _start 函数中,会执行⼀系列初始化操作,这些操作包括:

  1. 设置堆栈:为程序创建⼀个初始的堆栈环境。
  2. 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位 置,并清零未初始化的数据段。
  3. 动态链接:这是关键的⼀步, _start 函数会调用动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确地映射到动态库中的实际地址

动态链接器:

动态链接器(如ld-linux.so)负责在程序运行时加载动态库。当程序启动时,动态链接器会解析程序中的动态库依赖,并加载这些库到内存中。

环境变量和配置文件:

Linux系统通过环境变量(如LD_LIBRARY_PATH)和配置文件(如/etc/ld.so.conf及其子配置文件)来指定动态库的搜索路径。这些路径会被动态链接器在加载动态库时搜索。

缓存文件:

为了提高动态库的加载效率,Linux系统会维护⼀个名为/etc/ld.so.cache的缓存文件。 该文件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会首先搜索这个缓存文件。

4.调用 __libc_start_main :⼀旦动态链接完成, _start 函数会调用__libc_start_main (这是glibc提供的⼀个函数)。 __libc_start_main 函数负责执行⼀些额外的初始化工作,比如设置信号处理函数、初始化线程库(如果使用了线程)等。

5.调用main 函数:最后, __libc_start_main 函数会调用程序的 main 函数,此时程序的执行控制权才正式交给用户编写的代码。

6.处理 main 函数的返回值:当 main 函数返回时, __libc_start_main 会负责处理这个返回值,并最终调用 _exit 函数来终止程序。

3.动态库中的相对地址

动态库为了随时进行加载,为了⽀持并映射到任意进程的任意位置,对动态库中的⽅法,统⼀编址, 采用相对编址的方案进行编制的(其实可执行程序也⼀样,都要遵守平坦模式,只不过exe是直接加载的)。一句话说明白,动态库也是ELF,我们也理解为起始地址(0)+ 偏移量。

复制代码
查看任意⼀个库的反汇编 
$ objdump -S /lib64/libc-2.17.so | less
4.我们的程序,怎么和库具体映射起来的

动态库也是⼀个文件,要访问也是要被先加载,要加载也是要被打开的。

让我们的进程找到动态库的本质:也是文件操作,不过我们访问库函数,通过虚拟地址进行跳转访问的,所以需要把动态库映射到进程的地址空间中。

5.我们的程序,怎么进行库函数调用

库已经被我们映射到了当前进程的地址空间中,库的虚拟起始地址我们也已经知道了,库中每⼀个方法的偏移量地址我们也知道,所有:访问库中任意⽅法,只需要知道库的起始虚拟地址+方法偏移量即可定位库中的方法,而且:整个调用过程,是从代码区跳转到共享区,调用完毕在返回到代码区,整个过程完全在进程地址空间中进行的。

6.全局偏移量表GOT(global offset table)

注意:

也就是说,我们的程序运行之前,先把所有库加载并映射,所有库的起始虚拟地址都应该提前知道,然后对我们加载到内存中的程序的库函数调用进行地址修改,在内存中⼆次完成地址设置 (这个叫做加载地址重定位)

修改的是代码区?不是说代码区在进程中是只读的吗?怎么修改?能修改吗?!!

所以:动态链接采用的做法是在 .data (可执行程序或者库自己)中专门预留一片区域用来存放函数的跳转地址,它也被叫做全局偏移表GOT表中每⼀项都是本运行模块要引用的⼀个全局变量或函数的地址。因为.data区域是可读写的,所以可以支持动态进行修改。

  • 由于代码段只读,我们不能直接修改代码段。但有了GOT表,代码便可以被所有进程共享。但在不同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到GOT表上,就是每个进程的每个动态库都有独立的GOT表,所以进程间不能共享GOT表。
  • 在单个.so下,由于GOT表与 .text 的相对位置是固定的,我们完全可以利用CPU的相对寻址来找 到GOT表。
  • 在调用函数的时候会首先查表,然后根据表中的地址来进行跳转,这些地址在动态库加载的时候会 被修改为真正的地址。
  • 这种方式实现的动态链接就被叫做 PIC 地址无关代码 。换句话说,我们的动态库不需要做任何修 改,被加载到任意内存地址都能够正常运行,并且能够被所有进程共享,这也是为什么之前我们给 编译器指定-fPIC参数的原因,PIC=相对编址+GOT。

PLT是什么?

7.库间依赖

**不仅仅有可执行程序调用库,库也会调用其他库,库之间是有依赖的。****如何做到库和库之间互相调用也是与地址⽆关的呢?**库中也有.GOT,和可执行⼀样!这也就是为什么大家为什么都是ELF的格式!

由于GOT表中的映射地址会在运行时去修改,我们可以通过gdb调试去观察GOT表的地址变化。在这里我们只用知道原理即可。

由于动态链接在程序加载的时候需要对大量函数进行重定位,这一部显然是非常耗时的。为了进⼀ 步降低开销,我们的操作系统还做了⼀些其他的优化,比如延迟绑定,或者也叫PLT(过程连接表 (Procedure Linkage Table))。与其在程序⼀开始就对所有函数进行重定位,不如将这个过程 推迟到函数第⼀次被调用的时候,因为绝大多数动态库中的函数可能在程序运行期间⼀次都不会被 使用到。

思路是:GOT中的跳转地址默认会指向⼀段辅助代码,它也被叫做桩代码/stup。在我们第⼀次调用函数的时候,这段代码会负责查询真正函数的跳转地址,并且去更新GOT表。于是我们再次调用函数的时候,就会直接跳转到动态库中真正的函数实现。

一句话总结:解析依赖关系的时候,就是加载并完善互相之间的GOT表的过程。

总而言之,动态链接实际上将链接的整个过程,比如符号查询、地址的重定位从编译时推迟到了程序 的运行时,它虽然牺牲了⼀定的性能和程序加载时间,但绝对是物有所值的。因为动态链接能够更有效的利用磁盘空间和内存资源,以极大⽅便了代码的更新和维护,更关键的是,它实现了⼆进制级别的代码复用。

8.3.4总结

静态链接的出现,提高了程序的模块化水平。对于⼀个大的项目,不同的人可以独立地测试和开发 自己的模块。通过静态链接,生成最终的可执行文件。

我们知道静态链接会将编译产⽣的所有目标文件,和用到的各种库合并成⼀个独立的可执行文件, 其中我们会去修正模块间函数的跳转地址,也被叫做编译重定位(也叫做静态重定位)。

而动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行⼀个程序,操作系 统会首先将程序的数据代码连同它用到的⼀系列动态库先加载到内存,其中每个动态库的加载地址 都是不固定的,但是无论加载到什么地方,都要映射到进程对应的地址空间,然后通过.GOT方式进行调用(运行重定位,也叫做动态地址重定位)。

相关推荐
十六年开源服务商2 小时前
WordPress服务器响应时间优化终极指南2026
android·运维·服务器
HealthScience2 小时前
Autoruns自启动/进程控制软件怎么使用?
linux·运维·服务器
Arvin_Rong2 小时前
Linux 服务器 /tmp 目录:使用机制与安全加固
linux·服务器·安全
gwjcloud2 小时前
Lvs+Keepalived详解
运维·lvs
RDCJM2 小时前
nginx 代理 redis
运维·redis·nginx
小江的记录本2 小时前
【RocketMQ】RocketMQ核心知识体系全解(5大核心模块:架构模型、事务消息两阶段提交、回查机制、延迟消息、顺序消息)
linux·运维·服务器·前端·后端·架构·rocketmq
数据知道2 小时前
claw-code 源码详细分析:命令宇宙 vs 工具宇宙——`commands` / `tools` 镜像清单如何驱动路由与 shim 执行?
linux·服务器·网络·python·ai·claude code
三万棵雪松2 小时前
【Linux 物联网网关主控系统-Web部分(二)】
linux·前端·物联网
一叶之秋14122 小时前
通信之道:解锁Linux进程间通信的无限可能(一)
linux·运维·服务器