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

Linux中《动/静态库原理》

目录

  • 目标文件
  • ELF文件
  • ELF从形成到加载轮廓
    • ELF形成可执行
    • readelf命令
    • ELF可执行文件加载
  • 理解连接与加载
    • 静态链接
    • ELF加载与进程地址空间
      • 虚拟地址/逻辑地址
    • 重新理解进程虚拟地址空间
  • 动态链接与动态库加载
    • 进程如何看到动态库
    • 进程间如何共享库的
    • 动态链接
      • 动态链接到底是如何工作的??
      • 我们的可执行程序被编译器动了手脚
      • 动态库中的相对地址
      • 我们的程序,怎么和库具体映射起来的
      • 我们的程序,怎么进行库函数调用
      • 全局偏移量表GOT(global offset table)
      • 库间依赖
    • 总结

目标文件

编译和链接这两个步骤,在Windows下被我们的IDE封装的很完美,我们⼀般都是⼀键构建⾮常⽅便,但⼀旦遇到错误的时候呢,尤其是链接相关的错误,很多⼈就束⼿⽆策了。在Linux下,我们之前也学习过如何通过gcc编译器来完成这⼀系列操作。
在这里插入图片描述
先来回顾下什么是编译呢?编译的过程其实就是将我们程序的源代码翻译成CPU能够直接运⾏的机器
代码。
⽐如:在⼀个源⽂件 hello.c ⾥便简单输出"hello world!",并且调⽤⼀个run函数,⽽这个函数被定义在另⼀个原⽂件 code.c 中。这⾥我们就可以调⽤ gcc -c 来分别编译这两个原⽂件。


在这里插入图片描述

在这里插入图片描述

可以看到,在编译之后会⽣成两个扩展名为 .o 的⽂件,它们被称作⽬标⽂件(可重定位目标文件)。要注意的是如果我们修改了⼀个原⽂件,那么只需要单独编译它这⼀个,⽽不需要浪费时间重新编译整个⼯程。⽬标⽂件是⼀个⼆进制的⽂件,⽂件的格式是 ELF ,是对⼆进制代码的⼀种封装。
在这里插入图片描述

file name //用于辨别文件类型

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

ELF形成可执行

step-1:将多份 C/C++ 源代码,翻译成为⽬标 .o ⽂件
step-2:将多份 .o ⽂件section进⾏合并

在这里插入图片描述
注意:

实际合并是在链接时进⾏的,但是并不是这么简单的合并,也会涉及对库合并。链接就是把.o目标文件,可执行程序和共享文件中的section进行合并。

readelf命令

readelf 是 Linux 系统中用于分析 ELF(Executable and Linkable Format)格式文件的命令行工具,常用于查看可执行文件、动态库、目标文件等的内部结构信息。
常用选项:

-h --file-header :显示 ELF 文件的头部信息,包括文件类型、机器架构、程序入口地址等。
-S --section-headers :显示节头表信息,包括节名、节类型、大小、偏移、属性等。
-l --program-headers :显示程序头信息,包括节区段的大小、偏移、虚拟地址等。
-s--symbols:显示符号表信息,包括符号名称、类型、绑定属性、大小、值等。
-r --relocs:显示重定位表信息,包括重定位节、符号、类型等。
-a --all:显示所有信息,相当于同时使用 -h、-l、-S、-s、-r、-d 等选项。

ELF可执行文件加载

  • ⼀个ELF会有多种不同的Section,在加载到内存的时候,也会进⾏Section合并,形成segment。
  • 合并原则:相同属性,⽐如:可读,可写,可执⾏,需要加载时申请空间等。
  • 这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到⼀起。
  • 很显然,这个合并⼯作也已经在形成ELF的时候,合并⽅式已经确定了,具体合并原则被记录在了ELF的 程序头表(Program header table) 中。
//查看可执⾏程序的section
readelf -S a.out
There are 29 section headers, starting at offset 0x1968:Section Headers:[Nr] Name              Type             Address           OffsetSize              EntSize          Flags  Link  Info  Align[ 0]                   NULL             0000000000000000  000000000000000000000000  0000000000000000           0     0     0[ 1] .interp           PROGBITS         0000000000000238  00000238000000000000001c  0000000000000000   A       0     0     1[ 2] .note.ABI-tag     NOTE             0000000000000254  000002540000000000000020  0000000000000000   A       0     0     4[ 3] .note.gnu.build-i NOTE             0000000000000274  000002740000000000000024  0000000000000000   A       0     0     4[ 4] .gnu.hash         GNU_HASH         0000000000000298  00000298000000000000001c  0000000000000000   A       5     0     8[ 5] .dynsym           DYNSYM           00000000000002b8  000002b800000000000000a8  0000000000000018   A       6     1     8[ 6] .dynstr           STRTAB           0000000000000360  000003600000000000000082  0000000000000000   A       0     0     1[ 7] .gnu.version      VERSYM           00000000000003e2  000003e2000000000000000e  0000000000000002   A       5     0     2[ 8] .gnu.version_r    VERNEED          00000000000003f0  000003f00000000000000020  0000000000000000   A       6     1     8[ 9] .rela.dyn         RELA             0000000000000410  0000041000000000000000c0  0000000000000018   A       5     0     8[10] .rela.plt         RELA             00000000000004d0  000004d00000000000000018  0000000000000018  AI       5    22     8[11] .init             PROGBITS         00000000000004e8  000004e80000000000000017  0000000000000000  AX       0     0     4[12] .plt              PROGBITS         0000000000000500  000005000000000000000020  0000000000000010  AX       0     0     16[13] .plt.got          PROGBITS         0000000000000520  000005200000000000000008  0000000000000008  AX       0     0     8[14] .text             PROGBITS         0000000000000530  0000053000000000000001b2  0000000000000000  AX       0     0     16[15] .fini             PROGBITS         00000000000006e4  000006e40000000000000009  0000000000000000  AX       0     0     4[16] .rodata           PROGBITS         00000000000006f0  000006f0000000000000001a  0000000000000000   A       0     0     4[17] .eh_frame_hdr     PROGBITS         000000000000070c  0000070c0000000000000044  0000000000000000   A       0     0     4[18] .eh_frame         PROGBITS         0000000000000750  000007500000000000000128  0000000000000000   A       0     0     8[19] .init_array       INIT_ARRAY       0000000000200db8  00000db80000000000000008  0000000000000008  WA       0     0     8[20] .fini_array       FINI_ARRAY       0000000000200dc0  00000dc00000000000000008  0000000000000008  WA       0     0     8[21] .dynamic          DYNAMIC          0000000000200dc8  00000dc800000000000001f0  0000000000000010  WA       6     0     8[22] .got              PROGBITS         0000000000200fb8  00000fb80000000000000048  0000000000000008  WA       0     0     8[23] .data #            PROGBITS         0000000000201000  000010000000000000000010  0000000000000000  WA       0     0     8[24] .bss              NOBITS           0000000000201010  000010100000000000000008  0000000000000000  WA       0     0     1[25] .comment          PROGBITS         0000000000000000  000010100000000000000029  0000000000000001  MS       0     0     1[26] .symtab           SYMTAB           0000000000000000  000010400000000000000618  0000000000000018          27    44     8[27] .strtab           STRTAB           0000000000000000  00001658000000000000020e  0000000000000000           0     0     1[28] .shstrtab         STRTAB           0000000000000000  0000186600000000000000fe  0000000000000000           0     0     1......
//查看可执行程序的ELF Header
readelf -h a.out
ELF Header:Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 //用来表示该文件是ELF格式的,告诉系统可以用ELF格式读取文件内容Class:                             ELF64Data:                              2's complement, little endianVersion:                           1 (current)OS/ABI:                            UNIX - System VABI Version:                       0Type:                              DYN (Shared object file)Machine:                           Advanced Micro Devices X86-64Version:                           0x1Entry point address:               0x530Start of program headers:          64 (bytes into file)Start of section headers:          6504 (bytes into file)Flags:                             0x0Size of this header:               64 (bytes)Size of program headers:           56 (bytes)Number of program headers:         9Size of section headers:           64 (bytes)Number of section headers:         29Section header string table index: 28
//知道了size和number,我们就可以用起始地址+偏移量的方式访文件内容了
//查看section合并的segment
readelf -l a.outElf file type is DYN (Shared object file)
Entry point 0x530
There are 9 program headers, starting at offset 64Program Headers:Type           Offset             VirtAddr           PhysAddrFileSiz            MemSiz              Flags  AlignPHDR           0x0000000000000040 0x0000000000000040 0x00000000000000400x00000000000001f8 0x00000000000001f8  R      0x8INTERP         0x0000000000000238 0x0000000000000238 0x00000000000002380x000000000000001c 0x000000000000001c  R      0x1[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]LOAD           0x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000878 0x0000000000000878  R E    0x200000LOAD           0x0000000000000db8 0x0000000000200db8 0x0000000000200db80x0000000000000258 0x0000000000000260  RW     0x200000
......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 .dynamic .got .data .bss 04     .dynamic 05     .note.ABI-tag .note.gnu.build-id 06     .eh_frame_hdr 07     08     .init_array .fini_array .dynamic .got 

.text是代码段(权限也是只读的),.data是数据段,.rodata是只读数据段,符合合并规则。
bss段:是 ELF(或其他可执行文件格式)中用于存储未初始化的全局变量和静态变量的内存区域,可以节省可执行程序占据的磁盘空间。

size a.outtext	   data	    bss	    dec	    hex	filename1580	    600	      8	   2188	    88c	a.out

⼀个ELF会有多种不同的Section,在加载到内存的时候,也会进⾏Section合并,形成segment
在这里插入图片描述

为什么要将section合并成为segment

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

对于 程序头表 和 节头表 ⼜有什么⽤呢,其实 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 ⽂件可以看到该节。
从 执行视图 来看:
• 告诉操作系统哪些模块可以被加载进内存。
• 加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执行的。

理解连接与加载

静态链接

  • ⽆论是⾃⼰的.o, 还是静态库中的.o,本质都是把.o⽂件进⾏连接的过程
  • 所以:研究静态链接,本质就是研究.o是如何链接的

在这里插入图片描述

查看编译后的.o⽬标⽂件

在这里插入图片描述

objdump -d 命令:将代码段(.text)进行反汇编查看
code.o 中的 main 函数不认识 printfrun 函数

 //cat code.c
#include <stdio.h>
void run();
int main()
{printf("Hello world\n");run();return 0;
}

我们可以看到这⾥的call指令,它们分别对应之前调⽤的printf和run函数,但是你会发现他们的跳转地址都被设成了0。那这是为什么呢?其实就是在编译 hello.c 的时候,编译器是完全不知道printfrun函数的存在的,⽐如他们位于内存的哪个区块,代码⻓什么样都是不知道的。因此,编辑器只能将这两个函数的跳转地址先暂时设为0。

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

//读取code.o的符号表
readelf -s code.o
Symbol table '.symtab' contains 13 entries:Num:    Value          Size Type    Bind   Vis      Ndx Name0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS code.c2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 8: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 9: 0000000000000000    33 FUNC    GLOBAL DEFAULT    1 main10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND run
 readelf -s hello.oSymbol table '.symtab' contains 12 entries:Num:    Value          Size Type    Bind   Vis      Ndx Name0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 8: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 9: 0000000000000000    19 FUNC    GLOBAL DEFAULT    1 run10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

puts:就是printf的实现
run:就是run的实现
UND就是:undefine,表⽰未定义说⽩了就是本.o⽂件找不到

//读取a.out的符号表
readelf -s a.outSymbol table '.dynsym' contains 7 entries:Num:    Value          Size Type    Bind   Vis      Ndx Name0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable6: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)Symbol table '.symtab' contains 65 entries:Num:    Value          Size Type    Bind   Vis      Ndx Name0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 1: 0000000000000238     0 SECTION LOCAL  DEFAULT    1 2: 0000000000000254     0 SECTION LOCAL  DEFAULT    2 3: 0000000000000274     0 SECTION LOCAL  DEFAULT    3 4: 0000000000000298     0 SECTION LOCAL  DEFAULT    4 5: 00000000000002b8     0 SECTION LOCAL  DEFAULT    5 6: 0000000000000360     0 SECTION LOCAL  DEFAULT    6 7: 00000000000003e2     0 SECTION LOCAL  DEFAULT    7 8: 00000000000003f0     0 SECTION LOCAL  DEFAULT    8 9: 0000000000000410     0 SECTION LOCAL  DEFAULT    9 10: 00000000000004d0     0 SECTION LOCAL  DEFAULT   10 11: 00000000000004e8     0 SECTION LOCAL  DEFAULT   11 12: 0000000000000500     0 SECTION LOCAL  DEFAULT   12 13: 0000000000000520     0 SECTION LOCAL  DEFAULT   13 14: 0000000000000530     0 SECTION LOCAL  DEFAULT   14 15: 00000000000006e4     0 SECTION LOCAL  DEFAULT   15 16: 00000000000006f0     0 SECTION LOCAL  DEFAULT   16 17: 000000000000070c     0 SECTION LOCAL  DEFAULT   17 18: 0000000000000750     0 SECTION LOCAL  DEFAULT   18 19: 0000000000200db8     0 SECTION LOCAL  DEFAULT   19 20: 0000000000200dc0     0 SECTION LOCAL  DEFAULT   20 21: 0000000000200dc8     0 SECTION LOCAL  DEFAULT   21 22: 0000000000200fb8     0 SECTION LOCAL  DEFAULT   22 23: 0000000000201000     0 SECTION LOCAL  DEFAULT   23 24: 0000000000201010     0 SECTION LOCAL  DEFAULT   24 25: 0000000000000000     0 SECTION LOCAL  DEFAULT   25 26: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c27: 0000000000000560     0 FUNC    LOCAL  DEFAULT   14 deregister_tm_clones28: 00000000000005a0     0 FUNC    LOCAL  DEFAULT   14 register_tm_clones29: 00000000000005f0     0 FUNC    LOCAL  DEFAULT   14 __do_global_dtors_aux30: 0000000000201010     1 OBJECT  LOCAL  DEFAULT   24 completed.769831: 0000000000200dc0     0 OBJECT  LOCAL  DEFAULT   20 __do_global_dtors_aux_fin32: 0000000000000630     0 FUNC    LOCAL  DEFAULT   14 frame_dummy33: 0000000000200db8     0 OBJECT  LOCAL  DEFAULT   19 __frame_dummy_init_array_34: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS code.c35: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c36: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c37: 0000000000000874     0 OBJECT  LOCAL  DEFAULT   18 __FRAME_END__38: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 39: 0000000000200dc0     0 NOTYPE  LOCAL  DEFAULT   19 __init_array_end40: 0000000000200dc8     0 OBJECT  LOCAL  DEFAULT   21 _DYNAMIC41: 0000000000200db8     0 NOTYPE  LOCAL  DEFAULT   19 __init_array_start42: 000000000000070c     0 NOTYPE  LOCAL  DEFAULT   17 __GNU_EH_FRAME_HDR43: 0000000000200fb8     0 OBJECT  LOCAL  DEFAULT   22 _GLOBAL_OFFSET_TABLE_44: 00000000000006e0     2 FUNC    GLOBAL DEFAULT   14 __libc_csu_fini45: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab46: 0000000000201000     0 NOTYPE  WEAK   DEFAULT   23 data_start47: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@@GLIBC_2.2.548: 0000000000201010     0 NOTYPE  GLOBAL DEFAULT   23 _edata49: 000000000000065b    19 FUNC    GLOBAL DEFAULT   14 run50: 00000000000006e4     0 FUNC    GLOBAL DEFAULT   15 _fini51: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_52: 0000000000201000     0 NOTYPE  GLOBAL DEFAULT   23 __data_start53: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__54: 0000000000201008     0 OBJECT  GLOBAL HIDDEN    23 __dso_handle55: 00000000000006f0     4 OBJECT  GLOBAL DEFAULT   16 _IO_stdin_used56: 0000000000000670   101 FUNC    GLOBAL DEFAULT   14 __libc_csu_init57: 0000000000201018     0 NOTYPE  GLOBAL DEFAULT   24 _end58: 0000000000000530    43 FUNC    GLOBAL DEFAULT   14 _start59: 0000000000201010     0 NOTYPE  GLOBAL DEFAULT   24 __bss_start60: 000000000000063a    33 FUNC    GLOBAL DEFAULT   14 main61: 0000000000201010     0 OBJECT  GLOBAL HIDDEN    23 __TMC_END__62: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable63: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@@GLIBC_2.264: 00000000000004e8     0 FUNC    GLOBAL DEFAULT   11 _init

在这里插入图片描述

两个.o进⾏合并之后,在最终的可执⾏程序中,就找到了run
000000000000065b:其实是地址,后⾯说
FUNC:表⽰run符号类型是个函数
14:就是run函数所在的section被合并最终的那⼀个section中了,14就是下标

 readelf -S a.out
There are 29 section headers, starting at offset 0x1968:Section Headers:[Nr] Name              Type             Address           OffsetSize              EntSize          Flags  Link  Info  Align
······[14] .text             PROGBITS         0000000000000530  0000053000000000000001b2  0000000000000000  AX       0     0     16
······
//[14]->.text代码段

静态链接就是把库中的.o进⾏合并,和上述过程⼀样。
所以链接其实就是将编译之后的所有⽬标⽂件连同⽤到的⼀些静态库运⾏时库组合,拼装成⼀个独⽴
的可执⾏⽂件。其中就包括我们之前提到的地址修正,当所有模块组合在⼀起之后,链接器会根据我
们的.o⽂件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从⽽修正它们的地址。这
其实就是静态链接的过程。
在这里插入图片描述

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

ELF加载与进程地址空间

虚拟地址/逻辑地址

问题:
• ⼀个ELF程序,在没有被加载到内存的时候,有没有地址呢?
• 进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪⾥来的?
答案:
⼀个ELF程序,在没有被加载到内存的时候,有地址,这个地址也叫做逻辑地址。
进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从ELF程序来的。

⼀个ELF程序,在没有被加载到内存的时候,本来就有地址,当代计算机⼯作的时候,都采⽤"平坦
模式"进⾏⼯作。所以也要求ELF对⾃⼰的代码和数据进⾏统⼀编址,下⾯是 objdump -S 反汇编
之后的代码
在这里插入图片描述

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

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

所以:虚拟地址机制,不光光OS要⽀持,编译器也要⽀持.
磁盘上的可执行程序代码和数据编址,其实就是虚拟地址的统一编址,在内存里叫虚拟地址,在磁盘上叫逻辑地址(线性地址),内存上面的地址叫做物理地址。

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

ELF 在被编译好之后,会把⾃⼰未来程序的⼊⼝地址记录在ELF header的Entry字段中:

在这里插入图片描述
在这里插入图片描述

动态链接与动态库加载

进程如何看到动态库

在这里插入图片描述

进程间如何共享库的

在这里插入图片描述

动态链接

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

ldd a.outlinux-vdso.so.1 (0x00007ffe9dfde000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fca4ba0f000)/lib64/ld-linux-x86-64.so.2 (0x00007fca4c002000)# ldd命令⽤于打印程序或者库⽂件所依赖的共享库列表。

这⾥的 libc.soC语⾔的运⾏时库,⾥⾯提供了常⽤的标准输⼊输出⽂件字符串处理等等这些功能。那为什么编译器默认不使⽤静态链接呢?静态链接会将编译产⽣的所有⽬标⽂件,连同⽤到的各种库,合并形成⼀个独⽴的可执⾏⽂件,它不需要额外的依赖就可以运⾏。照理来说应该更加⽅便才对是吧?

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

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

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

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

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

在这里插入图片描述

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 函数来终⽌程序。

上述过程描述了C/C++程序在 main 函数之前执⾏的⼀系列操作,但这些操作对于⼤多数程序员来说是透明的。程序员通常只需要关注main 函数中的代码,⽽不需要关⼼底层的初始化过程。然⽽,了解这些底层细节有助于更好地理解程序的执⾏流程和调试问题。

动态库中的相对地址

动态库为了随时进⾏加载,为了⽀持并映射到任意进程的任意位置,对动态库中的⽅法,统⼀编址,采⽤相对编址的⽅案进⾏编制的(其实可执⾏程序也⼀样,都要遵守平坦模式,只不过exe是直接加载的)。

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

注意:

  1. 动态库也是⼀个⽂件,要访问也是要被先加载,要加载也是要被打开的
  2. 让我们的进程找到动态库的本质:也是⽂件操作,不过我们访问库函数,通过虚拟地址进 ⾏跳转访问的,所以需要把动态库映射到进程的地址空间中

在这里插入图片描述

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

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

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

注意:
• 也就是说,我们的程序运⾏之前,先把所有库加载并映射,所有库的起始虚拟地址都应该提前知道
• 然后对我们加载到内存中的程序的库函数调⽤进⾏地址修改,在内存中⼆次完成地址设置(这个叫做加载地址重定位)
• 等等,修改的是代码区?不是说代码区在进程中是只读的吗?怎么修改?能修改吗?
所以:动态链接采⽤的做法是在 .data (可执⾏程序或者库⾃⼰)中专⻔预留⼀⽚区域⽤来存放函数
的跳转地址,它也被叫做全局偏移表GOT,表中每⼀项都是本运⾏模块要引⽤的⼀个全局变量或函数
的地址。

• 因为.data区域是可读写的,所以可以⽀持动态进⾏修改

 readelf -S a.out
There are 29 section headers, starting at offset 0x1968:Section Headers:[Nr] Name              Type             Address           OffsetSize              EntSize          Flags  Link  Info  Align
......[14] .text             PROGBITS         0000000000000530  0000053000000000000001b2  0000000000000000  AX       0     0     16
......00000000000001f0  0000000000000010  WA       6     0     8[22] .got              PROGBITS         0000000000200fb8  00000fb8
......
readelf -l a.out 
......03     .init_array .fini_array .dynamic .got .data .bss 
......
# .got在加载的时候,会和.data合并成为⼀个segment,然后加载在⼀起
  1. 由于代码段只读,我们不能直接修改代码段。但有了GOT表,代码便可以被所有进程共享。但在不同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到GOT表上,就是每个进程的每个动态库都有独⽴的GOT表,所以进程间不能共享GOT表。
  2. 在单个.so下,由于GOT表与 .text 的相对位置是固定的,我们完全可以利⽤CPU的相对寻址来找到GOT表。
  3. 在调⽤函数的时候会⾸先查表,然后根据表中的地址来进⾏跳转,这些地址在动态库加载的时候会被修改为真正的地址。
  4. 这种⽅式实现的动态链接就被叫做PIC地址⽆关代码 。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运⾏,并且能够被所有进程共享,这也是为什么之前我们给编译器指定-fPIC参数的原因,PIC=相对编址+GOT

在这里插入图片描述
PLT是什么?

库间依赖

注意:
• 不仅仅有可执⾏程序调⽤库
• 库也会调⽤其他库!!库之间是有依赖的,如何做到库和库之间互相调⽤也是与地址⽆关的呢??
• 库中也有.GOT,和可执⾏⼀样!这也就是为什么⼤家为什么都是ELF的格式!
在这里插入图片描述
由于动态链接在程序加载的时候需要对⼤量函数进⾏重定位,这⼀步显然是⾮常耗时的。为了进⼀步降低开销,我们的操作系统还做了⼀些其他的优化,⽐如延迟绑定,或者也叫PLT(过程连接表
Procedure Linkage Table))。与其在程序⼀开始就对所有函数进⾏重定位,不如将这个过程推迟到函数第⼀次被调⽤的时候,因为绝⼤多数动态库中的函数可能在程序运⾏期间⼀次都不会被使⽤到。

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

在这里插入图片描述

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

总结

• 静态链接的出现,提⾼了程序的模块化⽔平。对于⼀个⼤的项⽬,不同的⼈可以独⽴地测试和开发⾃⼰的模块。通过静态链接,⽣成最终的可执⾏⽂件。
• 我们知道静态链接会将编译产⽣的所有⽬标⽂件,和⽤到的各种库合并成⼀个独⽴的可执⾏⽂件,其中我们会去修正模块间函数的跳转地址,也被叫做编译重定位(也叫做静态重定位)。
• ⽽动态链接实际上将链接的整个过程推迟到了程序加载的时候。⽐如我们去运⾏⼀个程序,操作系统会⾸先将程序的数据代码连同它⽤到的⼀系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,但是⽆论加载到什么地⽅,都要映射到进程对应的地址空间,然后通过.GOT⽅式进⾏调⽤(运⾏重定位,也叫做动态地址重定位)。

http://www.lqws.cn/news/566407.html

相关文章:

  • Redis缓存击穿深度解析:从现象到实战的完整解决方案
  • github上传代码步骤(http)
  • Cesium快速入门到精通系列教程十二:Cesium1.74中环绕地球生成​​经线环​​
  • Javaweb - 7 xml
  • 【智能协同云图库】智能协同云图库第三弹:基于腾讯云 COS 对象存储—开发图片模块
  • 日常 AI 工具汇总
  • Oracle 递归 + Decode + 分组函数实现复杂树形统计进阶(第二课)
  • 深入剖析 Linux 内核网络核心:sock.c 源码解析
  • 阿里云ACP-数据湖和机器学习
  • 解锁Ubuntu安装:从新手到高手的通关秘籍
  • Java 大视界 -- 基于 Java 的大数据分布式存储在科研大数据归档与长期保存中的应用(328)
  • 从UI设计到数字孪生实战演练:打造智慧交通的综合管理平台
  • 鸿蒙 Swiper 组件解析:轮播交互与动画效果全指南
  • 基于STM32的数字频率计设计
  • LoRA训练-理论基础
  • 大模型在恶性心律失常预测及治疗方案制定中的应用研究
  • 智慧水务:未来城市水务管理的创新实践与科技飞跃
  • Go 中的 range 表达式详解:遍历数组、切片、字符串与 Map
  • Docker错误问题解决方法
  • Wpf布局之Canvas面板!
  • 使用 em 单位的好处,以及 em、rem、px 的区别
  • Django ORM 2. 模型(Model)操作
  • 【记录】服务器多用户共享Conda环境——Ubuntu24.04
  • 利用imx6ull板学习裸机arm板开发(6.22-6.24)
  • 商业秘密保护新焦点:企业如何守护核心经营信息?
  • Python商务数据分析——Matplotlib 数据可视化学习笔记
  • Windows环境下C语言汇编语言编辑器及环境安装
  • Windows 环境下设置 RabbitMQ 的 consumer_timeout 参数
  • NoSQL与Redis、HBase、分布式系统详解
  • 深入理解 Dubbo 负载均衡:原理、源码与实践