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

【Linux】库的链接与加载

目录

一、静态链接

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

2.1 虚拟地址/逻辑地址

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

三、动态链接与动态库加载

3.1 进程如何看到动态库

3.2 进程间如何共享库的

3.3 动态链接

3.3.1 概要

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

3.3.3 动态库中的相对地址

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

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

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

3.3.7 库间依赖(简单说明)

3.4 总结


一、静态链接

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

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

# objdump -d hello.o

hello.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <func>:
   0:   f3 0f 1e fa             endbr64
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax        # f <func+0xf>
   f:   48 89 c7                mov    %rax,%rdi
  12:   e8 00 00 00 00          call   17 <func+0x17>
  17:   90                      nop
  18:   5d                      pop    %rbp
  19:   c3                      ret

# objdump -d main.o

main.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:   f3 0f 1e fa             endbr64
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   b8 00 00 00 00          mov    $0x0,%eax
   d:   e8 00 00 00 00          call   12 <main+0x12>
  12:   b8 00 00 00 00          mov    $0x0,%eax
  17:   5d                      pop    %rbp
  18:   c3                      ret

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

我们可以看到这里的 call 指令,它们分别对应调用之前的 printf 和 func 函数,但是我们能发现它们的跳转地址都被设置成了0,这是为什么呢?

其实就是在编译 main.c 的时候,编译器是完全不知道 func 函数的存在的,比如它位于内存的哪个区块,代码长什么样都是不知道的。因此,编译器只能将这个函数的地址暂时设置成0。

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

注意:printf 还涉及到了动态库,这里暂不说明。

// 读取hello.o的符号表
# readelf -s hello.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 hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 .rodata
     4: 0000000000000000    26 FUNC    GLOBAL DEFAULT    1 func
     5: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

// puts:就是printf的实现
// UND:undefine,表示未定义

// 读取main.o的符号表
# readelf -s main.o

Symbol table '.symtab' contains 5 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000    25 FUNC    GLOBAL DEFAULT    1 main
     4: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND func

// func:我们自己的方法在main.o中未定义(因为定义在hello.o中)
// UND:undefine,表示未定义

// 读取main.exe符号表
# readelf -s main.exe

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 main.c
    13: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    14: 0000000000002118     0 OBJECT  LOCAL  DEFAULT   20 __FRAME_END__
    15: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 
    16: 0000000000003dc8     0 OBJECT  LOCAL  DEFAULT   23 _DYNAMIC
    17: 0000000000002014     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: 000000000000117c     0 FUNC    GLOBAL HIDDEN    17 _fini
    25: 0000000000004000     0 NOTYPE  GLOBAL DEFAULT   25 __data_start
    26: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    27: 0000000000004008     0 OBJECT  GLOBAL HIDDEN    25 __dso_handle
    28: 0000000000002000     4 OBJECT  GLOBAL DEFAULT   18 _IO_stdin_used
    29: 0000000000001149    26 FUNC    GLOBAL DEFAULT   16 func
    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: 0000000000001163    25 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

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

// 0000000000001149:其实就是地址

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

// 16:就是 func 函数所在的 section 被合并最终的那一个 section 中了,16就是下标

// 读取可执行程序最终的所有的section清单

# readelf -S main.exe
There are 31 section headers, starting at offset 0x36d8:

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
       0000000000000024  0000000000000000   A       6     0     8
  [ 6] .dynsym           DYNSYM           00000000000003d8  000003d8
       00000000000000a8  0000000000000018   A       7     1     8
  [ 7] .dynstr           STRTAB           0000000000000480  00000480
       000000000000008d  0000000000000000   A       0     0     1
  [ 8] .gnu.version      VERSYM           000000000000050e  0000050e
       000000000000000e  0000000000000002   A       6     0     2
  [ 9] .gnu.version_r    VERNEED          0000000000000520  00000520
       0000000000000030  0000000000000000   A       7     1     8
  [10] .rela.dyn         RELA             0000000000000550  00000550
       00000000000000c0  0000000000000018   A       6     0     8
  [11] .rela.plt         RELA             0000000000000610  00000610
       0000000000000018  0000000000000018  AI       6    24     8
  [12] .init             PROGBITS         0000000000001000  00001000
       000000000000001b  0000000000000000  AX       0     0     4
  [13] .plt              PROGBITS         0000000000001020  00001020
       0000000000000020  0000000000000010  AX       0     0     16
  [14] .plt.got          PROGBITS         0000000000001040  00001040
       0000000000000010  0000000000000010  AX       0     0     16
  [15] .plt.sec          PROGBITS         0000000000001050  00001050
       0000000000000010  0000000000000010  AX       0     0     16
  [16] .text             PROGBITS         0000000000001060  00001060
       000000000000011c  0000000000000000  AX       0     0     16

  [17] .fini             PROGBITS         000000000000117c  0000117c
       000000000000000d  0000000000000000  AX       0     0     4
  [18] .rodata           PROGBITS         0000000000002000  00002000
       0000000000000011  0000000000000000   A       0     0     4
  [19] .eh_frame_hdr     PROGBITS         0000000000002014  00002014
       000000000000003c  0000000000000000   A       0     0     4
  [20] .eh_frame         PROGBITS         0000000000002050  00002050
       00000000000000cc  0000000000000000   A       0     0     8
  [21] .init_array       INIT_ARRAY       0000000000003db8  00002db8
       0000000000000008  0000000000000008  WA       0     0     8
  [22] .fini_array       FINI_ARRAY       0000000000003dc0  00002dc0
       0000000000000008  0000000000000008  WA       0     0     8
  [23] .dynamic          DYNAMIC          0000000000003dc8  00002dc8
       00000000000001f0  0000000000000010  WA       7     0     8
  [24] .got              PROGBITS         0000000000003fb8  00002fb8
       0000000000000048  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         0000000000004000  00003000
       0000000000000010  0000000000000000  WA       0     0     8
  [26] .bss              NOBITS           0000000000004010  00003010
       0000000000000008  0000000000000000  WA       0     0     1
  [27] .comment          PROGBITS         0000000000000000  00003010
       000000000000002b  0000000000000001  MS       0     0     1
  [28] .symtab           SYMTAB           0000000000000000  00003040
       0000000000000390  0000000000000018          29    19     8
  [29] .strtab           STRTAB           0000000000000000  000033d0
       00000000000001e7  0000000000000000           0     0     1
  [30] .shstrtab         STRTAB           0000000000000000  000035b7
       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),
  D (mbind), l (large), p (processor specific)

// main.o 和 hello.o 的 .text 被合并了,是 main.exe 的第16个section

// 怎么证明上面的说法?

// 关于 hello.o 或者 main.o call 后面的00 00 00 00有没有被修改成具体的最终函数地址呢?

// 反汇编 main.exe 只查看代码段信息,包含源代码

# objdump -d main.exe

main.exe:     file format elf64-x86-64


Disassembly of section .init:

0000000000001000 <_init>:
    1000:       f3 0f 1e fa             endbr64
    1004:       48 83 ec 08             sub    $0x8,%rsp
    1008:       48 8b 05 d9 2f 00 00    mov    0x2fd9(%rip),%rax        # 3fe8 <__gmon_start__@Base>
    100f:       48 85 c0                test   %rax,%rax
    1012:       74 02                   je     1016 <_init+0x16>
    1014:       ff d0                   call   *%rax
    1016:       48 83 c4 08             add    $0x8,%rsp
    101a:       c3                      ret

Disassembly of section .plt:

0000000000001020 <.plt>:
    1020:       ff 35 9a 2f 00 00       push   0x2f9a(%rip)        # 3fc0 <_GLOBAL_OFFSET_TABLE_+0x8>
    1026:       ff 25 9c 2f 00 00       jmp    *0x2f9c(%rip)        # 3fc8 <_GLOBAL_OFFSET_TABLE_+0x10>
    102c:       0f 1f 40 00             nopl   0x0(%rax)
    1030:       f3 0f 1e fa             endbr64
    1034:       68 00 00 00 00          push   $0x0
    1039:       e9 e2 ff ff ff          jmp    1020 <_init+0x20>
    103e:       66 90                   xchg   %ax,%ax

Disassembly of section .plt.got:

0000000000001040 <__cxa_finalize@plt>:
    1040:       f3 0f 1e fa             endbr64
    1044:       ff 25 ae 2f 00 00       jmp    *0x2fae(%rip)        # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
    104a:       66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

Disassembly of section .plt.sec:

0000000000001050 <puts@plt>:
    1050:       f3 0f 1e fa             endbr64
    1054:       ff 25 76 2f 00 00       jmp    *0x2f76(%rip)        # 3fd0 <puts@GLIBC_2.2.5>
    105a:       66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

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 e4 00 00 00    lea    0xe4(%rip),%rdi        # 1163 <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    $1,%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 <func>:
    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:       90                      nop
    1161:       5d                      pop    %rbp
    1162:       c3                      ret

0000000000001163 <main>:
    1163:       f3 0f 1e fa             endbr64
    1167:       55                      push   %rbp
    1168:       48 89 e5                mov    %rsp,%rbp
    116b:       b8 00 00 00 00          mov    $0x0,%eax
    1170:       e8 d4 ff ff ff          call   1149 <func>
    1175:       b8 00 00 00 00          mov    $0x0,%eax
    117a:       5d                      pop    %rbp
    117b:       c3                      ret

Disassembly of section .fini:

000000000000117c <_fini>:
    117c:       f3 0f 1e fa             endbr64
    1180:       48 83 ec 08             sub    $0x8,%rsp
    1184:       48 83 c4 08             add    $0x8,%rsp
    1188:       c3                      ret

最终:

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

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

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

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

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

2.1 虚拟地址/逻辑地址

问题:

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

答案:

  • 一个 ELF 程序,在没有被加载到内存的时候,本来就有地址,当代计算机工作的时候,都采用“平坦模式”进行工作。所以也要求 ELF 对自己的代码和数据进行统一编址,下面是 objdump -S 反汇编之后的代码

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

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

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

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

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

三、动态链接与动态库加载

3.1 进程如何看到动态库

3.2 进程间如何共享库的

3.3 动态链接

3.3.1 概要

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

ldd命令:用于打印程序或者库文件所依赖的共享库列表。

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

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

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

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

那么动态链接是如何工作的呢?

首先要交代一个结论,动态链接实际是将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序,操作系统会首先将它的数据代码和连同它用到的一系列动态库先加载到内存,其中每个动态库加载的地址是不固定的,操作系统会根据当前地址空间的使用情况为它们动态分配一段内存。

当动态库被加载到内存以后,一旦它的内存地址确定,我们就可以去修正那些动态库中的那些函数跳转地址了。

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

# ldd /usr/bin/ls
        linux-vdso.so.1 (0x00007ffe5ddf0000)
        libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x000076232f616000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000076232f400000)
        libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x000076232f366000)
        /lib64/ld-linux-x86-64.so.2 (0x000076232f671000)

# ldd main.exe
        linux-vdso.so.1 (0x00007ffec5feb000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000735b22400000)
        /lib64/ld-linux-x86-64.so.2 (0x0000735b22812000)

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

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

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

动态链接器:

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

环境变量和配置文件:

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

缓存文件:

  • 为了提高动态库的加载效率,Linux会维护一个名为/etc/ld.so.cache的缓存文件。
  • 该文件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会首先搜索这个缓存文件。
  • 调用 __libc_start_main:一旦动态链接完成,_start 函数会调用 __libc_start_main(这是glibc提供的一个函数)。 __libc_start_main 负责执行一些额外的初始化工作,比如设置信号处理函数,初始化线程库(如果使用了线程)等等。
  • 调用 main 函数:最后, __libc_start_main 函数会调用程序的 main 函数,此时程序的执行控制权才正式交给用户编写的代码。
  • 处理 main 函数的返回值:当 main 函数返回时, __libc_start_main 会负责处理这个返回值,并最终调用 _exit 函数终止程序。

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

3.3.3 动态库中的相对地址

动态库为了随时进行加载,为了支持并映射到任意进程的任意位置,对动态库中的方法,统一编址,采用相对编址的方法编址的(其实可执行程序也一样,都要遵循平坦模式,只不过exe是直接加载的)。

// ubuntu下查看任意一个库的反汇编

objdump -S /lib/x86_64-linux-gnu/libc-2.31.so | less

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

注意:

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

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

注意:

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

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

注意:

  • 也就是说,我们的程序运行之前,先把所有库加载并映射,所有库的起始虚拟地址都应该提前知道的。
  • 然后对我们加载到内存的程序的库函数调用进行地址修正,在内存中二次完成地址设置(这个叫做加载地址重定位)。
  • 这时我们好像修改的代码区?但是代码区在进程中不是只读的吗?能修改吗?怎么修改呢?

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

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

# readelf -S main.exe

. . .

[24] .got              PROGBITS         0000000000003fb8  00002fb8
       0000000000000048  0000000000000008  WA       0     0     8

. . .

# readelf -l main.exe// .got 在加载的时候,会和 .data合并成一个 segment,然后加载在一起

. . .

  05     .init_array .fini_array .dynamic .got .data .bss 

. . .

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

# objdump -S main.exe

. . .

0000000000001050 <puts@plt>:
    1050:       f3 0f 1e fa             endbr64
    1054:       ff 25 76 2f 00 00       jmp    *0x2f76(%rip)        # 3fd0 <puts@GLIBC_2.2.5>
    105a:       66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

. . .

. . .

0000000000001149 <func>:
    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>

. . .

3.3.7 库间依赖(简单说明)

注意:

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

由于GOT表中的映射地址会在运行时去修改,我们可以通过gdb调试去观察GOT表的地址变化。我们这里只用知道原理即可,感兴趣的可以参考下面这篇文章:通过GDB观察GOT

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

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

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

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

3.4 总结

  • 静态链接的出现,提高了程序的模块化水平。对于一个大的项目,不同的人可以独立的测试和开发自己的模块。通关静态链接,生成最终可执行文件。
  • 我们知道静态链接会将编译产生的所有目标文件,和用到的各种库合并为一个独立的可执行文件,其中我们会去修正模块间函数的跳转地址,也叫做编译重定位(静态重定位)。
  • 而动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序,操作系统会首先将它的数据代码连同它用到的一系列动态库先加载到内存中,其中每个动态库的加载地址都是不固定的,但是无论加载到什么地方,都要映射到进程对应的地址空间中,然后通过.GOT方式进行调用(运行重定位,也叫做动态地址重定位)。
http://www.dtcms.com/a/424036.html

相关文章:

  • MySQL故障排查全攻略
  • 做二手房产网站多少钱深圳人为什么不想去龙岗
  • 培育新质生产力,增强发展新动能
  • 免费仓储服务上线!世强硬件创新全链条服务平台助力原厂供应商,快速触及新客户 新项目
  • 透镜曲率半径测量精度低?OAS 软件牛顿环案例解难题
  • 齐齐哈尔城市建设档案馆网站小红书seo排名
  • dede网站后台模板广州有哪些软件开发公司
  • mpich与openmpi
  • PHP多商户接入阿里云识图找商品
  • 8. Spring AI tools/function-call
  • gRPC众0到1系列【7】
  • 计算机网络技专业术网站开发怎样制作公司的网页
  • 【完整源码+数据集+部署教程】微生物菌落图像分割系统: yolov8-seg-slimneck
  • jsp做网站怎么打开玩游戏的网页
  • 个人备案域名可以做企业网站吗dedecms手机网站插件
  • 苹果上架 App 全流程详解,iOS 应用发布步骤、ipa 文件上传工具、TestFlight 测试与 App Store 审核经验
  • 网站建设中模版靖江做网站的单位
  • 协会网站信息平台建设住房和城乡建设部网站
  • 整站优化哪家专业天府健康通二维码图片高清下载
  • 静态网站是什么怎么做网站登录站
  • AI大模型是怎么工作的?从石头分类说起
  • 苹果群控系统如何做到游戏数据精准采集
  • 分布式任务调度系统中的线程池使用详解
  • pc开奖网站开发濮阳建网站
  • JWT token 简要介绍以及使用场景和案例
  • 网站在线留言怎么做行政法规
  • 语义网络(Semantic Net)对人工智能中自然语言处理的深层语义分析的影响与启示
  • 南通网站建设优化网站建设服务器配置
  • “AI+“行动下的可控智能体:GPT-5 与 GPT-OSS 高性能推理 安全可控 产业落地 GPT-OSS 一可控AI目前全球唯一开源解决方案
  • 零基础网站建设视频想做一个自己的网站怎么做