动静态链接与加载

news2025/2/23 16:50:45

目录

静态链接

ELF加载与进程地址空间(静态链接)

动态链接与动态库加载

GOT表


静态链接

对于多个.o文件在没有链接之前互相是不知到对方存在的,也就是说这个.o文件中调用函数的的跳转地址都会被设定为0(当然这个函数是在其他.o文件中定义的)这个地址会在哪个时候被修正?链接的时候!为了让链接器将来在链接时能够正确定位到这些被修正 的地址,在代码块(.data)中还存在⼀个重定位表,这张表将来在链接的时候,就会根据表⾥记录的地址将其修正。这也就是为什么.o文件叫做可重定位文件。

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

ELF加载与进程地址空间(静态链接)

从上面的连接过程可以看到,在我们链接完成的之后形成的可执行程序中是有地址的,这个时候程序显然没有加载到内存中那这个地址就不可能是内存中的物理地址。事实上这个地址是一种逻辑地址,其思想与虚拟地址类似,也与虚拟地址对应,也就是说磁盘上的逻辑地址就是以后运行可执行程序时的虚拟地址。在当代计算机内部,这个逻辑地址采用平坦模式进行编址(也就是从0开始编址)。所以也要求ELF文件对自己的代码和数据进行统一编址。

简直巧妙,原来虚拟地址跟磁盘中可执行文件的逻辑地址是对应的。我们知道可执行程序的执行需要os创建子进程来执行,那么mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪⾥来?就从逻辑地址来。从ELF各个segment来,每个segment有⾃⼰的起始地址和⾃⼰的⻓度,⽤来初始化内核结构中的[start, end] 等范围数据,另外再⽤详细地址,填充⻚表。

mm_struct  描述进程的整个虚拟地址空间,包含所有 vm_area_struct 的链表或红黑树。例如:

struct mm_struct {
    struct vm_area_struct *mmap;  // 虚拟内存区域链表
    unsigned long start_code;      // 代码段起始地址(ELF的 .text)
    unsigned long end_code;
    unsigned long start_data;     // 数据段起始地址(ELF的 .data)
    unsigned long end_data;
    // ...
};

vm_area_struct   描述一个连续的虚拟内存区域(如一个ELF段),包括权限、文件映射信息等。

struct vm_area_struct {
    unsigned long vm_start;        // 起始虚拟地址(ELF的 p_vaddr)
    unsigned long vm_end;          // 结束虚拟地址
    struct file *vm_file;          // 关联的ELF文件
    unsigned long vm_pgoff;        // 文件中的偏移(对应ELF段在文件中的位置)
    pgprot_t vm_page_prot;         // 访问权限(如可读、可执行)
    // ...
};

示例:ELF加载到虚拟地址空间

假设一个ELF文件有两个可加载段:

  1. 代码段:.textp_vaddr = 0x400000p_memsz = 0x1000

  2. 数据段:.datap_vaddr = 0x401000p_memsz = 0x2000

进程创建时,内核会:

  1. 创建两个 vm_area_struct

    • 代码段:vm_start=0x400000vm_end=0x401000, 权限为 RX(读+执行)。

    • 数据段:vm_start=0x401000vm_end=0x403000, 权限为 RW(读+写)。

  2. 通过 mmap 将这两个段映射到虚拟地址空间,但物理内存尚未分配。

  3. 程序先加载到内存,用虚拟地址初始化了mm_struct,当进程首次执行 0x400000 处的指令时,触发缺页中断,内核将 .text 段的内容从磁盘加载到物理内存,并更新页表。

问题是cpu怎么知道从哪里开始执行呢?ELF文件的LEF Header中有一个Entry point address 这个就是程序的入口地址。cpu中有一个寄存器EIP其中存放的是当前执行指令的下一条指令的地址,CR3寄存器执行页表。所以当程序开始执行的就时候就将Entry point address中的地址load到cpu中的EIP寄存器中,然后程序从入口开始执行。

动态链接与动态库加载

我们知道动态库跟我们编译链接好的可执行和程序之间是独立的存在于磁盘的。

我们的所有依赖于动态库的可执行文件都依赖于一个这个库:/lib64/ld-linux-x86-64.so.2,lib64/ld-linux-x86-64.so.2 是 Linux 系统中的一个动态链接器库文件,主要用于在程序运行时动态加载和链接共享库(.so 文件)

在我们要运行可执行程序时,我们先是跟静态库一样的过程,先通过Entry point address找到程序的入口,事实上程序的入口就是_start函数,这是一个由C运⾏时库(通常是glibc)或链接器(如ld)提供的特殊函数。在_start函数中会执行一下一系列操作:

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

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

3.动态链接:_start函数会调用动态链接器的代码来解析和加载程序运行所需要的动态库,动态连接器会处理所有的符号解析和重定位,确保程序中的调用函数和变量访问能够正确的映射到动态库中的实际地址。(动态链接实际上将链接的整个过程推迟到了程序加载的时候

动态连接的优点:可以看到对于不同的进程如果需要同一个库中的函数,我们只需要在内存中加载一份动态库,分配一份物理地址即可,但是对于静态库来说,其可执行文件就是已经包含静态库中的函数的了,所以其磁盘空间和内存空间都是会产生浪费的。

动态链接器                                                                                                                                      动态链接器(如ld-linux.so)负责在程序运⾏时加载动态库

当程序启动时,动态链接器会解析程序中的动态库依赖,并加载这些库到内存中。
环境变量和配置⽂件
Linux系统通过 环境变量(如LD_LIBRARY_PATH) 配置⽂件(如/etc/ld.so.conf 及其⼦配置
⽂件)来指定动态库的搜索路径。 这些路径会被动态链接器在加载动态库时搜索。
缓存文件
为了提⾼动态库的加载效率,Linux系统会维护⼀个名为 /etc/ld.so.cache的缓存⽂件 。 该⽂件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会⾸先 搜索这个缓存⽂件。
4.调⽤ __libc_start_main :⼀旦动态链接完成, _start 函数会调⽤ __libc_start_main (这是glibc提供的⼀个函数)。 __libc_start_main 函数负责执⾏ ⼀些额外的初始化⼯作,⽐如设置信号处理函数、初始化线程库(如果使⽤了线程)等。
5. 调⽤ main 函数:最后, __libc_start_main 函数会调⽤程序的 main 函数,此时程序的执⾏控制权才正式交给⽤⼾编写的代码。
6. 处理 main 函数的返回值:当 main 函数返回时, __libc_start_main 会负责处理这个返回
值,并最终调⽤ _exit 函数来终⽌程序。
上述过程描述了C/C++程序在 main 函数之前执⾏的⼀系列操作,但这些操作对于⼤多数程序员来说 是透明的。程序员通常只需要关注 main 函数中的代码,⽽不需要关⼼底层的初始化过程。然⽽,了解这些底层细节有助于更好地理解程序的执⾏流程和调试问题。

但是我们的程序具体是怎么和库映射起来的?

首先可执行程序中存有依赖的动态库的路径,通过这个路径可以将动态库加载到物理内存。动态库也是采用了平坦模式进行编址,我们叫做库中方法的偏移量。然后通过创建新的mm_area_struct用库的大小开辟一段新的进程地址空间,就能得到库的虚拟地址,并建立页表映射关系。通过库的虚拟地址和库中的偏移量就能找到对应的方法。


 所以库函数的调用机制如下:                                                                                                         库已经被我们映射到了当前进程的地址空间中 库的虚拟起始地址我们也已经知道了,库中每⼀个   ⽅法的偏移量地址我们也知道

 所有:访问库中任意⽅法,只需要知道库的起始虚拟地址+⽅法偏移量即可定位库中的⽅法

 ⽽且:整个调⽤过程,是从 代码区跳转到共享区,调⽤完毕在返回到代码区 ,整个过程完全在进   程地址空间中进⾏的。

GOT表

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

那GOT具体是怎么工作的呢?     比如,程序在编译时,对于外部函数比如printf,编译器并不知道它运行时的具体地址,所以会在GOT中生成一个条目。当程序第一次调用printf时,动态链接器(如ld-linux.so)会找到printf的实际地址并填入GOT中,之后的调用就直接使用这个地址了。这样可以实现延迟绑定,也就是PLT(Procedure Linkage Table)和GOT配合使用。PLT负责跳转到GOT中的地址,而GOT存储实际的地址。第一次调用时,GOT中的地址可能指向PLT中的解析代码,由动态链接器完成地址解析后,GOT中的条目会被更新为正确的地址。另外,GOT还可能用于全局变量的访问,因为动态库中的全局变量地址在加载时确定,也需要通过GOT来间接访问。

但在不 同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到GOT表上 ,就是每个进程的 每个动态库都有独⽴的GOT表 ,所以进程间不能共享GOT表。
在单个.so下,由于GOT表与 .text 的相对位置是固定的,我们完全可以利⽤CPU的相对寻址来找
到GOT表。
在调⽤函数的时候会⾸先查表,然后根据表中的地址来进⾏跳转,这些地址在动态库加载的时候会
被修改为真正的地址。
这种⽅式实现的动态链接就被叫做 PIC 地址⽆关代码 。换句话说,我们的动态库不需要做任何修
改,被加载到任意内存地址都能够正常运⾏,并且能够被所有进程共享,这也是为什么之前我们给
编译器指定-fPIC参数的原因,PIC=相对编址+GOT。
总结: GOT表中存储的地址应该是虚拟地址。当程序执行跳转指令时,使用这个虚拟地址,然后由MMU通过页表将其转换为物理地址,从而访问实际的内存位置其他时候都是直接通过页表维持虚拟地址跟物理地址之间的关系。

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

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

相关文章

83_CentOS7通过yum无法安装软件问题解决方案

大家好,我是袁庭新。很多小伙伴在CentOS 7中使用yum命令安装软件时,出现无法安装成功的问题,今天给大家分享一套解决方案~ 在CentOS 7中,yum是一个常用的包管理工具,它基于RPM包管理系统。如果你发现yum无法使用,可能是由于多种原因造成的。以下是一些解决步骤,可以帮…

数据包在客户端和服务端,以及网络设备间如何传输的?

声明:文章中图片来自于网络收集,整体流程自己梳理。 目录 问题:如下socket客户端请求数据包如何传输的? 拓扑环境 数据包在分层间传输 网络分层L2/L3/L4 数据包收发-在各分层间变化 各层头部中-核心信息 数据包在不同设备…

用Python实现Excel数据同步到飞书文档

目录 一、整体目标 二、代码结构拆解 三、核心逻辑讲解(重点) 1. 建立安全连接(获取access_token) 2. 定位文档位置 3. 数据包装与投递 四、异常处理机制 五、函数讲解 get_access_token() 关键概念解释 1. 飞书API访问…

25林业研究生复试面试问题汇总 林业专业知识问题很全! 林业复试全流程攻略 林业考研复试真题汇总

25 林业考研复试,专业面试咋准备?学姐来支招! 宝子们,一提到林业考研复试面试,是不是就慌得不行,感觉老师会扔出一堆超难的问题?别怕别怕,其实林业考研复试就那么些套路,…

vxe-table实现动态列

vxe-table实现动态列 1.动态列解释2.解决步骤2.1将后端返回的动态列表头,按照格式拼接在固定列表头上2.2将后端返回的列表数据按照键值对格式组装 1.动态列解释 正常列表是有固定的列;我的需求是,最初只知道表格的固定两列,查询数…

尚硅谷爬虫note009

一、jsonpath 1.安装 pip install jsonpath 2.使用 只能解析本地文件 .json文件 {"store": {"book": [{"category": "reference","author": "Nigel Rees","title": "Sayings of the Century&qu…

verilog笔记

Verilog学习笔记(一)入门和基础语法BY电棍233 由于某些不可抗拒的因素和各种的特殊原因,主要是因为我是微电子专业的,我需要去学习一门名为verilog的硬件解释语言,由于我是在某西部地区的神秘大学上学,这所…

Java+SpringBoot+Vue+数据可视化的综合健身管理平台(程序+论文+讲解+安装+调试+售后)

感兴趣的可以先收藏起来,还有大家在毕设选题,项目以及论文编写等相关问题都可以给我留言咨询,我会一一回复,希望帮助更多的人。 系统介绍 在当今社会,随着人们生活水平的不断提高和健康意识的日益增强,健…

python绘制年平均海表温度、盐度、ph分布图

python绘制年平均海表温度、盐度、ph图 文章目录 python绘制年平均海表温度、盐度、ph分布图前言一、数据准备二、代码编写2.1. python绘制年平均海表温度(主要)2.2. python绘制年平均海表盐度(选看)2.3. python绘制年平均海表ph&…

网络空间安全(2)应用程序安全

前言 应用程序安全(Application Security,简称AppSec)是一个综合性的概念,它涵盖了应用程序从开发到部署,再到后续维护的整个过程中的安全措施。 一、定义与重要性 定义:应用程序安全是指识别和修复应用程序…

全链路优化:如何让单点登录认证接口并发性能翻倍?

背景 最近针对一个单点登录认证项目进行性能优化,在 8核 16G 环境下的认证并发能力从每秒800次提升至每秒1600次,性能提升一倍,整理此次优化过程中的相关性能优化操作总结和大家分享一下。 Nginx配置优化 在并发认证场景下,Ngi…

http代理IP怎么实现?如何解决代理IP访问不了问题?

HTTP代理是一种网络服务,它充当客户端和目标服务器之间的中介。当客户端发送请求时,请求首先发送到代理服务器,然后由代理服务器转发到目标服务器。同样,目标服务器的响应也会先发送到代理服务器,再由代理服务器返回给…

USC安防平台之地图临近资源列表

USC安防平台通过配置多层地图,并把相关的摄像机和门禁对象配置到数据上,用户可以方便的在地图上查看并操作。 但是对于大型的视频监控项目,同一个经纬度可能安装了很多台摄像机,这时候就需要显示同一个经纬度的临近资源列表&…

JavaScript数组方法reduce详解

JavaScript数组方法reduce详解 目录 JavaScript数组方法reduce详解一,前言二,核心语法三,案例1.求和2.找最大值3.数组转对象4.复合操作(同时实现 map filter) 四,常见错误1.空数组没有初始值2.没有返回累加…

计算机毕业设计SpringBoot+Vue.js服装商城 服装购物系统(源码+LW文档+PPT+讲解+开题报告)

温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…

Web自动化中Selenium下Chrome与Edge的Webdriver常用Options参数

目录 引言 说明 Add_argument() 添加方式 常用参数 Add_experimental_option() 添加方式 常用方法 任务结束后仍然保持浏览器打开 禁用“Chrome 正受到自动测试软件的控制”提示 设置下载路径 禁用弹窗拦截 禁用图片加载 禁用 JavaScript 注意 引言 …

现代未来派品牌海报徽标设计无衬线英文字体安装包 THANKS LAB

THANK LAB 是一种高级未来主义的软字体,将时尚的现代设计与光滑圆润的边缘相结合,营造出大胆而平易近人的美感。这款字体非常适合品牌、海报、标题、UI/UX 和科幻主题项目,旨在激发创造力。THANK LAB Futuristic Soft Font 完全支持拉丁字母、…

【算法通关村 Day6】二叉树层次遍历

树与层次遍历青铜挑战 理解树的结构 通过中序和后序遍历序列恢复二叉树是一个经典的二叉树构建问题。给定二叉树的中序遍历序列和后序遍历序列,我们可以利用以下步骤进行恢复。 思路: 后序遍历的特点: 后序遍历的最后一个节点是树的根节点…

安全面试2

文章目录 简单描述一下什么是水平越权,什么是垂直越权,我要发现这两类漏洞,那我代码审计要注意什么地方水平越权:垂直越权:水平越权漏洞的审计重点垂直越权漏洞的审计重点 解释一下ssrf漏洞原理攻击场景修复方法 横向移…

【JavaScript进阶】构造函数数据常用函数

目录 本章节用到的所有素材都可以找到:素材自取~~~~ 1、深入对象 1.1创建对象三种方式 1.2 构造函数 练习 利用构造函数创建多个对象 实例化执行过程 1.3实例成员&静态成员 2. 内置构造函数 2.1 Object 2.2 Array 练习 员工涨薪计算成本 2.3 St…