当前位置:首页 > 公众号精选 > IOT物联网小镇
[导读]程序的结构bootloader把程序从硬盘读取到内存代码重定位程序入口点重定位段表重定位跳转到程序的入口地址操作系统程序的执行在上一篇文章中Linux从头学05-系统启动过程中的几个神秘地址,你知道是什么意思吗?,我们以几个重要的内存地址为线索,介绍了x86系统在上电开机之后:C...

  • 程序的结构


  • bootloader 把程序从硬盘读取到内存


  • 代码重定位


    • 程序入口点重定位


    • 段表重定位


  • 跳转到程序的入口地址


  • 操作系统程序的执行


在上一篇文章中Linux从头学05-系统启动过程中的几个神秘地址,你知道是什么意思吗?,我们以几个重要的内存地址为线索,介绍了 x86 系统在上电开机之后:


  1. CPU 如何执行第一条指令;


  2. BIOS 中的程序如何被执行;


  3. 操作系统的引导代码(bootloader) 被读取到物理内存中被执行;


下一个环节,就应该是引导程序(bootloader)把操作系统程序,读取到内存中,然后跳入到操作系统的第一条指令处开始执行


这篇文章,我们继续以8086这个简单的处理器为原型,把程序的加载过程描述一下。其中的重点部分就是代码重定位,我们用画图的方式,尽我所能,把程序加载、地址重定位的计算过程描述清楚。


PS: 文中所说的程序、操作系统文件,都是指同一个东西。


程序的结构

为了便于下面的理解,我们有必要把待加载的操作系统程序的文件结构先介绍一下。


当然了,这里介绍的文件结构,是一个非常简化版本的操作系统程序,本质上与我们平常所写的应用程序没有什么差别,因此我们也可以把它看做一个普通的程序文件。


操作系统程序静静的躺在硬盘中,等待bootloader来读取,此时bootloader可以看做一个加载器


它俩毕竟是属于两个不同的东西,为了让bootloader知道程序的长度,需要某种“协议”来进行沟通,这个“协议”就是程序文件的头信息(Header)。


也就是说,在程序的开头部分,会详细的介绍自己,包括:程序的总长度是多少字节,一共有多少个段,入口地址在什么位置等等。


还记得之前介绍过的Linux系统中使用的ELF文件格式吗?Linux系统中编译、链接的基石-ELF文件:扒开它的层层外衣,从字节码的粒度来探索


那篇文章把一个典型的Linux ELF格式的可执行文件彻底拆解了一遍,可以看到,在ELF文件的头部信息中,详细描述了文件中每一部分内容。


其实Windows中的程序格式(PE格式)也是类似的,它与ELF格式来源于同一个祖宗。


1. 程序头(Header)的描述信息

为了便于描述,我们假设程序中包括3个段:代码段,数据段和栈段,再加上程序头部信息,一共是4个组成部分。如下所示:


Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理为什么中间留有白色的空白?


因为每一个段并不是紧挨着排列的,为了段地址能够内存对齐(16个字节对齐),段与段之间可能会空余一段空间,这些空间里的数据都是无效的。


刚才说了,为了能够让加载器(bootloader)尽可能的了解自己,程序文件会在自己的Header部分,详细的描述自己的信息:


Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理有了这样的描述信息,bootloader就能够知道一共要读取多少个字节的程序文件,跳转到哪个位置才能让操作系统的指令开始执行。


2. 关于汇编地址

在程序的头信息中,可以看到汇编地址和偏移量这样的信息。


编译器在编译源代码的时候,它是不知道bootloader会把程序加载到内存中的什么位置的。


bootloader会查看哪个位置有足够的空间,找到一个可用的位置之后,就把操作系统程序读取到这个位置,可以看做是一个动态的过程


因此,编译器在编译阶段用来定位变量、标签等使用的地址,都是相对于当前段的开始地址来计算的。


还是拿刚才的图片来举例:


Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理我们假设Header部分是32个字节,三个段的开始地址分别是:


代码段 addrCodeStart: 0x00020(距离文件的第一个字节是 32 Bytes);


数据段 addrDataStart: 0x01000(距离文件的第一个字节是 4K Bytes);


栈段   addrStackStart:0x01200(距离文件的第一个字节是 4K 512 Bytes);


在代码段中,定义了一个标签label_1,它距离代码段的开始位置(0x00020)是512个字节(0x0200)。


同时,可以算出它距离文件开头的第一个字节就是 512 32 = 544 字节,因为代码段的开始地址距离文件头部是32个字节。


在label_1之前的代码中,会引用到这个标签。


那么在使用的地方,将会填上0x0200,表示:引用的这个位置是距离代码段开始地址的 512 字节处


以上的这些地址,指的就是汇编地址


我们再来拿程序的入口地址偏移量来举例,入口地址是通过start标签来定义的:


Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理假设:在代码段中,入口地址标签start位于代码段开始位置的0x0100偏移处,也就是距离代码段开始位置的256个字节。


那么,在程序的Header信息中,入口点偏移量的位置就要填写0x0100,这样的话,bootloader把程序读取到内存中之后,就能从这里获取到程序入口点的偏移地址,然后经过一系列的重定位,就可以准确跳转到程序的第一条指令的地方去执行了。


按照刚才假设的地址信息,程序头Header中的信息就是下面这个样子:


Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理最右侧的蓝色字体,表示每一个项目占用的字节数,一共是24个字节。


刚才说到,每一个段的开始地址都是按照16字节对齐的,因此在Header之后,要空余8个字节的空间,之后,才是代码段的开始地址(0x00020 = 32 Bytes)。


bootloader 把程序从硬盘读取到内存

1. 读取到内存中的什么位置?

bootloader在把操作系统文件,从硬盘上读取到内存之前,必须决定一件事情:把文件内容存放到内存中的什么位置?


从上一篇文章我们了解到,在读取操作系统之前,内存布局模型是下面这样的:


Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理注意:这是8086系统中,20根地址线能够寻址的1 MB的地址空间。


其中顶部的64 KB,映射到ROM中的BIOS程序。


底部从0开始的1 KB地址空间,是存储256个中断向量(下一篇文章准备聊聊中断的事情)。


中间的从0x07C00地址开始的地方,是BIOS从硬盘的引导区读取的bootloader程序所存放的地方。


黄色部分的空间一共是640 KB的空间,都是映射到RAM中的,因此,有足够大的空闲地址空间来存储操作系统程序文件


假设:bootloader就决定从地址0x20000开始(128 KB),存放从硬盘中读取的操作系统程序文件。


2. bootloader 设置数据段基地址

从硬盘上读取文件,是按照扇区为读取单位的,也就是每次读取一个扇区(512字节)。


至于如何通过指定扇区号、发送端口命令,来从硬盘上读取数据,这是另一个话题,暂且不表,我们把目光集中在bootloader上。


对于bootloader来说,读取操作系统文件就相当于读取普通的数据


既然已经决定把读取的数据从地址0x20000开始存放,那么bootloader就要把数据段寄存器ds设置为0x2000,这样的话,经过逻辑地址的计算公式:


物理地址 = 逻辑段地址 * 16 偏移地址


才能得到正确的物理地址,例如:


读取的第 1 个扇区的数据放在:0x2000:0x0000 地址处;


读取的第 2 个扇区的数据放在:0x2000:0x0200 地址处;


读取的第 3 个扇区的数据放在:0x2000:0x0400 地址处;


...


读取的第 10 个扇区的数据放在:0x2000:0x1200 地址处;


Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理

3. bootloader 读取所有扇区

bootloader需要把操作系统程序的所有内容读取到内存中,需要读取的长度是多少呢?


程序文件的Header中有这个信息,因此,bootloader需要先读取程序文件的第一个扇区,也就是512字节,放在0x20000开始的位置。


我们继续假设一下:程序的总长度是5K字节(0x01400),那么程序文件的前512个字节(第一个扇区)读取到内存中,就是下面这个样子:


Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理注意:这是文件内容被读取到内存中的布局,最下面是低地址,最上面是高地址,这与前面描述静态文件中内容的顺序是相反的


读取了第一个扇区之后,就可以取出0x20000开始的 4 个字节的数据:0x01400,得到程序文件的总长度: 5 K 字节


每个扇区是512字节,5 K字节就是10个扇区。


第一个扇区已经读取了,那么还需要继续读取剩下的9个扇区。


于是,bootloader把所有扇区的数据,依次读取到:0x2000:0x0000, 0x2000:0x0200,  0x2000:0x0400, ... 0x2000:0x1200 地址处。


4. 如果程序文件超过 64 KB 怎么办?

这里有一个延伸的问题可以思考一下:


8086 的段寻址方式,由于偏移量寄存器的长度是16位,最大只能表示64 KB的空间。


我们所假设的例子中,程序文件只有5 KB,在一个数据段内完全可以包括,因此bootloader可以一直用 0x2000:偏移量 的方式来读取文件内容。


那如果程序的长度是100 KB,超过了偏移量的64 KB最大寻址空间,那么bootloader应该怎么样做才能正确把100 KB的程序读取到内存中?


解答:


可以在读取文件的过程中,动态的增加数据段逻辑地址


Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理比如,在读取前面的64 KB数据(扇区 1 ~ 扇区 128)时,段寄存器ds设置为0x2000。


在读取第65 KB数据(扇区 129)之前,把段寄存器ds设置为0x3000,这样读取的数据就从0x3000:0x0000处开始存放了。


代码重定位

现在,操作系统程序已经被读取到内存中了,下一个步骤就是:跳转到操作系统的程序入口点去执行!


程序入口点重定位

程序入口点的偏移量,已经被记录在Header中了(0x04 ~ 0x05字节,橙色部分):


Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理Header中记录的代码段中入口点start标签的偏移量是0x100,即:入口点距离代码段的开始地址是 256 个字节


同样的道理,代码段中所有相关的地址,都是相对于代码段的开始地址来计算偏移量的。


因此,如果(这里是如果啊)bootloader把代码段的开始地址(不是整个文件的开始),直接放到内存的0x00000地址处,那么代码段里所有地址就都不用再修改了,可以直接设置:cs = 0x0000, ip=0x0100,这样就直接跳转到start标签的地方开始执行了。


可惜,bootloader是把操作系统程序读取到地址0x20000开始的地方,因此,需要把代码段寄存器cs设置为当前代码段在内存中的实际开始位置,也即是下面这个五角星的位置:


Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理
以上两段文字,可以再多读几遍!


在Header中,0x06,0x07, 0x08, 0x09 这4个字节的数据0x00020,就是代码段的开始位置距离程序文件开头的字节数。


只要把这个数值(0x00020),与文件存储的开始地址(0x20000)相加,就可以得到代码段的开始地址在物理内存中的绝对地址


0x00020 0x20000 = 0x20020


即:代码段的开始地址,位于物理内存中0x20020的位置。


对于一个物理地址,我们可以用多种不同的逻辑地址来表示,例如:


0x20020 = 0x2002:0x0000
0x20020 = 0x2000:0x0020
0x20020 = 0x1FF0:0x0120


面对这3个选择,我们当然是选择第1个,而且只能选择第1个,因为代码段内部所有的地址偏移,在编译的时候都是基于0地址的(也即是上面所说的汇编地址),或者称作相对地址。


明白了这个道理之后,就可以把cs:ip设置为0x2002:0x0100,这样CPU就会到start标签处执行了。


但是,在进行这个操作之前还有其他几件事情需要处理,因此,要把代码段的逻辑段地址0x2002, 写回到Header中的0x06 ~ 0x09这4个字节中保存起来(橙色部分):


Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理

段表重定位

此时,系统还是在bootloader的控制之下,数据段寄存器ds仍然为0x2000,想一想为什么?


因为 bootloader 读取操作系统程序的第一扇区之前,希望把数据读取到物理地址 0x20000 的地方,右移一位就得到了逻辑段地址 0x2000,把它写入到数据段寄存器 ds 中。


我们一直忽略了 bootloader 使用的栈空间,因为这部分与文件主题无关。


操作系统程序如果想要执行,必须使用自己程序文件中的数据段和栈段


但是,Header中记录的这2个段的开始地址,都是相对于程序文件开头而言的


而且操作系统文件并不知道:自己被 bootloader 读取到内存中的什么位置?


因此,bootloader也需要把这2个段,在内存中的开始地址进行重新计算,然后更新到Header中。


这样的话,当操作系统程序开始执行的时候,才能从Header中得到数据段和栈段的逻辑段地址


当然了,这里所举的示例中只有3个段,一个实际的程序可能会包括很多个段,每一个段的地址都需要进行重定位。


bootloader从Header的0x0A ~ 0x0B这 2 个字节,可以得到一共有多少个段地址需要重定位。


然后按照顺序,依次读取每一个段的偏移地址,加上程序文件的加载地址(0x20000),计算出实际的物理地址,然后再得到逻辑段地址,具体如下:


代码段偏移量 0x00020:0x20000 0x00020 = 0x20020(物理地址),右移一位得到逻辑段地址:0x2002;


数据段偏移量 0x0x01000: 0x20000 0x01000 = 0x21000(物理地址),右移一位得到逻辑段地址:0x2100;


栈段 段偏移量 0x0x01200: 0x20000 0x01200 = 0x21200(物理地址),右移一位得到逻辑段地址:0x2120;


下图橙色部分:


Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理我们把代码段、数据段、栈段在内存中的布局模型全部画出来:


Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理

跳转到程序的入口地址

万事俱备,只欠东风!


一切工作已经准备就绪,最后一步就是进入操作系统程序中代码段的start入口点了。


在上面的准备工作中,bootloader已经把程序代码段的逻辑段地址0x2002,保存在Header中的 0x06 ~ 0x09 这 4 个字节中了,只要把它赋值给代码段寄存器cs即可。


程序入口点位于start标签处,它距离代码段的开始位置偏移0x100,保存在Header中的 0x04 ~ 0x05 这 2 个字节,只要把它赋值给指令指针寄存器ip即可。


我们可以手动从内存中读取,然后赋值给 cs 和 ip 寄存器。


也可以直接利用 8086 CPU 中的这条指令:jmp [0x04] 来实现cs:ip的赋值。


Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理因为此刻还是在bootloader的控制下,数据段寄存器ds的值仍然为 0x2000,因此跳转到0x2000:0x04内置中所表示的地址,就可以把正确的逻辑段地址和指令地址赋值给cs:ip,从而开始执行操作系统程序的第一条指令。


操作系统程序的执行

操作系统的第一条指令在执行时,数据段寄存器ds和 栈段寄存器cs中的值,仍然为bootloader中所设置的值。


因此,操作系统首先要把这2个段寄存器设置为自己程序文件的值,然后才能开始后续指令的执行。


上文已经说过,每一个段在内存中的逻辑段地址,已经被bootloader重新计算,并且更新到了Header中。


所以,操作系统就可以从 ds:0x14 的位置,读取新的栈段逻辑地址 0x2120,并把它赋值给栈段寄存器cs。


从这个时候开始,所有的栈操作就是操作系统程序自己的了。


注意:此时数据段寄存器 ds 仍然没有改变,仍然是 bootloader 中使用的 0x2000。


然后再从 ds:0x10 的位置读取新的数据段逻辑地址 0x2100,并把它赋值给数据段寄存器ds。


从这个时候开始,所有的数据操作就是操作系统程序自己的了。


Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理注意:给cs、ds的赋值顺序不能颠倒


如果先给ds赋值,那么再去Header中读取cs逻辑段地址的时候,就没法定位了。


因为此时ds寄存器已经指向了新的地址(ds = 0x2100),没法再去0x2000:0x14地址处获取数据了。


最后还有一点,对于栈操作,除了设置栈的段寄存器ss外,还需要设置栈顶指针寄存器sp。


我们假设程序中设置的栈空间是512字节,栈顶指针是向低地址方向增长的,因此,需要把sp初始化为512。


至此,操作系统程序终于可以愉快的开始执行了!



------ End ------
这篇文章,我们描述了关于代码重定位的最底层原理。


在以后学习到Linux中的重定位相关知识时,会接触到更多的概念和技巧,但是最底层的基本原理都是相通的。


希望这篇文章,能够成为你前进路上的垫脚石!


本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭
关闭