linux基础IO——动静态库——进程编址、进程执行、动态库加载

2024-09-07 21:04

本文主要是介绍linux基础IO——动静态库——进程编址、进程执行、动态库加载,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

        前言:本节内容为基础IO部分的最后一节, 主要是为了讲一下动静态库里面的动态库如何加载到内存, 动态库的地址等等。 但是,这些内容牵扯到了程序的编址, 程序的加载, 进程的执行等等知识点, 所以,我们会从程序的编址讲起, 一直到进程的执行, 以及动态库加载结束。

        ps:本节内容涉及到了进程地址空间, 磁盘的内容, 建议友友们了解相关知识后再来观看。

目录

程序的编址

cpu为什么可以执行指令

程序加载后的地址

理解物理地址和逻辑地址(也叫做虚拟地址)

cpu执行进程命令流程

动态库的加载


程序的编址

        在谈程序的编址之前, 我们要谈一个事情, 就是程序里面有没有地址呢?

        答案是有的, 当我们编译形成了程序, 比如1.exe, 2.exe, 这个时候, 程序内部其实已经生成了地址了

  •         就比如我们调试的时候能够进入反汇编, 进入反汇编之后, 我们就会发现, 我们的每一行代码都是有地址的, 比如调用一个函数, call后面就会跟一个函数名。 而实际上呢? 我们编译之后, 无论是我们的函数名, 还是我们的变量名, 实际上都没有了, 都会被转化为地址。 所以, 我们对应的一个可执行程序在编译完成之后有没有地址呢? 答案是有的!!!
  •         又比如我们我们回忆一下c++里面的多态, 还记不记得多态里面有一张虚函数表, 程序呢在编译的时候就已经形成了这张虚函数表, 里面填充的是每一个子类具体的方法实现。

        什么意思呢? 就比如我们看下图, 就比如我们有下图的一串伪代码。 然后呢, 当我们编译的时候, 编译到main函数里面的func(), 也就是0x5地址的时候, 就将我们的func()编译成了0x1(因为func()在0x1处)——这就是当我们的代码在编译的时候就已经编好址了, 有了地址之后就不要函数名了。

        当我们程序还没有加载的时候, 我们的程序是有地址的, 在很早之前,我们的地址是采用短地址 + 偏移量的方式。 现在我们的程序内部的编制方式已成变成了平坦模式——就是从0 ~ 4GB访问方式而且这个平坦模式就是按照我们的进程地址空间的规则, 把我们的代码进行编址, 也就是说,这个平坦模式编制规则下生成的程序的内部的编址, 其实就是加载到内存后, 进程建立好映射关系后的虚拟地址!!!

那么, 既然磁盘中程序内部的编址和我们在虚拟地址空间的一样, 所以我们下面的地址(右边的才是真正的内部的地址,也可以叫做逻辑地址!! 左边的是伪汇编), 其实就已经可以叫做虚拟地址了!!!


        

现在我们打开文件, 在文件中写入下面的代码:

我们生成可执行程序a.out:

然后将我们的程序反汇编出来,如同下图:

        我们可以看到, 左边的这个红框框是什么呢? 这个东西, 其实就是每一行所对应的地址

        我们同样可以看到, 这些地址, 每两个之间的偏移量大小是不同的。 有可能偏移一个, 有可能偏移两个, 这个是为什么呢?

        那么这个主要是因为这些指令每个占用的二进制大小不同, 有的占用大一些, 有的占用小一些。       

        未来呢, 我们的这些左边的地址可以不用存在, 但是指令是有长度的, 而且像是函数调用的汇编代码还会有地址在指令中。 

        就比如这串指令, 也就是说, 我们的左边的地址可以不要。 只需要有我们的地址, 有第一条指令, 就可以依次向下执行了

cpu为什么可以执行指令

那么cpu是怎么知道要去执行这些指令呢? ——这就比如一个刚出生的婴儿, 变成了一个小孩, 这个小孩一开始可能不会说长句子, 只认识爸爸妈妈爷爷奶奶这样的词语, 这个小孩呢, 只需要知道一些简单的词语。 它的老爹呢, 就会和他说, 玩具拿起来,这个小孩听到“玩具“、”拿起来”两个单词, 就会执行相应的动作。——对应的呢, 这里面的每一个汇编语句都是一个操作, 一个小孩能知道老爹老妈让自己去干什么, 从根本上是老爸教过自己“玩具” “拿起来”这些词语。 而这里的小孩就是cpu, 这些指令就是“玩具“”拿起来”这样的单词。 cpu能认识这些指令, 就是因为cpu提前内置了能够认识这些指令的东西。 ——只不过可能不是指令本身, 可能是二进制, 比如0001, 00002, 0003等等cpu呢, 只认识一些基本指令, 经过组合这些指令, 就能组合成各种各样的短语汇编, 短语汇编一多, 一长, 就能组合成各种计算的任务!!!

程序加载后的地址

理解物理地址和逻辑地址(也叫做虚拟地址)

        看上面这个图片, 我们知道, 磁盘程序内部是有地址的程序加载到物理内存是一个块一个块的加载的。 而这些块加载到内存又是有自己的物理地址的。 ——也就是说, 这些指令, 要不要占据内存空间呢?——答案是的, 因为加载到内存的就是这些指令的二进制!(而我们知道, 这些指令在整个的程序的内部也是有属于自己的逻辑地址。)

        什么意思呢?就比如一个学生, 我们在学校里有自己对应的学号, 比如说3号。 而到了教室呢, 教室里有我们对应的桌子, 比如说是第一排第二列的位置。 那么这个学生, 他是不是就天然有了两个地址, 一个是自己在学校的地址, 这个地址是3号, 另一个是在教室的地址, 这个地址是第一排第二列。 那么对应的, 我们编译好的指令呢?就比如上面的call指令, 这个call指令在磁盘里的时候有自己的一个地址, 也就是自己的逻辑地址。 就对应着我们的学生的学号!!!这个call指令加载到内存的时候有自己的一个另一个地址, 也就是自己的物理地址。 就对应着我们学生在教室的位置!!

        那么友友们就可能会疑惑, 怎么一会儿这个地址, 一会儿那个地址? ——是的, 就是一会儿这个地址, 一会儿那个地址。 还是那个学生, 当那个学生在教室外边的时候, 他是处在学校中的, 它拥有的是自己的学号。 是因为这个学号, 让他成为了学校的医院, 当这个学生去了教室上课, 做到了第一排第二列的桌子上的时候, 这个桌子就成了他的编号。 老师可能不认识他, 管他叫张三还是李四, 但是老师一定知道他的桌子在哪个位置。而张三从教室外做到教室内的这个过程, 就是加载的过程!!!对应的就是我们指令从磁盘加载到物理内存的过程!!!

cpu执行进程命令流程

现在看下面这张图:

这张图中有cpu, 有进程, 有地址空间, 有页表, 有物理内存, 有磁盘。 我们知道磁盘程序要映射到内存, 然后进程地址空间要和物理空间进行映射。整个的连接后的图我们也知道,但是我们不知道的是这里面的细节, 就比如是先程序加载到内存中呢?还是先地址空间映射到页表呢?还是先物理内存映射到页表呢?——而现在我们就要说一下这个这流程了:

        ps:图中文字可以看一下, 但是顺序可能会有问题, 所以友友们可以先看下面的文字, 再回过头看一下图中的文字, 是一模一样的。

  1.         其实我们的可执行程序在编译的时候, 除了形成本身代码的各种指令, 还会生成一个入口地址entry, 并且这个可执行程序的入口地址所在的位置是这个可执行程序的表头。 
  2.         那么这个entry的入口地址, 是不是物理地址呢? 答案是不是的, 很显然不是!!!因为这个entry一开始是在可执行程序里内部的,是磁盘里面的!!!而可执行程序根本没有物理地址的概念!!!我们的物理地址是什么? 物理地址就是加载程序的时候的一个随机的地址, 磁盘在加载内存之前根本看不到物理内存, 它怎么保存一个物理地址呢?
  3.         那么, 我们的entry不是物理地址之后, 那么很显然, entry是逻辑地址!又因为我们的cpu中有一个寄存器, 叫做EIP/PC指针,这个寄存器保存的就是我们下一步的地址, 有了这个, 我们的cpu就能知道他下一步要做什么。 
  4.         而要让cpu将进程执行起来, 那么第一步就是将entry加载到我们的EIP寄存器中, 然后找到对应的虚拟内存。        
  5.         然后, 虚拟内存又通过页表找到物理内存, 那么问题来了, 我们知道, 我们把程序的入口喂给了cpu,cpu就去执行指令了,然后虚拟地址映射到了页表, 也就是现在这个时刻。 而此时页表并没有映射物理内存啊, 那么是不是就势必会发生缺页中断?那么发生了缺页中断后, 我们的程序就可以加载到物理内存中了!!而一旦加载到了物理内存中, 那么程序天然就具备了物理地址!!所以, 加载进来后, 我们的页表就有了左侧的虚拟地址, 以及右侧的物理地址。 
  6.          而且我们又知道, 程序加载进入内存一定会以4kb的块为一个单位, 那么这4kb里面就有这许多的代码指令, 其实每个代码都有自己的相对于entry的偏移量——其实就有逻辑地址, 而这个逻辑地址在程序内部已经准备好了, cpu可以直接拿来使用。并且我们的块一旦加载到物理内存, 就天然的具备了物理地址。 而这两个虚拟, 物理地址一填。 此时, 页表的映射关系就会创建好了!!!

以上, 就是cpu执行进程的整个流程!!! 

        当我们cpu执行指令的时候, 执行到了函数指令。 ——cpu内部读指令, 内部可能有数据, 也可能有地址。 ——这个地址是什么呢?——虚拟地址(本质上就是逻辑地址, 但是叫法变了, 因为程序加载到内存后, 就没有逻辑地址的概念了, 这个逻辑地址是磁盘里面的概念, 进程里叫虚拟地址)

        那么此时我们说执行到了这个函数指令, 怎么办呢?——其实没有关系, 只要去执行这个地址处的指令即可, 也就是说, 如果call了一个0x1111地址, 那么就在地址空间去找到0x1111处的地址, 再通过页表映射我们的物理地址去执行就可以了。——如果没有映射关系怎么办呢?——那就发生缺页中断。 

        那么这里我们这里就可以下一个结论——就是从我们程序打开开始执行, 到一步步执行指令, 一直到这个程序执行完毕。 中间的过程, 用到的指令, 除了页表映射的块地址, 其他的全部为虚拟地址!!!

        我们的cpu在这个过程中, 只接收到我们块中的虚拟地址, cpu拿到虚拟地址, 但是cpu想要真正的指令, 这个虚拟地址再怎么厉害也只是指令之间的虚拟地址, cpu想要找到这些指令还是需要物理地址, 然后cpu就会转化虚拟地址为物理地址——通过页表和mmu这样的硬件。——然后就找到了我们对应物理地址处的正确的指令, 然后cpu再找到虚拟地址, 执行下一跳指令, 就完成了一个循环的过程!!!

动态库的加载

        我们再来画一下上面的图, 不同的是, 这一次要新添一个新的文件——libc.so, 这个libc.so是一个动态库文件。 

        假如这个代码段里有一个函数 ——printf, 这个printf我们知道在编译的时候,编译出来的是一个地址。 就比如printf->0x1234, 而这个0x1234就能找到我们的动态库的位置。 

        libc.so放到物理内存后, 就会发生缺页中断, 然后将libc.so映射到虚拟地址空间的共享区里面。

        但是,这里的主要问题是, 共享区很大, libc.so具体的映射到共享区哪里呢?那么还记不记得, 我们的程序代码中保存的libc.so的地址是0x1234, 那么, 未来, 我们的代码在去共享区寻找共享库的时候, 就只能去我们的0x1234去寻找了。 

        因为我们说过, 我们程序的内部的地址, 都是编译好的线性地址。 也就是说, 我们的库, 一定会在地址空间的固定地址处。 但是, 我们要知道, 我们的库, 一定会在地址空间的固定地址处, 但是我们要知道, 我们的库的数量是不一定的, 可是我们的库有十几个二十几个, 所以, 我们怎么知道,这些库的地址不会发生冲突呢?动态库被加载到固定的地址空间是不可能的!——那么也就是说, 我们必须把我们的库设计成可以在虚拟内存中, 任意位置加载的库!!!——怎么形成呢?——就是让库在形成的时候, 让自己内部函数不要采用绝对编制, 而是采用相对以逻辑地址加偏移量的方式, 也可以说成是只表示函数在库中的偏移量即可!!!

        什么意思呢? 就是说, 我们的0x1234不是这个库在虚拟空间内的绝对地址, 而是内部的函数, 比如printf在库内部相对于库的起始位置的偏移量是多少。 

        所以呢, 现在的规则就是, 当我们的动态库加载到虚拟地址空间里, 就可以随便放, 形成了我们所谓的库, 只需要最终帮我们记住, 我们的libc.so在虚拟地址当中的起始地址即可。 因为操作系统要对库做管理, 所以, 这个库被加载到了地址空间的什么位置, 也是要被知道的。 那么未来我们libc.so:start如果是90000, 那么我们就可以找到对应的printf方法所在的地址是91234, 那么我们就能去这里寻找printf方法

        那么gcc在生产目标文件时使用的与位置无关码FPIC是什么意思呢?——就是直接用偏移量对库中函数进行编址!!!并且我们在编址的时候可以在虚拟内存中的任意位置加载。——这里有一个问题, 就是静态库为什么不谈加载不谈与位置无关??因为我们静态库的方法是直接拷贝到程序里面的, 就相当于库中的方法就是这个程序的方法了。 就相当于我们在编址的时候将我们的拷贝来的方法一起编进来了

 ————以上就是本篇的全部内容, 本篇内容到此就结束啦, 感谢友友的阅读, 下面是本节的笔记, 和正文几乎一样的, 觉得本节内容有用的话可以保存方便查阅哦

这篇关于linux基础IO——动静态库——进程编址、进程执行、动态库加载的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1146154

相关文章

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

第10章 中断和动态时钟显示

第10章 中断和动态时钟显示 从本章开始,按照书籍的划分,第10章开始就进入保护模式(Protected Mode)部分了,感觉从这里开始难度突然就增加了。 书中介绍了为什么有中断(Interrupt)的设计,中断的几种方式:外部硬件中断、内部中断和软中断。通过中断做了一个会走的时钟和屏幕上输入字符的程序。 我自己理解中断的一些作用: 为了更好的利用处理器的性能。协同快速和慢速设备一起工作

动态规划---打家劫舍

题目: 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。 思路: 动态规划五部曲: 1.确定dp数组及含义 dp数组是一维数组,dp[i]代表

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Flutter 进阶:绘制加载动画

绘制加载动画:由小圆组成的大圆 1. 定义 LoadingScreen 类2. 实现 _LoadingScreenState 类3. 定义 LoadingPainter 类4. 总结 实现加载动画 我们需要定义两个类:LoadingScreen 和 LoadingPainter。LoadingScreen 负责控制动画的状态,而 LoadingPainter 则负责绘制动画。