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

操作系统 : Linux库制作与原理

操作系统 : Linux库的制作与原理

目录

  • 操作系统 : Linux库的制作与原理
    • 引言
    • 1. 静态库
      • 1.1 静态库的生成
      • 1.2 静态库的使用
    • 2.动态库
      • 2.1 动态库的生成
      • 2.2 动态库的使用
    • 3. 库运行搜索路径
    • 4. 目标文件
    • 5. ELF文件
    • 6. ELF文件从形成到加载轮廓
      • 6.1 ELF文件形成可执行文件
      • 6.2 ELF可执行文件加载
    • 7. 理解连接与加载
      • 7.1 静态链接
      • 7.2 ELF加载与进程地址空间
        • 7.2.1 虚拟地址/逻辑地址
        • 7.2.2 虚拟地址空间
      • 7.3 动态链接与动态库加载
        • 7.3.1 进程如何看到动态库
        • 7.3.2 进程间如何共享动态库
        • 7.3.3 动态链接
    • 8. 关于ELF的操作(附录选看)

引言

在Linux系统开发中,库(Library)是代码复用和模块化开发的核心组件之一。无论是静态库还是动态库,它们都在程序构建和运行过程中扮演着至关重要的角色。静态库通过将代码直接嵌入可执行文件,确保程序运行的独立性;而动态库则在程序运行时加载,节省内存和磁盘空间,并支持多进程共享。

本文将深入探讨Linux库的制作与原理,涵盖以下内容:

  • 静态库和动态库的生成与使用方法
  • 库的搜索路径配置技巧
  • ELF文件格式的详细解析
  • 程序从编译到加载的全过程剖析
  • 静态链接与动态链接的核心机制

通过理解这些底层原理,开发者能够更高效地管理项目依赖、优化程序性能,并解决实际开发中遇到的链接和加载问题。无论您是系统级开发者还是应用层程序员,掌握这些知识都将为您的Linux开发之旅奠定坚实基础。


库本质上来说是一种可执行代码的二进制形式,可以被操作系统载入内存执行。

库有两种:

静态库:Linux下.a;Windows下.lib

动态库:Linux下.so;Windows下.dll

1. 静态库

  • 程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库。
  • 一个可执行程序可能用到许多的库,这些库运行有的是静态库,有的是动态库,而我们的编译默认使用动态链接库,只有在该库下找不到动态.so的时候才会采用同名静态库。我们也可以使用 gcc-static 强制设置链接静态库

1.1 静态库的生成

Makefile

libmystdio.a:my_stdio.o my_string.o@ar -rc $@ $^@echo "build $^ to $@ ... done"
%.o:%.c@gcc -c $<@echo "compling $< to $@ ... done"
.PHONY:clean
clean:@rm -rf *.a *.o stdc*@echo "clean ... done"
.PHONY:output
output:@mkdir -p stdc/include@mkdir -p stdc/lib@cp -f *.h stdc/include@cp -f *.a stdc/lib@tar -czf stdc.tgz stdc@echo "output stdc ... done"
$ ar -tv libmystdio.a
rw-rw-r-- 1000/1000 2848 Oct 29 14:35 2024 my_stdio.o
rw-rw-r-- 1000/1000 1272 Oct 29 14:35 2024 my_string.o
  • argnu归档工具,rc表示(replace and create)
  • t:列出静态库中的文件
  • v:verbose:详情信息

1.2 静态库的使用

代码示例

#include "my_stdio.h"
#include "my_string.h"
#include <stdio.h>
int main()
{const char *s = "abcdefg";printf("%s: %d\n", s, my_strlen(s));mFILE *fp = mfopen("./log.txt", "a");if(fp == NULL) return 1;mfwrite(s, my_strlen(s), fp);mfwrite(s, my_strlen(s), fp);mfwrite(s, my_strlen(s), fp);mfclose(fp);return 0;
}
// 场景1:头⽂件和库⽂件安装到系统路径下
$ gcc main.c -lmystdio
// 场景2:头⽂件和库⽂件和我们⾃⼰的源⽂件在同⼀个路径下
$ gcc main.c -L. -lmymath
// 场景3:头⽂件和库⽂件有⾃⼰的独⽴路径
$ gcc main.c -I头⽂件路径 -L库⽂件路径 -lmymath
  • -L:指定库路径
  • -I:指定头文件搜索路径
  • -l:指定库名
  • 目标文件生成后,静态库删掉,程序照样可以运行
  • 库文件名称和引入库的名称:去掉前缀lib,去掉后缀.so/.a。如:libc.so库文件名称就是c

2.动态库

  • 程序在运行的时候才去链接动态库的代码,多个程序共享库的代码。
  • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
  • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking
  • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

2.1 动态库的生成

Makefile

libmystdio.so:my_stdio.o my_string.ogcc -o $@ $^ -shared
%.o:%.cgcc -fPIC -c $<
.PHONY:clean
clean:@rm -rf *.so *.o stdc*@echo "clean ... done"
.PHONY:output
output:@mkdir -p stdc/include@mkdir -p stdc/lib@cp -f *.h stdc/include@cp -f *.so stdc/lib@tar -czf stdc.tgz stdc@echo "output stdc ... done"
  • shared:表示生成共享库格式
  • fPIC:产生位置无关码(position independent code
  • 库名规则:libxxx.so

2.2 动态库的使用

代码示例

// 场景1:头⽂件和库⽂件安装到系统路径下
$ gcc main.c -lmystdio
// 场景2:头⽂件和库⽂件和我们⾃⼰的源⽂件在同⼀个路径下
$ gcc main.c -L. -lmymath // 从左到右搜索-L指定的⽬录
// 场景3:头⽂件和库⽂件有⾃⼰的独⽴路径
$ gcc main.c -I头⽂件路径 -L库⽂件路径 -lmymath
$ ldd libmystdio.so // 查看库或者可执⾏程序的依赖
linux-vdso.so.1 => (0x00007fffacbbf000)
libc.so.6 => /lib64/libc.so.6 (0x00007f8917335000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8917905000)// 以场景2为例
$ ll
total 24
-rwxrwxr-x 1 whb whb 8592 Oct 29 14:50 libmystdio.so
-rw-rw-r-- 1 whb whb 359 Oct 19 16:07 main.c
-rw-rw-r-- 1 whb whb 447 Oct 29 14:50 my_stdio.h
-rw-rw-r-- 1 whb whb 447 Oct 29 14:50 my_string.h
$ gcc main.c -L. -lmystdio
$ ll
total 36
-rwxrwxr-x 1 whb whb 8600 Oct 29 14:51 a.out
-rwxrwxr-x 1 whb whb 8592 Oct 29 14:50 libmystdio.so
-rw-rw-r-- 1 whb whb 359 Oct 19 16:07 main.c
-rw-rw-r-- 1 whb whb 447 Oct 29 14:50 my_stdio.h
-rw-rw-r-- 1 whb whb 447 Oct 29 14:50 my_string.h
[whb@bite-alicloud other]$ ./a.out
... 

3. 库运行搜索路径

配置库运行的搜索路径有以下几种方法:(如果不配置系统很有可能找不到要连接的动态库)

  • 拷贝 .so 文件到系统共享库路径下,一般指 /usr/lib/usr/local/lib/lib64 或者开篇指明的库路径等。

  • 向系统共享库路径下建立同名软链接。

  • 更改环境变量:LD_LIBRARY_PATH

  • ``ldconfig方案:配置/etc/ld.so.conf.d/,执行 ldconfig` 更新。

    [root@localhost linux]# cat /etc/ld.so.conf.d/bit.conf
    /root/tools/linux
    [root@localhost linux]# ldconfig // 要⽣效,这⾥要执⾏ldconfig,重新加载库搜索路径
    

4. 目标文件

在这里插入图片描述

通过gcc -c编译两个源文件形成.o文件,这个.o文件被称作目标文件。要注意的是如果我们修改了一个原文件,那么只需要单独编译它这一个,而不需要浪费时间重新编译整个工程。目标文件是一个二进制的文件,文件的格式是ELF,是对二进制代码的一种封装。

$ file hello.o		#file命令用于辨识文件类型
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

5. ELF文件

要理解编译链链接的细节,我们不得不了解一下ELF文件。

其实有以下四种文件其实都是ELF文件:

  • 可重定位文件(Relocatable File):即 xxx.o 文件。包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
  • 可执行文件(Executable File):即可执行程序。
  • 共享目标文件(Shared Object File):即 xxx.so文件。
  • 内核转储(core dumps),存放当前进程的执行上下文,用于dump信号触发。

一个ELF文件由以下四部分组成:

  • ELF头(ELF header):描述ELF文件的主要特性。其位于文件的开始位置,它的主要目的是定位文件的其他部分。
  • 程序头表(Program header table):列举了所有有效的段(segments)和他们的属性。表里记着每个段的开始的位置和位移(offset)、长度,毕竟这些段,都是紧密的放在二进制文件中,需要段表的描述信息,才能把他们每个段分割开。
  • 节头表(Section header table):包含对节(sections)的描述。
  • 节(Section):ELF文件中的基本组成单位,包含了特定类型的数据。ELF文件的各种信息和数据都存储在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。

最常见的节:

  • 代码节(.text):用于保存机器指令,是程序的主要执行部分。
  • 数据节(.data):保存已初始化的全局变量和局部静态变量。

在这里插入图片描述


6. ELF文件从形成到加载轮廓

6.1 ELF文件形成可执行文件

  • 第一步:将多份C/C++源代码翻译成目标文件.o文件。
  • 将多份.o文件section部分进行合并(链接时进行)。

在这里插入图片描述


6.2 ELF可执行文件加载

  • 一个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment

  • 合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等部分合并到一起。

  • 这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到一起。

  • 在形成ELF的时候,合并方式已经确定了,具体合并原则被记录在了ELF的 程序头表(Program header table) 中。

  • section合并成为segment的原因:

    • Section合并的主要原因是为了减少页面碎片,提高内存使用效率。如果不进行合并,假设页面大小为4096字节(内存块基本大小,加载,管理的基本单位),如果.text部分为4097字节.init部分为512字节,那么它们将占用3个页面,而合并后,它们只需2个页面。
    • 此外,操作系统在加载程序时,会将具有相同属性的section合并成一个大的segment,这样就可以实现不同的访问权限,从而优化内存管理和权限访问控制。
  • Program Header Table程序头表和Section Header Table节头表的作用:

    • 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节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。使用 readelf 命令查看 .so 文件可以看到该节。

从 执行视图 来看:

  • 告诉操作系统哪些模块可以被加载进内存。
  • 加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执行的。

我们可以在 ELF头 中找到文件的基本信息,以及可以看到ELF头是如何定位程序头表和节头表的。

例如我们查看下hello.o这个可重定位文件的主要信息:

// 查看目标文件
$ readelf -h hello.o
ELF Header:Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00Class:    ELF64    # 文件类型Data:    2's complement, little endian    # 指定的编码方式Version:    1 (current)OS/ABI:    UNIX - System VABI Version:    0Type:    REL (Relocatable file)    # 指出ELF文件的类型Machine:    Advanced Micro Devices X86-64    # 该程序需要的体系结构Version:    0x1Entry point address:    0x0    # 系统第一个传输控制的虚拟地址,在那启动进程。假如文件没有如何关联的入口点,该成员就保持为0。Start of program headers:    0 (bytes into file)Start of section headers:    728 (bytes into file)Flags:    0x0Size of this header:    64 (bytes)    # 保存着ELF头大小(以字节计数)Size of program headers:    0 (bytes)    # 保存着在文件的程序头表(program header table)中一个入口的大小Number of program headers:    0    # 保存着在程序头表中入口的个数。因此,e_phentsize和e_phnum的乘机就是表的大小(以字节计数).假如没有程序头表,变量为0。Size of section headers:    64 (bytes)    # 保存着section头的大小(以字节计数)。一个section头是在section头表的一个入口Number of section headers:    13    # 保存着在section header table中的入口数目。因此,e_shentsize和e_shnum的乘积就是section头表的大小(以字节计数)。假如文件没有section头表,值为0。Section header string table index:    12    # 保存着跟section名字字符表相关入口的section头表(section header table)索引。// 查看可执行程序
$ gcc *.o
$ readelf -h a.out
ELF Header:Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00Class:    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:    0x1060Start of program headers:    64 (bytes into file)Start of section headers:    14768 (bytes into file)Flags:    0x0Size of this header:    64 (bytes)Size of program headers:    56 (bytes)Number of program headers:    13Size of section headers:    64 (bytes)Number of section headers:    31Section header string table index:    30

ELF HEADER部分的主要目的是定位文件的其他部分。


7. 理解连接与加载

7.1 静态链接

$ ll
-rw-rw-r-- 1 whb whb 62 Oct 31 15:36 code.c
-rw-rw-r-- 1 whb whb 103 Oct 31 15:36 hello.c$ gcc -c *.c
$ gcc *.o -o main.exe
$ ll
-rw-rw-r-- 1 whb whb 62 Oct 31 15:36 code.c
-rw-rw-r-- 1 whb whb 1672 Oct 31 15:46 code.o
-rw-rw-r-- 1 whb whb 103 Oct 31 15:36 hello.c
-rw-rw-r-- 1 whb whb 1744 Oct 31 15:46 hello.o
-rwxrwxr-x 1 whb whb 16752 Oct 31 15:46 main.exe*

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

$ objdump -d code.o
code.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <run>:0:   f3 0f 1e fa             endbr64 4:   55                      push   %rbp5:   48 89 e5                mov    %rsp,%rbp8:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # f <run+0xf>f:  # e8 00 00 00 00          callq  14 <run+0x14>14:   90                      nop15:   5d                      pop    %rbp16:   c3                      retq   $ objdump -d hello.o
hello.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:0:   f3 0f 1e fa             endbr64 4:   55                      push   %rbp5:   48 89 e5                mov    %rsp,%rbp8:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # f <main+0xf>f:  # e8 00 00 00 00          callq  14 <main+0x14>14:   b8 00 00 00 00          mov    $0x0,%eax19: #  e8 00 00 00 00          callq  1e <main+0x1e>1e:   b8 00 00 00 00          mov    $0x0,%eax23:   5d                      pop    %rbp24:   c3                      retq   
  • objdump -d命令:将代码段.text进行反汇编查看

  • hello.o中的main函数不认识printfrun函数,code.o不认识printf函数,他们后边的地址都是00 00 00 00

  • 在编译 hello.c 的时候,编译器是完全不知道 printfrun 函数的存在的,比如他们位于内存的哪个区块,代码长什么样都是不知道的。因此,编辑器只能将这两个函数的跳转地址先暂时设为0。

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

  • 静态链接的过程:

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

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

在这里插入图片描述


7.2 ELF加载与进程地址空间

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

    在这里插入图片描述

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

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


7.2.2 虚拟地址空间

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

$ gcc *.o
$ readelf -h a.out
ELF Header:Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class:                             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:                           0x1
# Entry point address:               0x1060Start of program headers:          64 (bytes into file)Start of section headers:          14768 (bytes into file)Flags:                             0x0Size of this header:               64 (bytes)Size of program headers:           56 (bytes)Number of program headers:         13Size of section headers:           64 (bytes)Number of section headers:         31Section header string table index: 30

在这里插入图片描述


7.3 动态链接与动态库加载

7.3.1 进程如何看到动态库

在这里插入图片描述


7.3.2 进程间如何共享动态库

在这里插入图片描述


7.3.3 动态链接
  • 动态链接的优点:

    静态链接最⼤的问题在于⽣成的⽂件体积⼤,并且相当耗费内存资源。随着软件复杂度的提升,我们的操作系统也越来越臃肿,不同的软件就有可能都包含了相同的功能和代码,显然会浪费⼤量的硬盘空间。这个时候,动态链接的优势就体现出来了,我们可以将需要共享的代码单独提取出来,保存成⼀个独⽴的动态链接库,等到程序运⾏的时候再将它们加载到内存,这样不但可以节省空间,因为同⼀个模块在内存中只需要保留⼀份副本,可以被不同的进程所共享

  • 动态链接的过程:

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

  • 编译器默认修改我们的可执行程序

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

    1. 设置堆栈:为程序创建一个初始的堆栈环境。

    2. 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段。

    3. 动态链接:这是关键的一步,_start函数会调用动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确地映射到动态库中的实际地址。

      $ ldd /usr/bin/lslinux-vdso.so.1 (0x00007fffdd85f000)libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1(0x00007f42c025a000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f42c0068000)libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0(0x00007f42bffd7000)libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f42bffd1000)
      #	/lib64/ld-linux-x86-64.so.2 (0x00007f42c02b6000)libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0(0x00007f42bffae000)
      $ ldd main.exelinux-vdso.so.1 (0x00007fff231d6000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f197ec3b000)
      #	/lib64/ld-linux-x86-64.so.2 (0x00007f197ee3e000)
      

      动态链接器:

      • 动态链接器(如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 函数来终止程序。

  • 动态库中的相对地址

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

    # ubuntu下查看任意⼀个库的反汇编
    objdump -S /lib/x86_64-linux-gnu/libc-2.31.so | less
    # Cetnos下查看任意⼀个库的反汇编
    $ objdump -S /lib64/libc-2.17.so | less
    
  • 进程与库的映射

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

    在这里插入图片描述

  • 进程如何进行库函数调用

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

    在这里插入图片描述

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

    程序运行之前,需要先把所有库加载并映射,所有库的起始虚拟地址都应该提前知道。然后对我们加载到内存中的程序的库函数调用进行地址修改,在内存中二次完成地址设置(这个叫做加载地址重定位)。但是我们其实并不是在代码区进行修改的,因为代码区权限是可读。动态链接采用的做法是在 .data(可执行程序或者库自己)中专门预留一片区域用来存放函数的跳转地址,它也被叫做全局偏移表GOT,表中每一项都是本运行模块要引用的一个全局变量或函数的地址。因为.data区域是可读写的,所以可以支持动态进行修改。

    $ readelf -S a.out
    ...
    #[24] .got PROGBITS 0000000000003fb8 00002fb8
    0000000000000048 0000000000000008 WA 0 0 8
    ...
    $ readelf -l a.out # .got在加载的时候,会和.data合并成为⼀个segment,然后加载在⼀起
    ...05 .init_array .fini_array .dynamic #.got .data .bss
    ...
    

    在这里插入图片描述

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

    库中也有.GOT,所以库和库之间互相调用也是与地址无关的。这也是大家为什么都是ELF格式的原因了。

    在这里插入图片描述

    由于动态链接在程序加载的时候需要对大量函数进行重定位,这一步显然是非常耗时的。为了进一步降低开销,我们的操作系统还做了一些其他的优化,比如延迟绑定,或者也叫PLT(过程连接表(Procedure Linkage Table))。与其在程序一开始就对所有函数进行重定位,不如将这个过程推迟到函数第一次被调用的时候,因为绝大多数动态库中的函数可能在程序运行期间一次都不会被使用到。
    思路是:GOT中的跳转地址默认会指向一段辅助代码,它也被叫做桩代码/stup。在我们第一次调用函数的时候,这段代码会负责查询真正函数的跳转地址,并且去更新GOT表。于是我们再次调用函数的时候,就会直接跳转到动态库中真正的函数实现。在这里插入图片描述

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


8. 关于ELF的操作(附录选看)

附录1:ELF结构

在这里插入图片描述


附录2:查看ELF Header

-h--file-feader:显示ELF文件的文件头信息。文件头包含了ELF文件的基本信息,比如文件类、机器类型、版本、入口点地址、程序头表和节头表的位置和大小等。

// readelf -h main 的输出结果
ELF Header:Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class:                             ELF64Data:                              2's complement, little endianVersion:                           1 (current)OS/ABI:                            UNIX - System VABI Version:                       0Type:                              EXEC (Executable file)Machine:                           Advanced Micro Devices X86-64Version:                           0x1Entry point address:               0x400640Start of program headers:          64 (bytes into file)Start of section headers:          7048 (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:         31Section header string table index: 30// 内核中ELF Header相关的数据结构
// 来自 /linux/include/elf.h
typedef struct elf32_hdr{unsigned char e_ident[EI_NIDENT];Elf32_Half    e_type;Elf32_Half    e_machine;Elf32_Word    e_version;Elf32_Addr    e_entry;  /* Entry point */Elf32_Off     e_phoff;Elf32_Off     e_shoff;Elf32_Word    e_flags;Elf32_Half    e_ehsize;Elf32_Half    e_phentsize;Elf32_Half    e_phnum;Elf32_Half    e_shentsize;Elf32_Half    e_shnum;Elf32_Half    e_shstrndx;
} Elf32_Ehdr;typedef struct elf64_hdr {unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */Elf64_Half    e_type;Elf64_Half    e_machine;Elf64_Word    e_version;Elf64_Addr    e_entry;  /* Entry point virtual address */Elf64_Off     e_phoff;  /* Program header table file offset */Elf64_Off     e_shoff;  /* Section header table file offset */Elf64_Word    e_flags;Elf64_Half    e_ehsize;Elf64_Half    e_phentsize;Elf64_Half    e_phnum;Elf64_Half    e_shentsize;Elf64_Half    e_shnum;Elf64_Half    e_shstrndx;
} Elf64_Ehdr;

附录3:ELF Program Header Table

-l--Program-headers:显示ELF文件的程序头部(也称为段头)信息。可执行文件在内存中的布局和加载过程非常重要。

/* * readelf -l main 的输出结果* 显示ELF可执行文件的程序头(Program Headers)信息*/
Elf file type is EXEC (Executable file)
Entry point 0x400640
There are 9 program headers, starting at offset 64Program Headers:Type           Offset             VirtAddr           PhysAddrFileSiz            MemSiz              Flags  AlignPHDR           0x0000000000000040 0x0000000000400040 0x00000000004000400x00000000000001f8 0x00000000000001f8  R E    8INTERP         0x0000000000000238 0x0000000000400238 0x00000000004002380x000000000000001c 0x000000000000001c  R      1[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]LOAD           0x0000000000000000 0x0000000000400000 0x00000000004000000x0000000000000d24 0x0000000000000d24  R E    200000LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e100x0000000000000254 0x0000000000000258  RW     200000DYNAMIC        0x0000000000000e28 0x0000000000600e28 0x0000000000600e280x00000000000001d0 0x00000000000001d0  RW     8NOTE           0x0000000000000254 0x0000000000400254 0x00000000004002540x0000000000000044 0x0000000000000044  R      4GNU_EH_FRAME   0x0000000000000b34 0x0000000000400b34 0x0000000000400b340x000000000000005c 0x000000000000005c  R      4GNU_STACK      0x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000  RW     10GNU_RELRO      0x0000000000000e10 0x0000000000600e10 0x0000000000600e100x00000000000001f0 0x00000000000001f0  R      1Section 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 /* 注释说明 */
// PhysAddr: 现代操作系统可以忽略物理地址(PhysAddr)的设定
// LOAD: 不是所有的section都要被加载到内存,带有LOAD标记的段是需要被加载到内存的部分/** 内核中关于ELF Program Header相关的数据结构* 定义在 /linux/include/elf.h*//* 32位ELF程序头结构 */
typedef struct elf32_phdr {Elf32_Word  p_type;    /* 段类型 */Elf32_Off   p_offset;  /* 段在文件中的偏移量 */Elf32_Addr  p_vaddr;   /* 段的虚拟地址 */Elf32_Addr  p_paddr;   /* 段的物理地址 */Elf32_Word  p_filesz;  /* 段在文件中的大小 */Elf32_Word  p_memsz;   /* 段在内存中的大小 */Elf32_Word  p_flags;   /* 段标志位 */Elf32_Word  p_align;   /* 段对齐方式 */
} Elf32_Phdr;/* 64位ELF程序头结构 */
typedef struct elf64_phdr {Elf64_Word  p_type;    /* 段类型 */Elf64_Word  p_flags;   /* 段标志位 */Elf64_Off   p_offset;  /* 段在文件中的偏移量 */Elf64_Addr  p_vaddr;   /* 段的虚拟地址 */Elf64_Addr  p_paddr;   /* 段的物理地址 */Elf64_Xword p_filesz;  /* 段在文件中的大小 */Elf64_Xword p_memsz;   /* 段在内存中的大小 */Elf64_Xword p_align;   /* 段对齐方式,包括文件和内存 */
} Elf64_Phdr;

附录4:ELF Section Header Table

-S--section-headers:显示ELF文件的节头信息。节头描述了ELF文件的各个节的起始地址、大小、标志等信息。

/** readelf -S main 的输出结果* 显示ELF文件的段头表(Section Headers)信息*/
There are 31 section headers, starting at offset 0x1b88:Section Headers:[Nr] Name              Type            Address          OffsetSize              EntSize          Flags  Link  Info  Align[ 0]                   NULL            0000000000000000  000000000000000000000000  0000000000000000           0     0     0[ 1] .interp           PROGBITS        0000000000400238  00000238000000000000001c  0000000000000000   A       0     0     1[ 2] .note.ABI-tag     NOTE            0000000000400254  000002540000000000000020  0000000000000000   A       0     0     4[ 3] .note.gnu.build-i NOTE            0000000000400274  000002740000000000000024  0000000000000000   A       0     0     4[ 4] .gnu.hash         GNU_HASH        0000000000400298  00000298000000000000001c  0000000000000000   A       5     0     8[ 5] .dynsym           DYNSYM          00000000004002b8  000002b80000000000000108  0000000000000018   A       6     1     8[ 6] .dynstr           STRTAB          00000000004003c0  000003c00000000000000076  0000000000000000   A       0     0     1[ 7] .gnu.version      VERSYM          0000000000400436  000004360000000000000016  0000000000000002   A       5     0     2[ 8] .gnu.version_r    VERNEED         0000000000400450  000004500000000000000030  0000000000000000   A       6     1     8[ 9] .rela.dyn         RELA            0000000000400480  000004800000000000000018  0000000000000018   A       5     0     8[10] .rela.plt         RELA            0000000000400498  0000049800000000000000d8  0000000000000018  AI       5    24     8[11] .init             PROGBITS        0000000000400570  00000570000000000000001a  0000000000000000  AX       0     0     4[12] .plt              PROGBITS        0000000000400590  0000059000000000000000a0  0000000000000010  AX       0     0     16[13] .plt.got          PROGBITS        0000000000400630  000006300000000000000008  0000000000000000  AX       0     0     8[14] .text             PROGBITS        0000000000400640  0000064000000000000004b2  0000000000000000  AX       0     0     16[15] .fini             PROGBITS        0000000000400af4  00000af40000000000000009  0000000000000000  AX       0     0     4[16] .rodata           PROGBITS        0000000000400b00  00000b000000000000000032  0000000000000000   A       0     0     8[17] .eh_frame_hdr     PROGBITS        0000000000400b34  00000b34000000000000005c  0000000000000000   A       0     0     4[18] .eh_frame         PROGBITS        0000000000400b90  00000b900000000000000194  0000000000000000   A       0     0     8[19] .init_array       INIT_ARRAY      0000000000600e10  00000e100000000000000008  0000000000000008  WA       0     0     8[20] .fini_array       FINI_ARRAY      0000000000600e18  00000e180000000000000008  0000000000000008  WA       0     0     8[21] .jcr              PROGBITS        0000000000600e20  00000e200000000000000008  0000000000000000  WA       0     0     8[22] .dynamic          DYNAMIC         0000000000600e28  00000e2800000000000001d0  0000000000000010  WA       6     0     8[23] .got              PROGBITS        0000000000600ff8  00000ff80000000000000008  0000000000000008  WA       0     0     8[24] .got.plt          PROGBITS        0000000000601000  000010000000000000000060  0000000000000008  WA       0     0     8[25] .data             PROGBITS        0000000000601060  000010600000000000000004  0000000000000000  WA       0     0     1[26] .bss              NOBITS          0000000000601064  000010640000000000000004  0000000000000000  WA       0     0     1[27] .comment          PROGBITS        0000000000000000  00001064000000000000002d  0000000000000001  MS       0     0     1[28] .symtab           SYMTAB          0000000000000000  000010980000000000000750  0000000000000018          29    49     8[29] .strtab           STRTAB          0000000000000000  000017e80000000000000291  0000000000000000           0     0     1[30] .shstrtab         STRTAB          0000000000000000  00001a79000000000000010c  0000000000000000           0     0     1Key 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)/* * .got 段说明:* GOT (Global Offset Table) 全局偏移表,用于动态链接* 包含动态链接过程中需要解析的全局变量和函数的地址*//** 内核中关于ELF Section Header相关的数据结构* 定义在 /linux/include/elf.h*//* 32位ELF段头结构 */
typedef struct {Elf32_Word  sh_name;      /* 段名(在.shstrtab中的索引) */Elf32_Word  sh_type;      /* 段类型 */Elf32_Word  sh_flags;     /* 段标志位 */Elf32_Addr  sh_addr;      /* 段的虚拟地址 */Elf32_Off   sh_offset;    /* 段在文件中的偏移 */Elf32_Word  sh_size;      /* 段的大小 */Elf32_Word  sh_link;      /* 链接到其他段的索引 */Elf32_Word  sh_info;      /* 附加信息 */Elf32_Word  sh_addralign; /* 段对齐要求 */Elf32_Word  sh_entsize;   /* 表项大小(如果段包含表格) */
} Elf32_Shdr;/* 64位ELF段头结构 */
typedef struct elf64_shdr {Elf64_Word   sh_name;      /* 段名(在.shstrtab中的索引) */Elf64_Word   sh_type;      /* 段类型 */Elf64_Xword  sh_flags;     /* 段标志位 */Elf64_Addr   sh_addr;      /* 段的虚拟地址 */Elf64_Off    sh_offset;    /* 段在文件中的偏移 */Elf64_Xword  sh_size;      /* 段的大小 */Elf64_Word   sh_link;      /* 链接到其他段的索引 */Elf64_Word   sh_info;      /* 附加信息 */Elf64_Xword  sh_addralign; /* 段对齐要求 */Elf64_Xword  sh_entsize;   /* 表项大小(如果段包含表格) */
} Elf64_Shdr;

附录5:查看具体的sections信息

main:     file format elf64-x86-64/** .init 段 - 程序初始化代码* 在main函数执行前运行,用于初始化操作*/
Disassembly of section .init:
0000000000400570 <_init>:400570:       48 83 ec 08             sub    $0x8,%rsp             ; 分配栈空间400574:       48 8b 05 7d 0a 20 00    mov    0x200a7d(%rip),%rax   ; 加载__gmon_start__地址; 0x600ff8 <__gmon_start__>40057b:       48 85 c0                test   %rax,%rax             ; 检查是否为NULL40057e:       74 05                   je     400585 <_init+0x15>   ; 如果为NULL则跳过400580:       e8 ab 00 00 00          callq  400630 <.plt.got>     ; 调用__gmon_start__400585:       48 83 c4 08             add    $0x8,%rsp             ; 恢复栈指针400589:       c3                      retq                         ; 返回/** .plt 段 - 过程链接表(Procedure Linkage Table)* 用于动态链接函数调用*/
Disassembly of section .plt:0000000000400590 <.plt>:  ; PLT基地址400590:       ff 35 72 0a 20 00       pushq  0x200a72(%rip)        ; 压入GOT+0x8的值; 0x601008 <_GLOBAL_OFFSET_TABLE_+0x8>400596:       ff 25 74 0a 20 00       jmpq   *0x200a74(%rip)       ; 跳转到GOT+0x10; 0x601010 <_GLOBAL_OFFSET_TABLE_+0x10>40059c:       0f 1f 40 00             nopl   0x0(%rax)            ; 空指令(对齐用); write函数的PLT条目
00000000004005a0 <write@plt>:4005a0:       ff 25 72 0a 20 00       jmpq   *0x200a72(%rip)       ; 直接跳转到GOT中的地址; 0x601018 <write@GLIBC_2.2.5>4005a6:       68 00 00 00 00          pushq  $0x0                  ; 压入重定位索引4005ab:       e9 e0 ff ff ff          jmpq   400590 <.plt>         ; 跳回PLT起始; printf函数的PLT条目
00000000004005b0 <printf@plt>:4005b0:       ff 25 6a 0a 20 00       jmpq   *0x200a6a(%rip)       ; 0x601020 <printf@GLIBC_2.2.5>4005b6:       68 01 00 00 00          pushq  $0x1                  ; 压入重定位索引(1)4005bb:       e9 d0 ff ff ff          jmpq   400590 <.plt>         ; 跳回PLT起始; close函数的PLT条目
00000000004005c0 <close@plt>:4005c0:       ff 25 62 0a 20 00       jmpq   *0x200a62(%rip)       ; 0x601028 <close@GLIBC_2.2.5>4005c6:       68 02 00 00 00          pushq  $0x2                  ; 压入重定位索引(2)4005cb:       e9 c0 ff ff ff          jmpq   400590 <.plt>         ; 跳回PLT起始; __libc_start_main函数的PLT条目
00000000004005d0 <__libc_start_main@plt>:4005d0:       ff 25 5a 0a 20 00       jmpq   *0x200a5a(%rip)       ; 0x601030 <__libc_start_main@GLIBC_2.2.5>4005d6:       68 03 00 00 00          pushq  $0x3                  ; 压入重定位索引(3)4005db:       e9 b0 ff ff ff          jmpq   400590 <.plt>         ; 跳回PLT起始

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

$objdump -d hello.o
hello.o:     file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:0:	55                   	push   %rbp1:	48 89 e5             	mov    %rsp,%rbp4:	bf 00 00 00 00       	mov    $0x0,%edi9:	e8 00 00 00 00       	callq  e <main+0xe>e:	b8 00 00 00 00       	mov    $0x0,%eax13:	e8 00 00 00 00       	callq 18 <main+0x18>18:	b8 00 00 00 00       	mov    $0x0,%eax1d:	5d                   	pop    %rbp1e:	c3                   	retq   
f ff          jmpq   400590 <.plt>         ; 跳回PLT起始; __libc_start_main函数的PLT条目
00000000004005d0 <__libc_start_main@plt>:4005d0:       ff 25 5a 0a 20 00       jmpq   *0x200a5a(%rip)       ; 0x601030 <__libc_start_main@GLIBC_2.2.5>4005d6:       68 03 00 00 00          pushq  $0x3                  ; 压入重定位索引(3)4005db:       e9 b0 ff ff ff          jmpq   400590 <.plt>         ; 跳回PLT起始
http://www.xdnf.cn/news/103429.html

相关文章:

  • PNG透明免抠设计素材大全26000+
  • DCAN,ECAN和MCAN的区别
  • Vue3 + TypeScript 实现二维码生成与展示
  • 开源AI客户端Cherry Studio本地化部署自建大模型服务在线平台
  • HTML+CSS对角背景变色
  • es-存储与搜索优化
  • 网络编程——通信三要素
  • 文档构建:Sphinx全面使用指南 — 强化篇
  • 常见基础电能
  • docker部署seafile修改默认端口并安装配置onlyoffice实现在线编辑
  • Shader CGInculde(六)
  • 关于日期的一些计算
  • 0-1背包的运算规则
  • 《重塑AI应用架构》系列: Serverless与MCP融合创新,构建AI应用全新智能中枢
  • (09)Vue脚手架的使用(Vite、vue-cli、create-vue)
  • 利用Python爬虫实现百度图片搜索的PNG图片下载
  • C++ 中 std::thread 的高级应用
  • [实战]zynq7000设备树自动导出GPIO
  • 基于 Spring Boot 瑞吉外卖系统开发(六)
  • ElasticSearch深入解析(三):Elasticsearch 7的安装与配置、Kibana安装
  • spark和Hadoop之间的对比与联系
  • 确保电力作业安全:安全工器具的检查与使用指南
  • 比较:AWS VPC peering与 AWS Transit Gateway
  • 云原生后端架构:重塑后端开发的新范式
  • Linux服务器:在ufw防火墙设置这套规则sudo ufw allow from 172.0.0.0/8,为什么容器就可以访问宿主机的服务了?
  • ReAct Agent 实战:基于DeepSeek从0到1实现大模型Agent的探索模式
  • leetcode-哈希表
  • 容器修仙传 我的灵根是Pod 第8章 护山大阵(DaemonSet)
  • React-实现切换tab高亮显示和排序
  • 【Python爬虫详解】第四篇:使用解析库提取网页数据——BeautifuSoup