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

【动态库的加载】【进程地址空间(三)】

目录

  • 1. 宏观看待动态库的加载
  • 2. 进程地址空间第二讲
    • 2.1 程序没有加载前的地址
    • 2.2 程序加载后的地址
  • 3. 动态库的地址

再谈进程地址空间时,【Linux】动静态库 我们先讲一个关于动态库是如何加载的话题,再引入进程地址空间,再次谈论该话题。

1. 宏观看待动态库的加载

当一个可执行程序被运行时,操作系统要为其创建内核数据结构 PCB、进程地址空间 和 页表,然后再把该程序的代码和数据从外设加载到内存中。可执行程序也是一个文件,只要是文件,就有路径和 inode,那么就可以通过路径找到该文件所在分区,再根据 inode 编号找到所在分组。而在加载可执行程序之前,需要先找到该文件,就可以通过其路径和 inode 找到其文件属性 + 文件内容,然后把它们加载到内存。

换言之,动态库也是一个文件,也有路径和 inode。当我们的程序需要访问动态库的代码时,那么这个动态库也需要被加载到内存。但是进程之间是相互独立的,一个进程是无法直接访问另一个进程的代码和数据的!所以动态库经过页表映射到进程地址空间中的共享区,而我们自己的进程的代码是位于正文代码区的,当我们的代码想调用动态库的方法时,只需要调整到共享区,执行动态库的代码,执行完后原路返回即可。

也可以理解为,建立映射后,我们执行的任何代码,都是在我们的进程地址空间中执行的!

接着,我们需要建立一个共识,一个进程是可能加载多个动态库的,反之,一个动态库也可能跟多个进程产生关联。所以在系统运行中一定会存在多个动态库,系统中有许多进程、文件,因此它们都需要被管理起来,同理,动态库也需要被操作系统所管理起来,换言之,操作系统对所有动态库的加载情况是非常清楚的。所以如果后续有另一个进程想要访问相同的动态库,操作系统能够分辨该库是否已经被加载,如果已经被加载到内存了,只需要在该进程的页表中做一下映射关系,该进程就能够跳转执行该库的代码。

  • C 标准库 libc.so 中有一个全局变量 errno,如果有多个进程共同访问这个库,并且自己的程序运行时都出问题了,即 errno 被写入了。它们访问的是同一个库,那么多进程之间会互相影响吗??

    共享区是位于堆栈之间的一段空间,属于用户空间。所以只要是对共享数据做写入操作,都会导致进程发生写时拷贝!我们之前讲用户缓冲区时就有过一个案例,在调用各种 fprint、fwrite 之后 fork() 创建子进程,C 式接口的输出信息输出了两次,因为缓冲区刷新的本质也是写入,所以进程对缓冲区做了写时拷贝,而缓冲区也是位于共享区中的!


2. 进程地址空间第二讲

2.1 程序没有加载前的地址

  • 程序编译完成之后,运行之前,程序内部有地址的概念吗?

    程序内部有地址。在 vs 编译调试时,如果你转到汇编上,你就可以看到编译之后的每条代码,都是有地址的,包括函数名、变量名,在编译之后都是以地址的形式进行地址,调用一个函数,会转变为 call 一个函数的地址。再诸如 C++ 的继承多态中的虚函数表,在编译时就已经为虚函数表分配地址了,包括表内有各种派生类的方法的地址。

    并且,在编译时为各段代码分配的地址,该地址是有分段的,诸如进程地址空间中分成了代码区、未初始化、已初始化等等各种区域,这是为了方便后续加载代码设计的,也就是说编译器,也是要考虑操作系统的(编译器编译的程序是要被操作系统加载的,所以需要照顾该程序加载到内存的问题,即进程地址空间的分段)。而在编译时给程序分配的地址,是虚拟地址!只不过如果该程序还没有被加载,我们更多的称为 逻辑地址。

    objdump -S a.out
    

    在这里插入图片描述

2.2 程序加载后的地址

程序加载后,就变为进程,所以探讨程序加载后的地址,其实就是探讨进程的地址。

当一个程序加载到内存,它的代码和数据在内存中也一定要占据内存空间(不管是从语言代码的角度看待,还是汇编指令的角度),这个程序的每一条指令 / 代码都有相应的 物理地址 ,但是这与上述说的,程序在编译完后,还没被加载的时候就已经有地址这件事不冲突,加载到内存后,代码存储在物理内存上需要空间,因此它有对应的物理地址,但是这个地址不是代码数据内部的地址,只是它存储在物理内存的位置而已,该程序内部各条指令 / 代码还是采用的虚拟地址(就好比每个学生都有自己的学号,等到了考场上,每个考生都有一个座位号,但不影响你有自己的学号这件事,座位号自己记录你的位置,学号才是你这个人的代表)。

  • 当进程的 PCB、进程地址空间、页表等结构都创建完成之后,该进程如何执行第一条指令呢

    当一个程序被编译完成之后,程序内部的表头就已经存储了一个 entry 程序入口地址的信息,这个入口地址也是逻辑地址(因为编译后就有这个地址了,还没加载到内存,所以不可能是物理地址)。而 CPU 为了知道下一次执行哪一条指令,在 CPU 内部维护了一个 PC 寄存器,用于存储下一条执行的指令地址。而当一个程序被加载到内存时,其入口地址就已经被加载到 CPU 内部的 PC 寄存器中了。而因为在编译后,入口地址就是虚拟地址了,因此 CPU 可以直接访问这个虚拟地址,然后开始执行该程序。

    接着,顺着这个虚拟地址到页表中寻址映射的物理地址,如果此时发现该程序还没有建立物理地址的映射(即该程序还没有被加载到内存中),那么就发生缺页中断,等程序的代码和数据加载到内存中了,每条指令天然的就有了物理地址,加上程序内部自己的虚拟地址,就能够完成对页表的虚拟地址到物理地址的映射!

    顺着程序中的代码执行下去,当下一条指令遇到函数调用处,其代码会被解释为 call 一个地址,假设 call 400450,那么 CPU 读取到的指令就是地址,这个地址是代码编译完后形成的地址!因此 CPU 读取到的地址也是虚拟地址!然后 CPU 再顺着该虚拟地址到进程地址空间中寻址,接着通过页表映射到物理地址,再次访问物理地址。后续可能还有调用函数,CPU 还是读取到虚拟地址,然后再顺着虚拟地址映射到物理地址进行访存,这不就是一个环吗! 执行该程序一套操作下来,你应该要知道,CPU 读取到的地址,全部都是虚拟地址!

所以现在也就能够进一步理解,编译器再设计的时候,就已经考虑到进程地址空间了,编译后形成程序内部的地址,即虚拟地址,等程序变为进程时,内部的地址直接套用即可,这也是编译器与操作系统互相协同最重要的表现之一。


3. 动态库的地址

  • 绝对地址:进程地址空间中规定的 0x0000 0000 ~ 0xFFFF FFFF 这样的地址,称为绝对地址
  • 相对地址:比如 int a 变量在虚拟地址中的地址为 0x11223344,int b 变量在距离 a 变量地址之后的 4 字节,即0x11223348,这样的描述称为相对地址。

现假设可执行程序 a.out 中调用了 libc.so 库的 printf 方法,所以当可执行程序运行起来,加载到内存之后,执行到调用处,在虚拟地址映射物理地址时,发现动态库还没有被加载到内存中,于是缺页中断,等动态库加载完成并且在页表建立中建立了映射关系之后,进而根据 cpu 读取到的 printf 的地址(虚拟地址 0x11223344)跳转到共享区执行库的方法。

  • 现在的问题是:动态库要映射到共享区的哪个地方呢??

    当我们的程序编译完成后,假如给动态库 libc.so 分配了 0x11223344 这样的地址,即地址已经被硬编码到程序内部了,所以当程序调用 printf 时,只能映射到虚拟地址中的 0x11223344,换言之,在虚拟地址中,动态库就必须被加载到 0x11223344 的位置处,如果不加载到这个位置处,将来程序跳转时,就找不到动态库。但是当系统加载了多个库时,凭什么保证 0x11223344 处一定加载的是 libc.so 这个动态库,可能还有其它库也同时被加载到内存呢??换言之,操作系统可无法保证哪个库一定被加载到固定的地方。所以在虚拟内存中,库的加载是任意位置的。

    所以在编译时,会让库函数不采用绝对编址,只采用偏移量表示每个函数在库中的偏移位置,进而找到对应的库函数。所以当动态库被任意的加载到虚拟地址的某一处时,操作系统只需要记住这个库的起始地址 start 即可, 当程序执行时调用库函数,程序即可根据 start + 编译时库函数形成的偏移量,即可跳转到对应库的对应方法处!

    讲 【Linux】动静态库 时,对如何编译形成动态库,当时有一个选项:-fPIC 产生与位置无关码,产生与位置无关码的意思就是告诉编译器,不要再采用绝对编制了,直接用偏移量对库函数进行编制!

  • 静态库为什么不谈加载,不谈与位置无关这些概念?

    现在我们就对动静态库的区别的理解更进一步了,为什么只有动态库有加载的概念,因为静态库是拷贝策略,自己的程序调用了库方法,用什么我拷什么即可,需要的库方法已经成为我的可执行程序的一部分了,所以没有静态库需要加载的说法。而当我们的程序被加载到内存时,在程序内部,任何指令可是都已经硬编址好的了,方法都已经拷贝到我自己的程序内部了,哪一条指令加载到虚拟地址的哪一地址处,都是清清楚楚的,还需要用什么偏移量?!还需要怕找不到库的问题吗?!(库方法就在你程序里面)


如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!

感谢各位观看!

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • iPhone16,超先进摄像头系统?丝滑的相机控制
  • Vue 依赖注入组件通信:provide / inject 使用详解
  • 链动3+1滑落模式小程序开发
  • 7、论等保的必要性
  • Linux之实战命令03:stat应用实例(三十七)
  • 分治策略:从入门到精通,10分钟带你玩转算法!
  • 软件测试 BUG 篇
  • INDEX函数和MATCH函数知识讲解与案例演示
  • Linux、Windows、Android下查看可执行文件、动态库和静态库信息的命令
  • 997. 找到小镇的法官(24.9.22)
  • docker 镜像,导入导出,
  • Springboot常见问题(bean找不到)
  • 分享课程:云LAN到家视频教程
  • WebServer
  • 系统架构笔记-4-信息安全技术基础知识
  • [case10]使用RSQL实现端到端的动态查询
  • [NodeJS] 关于Buffer
  • 【翻译】Mashape是如何管理15000个API和微服务的(三)
  • 【每日笔记】【Go学习笔记】2019-01-10 codis proxy处理流程
  • 2017-08-04 前端日报
  • crontab执行失败的多种原因
  • IDEA常用插件整理
  • Java IO学习笔记一
  • MySQL的数据类型
  • PHP变量
  • ReactNativeweexDeviceOne对比
  • React组件设计模式(一)
  • Spring Cloud中负载均衡器概览
  • springMvc学习笔记(2)
  • V4L2视频输入框架概述
  • vagrant 添加本地 box 安装 laravel homestead
  • vue总结
  • 前端面试题总结
  • 如何优雅地使用 Sublime Text
  • 微信如何实现自动跳转到用其他浏览器打开指定页面下载APP
  • 正则表达式-基础知识Review
  • # 利刃出鞘_Tomcat 核心原理解析(七)
  • #07【面试问题整理】嵌入式软件工程师
  • #数学建模# 线性规划问题的Matlab求解
  • (06)金属布线——为半导体注入生命的连接
  • (33)STM32——485实验笔记
  • (52)只出现一次的数字III
  • (论文阅读31/100)Stacked hourglass networks for human pose estimation
  • (三分钟)速览传统边缘检测算子
  • (十)【Jmeter】线程(Threads(Users))之jp@gc - Stepping Thread Group (deprecated)
  • (四)js前端开发中设计模式之工厂方法模式
  • (四)TensorRT | 基于 GPU 端的 Python 推理
  • (一)spring cloud微服务分布式云架构 - Spring Cloud简介
  • (原创) cocos2dx使用Curl连接网络(客户端)
  • (转)Android学习系列(31)--App自动化之使用Ant编译项目多渠道打包
  • (转)甲方乙方——赵民谈找工作
  • ***linux下安装xampp,XAMPP目录结构(阿里云安装xampp)
  • **《Linux/Unix系统编程手册》读书笔记24章**
  • .net 获取url的方法
  • .net(C#)中String.Format如何使用