什么是库

我们知道在我们编写c语言代码或者c++代码的时候会调用雷素与printf和cout之类的函数,我们并没有具体实现这个函数,而这个函数就是库里面的函数,简称库函数,库和我们的代码一样都是以二进制的形式存放在我们的磁盘上的

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

这上面就可以看到我们的程序是依赖于动态库。

我们的可执行程序和库一样在磁盘中都是二进制程序,那么操作系统怎么识别这个二进制程序是可执行程序或者库的呢

目标文件

在讲库的理解与加载之前,我们先来说说程序是如果加载到内存中的,cpu又是怎么寻址找到程序的,库和程序本质是一个东西,都是二进制文件

让我们先来了解一下目标文件 目标文件就是.o文件

我们深⼊探讨⼀下编译和链接的整个过程,来更好的理解动静态库的使⽤原理。下面的图是我们编译链接的过程,编译器最终会生成.o文件,链接器会把.o文件和库函数进行链接最终形成可执行程序

编译的过程其实就是将我们程序的源代码翻译成CPU能够直接运⾏的机器 代码。
⽐如:在⼀个源⽂件 hello.c ⾥便简单输出"hello world!",并且调⽤⼀个run函数,⽽这个函数被
定义在另⼀个原⽂件 code.c 中。这⾥我们就可以调⽤ gcc -c 来分别编译这两个原⽂件。
⽬标⽂件 是⼀个⼆进制的⽂件,⽂件的格式是 ELF ,是对⼆进制代码的⼀种封装。

ELF⽂件

在磁盘中库和我们的可执行程序还有.o目标文件一样都是以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):保存已初始化的全局变量和局部静态变量。这个数据节的作用是在磁盘中保存全局变量,假设我们在内存中需要申请50个int 的数据,而在磁盘上不会真真的会存50个int,而会以int 50的形式来保存,以后在内存中申请的总数。

ELF从形成到加载轮廓

而每一个.o的文件都会以ELF格式存储,而最终所有的ELF会最终形成一个ELF,合并完所有 Section 后, 把多个相邻、权限相同的 Section 打包成一个 Segment

ELF形成可执⾏
step-1:将多份 C/C++ 源代码,翻译成为⽬标 .o ⽂件 + 动静态库(ELF)
step-2:将多份 .o ⽂件section进⾏合并

合并是在链接的时候进行合并,但合并不是简单的合并,还会包含库函数的合并

ELF可执⾏⽂件加载
⼀个ELF会有多种不同的Section,在加载到内存的时候,也会进⾏Section合并,形成segment
合并原则:相同属性,⽐如:可读,可写,可执⾏,需要加载时申请空间等.
这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到⼀起
很显然,这个合并⼯作也已经在形成 ELF 的时候,合并⽅式已经确定了,具体合并原则被记录在
ELF 的 程序头表(Program header table)
[thr@linux ~]$ readelf -S a
There are 31 section headers, starting at offset 0x1ab0:

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
       000000000000001c  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002b8  000002b8
       00000000000000f0  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           00000000004003a8  000003a8
       0000000000000065  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           000000000040040e  0000040e
       0000000000000014  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400428  00000428
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000400448  00000448
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400460  00000460
       00000000000000c0  0000000000000018  AI       5    24     8
  [11] .init             PROGBITS         0000000000400520  00000520
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000400540  00000540
       0000000000000090  0000000000000010  AX       0     0     16
  [13] .plt.got          PROGBITS         00000000004005d0  000005d0
       0000000000000008  0000000000000000  AX       0     0     8
  [14] .text             PROGBITS         00000000004005e0  000005e0
       0000000000000252  0000000000000000  AX       0     0     16
  [15] .fini             PROGBITS         0000000000400834  00000834
       0000000000000009  0000000000000000  AX       0     0     4
  [16] .rodata           PROGBITS         0000000000400840  00000840
       0000000000000027  0000000000000000   A       0     0     8
  [17] .eh_frame_hdr     PROGBITS         0000000000400868  00000868
       000000000000003c  0000000000000000   A       0     0     4
  [18] .eh_frame         PROGBITS         00000000004008a8  000008a8
       0000000000000114  0000000000000000   A       0     0     8
  [19] .init_array       INIT_ARRAY       0000000000600e10  00000e10
       0000000000000008  0000000000000008  WA       0     0     8
  [20] .fini_array       FINI_ARRAY       0000000000600e18  00000e18
       0000000000000008  0000000000000008  WA       0     0     8
  [21] .jcr              PROGBITS         0000000000600e20  00000e20
       0000000000000008  0000000000000000  WA       0     0     8
  [22] .dynamic          DYNAMIC          0000000000600e28  00000e28
       00000000000001d0  0000000000000010  WA       6     0     8
  [23] .got              PROGBITS         0000000000600ff8  00000ff8
       0000000000000008  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000601000  00001000
       0000000000000058  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         0000000000601058  00001058
       0000000000000004  0000000000000000  WA       0     0     1
  [26] .bss              NOBITS           000000000060105c  0000105c
       0000000000000004  0000000000000000  WA       0     0     1
  [27] .comment          PROGBITS         0000000000000000  0000105c
       000000000000005a  0000000000000001  MS       0     0     1
  [28] .symtab           SYMTAB           0000000000000000  000010b8
       00000000000006a8  0000000000000018          29    47     8
  [29] .strtab           STRTAB           0000000000000000  00001760
       0000000000000244  0000000000000000           0     0     1
  [30] .shstrtab         STRTAB           0000000000000000  000019a4
       000000000000010c  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)

查看section合并的segment

[thr@linux ~]$ readelf -l a

Elf file type is EXEC (Executable file)
Entry point 0x4005e0
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
                 0x00000000000009bc 0x00000000000009bc  R E    200000
  LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x000000000000024c 0x0000000000000250  RW     200000
  DYNAMIC        0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x0000000000000868 0x0000000000400868 0x0000000000400868
                 0x000000000000003c 0x000000000000003c  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x00000000000001f0 0x00000000000001f0  R      1

 Section to Segment mapping:
  Segment Sections...
   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 .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .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 .dynamic .got 

为什么要把section合并成segmnet,答案是在磁盘和内存中最小存储单位是4kb,假设每一个section只占用-2kb,那么如果不进行合并会存在大量的资源浪费这样操作系统在加载程序时,会将具有相同属性的section合并成⼀个⼤的 segment,这样就可以实现不同的访问权限,从⽽优化内存管理和权限访问控制。

对于 程序头表 和 节头表 ⼜有什么⽤呢,其实 ELF ⽂件提供 2 个不同的视图/视⻆来让我们理解这
两个部分,说白了就是一个在程序运行加载时起作用,一个在程序链接时起作用

这样ELF就形成好了,下面我们来说说ELF如何加载到内存的

ELF是怎么加载到内存的

首先想一个问题操作系统是怎么找到他的,答案是路径+文件名,有了路径很文件名操作系统就会很容易的找到elf所在的磁盘分区,通过路劲解析查看struct dentry树,对磁盘io,对应的文件名和inode的映射,最终找到elf,那么ELF是如何转化为进程的?

我们想一个问题,进程在没被加载到内存中时,他有地址吗?,答案是有的,这个地址不是内存地址是一个逻辑地址

现代计算机的编址方法

    现代计算机的编址方法统一以一个叫做平坦模式的编址方法进行编址,这个编址模式是怎么一回事,他会从0地址开始编址,线性递增供单一、无重叠的全局虚拟地址空间,链接器可以直接给每组同权限 section 分配一段连续虚拟地址,打包成独立  Segment,操作系统直接按Segment 地址映射内存,无需分段地址换算,这个地址就是逻辑地址!我们可以用反汇编查看一下

我们可以看到这里的地址的确是从接近0号地址开始编址的,这里我们的程序还没有运行,这里的地址是在磁盘上的逻辑地址。

程序是怎么确定哪一段内存对应的是代码段或者数据区的呢,这里可以用起始地址加偏移量来确定

如果每一个segment的开始地址都是0,那么所有的可执行程序就是一个segment,所有函数变量地址都是从0开始的!
 

磁盘上的可执行程序的编址其实就是虚拟地址的统一编址,这里我们可以想到进程当中的的虚拟地址空间,而虚拟地址空间不仅仅是进程看待内存的方式,磁盘当中的可执行程序ELF,ELF 文件静态保存了构建该虚拟地址空间的映射规则,也是ELF看待内存的方式!逻辑地址和虚拟地址就相当于是一个硬币的两面

ELF加载与进程地址空间

关于这个问题我们需要画图看看

我们看看上面的图片,这里有个Entry point address 这个就是程序的入口地址,我们把这个地址复制下来用objdump反汇编看看

这个就是程序的入口地址,操作系统只需要找到这个地址就定位了程序的入口了,下面再说说具体流程

               

下面就是cpu怎么调度的了,这个页表里面的虚拟地址就是磁盘里面存放的程序的逻辑地址

这样整个体系结构就转起来了

一句话总结:

内核先创建task_struct进程管理结构,再加载磁盘 ELF 程序并搭建虚拟内存映射;CPU 通过 EIP 存放虚拟地址,借助 CR3 指向的页表经 MMU 转换为物理地址,最终从物理内存读取指令,以程序入口地址启动运行。

有了以上对于程序的加载运行理解之后,我们再来谈谈库

静态库的链接与理解

我们来看看下面的代码

我们这两个代码里面分别写了main的实现和再main里面的对于func的调用,head.h里面放了func的实现

我们用readelf 去读取对应的.o文件发现func这个函数的地址是空的,puts函数也是空的,为什么,因为这个时候还没与head.o进行链接还有对stdio库进行链接 其实puts就是printf

我们把head.o 和test.o进行链接一下

这时候对应的地址就有了

静态链接的本质:链接器从静态库归档包中抽取程序所需的目标文件,与用户源码生成的.o文件合并各自同名 section,整合为单一 ELF 可执行文件。静态库的加载和可执行程序一样,他是包含再可执行程序里面了。

动态库的链接与理解

动态库的加载其实也可以和可执行程序进行关联

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

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

动态库也是⼀个⽂件,要访问也是要被先加载,要加载也是要被打开的
让我们的进程找到动态库的本质:也是⽂件操作,不过我们访问库函数,通过虚拟地址进
⾏跳转访问的,所以需要把动态库映射到进程的地址空间中
我们可以看看下面的图片
上面的图片清晰展示了库是如何记载到内存中的还有与程序地址1空间建立关系,程序怎么才能在库对应的代码块中找到对应方法的地址呢,这里还是用了起始地址加偏移量来计算的。
那么我们知道库是再我们运行程序的时候才加载的,那么我们知道再程序编译链接时,库函数的地址是不确定的,那么是怎么做到库加载的时候确定地址的呢?是通过加载完库之后,再去修改代码对应的地址吗,答案是不是的!
这里就要引入一个东西了,这个东西叫做全局偏移量表GOT
所以:动态链接采⽤的做法是在 .data (可执⾏程序或者库⾃⼰)中专⻔预留⼀⽚区域⽤来存放函数 的跳转地址,它也被叫做全局偏移表GOT,表中每⼀项都是本运⾏模块要引⽤的⼀个全局变量或函数 的地址。
如下图所示
由于代码是只读的,我们不能修改他,所以就有了GOT表,每个程序都独有一份,为什么因为对应的每个程序地址空间对应的库的代码段的地址都是不一样的,所以需要各自程序独有一份
在调⽤函数的时候会⾸先查表,然后根据表中的地址来进⾏跳转,这些地址在动态库加载的时候会
被修改为真正的地址。
动态库内每个函数、全局变量相对于库基址的偏移在编译时就固定;加载时操作系统确定库的虚拟起始基址,动态链接器通过「基址 + 固定偏移」算出符号真实地址,回填到编译阶段就已存在的 GOT 表中,供代码访问全局符号
这种⽅式实现的动态链接就被叫做 PIC 地址⽆关代码 。换句话说,我们的动态库不需要做任何修
改,被加载到任意内存地址都能够正常运⾏,并且能够被所有进程共享,这也是为什么我们自己写动态库时编译器指定-fPIC参数的原因,PIC=相对编址+GOT。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐