当前位置: 首页 > news >正文

Linux——库的制作与原理

目录

一、库的概念

二、静态库

2.1、静态库的制作

打包 ar -rc libmyc.a *.o

寻找静态库命令 -L接路径,-l(小写L)接库名称

寻找头文件-I(大写i)接上路径

写到makefile下

三、动态库

3.1、动态库的制作

生成动态.o文件的命令要加-fPIC

动态库打包

3.2、使用动态库形成的可执行文件为什么不能执行

解决方法:

3.3、几个结论

1、动静态库同时存在时,默认使用动态库;若是要使用静态库,编译时加上-static选项;只存在静态库时,无论加不加-static都默认使用静态库

2、在Linux系统下安装的库大部分默认都是动态库

3、库和应用程序比为1:n

4、VS不仅仅能够生成可执行文件,还能生成动静态库

四、ELF文件

五、ELF从形成到加载轮廓

5.1、ELF形成可执行文件

5.2、ELF可执行文件加载

 为什么要合并section

程序头表和节头表分别有什么作用?

5.3、ELF header

六、理解连接与加载

6.1、静态连接

6.2、ELF加载与进程地址空间

6.2.1、虚拟地址/逻辑地址

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

6.3、动态库链接和动态库加载

6.3.1、进程如何看到动态库

6.3.2、进程间是如何共享库的

6.3.3、动态链接

我们的可执行程序被编译器动了手脚

动态库中的相对地址

我们的程序和动态库是怎么映射起来的

我们的程序怎样进行库函数调用

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

库间依赖

plt

总结


一、库的概念

在计算机编程中,“库”(Library)是一组预先编写好的代码组件(如函数、类、数据结构等)的集合,用于提供特定功能或解决特定领域的问题。库的核心目标是复用代码,避免开发者重复造轮子,提高开发效率和代码质量。

本质上来说库是⼀种可执行代码的⼆进制形式,可以被操作系统载⼊内存执行。库有两种:
  • 静态库 .a[Linux].lib[windows]
  • 动态库 .so[Linux].dll[windows]

见一见库:

// 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-linux
gnu/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-redhat
linux/4.8.2/libstdc++.

二、静态库

静态库(.a):程序在编译链接的时候把库的代码链接到可执⾏⽂件中,程序运⾏的时候将不再
需要静态库。
⼀个可执⾏程序可能⽤到许多的库,这些库运⾏有的是静态库,有的是动态库,⽽我们的编译默
认为动态链接库,只有在该库下找不到动态.so的时候才会采⽤同名静态库。我们也可以使⽤ gcc
-static 强转设置链接静态库。

2.1、静态库的制作

动静态库中不要包含main函数

动静态库的都是源文件对应的.o文件

假设现有两个用户,一个为库和头文件的编写者writer,另一个为使用者user。那么因为静态库就是所有.o文件,所以需要把.o文件传给user,此外还需要将头文件传过去,这样才能找到声明以及后面正确地预处理(预处理展开头文件)

以上为writer下的初始文件,现在将.c文件全部编译成.o文件,可以正常运行:

之后传给user使用,这就相当于user用户在使用库了,现演示:

将writer中的头文件和.o文件传过来,可以正常运行

打包 ar -rc libmyc.a *.o

静态库的本质就是所有的.o文件打了一个包所形成的

打包的方法:ar -rc lib###.a *.o (lib###.a中###为实际库名称,rc表示replace和create,也就是打包的.o文件中已经存在就替换,不存在就创建进包中)

在writer中打包:

这样打包之后是一种.a文件,这是归档文件,不需要使用者解包,直接gcc/g++链接即可

之后让user使用这个打包的.a文件(名称定为libmyc.a),当.o文件很多时打包很方便。打包的内容是全部的.o文件,user直接使用即可:

寻找静态库命令 -L接路径,-l(小写L)接库名称

但是发现无法链接,这是因为系统在默认的路径下找指定的库,而我们写的库libmyc.a是找不到的,要使用命令来查找:gcc usercode.c -o usercode -L. -lmyc(-L后面接上库的路径,-l接上要使用的库的真正名称即去掉lib和后缀.a之后的名称)

每次分别传头文件和库文件很繁琐,直接将它们打个包(tar)给user使用(头文件和.o文件在同一个文件中),之后user使用这个打包的lib文件:

在user2中将这个压缩包解压,之后形成一个目录:

在lib中整理头文件和库文件:

开始使用这个库:

寻找头文件-I(大写i)接上路径

出现头文件找不到,这是因为头文件不在当前目录下,而在/lib/include中,要使用命令 -I(大写i) 接上指定路径

又出现了链接问题,要使用-L -l(小写l)来找到我们自己写的库

库也是需要安装在系统中的,看看系统中的头文件(/usr/include)以及库(/lib64)

而这些库文件也是需要安装的,若是我们将我们写的库和头文件拷贝进系统对应位置,那么就不需要指定路径了,只需要-l(小写l)接上要找的库的名称

系统默认的库文件libc.a不用这个选项,是因为系统默认去链接这个库文件

写到makefile下

接下来要在指定路径下形成并且直接打包,这样写:

先make生成.a和.o文件之后make output生成目录以及压缩包:


三、动态库

3.1、动态库的制作

生成动态.o文件的命令要加-fPIC

开篇说了,静、动态库的本质都是.o文件,所以动态库也是要编译而来的,对应命令:

gcc -fPIC -c  *.c(由.c生成可以动态链接的.o文件)

动态库打包

动态库打包命令:

gcc -shared -o libmyc.so *.o(不同于静态库的ar)

将静态库的makefile做一个备份,之后将动态库的这些相对于静态库生成打包的小变化写到makefile中:

之后make和make output生成压缩包,传给zhangsan使用:

开始使用:

之后运行usercode,找不到共享目标文件,文件不存在

这是什么原因?

3.2、使用动态库形成的可执行文件为什么不能执行

我们ldd看一下usercode依赖的库:

找不到,为什么?

上述的gcc后面的一堆命令只是告诉了gcc动态库在哪,所以这里的gcc可以链接成功;而没有告诉系统,使用到动态库的文件在执行时会去系统中找动态库再将需要的库加载到内存中,所以显示找不到,形成的可执行文件无法运行

为什么使用静态库的能运行,因为在执行前链接时,所需要的静态库都会拷贝到可执行文件中,所以可以直接执行

解决方法:

1、将使用到的动态库拷贝到系统的/lib64路径下:

2、在/lib64下建立一个指向libmyc.so的软连接,在运行时通过软链接找到这个库(运行时去系统中/lib64中找到的是所需动态库的软连接,之后通过软链接找到动态库)

3、导入环境变量,在运行程序时OS除了在系统中找之外,还会在环境变量LD_LIBRARY_PATH下找。那么将动态库的路径导入这个环境变量就可运行了

导入环境变量是内存级的,所以要一劳永逸就需要将导入的内容写到配置文件中

4、  新增配置文件,在/etc/ld.so.conf.d/配置文件下建立一个配置文件,里面的内容为所需动态库所在的路径,之后再重新加载配置文件ldconfig就可以运行了

3.3、几个结论

1、动静态库同时存在时,默认使用动态库;若是要使用静态库,编译时加上-static选项;只存在静态库时,无论加不加-static都默认使用静态库

加上-static

若是只有动态库,加上-static则出错

删掉动态库,则默认使用静态库

2、在Linux系统下安装的库大部分默认都是动态库

动态库的默认地位是Linux在效率、灵活性和可维护性之间权衡的结果。它适应了开源生态中协作开发、快速迭代的需求,同时通过技术手段(如符号版本控制、包管理)解决了潜在的问题(如依赖冲突)。静态库则作为补充,在特定场景下发挥作用。

3、库和应用程序比为1:n

1:n 关系的本质:通过库实现代码复用和功能抽象,降低软件开发的复杂度和成本。 类比: 库像“工具箱”,应用程序像“工匠”。一个工具箱(库)可以被多个工匠(应用)使用,每个工匠只需专注自己的作品(应用逻辑)。 延伸思考: 现代软件开发中,库的层级可能更复杂(如库依赖其他库),但1:n的基本原则仍然适用。 微服务架构中,服务之间的调用类似库与应用的关系,但通过网络通信实现(而非内存调用)。

4、VS不仅仅能够生成可执行文件,还能生成动静态库

为什么需要生成库(动态/静态)? (1) 代码复用 场景:多个项目需要共享相同的功能(如数学计算、日志记录、网络通信)。 问题:如果每个项目都重复实现这些功能,会导致: 代码冗余:相同的逻辑分散在多个项目中。 维护困难:修复一个漏洞需要更新所有相关项目。 解决方案:将通用功能封装成库,供多个项目调用。 (2) 模块化开发 职责分离:库专注于单一功能(如加密、图形渲染),应用程序只需调用库的接口,无需关心实现细节。 团队协作:不同团队可以独立开发库和应用,提高开发效率。 (3) 性能与资源优化 动态库(.dll):多个程序共享同一份内存中的库代码,节省内存和磁盘空间。 静态库(.lib):编译时直接嵌入代码,避免运行时依赖,适合嵌入式系统或对稳定性要求高的场景。


四、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从形成到加载轮廓

5.1、ELF形成可执行文件

第一步将多份源代码编译成.o文件,第二部将ELF格式的.o文件的section进行合并

5.2、ELF可执行文件加载

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

readelf -S /usr/bin/ls
There are 30 section headers, starting at offset 0x1c3e8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       0000000000000038  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002d0  000002d0
       0000000000000c18  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400ee8  00000ee8
       0000000000000572  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           000000000040145a  0000145a
       0000000000000102  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000401560  00001560
       0000000000000090  0000000000000000   A       6     2     8
  [ 9] .rela.dyn         RELA             00000000004015f0  000015f0
       00000000000000d8  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             00000000004016c8  000016c8
       0000000000000ac8  0000000000000018  AI       5    24     8
  [11] .init             PROGBITS         0000000000402190  00002190
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         00000000004021b0  000021b0
       0000000000000740  0000000000000010  AX       0     0     16
  [13] .text             PROGBITS         00000000004028f0  000028f0
       000000000001014a  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         0000000000412a3c  00012a3c
       0000000000000009  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         0000000000412a60  00012a60
       0000000000003cce  0000000000000000   A       0     0     32
  [16] .eh_frame_hdr     PROGBITS         0000000000416730  00016730
       0000000000000754  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         0000000000416e88  00016e88
       0000000000002704  0000000000000000   A       0     0     8
  [18] .init_array       INIT_ARRAY       000000000061a328  0001a328
       0000000000000008  0000000000000008  WA       0     0     8
  [19] .fini_array       FINI_ARRAY       000000000061a330  0001a330
       0000000000000008  0000000000000008  WA       0     0     8
  [20] .jcr              PROGBITS         000000000061a338  0001a338
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .data.rel.ro      PROGBITS         000000000061a340  0001a340
       0000000000000a68  0000000000000000  WA       0     0     32
  [22] .dynamic          DYNAMIC          000000000061ada8  0001ada8
       0000000000000200  0000000000000010  WA       6     0     8
  [23] .got              PROGBITS         000000000061afa8  0001afa8
       0000000000000048  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         000000000061b000  0001b000
       00000000000003b0  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         000000000061b3c0  0001b3c0
       0000000000000240  0000000000000000  WA       0     0     32
  [26] .bss              NOBITS           000000000061b600  0001b600
       0000000000000d20  0000000000000000  WA       0     0     32
  [27] .gnu_debuglink    PROGBITS         0000000000000000  0001b600
       0000000000000010  0000000000000000           0     0     4
  [28] .gnu_debugdata    PROGBITS         0000000000000000  0001b610
       0000000000000cb8  0000000000000000           0     0     1
  [29] .shstrtab         STRTAB           0000000000000000  0001c2c8
       000000000000011a  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)
 

总共有三十个节,每个节中存放的都是特定类型的数据,通过下标(即每个节的偏移量和大小来找确定位置)

readelf -l 文件名(查看合并之后的即segment)

Elf file type is EXEC (Executable file)
Entry point 0x404324
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x000000000001958c 0x000000000001958c  R E    200000
  LOAD           0x000000000001a328 0x000000000061a328 0x000000000061a328
                 0x00000000000012d8 0x0000000000001ff8  RW     200000
  DYNAMIC        0x000000000001ada8 0x000000000061ada8 0x000000000061ada8
                 0x0000000000000200 0x0000000000000200  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x0000000000016730 0x0000000000416730 0x0000000000416730
                 0x0000000000000754 0x0000000000000754  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x000000000001a328 0x000000000061a328 0x000000000061a328
                 0x0000000000000cd8 0x0000000000000cd8  R      1

 Section to Segment mapping:
  Segment Sections...

下面就是30个section合并成了8个segment,例如03中不同的节.data .bss都被合并进了一个段segment
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .data.rel.ro .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .data.rel.ro .dynamic .got

 为什么要合并section

为什么要将section合并成为segment
Section合并的主要原因是为了减少⻚⾯碎⽚,提⾼内存使⽤效率。如果不进⾏合并,
假设⻚⾯⼤⼩为4096字节(内存块基本⼤⼩,加载,管理的基本单位),如果.text部分
为4097字节,.init部分为512字节,那么它们将占⽤3个⻚⾯,⽽合并后,它们只需2个
⻚⾯。(交互基本单位为4kb)
此外,操作系统在加载程序时,会将具有相同属性的section合并成⼀个⼤的
segment,这样就可以实现不同的访问权限,从⽽优化内存管理和权限访问控制。

程序头表和节头表分别有什么作用?

ELF提供了两个视图让我们理解

链接视图(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 ⽂件可以看到该节。
从 执⾏视图 来看:
告诉操作系统哪些模块可以被加载进内存。
加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执⾏的。

5.3、ELF header

我们可以在 ELF头 中找到⽂件的基本信息,以及可以看到ELF头是如何定位程序头表和节头表的。
如我们查看下hello.o这个可重定位文件的主要信息:
只需要直到头的作用是定位文件的其他部位即可
// 查看⽬标⽂件
$ readelf -h hello.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64 # ⽂件类型
Data: 2's complement, little endian # 指定的编码⽅式
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file) # 指出ELF⽂件的类型
Machine: Advanced Micro Devices X86-64 # 该程序需要的体系结构
Version: 0x1
Entry point address: 0x0 # 系统第⼀个传输控制的虚拟地址,在那启动进程。假如⽂件没有如何关联的⼊⼝点,该成员就保持为0
Start of program headers: 0 (bytes into file)
Start of section headers: 728 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes) # 保存着ELF头大小(以字节计数)
Size of program headers: 0 (bytes) # 保存着在文件的程序头表(program header table)中⼀个⼊⼝的⼤⼩
Number of program headers: 0 # 保存着在程序头表中⼊⼝的个数。因此,e_phentsizee_phnum的乘机就是表的⼤⼩(以字节计数).假如没有程序头表,变量为0
Size of section headers: 64 (bytes) # 保存着section头的⼤⼩(以字节计数)。⼀个section头是在section头表的⼀个⼊⼝
Number of section headers: 13 # 保存着在section header table中的入口数目。e_shentsizee_shnum的乘积就是section头表的大小(以字节计数)。假如⽂件没有section头表,值为0
Section header string table index: 12 # 保存着跟section名字字符表相关⼊⼝的section头表(section header table)索引。
// 查看可执⾏程序
$ gcc *.o
$ readelf -h a.out
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1060
Start of program headers: 64 (bytes into file)
Start of section headers: 14768 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30
再一点:ELF中各个部分的定位位置,都可以通过各自的起始地址和大小来确定(ELF与文件偏移量的关系)

六、理解连接与加载

6.1、静态连接

静态链接就是.o文件进行合并的过程

objdump -d 目标文件 : 反汇编目标文件的代码段进行查看

call表示要调用哪个函数的目标地址,发现code.c的e8后面为全0,这是因为还没有进行链接,找不到库里面的printf函数;同样,hello.c找不到run函数定义以及printf函数定义

这个地址会在哪个时候被修正?链接的时候!为了让链接器将来在链接时能够正确定位到这些被修正的地址,在代码块(.data)中还存在⼀个重定位表,这张表将来在链接的时候,就会根据表⾥记录的地址将其修正。

也就是说在链接之前,多个.o文件彼此不知道对方;有一个现象可以佐证,就是在code.里面随便定义一个函数,这个函数没有定义只有code.c的调用,此时发现编译也可以成功,就是因为还没有链接,这个函数的call调用地址为0,发现不了错误

删掉f函数,恢复成之前的样子,来看目标文件的符号表:

puts就是printf函数的实现,发现code.c的符号表中这个实现未定义,说明找不到,也就是未链接;hello.c里面puts和run也是未定义

将它们静态链接成main.exe查看这个链接之后的目标文件的符号表

FUNC标明这是函数;

run和main函数都可以找到(未显示UND),fputs函数找不到,这涉及到动态链接,后面说

函数名前面的数字就是合并之后的下标,readelf -S 查看section

发现合并在了.text代码段

反汇编main.exe的代码段,查看函数调用情况

最终:
1. 两个.o的代码段合并到了⼀起,并进⾏了统⼀的编址
2. 链接的时候,会修改.o中没有确定的函数地址,在合并完成之后,进⾏相关call地址,完成代码
调⽤
所以链接其实就是将编译之后的所有⽬标⽂件连同⽤到的⼀些静态库运⾏时库组合,拼装成⼀个独⽴的可执⾏⽂件。其中就包括我们之前提到的地址修正,当所有模块组合在⼀起之后,链接器会根据我们的.o⽂件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从⽽修正它们的地址。这其实就是静态链接的过程。
所以.o文件被称为可重定位目标文件的原因就是合并之后每个函数调用的函数的目标地址被改变,进行了重定位

6.2、ELF加载与进程地址空间

6.2.1、虚拟地址/逻辑地址

一个elf程序,在加载到内存中前,也存在地址,这个地址是各个section进行合并之后的统一编址,在磁盘上,这种地址叫做逻辑地址

最左侧的就是ELF的虚拟地址,其实,严格意义上应该叫做逻辑地址(起始地址+偏移量), 但是我们
认为起始地址是0.也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执
行程序进行统⼀编址了,也称为平坦模式
这种从0开始统一编址的方式和虚拟地址空间的编址一样,实际上,逻辑地址和虚拟地址是一个东西的两种叫法,只不过一个在磁盘上,一个在内核数据结构中(也就是进程看待的方式)
这是各个相同属性的section进行合并之后的segment的统一编址,因此进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪⾥来的?从ELF各个segment来,每个segment有⾃⼰的起始地址和⾃⼰的⻓度,⽤来初始化内核结构中的[start, end] 等范围数
所以虚拟地址机制不仅仅是操作系统要支持,编译器也要支持

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

首先各个.o文件进行合并,即section合并形成segment,再进行统一编址,这也就是逻辑地址。之后这个ELF程序要执行,先为进程申请内核数据结构例如task_struct以及mm_struct以及vm_area_struct并且使用统一编址的起始地址和各个segment的偏移量等等对内核数据结构进行初始化。之后逐步将内容加载到物理内存,在磁盘上的逻辑地址也作为内容加载到了物理内存,在物理内存中,对加载进来的内容又进程编址,这个地址为物理地址。之后将虚拟地址和物理地址的映射关系填入该进程的页表中。程序开始执行时,因为elf程序的header里面早就记录了entry point address,那么将这个虚拟地址load到cpu的EIP寄存器中,就可以开始运行程序了

注意cpu中只用虚拟地址,也就是进程内核数据结构中的地址,之后才通过页表的映射关系去物理内存中取指令

下图为初始化mm_struct

也就是用segment的起始地址和偏移量去初始化各个划分区域的start和end

6.3、动态库链接和动态库加载

6.3.1、进程如何看到动态库

与静态库一样,动态库也是elf程序,也会有自己的进程,映射到虚拟地址空间的共享区,通过共享区的地址以及页表找到加载到物理内存中的代码数据

6.3.2、进程间是如何共享库的

不同进程的共享区会有到它们共享的那个库的映射,共享库只需要加载到物理内存并且建立好映射关系即可;当我们写的可执行程序运行到调用库函数的时候,此时会从代码区跳转到共享区进行库函数调用

动态库中的代码在内存中不会重复出现

6.3.3、动态链接

动态链接相较于静态链接更加常用,ldd查看一个程序依赖的共享库,可以看看main.exe依赖的共享库

为什么动态链接更常用?

静态链接最⼤的问题在于⽣成的⽂件体积⼤,并且相当耗费内存资源。随着软件复杂度的提升,我们的操作系统也越来越臃肿,不同的软件就有可能都包含了相同的功能和代码,显然会浪费⼤量的硬盘空间。这个时候,动态链接的优势就体现出来了,我们可以将需要共享的代码单独提取出来,保存成⼀个独⽴的动态链接库,等到程序运⾏的时候再将它们加载到内存,这样不但可以节省空间,因为同⼀个模块在内存中只需要保留⼀份副本,可以被不同的进程所共享。
动态链接到底是如何⼯作的??
⾸先要交代⼀个结论,动态链接实际上将链接的整个过程推迟到了程序加载的时候。⽐如我们去运⾏⼀个程序,操作系统会⾸先将程序的数据代码连同它⽤到的⼀系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使⽤情况为它们动态分配⼀段内存。当动态库被加载到内存以后,⼀旦它的内存地址被确定,我们就可以去修正动态库中的那些函数跳转地址了。
我们的可执行程序被编译器动了手脚

在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的缓存⽂件。
该⽂件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会⾸先搜索这个缓存⽂件。
这也是为什么之前可通过导环境变量以及配置相关配置文件来找到动态库,因为可执行程序会先通过_start调用动态链接器ld,而ld会在通过环境变量和配置文件来指定动态库的搜索路径
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 函数来终⽌程序。

动态库中的相对地址
动态库为了随时进⾏加载,为了⽀持并映射到任意进程的任意位置,对动态库中的⽅法,统⼀编址,采⽤相对编址的⽅案进⾏编制的(其实可执⾏程序也⼀样,都要遵守平坦模式,只不过exe是直接加载的)。
实际上相对地址就是动态库中每行代码的偏移量,最终在进程地址空间中,虚拟起始地址加上这个偏移量就可以找到对应代码

我们的程序和动态库是怎么映射起来的
动态库也是⼀个⽂件,要访问也是要被先加载,要加载也是要被打开的所以先会通过struct_file来打开文件struct file 是 Linux 内核中用于管理已打开文件的一种数据结构,它并不是进程特有的内核数据结构,而是属于文件系统层的通用数据结构,并且在使用时不需要用动态库中的字段初始化)
让我们的进程找到动态库的本质:也是⽂件操作,不过我们访问库函数,通过虚拟地址进
⾏跳转访问的,所以需要把动态库映射到进程的地址空间中
  1. “找到磁盘文件的数据块”
    内核通过 struct file → dentry → ext2_inode → i_block,定位到 libc.so 在磁盘上的数据块位置。

  2. “库加载”
    动态库(libc.so)被加载到内核页缓存(物理内存的一部分,图中 “库的代码和数据”)。注意:图中 “备注” 提到 “库内容其实就是在文件内核缓冲区中”,本质是 Linux 的 “页缓存(Page Cache)” 机制 —— 文件内容先缓存到内核态内存,再映射到用户进程空间。

  3. “映射”
    通过页表(Page Table) 建立 “进程虚拟地址 ↔ 物理内存(内核页缓存)” 的映射。

    进程访问共享区虚拟地址时,页表会将其转换为物理内存地址,实际访问内核缓存的动态库内容。
  4. “得到库起始虚拟地址”
    动态链接器(ld.so)完成加载后,会在进程虚拟地址空间的共享区分配一段连续虚拟地址(由 vm_area_struct 标记 vm_start),作为 libc.so 的起始地址,让进程代码能通过该地址调用库函数。

我们的程序怎样进行库函数调用
库已经被我们映射到了当前进程的地址空间中
库的虚拟起始地址我们也已经知道了
库中每⼀个⽅法的偏移量地址我们也知道
所有:访问库中任意⽅法,只需要知道库的起始虚拟地址+⽅法偏移量即可定位库中的⽅
⽽且:整个调⽤过程,是从代码区跳转到共享区,调⽤完毕在返回到代码区,整个过程完
全在进程地址空间中进⾏的.
具体过程:
在编译阶段,库中的每个方法的偏移量也就是相对地址已经知道了,并且会将使用到的库函数所在的库名称标记在对应代码处,其后还有一个库函数在库中的偏移量
之后库加载完毕,会被分配到一个虚拟起始地址,并且这个虚拟起始地址会替换之前那个标记的库名称。此后运行到调用库函数那一行时,会通过替换库名称得到的库的虚拟起始地址以及该库函数的偏移量跳转到共享区并且通过页表在内存中找到具体方法的实现,之后返回代码区
全局偏移量表GOT(global offset table)
也就是说,我们的程序运⾏之前,先把所有库加载并映射,所有库的起始虚拟地址都应该
提前知道。然后对我们加载到内存中的程序的库函数调⽤进⾏地址修改,在内存中⼆次完成地址设置(这个叫做加载地址重定位) 。但是代码区的内容不能被修改,代码区只是可读,所以这样并不可行
所以:动态链接采⽤的做法是在 .data (可执⾏程序或者库⾃⼰)中专⻔预留⼀⽚区域⽤来存放函数的跳转地址,它也被叫做全局偏移表GOT,表中每⼀项都是本运⾏模块要引⽤的⼀个全局变量或函数的地址。
因为.data区域是可读写的,所以可以⽀持动态进⾏修改
$ readelf -S a.out
...
[24] .got PROGBITS 0000000000003fb8 00002fb8
0000000000000048 0000000000000008 WA 0 0 8
...
$ readelf -l a.out # .got在加载的时候,会和.data合并成为⼀个segment,然后加载在⼀起
……
05 .init_array .fini_array .dynamic .got .data .bss
...

此时有了got表,在代码区记录的就是got地址和所调用的库函数在got表中的偏移量(在编译时确定),并且got表中各个库函数的偏移量也是编译时确定的,那么就不会再改变。当需要调用库函数时,就通过got地址以及库函数在表中的偏移量找到起始地址以及库函数偏移量。

data代码区是可写的,那么加载完毕后,当替换成虚拟起始地址时,就可以在got表中进行替换 

1. 由于代码段只读,我们不能直接修改代码段。但有了GOT表,代码便可以被所有进程共享。但在不同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到GOT表上,就是每个进程的每个动态库都有独⽴的GOT表,所以进程间不能共享GOT表
2. 在单个.so下,由于GOT表与 .text 的相对位置是固定的,我们完全可以利⽤CPU的相对寻址来找到GOT表。(got地址相对于代码区位置不变)
3. 在调⽤函数的时候会⾸先查表,然后根据表中的地址来进⾏跳转,这些地址在动态库加载的时候会被修改为真正的地址。
4. 这种⽅式实现的动态链接就被叫做 PIC 地址⽆关代码 。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运⾏,并且能够被所有进程共享,这也是为什么之前我们给编译器指定-fPIC参数的原因,PIC=相对编址+GOT
库间依赖
不仅仅有可执⾏程序调⽤库。库也会调⽤其他库!!库之间是有依赖的,如何做到库和库之间互相调⽤也是与地址⽆关的呢??库中也有.GOT,和可执⾏⼀样!这也就是为什么⼤家为什么都是ELF的格式!
plt

objdump -S main.exe,看到库方法后面是plt,这是为什么?

由于动态链接在程序加载的时候需要对⼤量函数进⾏重定位,这⼀步显然是⾮常耗时的。为了进⼀
步降低开销,我们的操作系统还做了⼀些其他的优化,⽐如延迟绑定,或者也叫PLT(过程连接表
(Procedure Linkage Table))。与其在程序⼀开始就对所有函数进⾏重定位,不如将这个过程
推迟到函数第⼀次被调⽤的时候,因为绝⼤多数动态库中的函数可能在程序运⾏期间⼀次都不会被
使⽤到。
思路是:GOT中的跳转地址默认会指向⼀段辅助代码,它也被叫做桩代码/stup。在我们第⼀次
调⽤函数的时候,这段代码会负责查询真正函数的跳转地址,并且去更新GOT表。于是我们再次
调⽤函数的时候,就会直接跳转到动态库中真正的函数实现。
PLT 延迟绑定就像是把原本在程序启动时一股脑要完成的函数地址解析工作,推迟到了真正要调用某个函数的时候。这样,程序启动的时候就不用花费时间和内存去处理那些暂时用不到的函数链接,只有在实际需要调用外部函数时,才去确定函数的真实地址,从而提升了程序的运行效率 ,特别是对于那些包含大量库函数调用,但很多函数不一定都会被执行到的程序来说,效果更为明显。
总⽽⾔之,动态链接实际上将链接的整个过程,⽐如符号查询、地址的重定位从编译时推迟到了程序的运⾏时,它虽然牺牲了⼀定的性能和程序加载时间,但绝对是物有所值的。因为动态链接能够更有效的利⽤磁盘空间和内存资源,以极⼤⽅便了代码的更新和维护,更关键的是,它实现了⼆进制级别的代码复⽤
总结
静态链接的出现,提⾼了程序的模块化⽔平。对于⼀个⼤的项⽬,不同的⼈可以独⽴地测试和开发
⾃⼰的模块。通过静态链接,⽣成最终的可执⾏⽂件。
我们知道静态链接会将编译产⽣的所有⽬标⽂件,和⽤到的各种库合并成⼀个独⽴的可执⾏⽂件,
其中我们会去修正模块间函数的跳转地址,也被叫做编译重定位(也叫做静态重定位)
⽽动态链接实际上将链接的整个过程推迟到了程序加载的时候。⽐如我们去运⾏⼀个程序,操作系
统会⾸先将程序的数据代码连同它⽤到的⼀系列动态库先加载到内存,其中每个动态库的加载地址
都是不固定的,但是⽆论加载到什么地⽅,都要映射到进程对应的地址空间,然后通过.GOT⽅式进⾏调⽤(运⾏重定位,也叫做动态地址重定位)
http://www.dtcms.com/a/285743.html

相关文章:

  • Linux 信号与中断 详解
  • ali linux 上安装swagger-codegen
  • Windows发现可疑的svchost程序
  • 深度理解 KVM:Linux 内核系统学习的重要角度
  • 【Linux】环境基础与开发工具的使用
  • Linux中的LVS集群技术
  • MySQL的基本操作及相关python代码
  • 基于极空间NAS+GL-MT6000路由器+Tailscale的零配置安全穿透方案
  • 云原生 DevOps 实战之Jenkins+Gitee+Harbor+Kubernetes 构建自动化部署体系
  • 小白学Python,网络爬虫篇(2)——selenium库
  • 图机器学习(13)——图相似性检测
  • 信息学奥赛一本通 1575:【例 1】二叉苹果树 | 洛谷 P2015 二叉苹果树
  • 短视频矩阵系统哪家好?全面解析与推荐
  • 香港服务器SSH安全加固方案与密钥认证实践
  • Flutter权限管理终极指南:实现优雅的Android 48小时授权策略
  • GLU 变种:ReGLU 、 GEGLU 、 SwiGLU
  • android 信息验证动画效果
  • 精通 triton 使用 MLIR 的源码逻辑 - 第002节:再掌握一些 triton 语法 — 通过 02 softmax
  • 续签人员李权
  • 掌上医院微信小程序平台如何对接医保在线支付?
  • vue自定义指令bug
  • poi-excel-添加水印
  • Vue3 学习教程,从入门到精通,Vue3 项目打包语法知识点及案例代码(9)
  • Windows Server 版本之间有什么区别?
  • 私有服务器AI智能体搭建配置选择记录
  • NGFW服务器安全防护
  • 浏览器信息隔离全指南:从多账号管理到隐私防护
  • VirtualBox + CentOS:启用 DHCP 获取 IPv4 地址
  • 数据结构:顺序表和链表
  • 【PTA数据结构 | C语言版】斜堆的合并操作