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

【Linux】库制作与原理(3)_动静态库的链接过程

hello~ 很高兴见到大家! 这次带来的是Linux系统中关于库制作与原理这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页: 默|笙


文章目录

  • 一、预备知识
    • 1.1 指令集
    • 1.2 _start函数
  • 二、静态链接的链接过程
  • 三、动态链接的链接过程
    • 3.1 动态库的加载和映射
    • 3.2 如何进行库函数调用
    • 3.3 全局偏移量表GOT

一、预备知识

1.1 指令集

  1. CPU 具备专属的指令集。CPU 执行计算和操作的本质,是逐条解析并执行机器指令;早期程序员直接用二进制机器指令编程,过程极其繁琐且易出错,因此人们设计了汇编语言 —— 为每一条机器指令匹配对应的助记符(如 mov、add),大幅降低编程难度;而 C 语言等高级语言,则是在汇编语言的基础上进一步抽象封装,让程序员能以更接近自然语言的方式编写代码,最终再通过编译器 / 汇编器将高级语言代码转换为 CPU 可执行的机器指令。不同的 CPU 具备的指令集也不同,所以安卓和苹果不兼容,本质是 CPU 不同。

  1. 可以使用objdump -d命令对可执行程序进行反汇编,查看其对应的汇编指令与机器码。CPU 执行的机器指令本质由指令长度、操作码(Opcode)和操作数(数据 / 地址)组成;在指令执行过程中,程序计数器(PC 指针)会自动更新 —— 本条指令执行完毕后,PC指针会加上当前指令的长度,从而精准跳转到下一条指令的内存地址,保证指令流按顺序执行。

1.2 _start函数

  1. 对用户而言,编写程序时约定俗成的入口是 main 函数;但对 Linux 系统的编译器 / 内核来说,程序真正的启动入口是名为_start 的函数 —— 内核加载可执行文件后,会先执行_start,再由_start 完成运行时环境初始化(如栈、参数、全局变量初始化),最终调用程序员编写的 main 函数。

_start函数作用:

  1. 设置堆栈,为程序创造一个初始的堆栈环境。
  2. 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段。
  3. 动态链接:_start函数会调用动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确地映射到动态库中的实际地址。在此过程中,程序的全局偏移量表(GOT 表)会被填充完毕 —— 动态链接器将解析到的动态库中符号的真实内存地址,写入 GOT 表对应的位置,确保程序中的函数调用和变量访问能正确映射到动态库的实际地址。GOT 表接下来会讲到。
  4. 调用 __libc_start_main:动态链接完成后,_start 函数会调用 glibc 提供的 __libc_start_main 函数。__libc_start_main 会执行一系列初始化工作,例如设置信号处理函数、初始化线程库等。
  5. 调用 main 函数:最后,__libc_start_main 会调用程序的 main 函数,执行控制权正式转交给用户编写的代码。
  6. 处理 main 函数的返回值:当 main 函数返回后,__libc_start_main 会处理返回值,并最终调用 _exit 函数终止程序。

二、静态链接的链接过程

  1. 静态链接的本质就是.o文件的连接过程,无论是静态库中的.o文件还是自己创建的.o文件,都是这样。那么静态链接究竟要干些什么呢?
  2. 这里我通过创建文件来模拟静态库链接的完整过程:准备 main.c 和 run.c 两个源文件,先将它们分别编译为 .o 目标文件,再对目标文件进行反汇编得到 .s 文件。可以把 run.o 直接看作一个 “人造静态库”—— 因为静态库本质就是多个 .o 目标文件的打包集合。
1#include<stdio.h>2voidrun()3{4printf("running...\n");5}

  1. 这是 run.c 和 run.s 文件:run.c 中调用了标准库函数 printf,因此在对应的汇编代码(run.s)或反汇编结果中,会出现 callq 指令(用来调用函数),这行指令本应指向 printf 函数的执行地址,但实际显示的地址却全为 0。
1#include<stdio.h>2voidrun();3intmain()4{5printf("hello world!\n");6run();7return0;8}

  1. 是 main.c 及其对应的汇编文件 main.s:能清晰看到,代码中两个函数调用指令(callq)后的目标地址均为0, 这是因为编译阶段编译器仅为外部函数预留了地址占位符,无法确定函数的最终内存地址,毕竟 .o 文件的生成时不需要其他文件,所以它当然不知道自己所调用的函数的地址。


  1. main.o 和 run.o 这两个 ELF 文件被合并成为了一个文件,查阅符号表(命令 readelf -s(小S) ELF文件名)可以看到两个函数被放在了同一个段13也就是代码段 text 里面。

  1. 对比反汇编结果可清晰看到:在 .o 目标文件的反汇编内容中,因外部函数符号未解析而地址尚不明确的函数调用(表现为 0 地址 / 占位地址),在链接生成可执行文件 target 后,其对应的反汇编文件 target.s 中,这些函数调用指令已指向最终的虚拟内存地址。这就是静态链接所要干的事情。

  1. 总结,静态链接其实就是干了两件事情:一是将所有用到的函数指令都拷贝到可执行文件的代码段(.text 段)中以及段合并,二是为这些函数重新分配唯一的虚拟内存地址(进行统一编址),最后把原本目标函数调用处的 0 占位地址,替换成重新分配好的最终地址。这也是为什么.o 文件被称作可重定位目标文件,因为在静态链接过程中,它内部的函数 / 数据地址需要经过链接器处理后重新分配,才能确定最终的虚拟内存地址。
  2. 静态库没有加载的过程,也就是不用加载进入内存,因为它在链接的时候代码就已经被加载进入可执行文件里面了。

三、动态链接的链接过程

3.1 动态库的加载和映射

  1. 进程创建的时候,是会先创建PCB然后再加载程序;那么动态链接的程序,是先加载程序还是先加载库呢?先加载库

  1. 动态库加载时会被映射到共享区,也就是栈区和堆区中间的那个区。动态库在物理内存里面只会存在一份,一份可以映射到多个进程虚拟地址空间的共享区,这也是为什么它叫做共享区的核心原因

  1. 但是OS它怎么知道这些库已经被加载到内存里面了?而且有这么多的库被加载到内存里面,OS要不要管理这些被加载进来的库呢?当然是要的。老规矩,先描述再组织,它也有对应的结构体对库进行管理。由于过于复杂且偏离主题,这里就不细讲这个结构体了。

3.2 如何进行库函数调用

  1. 动态库为了支持被随时加载并映射到任意进程的任意内存位置,会对库内的函数 / 数据进行统一编址,也就是以 0 为基址向后编址,确保动态库加载到任意内存地址时都能正常运行。
  2. 这样只要知道了库文件在内存里面的起始位置,再根据文件内部各个函数 / 数据的偏移量就能够找到对应的函数进行调用。也就是:库的起始地址 + 方法偏移量 = 函数在内存中的实际调用地址。所有库的起始地址OS通过管理库的结构体就都能知道,而动态库的偏移量就储存在库的符号表中。
  3. 而且整个调用过程,是从代码区跳到共享区,调用完毕再回到代码区,整个过程都是在进程虚拟地址空间进行的。
  4. 静态链接会在链接阶段就完成函数调用地址的重定位,而动态链接则是将这一地址重定位过程推迟到程序加载进入内存后(运行时)才执行
  5. 但此时新的问题出现了:程序的代码段会被操作系统映射到内存的只读代码区,代码区不允许修改,那么我们该如何动态修改函数调用对应的目标地址(比如动态库函数的真实地址)呢?这就要谈到全局偏移量表GOT了。

3.3 全局偏移量表GOT

  1. 动态链接在数据段.data(可读写)专门预留了一块空间,用来存储函数的跳转地址,它也被叫做全局偏移量表GOT,表中每一项都存放的是要用到的全局变量或动态库函数的最终内存地址
  2. 所以call后面跟着的地址是 got 初始地址 + got 表中的偏移地址,说明调用的时候会首先查 got 表找到对应函数,而 got 表里面存储的就是对应函数的最终内存地址,它由库的起始地址 + 方法偏移量 = 函数在内存中的实际调用地址而来,然后根据这个最终地址进行跳转,到共享区完成对函数的调用,之后再回到代码区。
  3. 这种方式实现的动态链接对应的就是 PIC(位置无关代码):借助相对编址 + GOT 表的组合机制,动态库无需做任何修改,即可被加载到任意内存地址并正常运行,同时还能被多个进程共享。这也是我们编译动态库时,需要给编译器指定 -fPIC 参数的核心原因 ——PIC 的实现本质就是 “相对编址(库内偏移)+ GOT 表(动态绑定地址)”。
  4. 动态链接由动态链接器在程序加载至内存时完成,它会扫描程序所依赖的动态库,依据系统指定的库搜索路径定位并加载这些动态库到内存中。

今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!

http://www.cnnetsun.cn/news/1255267.html

相关文章:

  • 汇川H5U PLC 程序框架:全面解析与代码示例
  • 基于改进K - means算法的含电动汽车负荷源荷场景聚类:MATLAB代码实现之旅
  • 探索Qt + OpenCV视觉通用框架:从原理到代码实践
  • 反爬虫大师的网络爬取API
  • 跨平台设计协作:使用Typora与万象熔炉·丹青幻境撰写图文技术博客
  • ARM64 多级页表映射机制与Linux内核实现剖析
  • Step3-VL-10B-Base模型API安全设计:防范常见网络攻击
  • AI龙虾:傅盛的救星,还是猎户星空的幻影?
  • 【PHP 8.9类型系统终极前瞻】:20年核心贡献者独家解密RFC草案未公开的5大类型安全增强机制
  • GLM-OCR模型在AIGC内容审核流程中的集成实践
  • Jetson Nano开发实战:eMMC存储与SDK Manager系统烧录全解析
  • Qt进度条实战:从QProgressBar到QProgressDialog的进阶应用
  • 从下载到对话:通义千问2.5-7B完整部署流程详解
  • 基于Mathcad的单相逆变器PI控制器参数设计与频域验证
  • 基于N32G430的便携式宽压可调DC电源设计
  • 基于通义千问1.5-1.8B的智能客服机器人:MySQL数据库对话日志分析与优化
  • 存储技术实践笔记3_深入NVMe磁盘操作(从用户态ioctl到内核态bio的完整路径解析)
  • 百川2-13B在边缘计算场景的探讨:模型轻量化与内网穿透部署
  • Z-Image-Turbo-辉夜巫女运维指南:使用Shell脚本实现模型服务的自动监控与重启
  • Python3.11镜像效果展示:独立环境管理,轻松复现实验结果
  • GESP备考 | 2024年06月1级-编程题2《立方数判定》实战解析(C++实现)
  • Qwen3-Embedding-4B效果可视化:余弦相似度分数保留4位小数的设计意义与浮点精度验证
  • ESP32 SDIO从机与SDHOST主机寄存器级驱动开发详解
  • DAMOYOLO-S模型服务化:使用Docker容器化与Kubernetes进行集群部署
  • 深入解析FOC:从电机电磁原理到SVPWM实现
  • PP-DocLayoutV3性能调优:降低响应延迟与提升吞吐量实践
  • 【密码学实战】从“与门”到“神经网络”:混淆电路如何守护隐私推理?
  • MogFace人脸检测模型WebUI项目重构:优化Java八股文中的设计模式应用
  • Protel 99 SE元件库创建与导入Sch文件报错解决方案全攻略
  • Qwen2.5-VL-7B-Instruct快速上手:微信小程序对接WebUI实现移动端图文问答