Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理
扫描二维码
随时随地手机看文章
-
程序的结构
-
bootloader 把程序从硬盘读取到内存
-
代码重定位
-
程序入口点重定位
-
段表重定位
-
跳转到程序的入口地址
-
操作系统程序的执行
下一个环节,就应该是引导程序(bootloader)把操作系统程序,读取到内存中,然后跳入到操作系统的第一条指令处开始执行。
- CPU 如何执行第一条指令;
- BIOS 中的程序如何被执行;
- 操作系统的引导代码(bootloader) 被读取到物理内存中被执行;
PS: 文中所说的程序、操作系统文件,都是指同一个东西。
程序的结构
为了便于下面的理解,我们有必要把待加载的操作系统程序的文件结构先介绍一下。
1. 程序头(Header)的描述信息
为了便于描述,我们假设程序中包括3个段:代码段,数据段和栈段,再加上程序头部信息,一共是4个组成部分。如下所示:
为什么中间留有白色的空白?
有了这样的描述信息,bootloader就能够知道一共要读取多少个字节的程序文件,跳转到哪个位置才能让操作系统的指令开始执行。
2. 关于汇编地址
在程序的头信息中,可以看到汇编地址和偏移量这样的信息。
我们假设Header部分是32个字节,三个段的开始地址分别是:
代码段 addrCodeStart: 0x00020(距离文件的第一个字节是 32 Bytes);在代码段中,定义了一个标签label_1,它距离代码段的开始位置(0x00020)是512个字节(0x0200)。数据段 addrDataStart: 0x01000(距离文件的第一个字节是 4K Bytes);
栈段 addrStackStart:0x01200(距离文件的第一个字节是 4K 512 Bytes);
假设:在代码段中,入口地址标签start位于代码段开始位置的0x0100偏移处,也就是距离代码段开始位置的256个字节。
最右侧的蓝色字体,表示每一个项目占用的字节数,一共是24个字节。
bootloader 把程序从硬盘读取到内存
1. 读取到内存中的什么位置?
bootloader在把操作系统文件,从硬盘上读取到内存之前,必须决定一件事情:把文件内容存放到内存中的什么位置?
注意:这是8086系统中,20根地址线能够寻址的1 MB的地址空间。
2. bootloader 设置数据段基地址
从硬盘上读取文件,是按照扇区为读取单位的,也就是每次读取一个扇区(512字节)。
物理地址 = 逻辑段地址 * 16 偏移地址才能得到正确的物理地址,例如:
读取的第 1 个扇区的数据放在:0x2000:0x0000 地址处;读取的第 2 个扇区的数据放在:0x2000:0x0200 地址处;
读取的第 3 个扇区的数据放在:0x2000:0x0400 地址处;
...
读取的第 10 个扇区的数据放在:0x2000:0x1200 地址处;
3. bootloader 读取所有扇区
bootloader需要把操作系统程序的所有内容读取到内存中,需要读取的长度是多少呢?
注意:这是文件内容被读取到内存中的布局,最下面是低地址,最上面是高地址,这与前面描述静态文件中内容的顺序是相反的。
4. 如果程序文件超过 64 KB 怎么办?
这里有一个延伸的问题可以思考一下:
比如,在读取前面的64 KB数据(扇区 1 ~ 扇区 128)时,段寄存器ds设置为0x2000。
代码重定位
现在,操作系统程序已经被读取到内存中了,下一个步骤就是:跳转到操作系统的程序入口点去执行!
程序入口点重定位
程序入口点的偏移量,已经被记录在Header中了(0x04 ~ 0x05字节,橙色部分):
Header中记录的代码段中入口点start标签的偏移量是0x100,即:入口点距离代码段的开始地址是 256 个字节。
同样的道理,代码段中所有相关的地址,都是相对于代码段的开始地址来计算偏移量的。因此,如果(这里是如果啊)bootloader把代码段的开始地址(不是整个文件的开始),直接放到内存的0x00000地址处,那么代码段里所有地址就都不用再修改了,可以直接设置:cs = 0x0000, ip=0x0100,这样就直接跳转到start标签的地方开始执行了。
以上两段文字,可以再多读几遍!在Header中,0x06,0x07, 0x08, 0x09 这4个字节的数据0x00020,就是代码段的开始位置距离程序文件开头的字节数。
0x00020 0x20000 = 0x20020即:代码段的开始地址,位于物理内存中0x20020的位置。
0x20020 = 0x2002:0x0000面对这3个选择,我们当然是选择第1个,而且只能选择第1个,因为代码段内部所有的地址偏移,在编译的时候都是基于0地址的(也即是上面所说的汇编地址),或者称作相对地址。
0x20020 = 0x2000:0x0020
0x20020 = 0x1FF0:0x0120
段表重定位
此时,系统还是在bootloader的控制之下,数据段寄存器ds仍然为0x2000,想一想为什么?
因为 bootloader 读取操作系统程序的第一扇区之前,希望把数据读取到物理地址 0x20000 的地方,右移一位就得到了逻辑段地址 0x2000,把它写入到数据段寄存器 ds 中。操作系统程序如果想要执行,必须使用自己程序文件中的数据段和栈段。我们一直忽略了 bootloader 使用的栈空间,因为这部分与文件主题无关。
代码段偏移量 0x00020:0x20000 0x00020 = 0x20020(物理地址),右移一位得到逻辑段地址:0x2002;下图橙色部分:数据段偏移量 0x0x01000: 0x20000 0x01000 = 0x21000(物理地址),右移一位得到逻辑段地址:0x2100;
栈段 段偏移量 0x0x01200: 0x20000 0x01200 = 0x21200(物理地址),右移一位得到逻辑段地址:0x2120;
我们把代码段、数据段、栈段在内存中的布局模型全部画出来:
跳转到程序的入口地址
万事俱备,只欠东风!
因为此刻还是在bootloader的控制下,数据段寄存器ds的值仍然为 0x2000,因此跳转到0x2000:0x04内置中所表示的地址,就可以把正确的逻辑段地址和指令地址赋值给cs:ip,从而开始执行操作系统程序的第一条指令。
操作系统程序的执行
操作系统的第一条指令在执行时,数据段寄存器ds和 栈段寄存器cs中的值,仍然为bootloader中所设置的值。
注意:此时数据段寄存器 ds 仍然没有改变,仍然是 bootloader 中使用的 0x2000。然后再从 ds:0x10 的位置读取新的数据段逻辑地址 0x2100,并把它赋值给数据段寄存器ds。
注意:给cs、ds的赋值顺序不能颠倒。
------ End ------
这篇文章,我们描述了关于代码重定位的最底层原理。





