Linux笔记---动静态库(原理篇)

news2025/4/19 13:45:34

1. ELF文件格式

动静态库文件的构成是什么样的呢?或者说二者的内容是什么?

实际上,可执行文件,目标文件,静态库文件,动态库文件都是使用ELF文件格式进行组织的。

ELF(Executable and Linkable Format)文件格式是Unix系统及其衍生系统中广泛使用的可执行文件、共享库和核心转储的二进制文件格式。

主要包括以下四种文件*

  • 可重定位文件(Relocatable File):包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据,如xxx.o文件。  
  • 可执行文件(Executable File):包含适合于执行的一个程序,规定了exec()如何创建一个程序的进程映像,如a.out文件。  
  • 共享目标文件(Shared Object File):包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理,生成另外一个目标文件;其次,动态链接器可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像,如xxx.so文件。  
  • 内核转储(core dumps):存放当前进程的执行上下文,用于dump信号触发。

结构组成

  • ELF头(ELF header):位于文件的开始位置,主要目的是定位文件的其他部分。

  • 程序头表(Program header table):列举了所有有效的段(segments)和它们的属性。

  • 节(Section):文件的最小逻辑组织单元,用于存储程序在编译、链接或执行过程中需要的特定类型数据。每个Section都有明确的用途,例如存储代码、全局变量、符号表或调试信息等。

  • 节头表(Section header table):包含对节(sections)的描述。

总结来说,除了节以外的三个部分实际上都是帮助定位的辅助信息,程序的核心信息在节部分。

我们可以通过 readelf 工具来查看一个可执行程序的各个部分,下面我们以ls为例展示各个部分。

1.1 ELF header

位于文件的开始位置,主要目的是定位文件的其他部分。包含文件类型、目标结构、ELF文件格式的版本、程序入口地址、程序头表的文件偏移、节头表的文件偏移等重要信息。

readelf -h /usr/bin/ls
shishen@hcss-ecs-b8e6:~$ readelf -h /usr/bin/ls
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 (Position-Independent Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x6aa0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          136232 (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
Magic:                            标识ELF文件的魔数字节序列。
Class:                            文件位数(32位或64位)。
Data:                             数据存储的字节序(小端或大端)。
Version:                          ELF格式版本号。
OS/ABI:                           目标操作系统和应用二进制接口(ABI)。
ABI Version:                      ABI的版本号。
Type:                             文件类型(可执行、共享库等)。
Machine:                          目标CPU架构。
Version:                          ELF版本(通常为1)。
Entry point address:              程序执行的起始地址。
Start of program headers:         程序头表在文件中的偏移量。
Start of section headers:         节头表在文件中的偏移量。
Flags:                            处理器特定的标志位。
Size of this header:              ELF头的大小(字节)。
Size of program headers:          单个程序头条目的大小。
Number of program headers:        程序头条目数量。
Size of section headers:          单个节头条目的大小。
Number of section headers:        节头条目数量。
Section header string table index:节名称字符串表的索引号。

 1.2 Section header table 

ELF文件的链接视图(Linking view),包含对节(sections)的描述。即一个ELF文件中到底有哪些具体的sections,以及这些sections的属性信息。

简单来说,这部分包含了一个个的section header,它们与section一一对应,指示各个section的属性信息,这与文件系统中的inode table的设计思路相同。

readelf -S /usr/bin/ls
shishen@hcss-ecs-b8e6:~$ readelf -S /usr/bin/ls
There are 31 section headers, starting at offset 0x21428:

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         0000000000000318  00000318
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.gnu.pr[...] NOTE             0000000000000338  00000338
       0000000000000030  0000000000000000   A       0     0     8
  [ 3] .note.gnu.bu[...] NOTE             0000000000000368  00000368
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .note.ABI-tag     NOTE             000000000000038c  0000038c
       0000000000000020  0000000000000000   A       0     0     4
  [ 5] .gnu.hash         GNU_HASH         00000000000003b0  000003b0
       000000000000004c  0000000000000000   A       6     0     8
  [ 6] .dynsym           DYNSYM           0000000000000400  00000400
       0000000000000b88  0000000000000018   A       7     1     8
  [ 7] .dynstr           STRTAB           0000000000000f88  00000f88
       00000000000005a6  0000000000000000   A       0     0     1
  [ 8] .gnu.version      VERSYM           000000000000152e  0000152e
       00000000000000f6  0000000000000002   A       6     0     2
  [ 9] .gnu.version_r    VERNEED          0000000000001628  00001628
       00000000000000c0  0000000000000000   A       7     2     8
  [10] .rela.dyn         RELA             00000000000016e8  000016e8
       0000000000001410  0000000000000018   A       6     0     8
  [11] .rela.plt         RELA             0000000000002af8  00002af8
       0000000000000960  0000000000000018  AI       6    25     8
  [12] .init             PROGBITS         0000000000004000  00004000
       000000000000001b  0000000000000000  AX       0     0     4
  [13] .plt              PROGBITS         0000000000004020  00004020
       0000000000000650  0000000000000010  AX       0     0     16
  [14] .plt.got          PROGBITS         0000000000004670  00004670
       0000000000000030  0000000000000010  AX       0     0     16
  [15] .plt.sec          PROGBITS         00000000000046a0  000046a0
       0000000000000640  0000000000000010  AX       0     0     16
  [16] .text             PROGBITS         0000000000004ce0  00004ce0
       00000000000123a2  0000000000000000  AX       0     0     16
  [17] .fini             PROGBITS         0000000000017084  00017084
       000000000000000d  0000000000000000  AX       0     0     4
  [18] .rodata           PROGBITS         0000000000018000  00018000
       0000000000004dcc  0000000000000000   A       0     0     32
  [19] .eh_frame_hdr     PROGBITS         000000000001cdcc  0001cdcc
       000000000000056c  0000000000000000   A       0     0     4
  [20] .eh_frame         PROGBITS         000000000001d338  0001d338
       0000000000002120  0000000000000000   A       0     0     8
  [21] .init_array       INIT_ARRAY       0000000000020fd0  0001ffd0
       0000000000000008  0000000000000008  WA       0     0     8
  [22] .fini_array       FINI_ARRAY       0000000000020fd8  0001ffd8
       0000000000000008  0000000000000008  WA       0     0     8
  [23] .data.rel.ro      PROGBITS         0000000000020fe0  0001ffe0
       0000000000000a78  0000000000000000  WA       0     0     32
  [24] .dynamic          DYNAMIC          0000000000021a58  00020a58
       0000000000000200  0000000000000010  WA       7     0     8
  [25] .got              PROGBITS         0000000000021c58  00020c58
       00000000000003a0  0000000000000008  WA       0     0     8
  [26] .data             PROGBITS         0000000000022000  00021000
       0000000000000278  0000000000000000  WA       0     0     32
  [27] .bss              NOBITS           0000000000022280  00021278
       00000000000012c0  0000000000000000  WA       0     0     32
  [28] .gnu_debugaltlink PROGBITS         0000000000000000  00021278
       0000000000000049  0000000000000000           0     0     1
  [29] .gnu_debuglink    PROGBITS         0000000000000000  000212c4
       0000000000000034  0000000000000000           0     0     4
  [30] .shstrtab         STRTAB           0000000000000000  000212f8
       000000000000012f  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),
  D (mbind), l (large), p (processor specific)

// 注:
// [Nr]	    节的索引号(从 1 开始),0 表示无效节。
// Name	    节的名称(如 .text、.data、.rodata)。
// Type	    节的类型(如 PROGBITS 表示程序数据,SYMTAB 表示符号表)。
// Address	节加载到内存时的虚拟地址(未加载时为 0)。
// Offset	节在文件中的起始偏移量(字节)。
// Size	    节的大小(字节)。
// EntSize	如果节是表格(如符号表),表示每个条目的大小;否则为 0。
// Flags	节的属性标志(如 A 可分配,X 可执行,W 可写)。
// Link	    链接到其他节的索引(如符号表会链接到字符串表)。
// Info	    节的附加信息(如符号表的局部符号起始索引)。
// Align	节的对齐要求(如 16 表示按 16 字节对齐)。

链接视图的含义是:目标文件以及动静态库在进行链接形成可执行程序时,是以section为单位进行的,即各个section各自进行合并。

1.3 Program header table

ELF文件的执行视图(execution view),列举了所有有效的段(segments)和它们的属性。

同样地,这部分包含了一个个的program header(或者说segment header),它们与segment一一对应,指示各个segment的属性信息。

可执行程序在被加载到内存当中时,多个节会合并成一个段(合并原则:相同属性,比如可读,可写,可执行,需要加载时申请空间等)。所以,Program header table其实就是对中间部分的一个重新划分。

readelf -l /usr/bin/ls
shishen@hcss-ecs-b8e6:~$ readelf -l /usr/bin/ls

Elf file type is DYN (Position-Independent Executable file)
Entry point 0x6aa0
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000002d8 0x00000000000002d8  R      0x8
  INTERP         0x0000000000000318 0x0000000000000318 0x0000000000000318
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000003458 0x0000000000003458  R      0x1000
  LOAD           0x0000000000004000 0x0000000000004000 0x0000000000004000
                 0x0000000000013091 0x0000000000013091  R E    0x1000
  LOAD           0x0000000000018000 0x0000000000018000 0x0000000000018000
                 0x0000000000007458 0x0000000000007458  R      0x1000
  LOAD           0x000000000001ffd0 0x0000000000020fd0 0x0000000000020fd0
                 0x00000000000012a8 0x0000000000002570  RW     0x1000
  DYNAMIC        0x0000000000020a58 0x0000000000021a58 0x0000000000021a58
                 0x0000000000000200 0x0000000000000200  RW     0x8
  NOTE           0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000030 0x0000000000000030  R      0x8
  NOTE           0x0000000000000368 0x0000000000000368 0x0000000000000368
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_PROPERTY   0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000030 0x0000000000000030  R      0x8
  GNU_EH_FRAME   0x000000000001cdcc 0x000000000001cdcc 0x000000000001cdcc
                 0x000000000000056c 0x000000000000056c  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x000000000001ffd0 0x0000000000020fd0 0x0000000000020fd0
                 0x0000000000001030 0x0000000000001030  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 
   03     .init .plt .plt.got .plt.sec .text .fini 
   04     .rodata .eh_frame_hdr .eh_frame 
   05     .init_array .fini_array .data.rel.ro .dynamic .got .data .bss 
   06     .dynamic 
   07     .note.gnu.property 
   08     .note.gnu.build-id .note.ABI-tag 
   09     .note.gnu.property 
   10     .eh_frame_hdr 
   11     
   12     .init_array .fini_array .data.rel.ro .dynamic .got 

// 注:
// Type	    段的类型(如 LOAD 表示可加载段,DYNAMIC 表示动态链接信息)。
// Offset	段在文件中的起始偏移量(字节)。
// VirtAddr	段加载到内存时的虚拟地址(程序运行时访问的地址)。
// PhysAddr	段加载到内存时的物理地址(通常与 VirtAddr 相同,现代系统忽略)。
// FileSiz	段在文件中的大小(字节)。
// MemSiz	段在内存中的大小(字节,可能大于 FileSiz,如 .bss 节会填充零)。
// Flags	段的权限标志(R=读,W=写,X=执行)。
// Align	段在内存和文件中的对齐要求(如 0x1000 表示按 4KB 对齐)。

Section to Segment mapping部分就显示了各个segment包含了哪些section。

执行视图的含义是:该部分负责告诉操作系统,如何加载可执行文件,完成进程内存的初始化。一个可执行程序的格式中,一定有 program header table。

  • 节(Section) 是链接时的逻辑划分(如 .text、.data),数量多且大小不一。

  • 段(Segment) 是执行时的物理加载单元(如代码段、数据段),合并相同权限的节后,操作系统只需按段映射内存,减少内存碎片和系统调用次数。

加载到内存时将节合并为段的原因

  • 符合操作系统的内存页管理:操作系统以 页(Page) 为单位管理内存(如4KB)。段会按页对齐,避免跨页的节导致内存浪费或权限冲突。如果不进行合并,假设页面大小为4096字节(内存块基本大小,加载,管理的基本单位),如果.text部分为4097字节,.init部分为512字节,那么它们将占用3个页面,而合并后,它们只需2个页面。
  • 统一内存访问权限:每个段有明确的权限(读/写/执行),而节可能分散且权限不同。

 2. 静态链接原理

2.1 地址重定位

我们知道,静态库实际上就是一系列目标文件的集合。所以,要理解静态链接,我们只需要知道目标文件在进行链接时发生了什么即可。

将各个节的数据分别合并到一起是必然的,但是除此之外呢?

以如下代码为例:

// 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");
}

我们将这两个原文件进行编译得到目标文件:

shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ gcc -c *.c
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ ls
code.c  code.o  hello.c  hello.o

这里,我们需要用到一个指令objdump -d:将代码段(.text)进行反汇编查看。

shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ objdump -d hello.o > hello.s
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ objdump -d code.o > code.s
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ ls
code.c  code.o  code.s  hello.c  hello.o  hello.s

 这里的call很明显就是函数调用,call指令的编码为e8。

对比源文件可以看出,无论是printf函数还是run函数,e8跳转到的地址都为0。这是因为,在完成链接之前,编译器并不知道这些外部函数的实现与定义,无法为其分配地址(逻辑地址),就以0代替其地址。

所以,在链接时,编译器还需要将代码段中这些为0的地址修改为实际为这些函数分配的地址。

要完成这项工作,编译器还需要符号表的帮助。每个目标文件都有自己的符号表,在进行链接时,大家相互对照符号表,就能找到外部函数或变量是在哪一个文件当中声明的了,进而就能为其分配地址。

readelf -s # 读取目标文件的符号表
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ readelf -s hello.o

Symbol table '.symtab' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 .rodata
     4: 0000000000000000    40 FUNC    GLOBAL DEFAULT    1 main
     5: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts
     6: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND run
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ readelf -s code.o

Symbol table '.symtab' contains 6 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS code.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 .rodata
     4: 0000000000000000    26 FUNC    GLOBAL DEFAULT    1 run
     5: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

// 注:printf底层就是puts

 其中,每一行就代表一个符号,我们主要关注Ndx列和Name列。

  • ABS:该符号的值是绝对地址,不依赖于任何节(Section),通常是文件名或特殊定义的全局符号。
  • UND:该符号在当前目标文件中未定义,需要在链接时从其他目标文件或库中解析(如外部函数或全局变量)。
  • 数字:表示符号定义在对应索引号的节中。例如:Ndx=1:符号属于 .text 节(代码段)。Ndx=5:符号属于 .rodata 节(只读数据段)。

 可以看到,在hello.o中,run和puts都是未定义,而code.o中,run有定义但puts依然未定义。

当我们将二者进行链接之后:

shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ gcc -o main hello.o code.o
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ objdump -d main > main.s

 puts我们暂时也不关心,因为puts使用的是动态链接。可以看到,run函数的地址为1171,该地址也标注在run函数的名称之前。

这就说明编译器在链接时,找到了run函数,并为其分配了地址。

shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ readelf -s main

Symbol table '.dynsym' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _[...]@GLIBC_2.34 (2)
     2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterT[...]
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (3)
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMC[...]
     6: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND [...]@GLIBC_2.2.5 (3)

Symbol table '.symtab' contains 38 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS Scrt1.o
     2: 000000000000038c    32 OBJECT  LOCAL  DEFAULT    4 __abi_tag
     3: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
     4: 0000000000001090     0 FUNC    LOCAL  DEFAULT   16 deregister_tm_clones
     5: 00000000000010c0     0 FUNC    LOCAL  DEFAULT   16 register_tm_clones
     6: 0000000000001100     0 FUNC    LOCAL  DEFAULT   16 __do_global_dtors_aux
     7: 0000000000004010     1 OBJECT  LOCAL  DEFAULT   26 completed.0
     8: 0000000000003dc0     0 OBJECT  LOCAL  DEFAULT   22 __do_global_dtor[...]
     9: 0000000000001140     0 FUNC    LOCAL  DEFAULT   16 frame_dummy
    10: 0000000000003db8     0 OBJECT  LOCAL  DEFAULT   21 __frame_dummy_in[...]
    11: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
    12: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS code.c
    13: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    14: 0000000000002120     0 OBJECT  LOCAL  DEFAULT   20 __FRAME_END__
    15: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 
    16: 0000000000003dc8     0 OBJECT  LOCAL  DEFAULT   23 _DYNAMIC
    17: 000000000000201c     0 NOTYPE  LOCAL  DEFAULT   19 __GNU_EH_FRAME_HDR
    18: 0000000000003fb8     0 OBJECT  LOCAL  DEFAULT   24 _GLOBAL_OFFSET_TABLE_
    19: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_mai[...]
    20: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterT[...]
    21: 0000000000004000     0 NOTYPE  WEAK   DEFAULT   25 data_start
    22: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5
    23: 0000000000004010     0 NOTYPE  GLOBAL DEFAULT   25 _edata
    24: 0000000000001171    26 FUNC    GLOBAL DEFAULT   16 run
    25: 000000000000118c     0 FUNC    GLOBAL HIDDEN    17 _fini
    26: 0000000000004000     0 NOTYPE  GLOBAL DEFAULT   25 __data_start
    27: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    28: 0000000000004008     0 OBJECT  GLOBAL HIDDEN    25 __dso_handle
    29: 0000000000002000     4 OBJECT  GLOBAL DEFAULT   18 _IO_stdin_used
    30: 0000000000004018     0 NOTYPE  GLOBAL DEFAULT   26 _end
    31: 0000000000001060    38 FUNC    GLOBAL DEFAULT   16 _start
    32: 0000000000004010     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
    33: 0000000000001149    40 FUNC    GLOBAL DEFAULT   16 main
    34: 0000000000004010     0 OBJECT  GLOBAL HIDDEN    25 __TMC_END__
    35: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMC[...]
    36: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@G[...]
    37: 0000000000001000     0 FUNC    GLOBAL HIDDEN    12 _init

从上面的结果中可以看到run函数对应的section的编号为16,这意味着run函数在编号为16的节中。

readelf -S main

从main.s中可以得到验证(只关注开头的描述以及main函数和run函数即可):

Disassembly of section .text:

0000000000001060 <_start>:
    1060:	f3 0f 1e fa          	endbr64 
    1064:	31 ed                	xor    %ebp,%ebp
    1066:	49 89 d1             	mov    %rdx,%r9
    1069:	5e                   	pop    %rsi
    106a:	48 89 e2             	mov    %rsp,%rdx
    106d:	48 83 e4 f0          	and    $0xfffffffffffffff0,%rsp
    1071:	50                   	push   %rax
    1072:	54                   	push   %rsp
    1073:	45 31 c0             	xor    %r8d,%r8d
    1076:	31 c9                	xor    %ecx,%ecx
    1078:	48 8d 3d ca 00 00 00 	lea    0xca(%rip),%rdi        # 1149 <main>
    107f:	ff 15 53 2f 00 00    	call   *0x2f53(%rip)        # 3fd8 <__libc_start_main@GLIBC_2.34>
    1085:	f4                   	hlt    
    1086:	66 2e 0f 1f 84 00 00 	cs nopw 0x0(%rax,%rax,1)
    108d:	00 00 00 

0000000000001090 <deregister_tm_clones>:
    1090:	48 8d 3d 79 2f 00 00 	lea    0x2f79(%rip),%rdi        # 4010 <__TMC_END__>
    1097:	48 8d 05 72 2f 00 00 	lea    0x2f72(%rip),%rax        # 4010 <__TMC_END__>
    109e:	48 39 f8             	cmp    %rdi,%rax
    10a1:	74 15                	je     10b8 <deregister_tm_clones+0x28>
    10a3:	48 8b 05 36 2f 00 00 	mov    0x2f36(%rip),%rax        # 3fe0 <_ITM_deregisterTMCloneTable@Base>
    10aa:	48 85 c0             	test   %rax,%rax
    10ad:	74 09                	je     10b8 <deregister_tm_clones+0x28>
    10af:	ff e0                	jmp    *%rax
    10b1:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)
    10b8:	c3                   	ret    
    10b9:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)

00000000000010c0 <register_tm_clones>:
    10c0:	48 8d 3d 49 2f 00 00 	lea    0x2f49(%rip),%rdi        # 4010 <__TMC_END__>
    10c7:	48 8d 35 42 2f 00 00 	lea    0x2f42(%rip),%rsi        # 4010 <__TMC_END__>
    10ce:	48 29 fe             	sub    %rdi,%rsi
    10d1:	48 89 f0             	mov    %rsi,%rax
    10d4:	48 c1 ee 3f          	shr    $0x3f,%rsi
    10d8:	48 c1 f8 03          	sar    $0x3,%rax
    10dc:	48 01 c6             	add    %rax,%rsi
    10df:	48 d1 fe             	sar    %rsi
    10e2:	74 14                	je     10f8 <register_tm_clones+0x38>
    10e4:	48 8b 05 05 2f 00 00 	mov    0x2f05(%rip),%rax        # 3ff0 <_ITM_registerTMCloneTable@Base>
    10eb:	48 85 c0             	test   %rax,%rax
    10ee:	74 08                	je     10f8 <register_tm_clones+0x38>
    10f0:	ff e0                	jmp    *%rax
    10f2:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)
    10f8:	c3                   	ret    
    10f9:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)

0000000000001100 <__do_global_dtors_aux>:
    1100:	f3 0f 1e fa          	endbr64 
    1104:	80 3d 05 2f 00 00 00 	cmpb   $0x0,0x2f05(%rip)        # 4010 <__TMC_END__>
    110b:	75 2b                	jne    1138 <__do_global_dtors_aux+0x38>
    110d:	55                   	push   %rbp
    110e:	48 83 3d e2 2e 00 00 	cmpq   $0x0,0x2ee2(%rip)        # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
    1115:	00 
    1116:	48 89 e5             	mov    %rsp,%rbp
    1119:	74 0c                	je     1127 <__do_global_dtors_aux+0x27>
    111b:	48 8b 3d e6 2e 00 00 	mov    0x2ee6(%rip),%rdi        # 4008 <__dso_handle>
    1122:	e8 19 ff ff ff       	call   1040 <__cxa_finalize@plt>
    1127:	e8 64 ff ff ff       	call   1090 <deregister_tm_clones>
    112c:	c6 05 dd 2e 00 00 01 	movb   $0x1,0x2edd(%rip)        # 4010 <__TMC_END__>
    1133:	5d                   	pop    %rbp
    1134:	c3                   	ret    
    1135:	0f 1f 00             	nopl   (%rax)
    1138:	c3                   	ret    
    1139:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)

0000000000001140 <frame_dummy>:
    1140:	f3 0f 1e fa          	endbr64 
    1144:	e9 77 ff ff ff       	jmp    10c0 <register_tm_clones>

0000000000001149 <main>:
    1149:	f3 0f 1e fa          	endbr64 
    114d:	55                   	push   %rbp
    114e:	48 89 e5             	mov    %rsp,%rbp
    1151:	48 8d 05 ac 0e 00 00 	lea    0xeac(%rip),%rax        # 2004 <_IO_stdin_used+0x4>
    1158:	48 89 c7             	mov    %rax,%rdi
    115b:	e8 f0 fe ff ff       	call   1050 <puts@plt>
    1160:	b8 00 00 00 00       	mov    $0x0,%eax
    1165:	e8 07 00 00 00       	call   1171 <run>
    116a:	b8 00 00 00 00       	mov    $0x0,%eax
    116f:	5d                   	pop    %rbp
    1170:	c3                   	ret    

0000000000001171 <run>:
    1171:	f3 0f 1e fa          	endbr64 
    1175:	55                   	push   %rbp
    1176:	48 89 e5             	mov    %rsp,%rbp
    1179:	48 8d 05 91 0e 00 00 	lea    0xe91(%rip),%rax        # 2011 <_IO_stdin_used+0x11>
    1180:	48 89 c7             	mov    %rax,%rdi
    1183:	e8 c8 fe ff ff       	call   1050 <puts@plt>
    1188:	90                   	nop
    1189:	5d                   	pop    %rbp
    118a:	c3                   	ret    

Disassembly of section .fini:

目标文件又叫可重定位文件,这里的重定位就是指的为这些外部函数或变量重新分配地址的过程。

总结来说,静态链接的原理就是将各个目标文件的对应节分别合并,并对照符号表完成对外部函数或变量的重定位。

2.1 虚拟地址空间补充

我们前面在main.s中看到,run函数的地址是1171,但实际上这个说法并不准确。准确的说法是:1171是run函数在代码段的地址,也即run函数在代码段的偏移量(各个段内部从0开始编址)。

run函数最终被加载到内存当中的虚拟地址应该是代码段的地址+偏移量

其中代码段的地址是在每次程序被加载到内存当中时随机分配的。

我们修改一下hello.c的代码:

#include<stdio.h>

void run();

int main() {
    printf("hello world!\n");
    run();
    printf("%p\n", &run);
    return 0;
}

重新编译链接之后,run函数的地址变为:

00000000000011af <run>:

运行./main可以看到结果:

shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ ./main
hello world!
running...
0x55b1458801af
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ ./main
hello world!
running...
0x55ca012441af
shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ ./main
hello world!
running...
0x56372f8ed1af

 很明显,run函数的虚拟地址是极富规律且与其偏移量强相关的。

ELF文件是地址空间初始化的基础,但完整的内存布局是内核、ELF、动态链接器共同作用的结果。

0c2446a5a8924ffaa82f49e01281a855.png

3. 动态链接的原理

进程是如何跳转到动态库并共享动态库的代码的呢?

概括来说很简单,将动态库函数的逻辑地址映射到物理地址空间中动态库代码所在位置即可。

库的起始虚拟地址 + 方法偏移量 ---> 库的起始物理地址 + 方法偏移量

但实际上其中的细节与机制并不简单。

3.1 链接的时机

将动态库函数的逻辑地址映射到物理地址空间中共享库代码所在位置,这一过程显然是在程序被加载到内存时完成的。也就是说动态链接的时机就是程序被加载到内存时。

对于这一点,我们还可以更加详细一点。

上文当中我们说到,目标文件完成链接之后,程序当中多了许多库当中的函数。

 除了main和run以外的函数。来自于库 /lib64/ld-linux-x86-64.so.2 ,用于程序初始化:

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

从main.s中可以看到,_start的地址位1060,而ELF header中指明的程序入口地址就是1060:

shishen@hcss-ecs-b8e6:~/113code/linux-c/动静态库/display$ readelf -h main
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 (Position-Independent Executable 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:          14032 (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

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

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

动态链接器:

  • 动态链接器(如ld-linux.so)负责在程序运行时加载动态库。
  • 当程序启动时,动态链接器会解析程序中的动态库依赖,并加载这些库到内存中。环境变量和配置文件:
  • Linux系统通过环境变量(如LD_LIBRARY_PATH)和配置文件(如/etc/ld.so.conf及其子配置文件)来指定动态库的搜索路径。
  • 这些路径会被动态链接器在加载动态库时搜索。
  • 缓存文件:为了提高动态库的加载效率,Linux系统会维护一个名为/etc/ld.so.cache的缓存文件。该文件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会首先搜索这个缓存文件。

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

3.2 全局偏移量表GOT

  • 动态库的代码可能是会变化的,所以在编译链接时可执行程序当中是无法完成动态库的重定位的。
  • 于是,我们只能在程序被加载到内存之后,找到被加载到内存中的动态库,再进行重定位。即,加载地址重定位。
  • 但代码段的权限为只读,我们无法对代码段进行修改。为了实现这一点,动态链接需要一种机制来在运行时查找和绑定符号的地址,这就是GOT的作用。

全局偏移量表(GOT,Global Offset Table)是Linux系统下ELF格式可执行文件中用于定位全局变量和函数的表,主要用于动态链接。

其实际上就是在专门预留的一片用来存放函数的跳转地址的区域:.got节。

 [24] .got              PROGBITS         0000000000003fb0  00002fb0
       0000000000000050  0000000000000008  WA       0     0     8

由于.got所在的段是可读可写的,所以就可以实现在运行当中动态地完成重定位。

    3.3 过程链接表PLT

    我们在main.s中可以看到,动态链接库函数的函数名之后都会跟着PLT:

    PLT(Procedure Linkage Table,过程链接表)是程序动态链接中的关键机制,主要用于延迟绑定(Lazy Binding)动态库中的函数地址。 

    延迟绑定:程序启动时不会立即解析所有动态库函数的地址,而是在首次调用时才通过PLT解析并缓存地址,减少启动时间。

    3.3.1 GOT与PLT的作用
    • 存储全局变量和函数地址:GOT存储了程序中使用的外部函数和全局变量的实际地址,使得程序在运行时能够正确地访问这些外部符号。

    • 支持动态链接:在动态链接过程中,GOT允许程序在运行时解析和绑定外部符号,而不需要在编译时就确定所有符号的地址。

    • 实现延迟绑定:通过GOT和PLT(过程链接表)的配合,实现了函数的延迟绑定,即函数在第一次被调用时才进行地址绑定,提高了程序的启动速度。

    3.3.2 工作原理
    • 第一次调用:当程序第一次调用某个外部函数时,会通过PLT跳转到GOT,由于GOT中此时没有该函数的地址,会再次跳转回PLT,PLT会将函数的ID压入栈中,然后调用_dl_runtime_resolve函数进行符号查找和重定位,找到函数地址后,将其填充到GOT中,之后再跳转到该函数地址执行。

    • 后续调用:当再次调用该函数时,PLT会直接跳转到GOT中存储的函数地址,无需再次进行符号查找和重定位。

    3.4 地址无关代码PIC

    动态库被加载到内存当中之后,其内部函数的虚拟地址就都是确定了的。当我们使用这些共享代码时,在我们进程的虚拟地址空间当中也应当为其分配对应的虚拟地址,否则代码与其地址就对应不起来了(汇编代码中,每条代码都有自己的地址)。

    这就会导致一个问题:两个动态库要求的地址发生冲突。

    为了解决这个问题,我们希望动态库中的代码被加载到任意位置都能运行,这就是地址无关代码。

    PIC(Position Independent Code)地址无关代码是一种编程技术,它使得代码不依赖于特定的内存地址。

    所以我们在编译动态库对应的目标文件时,需要加上-fPIC选项:

    gcc -fPIC -c
    3.4.1 原理
    • 相对寻址:PIC代码通过使用相对寻址方式来访问数据和代码,而不是使用绝对地址。这意味着代码可以在内存中的任何位置加载和执行,而不需要进行重定位。

    • 全局偏移表(GOT):在PIC中,全局变量和函数的地址是通过全局偏移表(GOT)来访问的。GOT是一个数据结构,用于存储全局变量和函数的实际地址。当代码需要访问这些全局符号时,它会通过GOT中的相应项来间接引用。

    本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2336928.html

    如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

    相关文章

    string的模拟实现 (6)

    目录 1.string.h 2.string.cpp 3.test.cpp 4.一些注意点 本篇博客就学习下如何模拟实现简易版的string类&#xff0c;学好string类后面学习其他容器也会更轻松些。 代码实现如下&#xff1a; 1.string.h #define _CRT_SECURE_NO_WARNINGS 1 #pragma once #include <…

    【野火模型】利用深度神经网络替代 ELMv1 野火参数化:机制、实现与性能评估

    目录 一、ELMv1 野火过程表示法&#xff08;BASE-Fire&#xff09;关键机制野火模拟的核心过程 二、采用神经网络模拟野火过程三、总结参考 一、ELMv1 野火过程表示法&#xff08;BASE-Fire&#xff09; ELMv1 中的野火模型&#xff08;称为 BASE-Fire&#xff09;源自 Commun…

    红宝书第四十七讲:Node.js服务器框架解析:Express vs Koa 完全指南

    红宝书第四十七讲&#xff1a;Node.js服务器框架解析&#xff1a;Express vs Koa 完全指南 资料取自《JavaScript高级程序设计&#xff08;第5版&#xff09;》。 查看总目录&#xff1a;红宝书学习大纲 一、框架定位&#xff1a;HTTP服务器的工具箱 共同功能&#xff1a; 快…

    嵌入式Linux设备使用Go语言快速构建Web服务,实现设备参数配置管理方案探究

    本文探讨&#xff0c;利用Go语言及gin框架在嵌入式Linux设备上高效搭建Web服务器&#xff0c;以实现设备参数的网页配置。通过gin框架&#xff0c;我们可以在几分钟内创建一个功能完善的管理界面&#xff0c;方便对诸如集中器&#xff0c;集线器等没有界面的嵌入式设备的管理。…

    【NLP 59、大模型应用 —— 字节对编码 bpe 算法】

    目录 一、词表的构造问题 二、bpe(byte pair encoding) 压缩算法 算法步骤 示例&#xff1a; 步骤 1&#xff1a;初始化符号表和频率统计 步骤 2&#xff1a;统计相邻符号对的频率 步骤 3&#xff1a;合并最高频的符号对 步骤 4&#xff1a;重复合并直至终止条件 三、bpe在NLP中…

    Python对ppt进行文本替换、插入图片、生成表格

    目录 1. 安装pptx2. 文本替换和插入图片3. 生成表格 1. 安装pptx pip install python-pptx2. 文本替换和插入图片 文本通过占位符例如{{$xxx}}进行标记&#xff0c;然后进行替换&#xff1b;图片通过ppt中的图形和图片中的占位符进行标记ppt如下 具体实现 from pptx import …

    AI(学习笔记第一课) 在vscode中配置continue

    文章目录 AI(学习笔记第一课) 在vscode中配置continue学习内容&#xff1a;1. 使用背景2. 在vscode中配置continue2.1 vscode版本2.2 在vscode中下载continue插件2.2.1 直接进行安装2.2.2 在左下角就会有continue的按钮2.2.3 可以移动到右上角2.2.3 使用的时候需要login 2.3 配…

    C++ (初始面向对象之继承,实现继承,组合,修饰权限)

    初始面向对象之继承 根据面向对象的编程思路&#xff0c;我们可以把共性抽象出来封装成类&#xff0c;然后让不同的角色去继承这些类&#xff0c;从而避免大量重复代码的编写 实现继承 继承机制是面向对象程序设计中使代码可以复用的最重要的手段&#xff0c;它允许程序员在保…

    每日搜索--12月

    12.1 1. urlencode是一种编码方式,用于将字符串以URL编码的形式进行转换。 urlencode也称为百分号编码(Percent-encoding),是特定上下文的统一资源定位符(URL)的编码机制。它适用于统一资源标识符(URI)的编码,也用于为application/x-www-form-urlencoded MIME准备数…

    一天一个java知识点----Tomcat与Servlet

    认识BS架构 静态资源&#xff1a;服务器上存储的不会改变的数据&#xff0c;通常不会根据用户的请求而变化。比如&#xff1a;HTML、CSS、JS、图片、视频等(负责页面展示) 动态资源&#xff1a;服务器端根据用户请求和其他数据动态生成的&#xff0c;内容可能会在每次请求时都…

    游戏报错?MFC140.dll怎么安装才能解决问题?提供多种MFC140.dll丢失修复方案

    MFC140.dll 是 Microsoft Visual C 2015 运行库的重要组成部分&#xff0c;许多软件和游戏依赖它才能正常运行。如果你的电脑提示 "MFC140.dll 丢失" 或 "MFC140.dll 未找到"&#xff0c;说明系统缺少该文件&#xff0c;导致程序无法启动。本文将详细介绍 …

    小白如何从0学习php

    学习 PHP 可以从零开始逐步深入&#xff0c;以下是针对小白的系统学习路径和建议&#xff1a; 1. 了解 PHP 是什么 定义&#xff1a;PHP 是一种开源的服务器端脚本语言&#xff0c;主要用于 Web 开发&#xff08;如动态网页、API、后台系统&#xff09;。 用途&#xff1a;构建…

    常见的 14 个 HTTP 状态码详解

    文章目录 一、2xx 成功1、200 OK2、204 No Content3、206 Partial Content 二、3xx 重定向1、301 Moved Permanently2、302 Found3、303 See Other注意4、Not Modified5、307 Temporary Redirect 三、4xx 客户端错误1、400 Bad Request2、401 Unauthorized3、403 Forbidden4、4…

    Linux Kernel 8

    可编程中断控制器&#xff08;Programmable Interrupt Controller&#xff0c;PIC&#xff09; 支持中断&#xff08;interrupt&#xff09;的设备通常会有一个专门用于发出中断请求Interrupt ReQuest&#xff0c;IRQ的输出引脚&#xff08;IRQ pin&#xff09;。这些IRQ引脚连…

    原子操作CAS(Compare-And-Swap)和锁

    目录 原子操作 优缺点 锁 互斥锁&#xff08;Mutex&#xff09; 自旋锁&#xff08;Spin Lock&#xff09; 原子性 单核单CPU 多核多CPU 存储体系结构 缓存一致性 写传播&#xff08;Write Propagation&#xff09; 事务串行化&#xff08;Transaction Serialization&#…

    【WPF】 在WebView2使用echart显示数据

    文章目录 前言一、NuGet安装WebView2二、代码部分1.xaml中引入webview22.编写html3.在WebView2中加载html4.调用js方法为Echarts赋值 总结 前言 为了实现数据的三维效果&#xff0c;所以需要使用Echarts&#xff0c;但如何在WPF中使用Echarts呢&#xff1f; 一、NuGet安装WebV…

    OpenCV 图像拼接

    一、图像拼接的介绍 图像拼接是一种将多幅具有部分重叠内容的图像合并成一幅完整、无缝且具有更广阔视野或更高分辨率图像的技术。其目的是通过整合多个局部图像来获取更全面、更具信息价值的图像内容。 二、图像拼接的原理 图像拼接的核心目标是将多幅有重叠区域的图像进行准…

    数学建模AI智能体(4.16大更新)

    别的不说就说下面这几点&#xff0c;年初内卷到现在&#xff0c;就现阶段AI水平&#xff0c;卷出了我比较满意的作品&#xff0c;这里分享给各位同学&#xff0c;让你们少走弯路&#xff1a; 1.轻松辅导学生 2.帮助学习 3.突破知识壁垒&#xff0c;缩短与大佬的差距 4.打破…

    Linux》》bash 、sh 执行脚本

    通常使用shell去运行脚本&#xff0c;两种方法 》bash xxx.sh 或 bash “xxx.sh” 、sh xxx.sh 或 sh “xxx.sh” 》bash -c “cmd string” 引号不能省略 我们知道 -c 的意思是 command&#xff0c;所以 bash -c 或 sh -c 后面应该跟一个 command。

    如何用“AI敏捷教练“破解Scrum项目中的“伪迭代“困局?

    一、什么是“伪迭代”&#xff1f; “伪迭代”是指团队表面上采用Scrum框架&#xff0c;但实际运作仍沿用瀑布模式的现象。例如&#xff1a;迭代初期开发人员集中编码、末期测试人员突击测试&#xff0c;导致资源分配不均&#xff1b;需求拆分粗糙&#xff0c;团队无法在固定时…