OSTaskStkInit_FPE_x86()--浮点仿真任务栈初始化函数(分段寻址的地址转换为线性地址)

段地址:段内偏移量寻址方案

在 uC/OS-II 随书光盘中针对 80x86 (不带硬件浮点运算单元)的移植源码中,有一个浮点仿真任务栈初始化函数 OSTaskStkInit_FPE_x86(),其中将分段寻址的地址转换为线性地址时,使用了如下的代码:

[code=cpp]
seg = FP_SEG(*pptos);
off = FP_OFF(*pptos);
lin_tos = ((INT32U)seg << 4) + (INT32U)off; [/code]

此转换过程涉及到内存的寻址方案,也就是“段地址:段内偏移量”寻址方案。此寻址方案在 x86 体系结构的实模式下广泛采用。在这篇文章中,详细说明了“段地址:段内偏移量”寻址方案的由来、内容及可能产生的问题,现翻译整理如下。

 

引言

我们知道,要对内存中的某个位置进行读写,必须有这个内存单元的地址才行。为了对内存中每个单元进行寻址,最直接的,就是绝对(absolute)寻址方案,或称线性(linear)寻址方案。也就是用十六进制数从 0 开始对连续的内存进行编号。这样得到的地址,对每个单元来说是唯一的。

有绝对寻址方案,就一定有相对(relative)寻址方案。早在 8086 时代,CPU 中的寄存器最大长度只有 16 位,这意味着它只能直接寻址 216=65,536 个字节 (64KB)。但随着运行的程序尺寸不断增大,所需寻址的范围也越来越大。此时有些 CPU 制造商使用更大的寄存器尺寸来解决问题。而 Intel 的设计师们(大概是为了控制硬件成本)则保留了 16 位长的寄存器,而采用了一种变通的方式:扩展指令集。通过这些扩展的指令集,在寻址超出 64KB 范围的内存时,CPU 能将两个 16 位长的寄存器组合起来当成一个 32 位的寄存器来用。

照理说组合后的 32 位寄存器足以寻址 232=4GB 的内存空间了吧。可当时许多人认为(包括我们可爱的盖茨大叔),“640KB 内存对任何人来说都足够了”。所以,为了避免因 32 位的线性寻址方案可能产生的问题,伟大的设计师们发明了“段地址:段内偏移量”寻址方案,以使 CPU 能有效地寻址大约 1MB 的内存空间。

此寻址方案工作原理为:将段地址寄存器中的值乘以 16(即左移一个十六进制位并在右边补上一个十六进制的0),再加上段内偏移量寄存器的值。所以,从“段地址:段内偏移量”得到绝对地址的公式如下:

绝对地址 = 段地址 * 16 + 段内偏移量

举几个例子。要计算分段的地址 F000:FFFD 对应的绝对地址,只需简单地在段地址后加一个 0(等同于乘以十进制的 16)再加上段内偏移量即可:

        F0000
      +  FFFD
      -------
        FFFFD
    

而对于分段的地址 923F:E2FF 则计算方法如下:

        923F0
      +  E2FF
      -------
        A06EF
    

再来看看用分段的地址能表示的最大绝对地址值:

        FFFF0
      +  FFFF
      -------
       10FFEF
    

事实上,一直到 8086 以后好长一段时间,这么大的地址(10FFEF)并没有真实的内存单元与之对应。所以当时的程序若使用“段地址:段内偏移量”访问超过 20 位的绝对地址(1MB)时,CPU 将截断最高位(因为 8086/8088 CPU 只有 20 条地址线),有效地将超过FFFFFh 的地址映射到内存的第一个段中。因此,10FFEFh 将映射到 FFEFh。

直到许多 PC 机都普遍使用超过 1MB 的内存时,程序员们才想方设法充分利用之。而超过 1MB 的部分,现在称之为高端内存区 High Memory Area (HMA)。此部分大小为 10FFEF - 100000 + 1 = FFF0 即 65520 个字节。在维基百科的High Memory Area 词条中,提到从 0000:0000 到 FFFF:FFFF 可寻址的内存大小为 17×64KB 再减去 16 个字节,此计算方法将在解释完重叠段的可视化后做说明。

使用“段地址:段内偏移量”的一个缺点就是许多地址对指向了同一个绝对地址单元。例如,如下的每个地址,都指向了绝对地址单元 07C00h:

  0007:7B90   0008:7B80   0009:7B70   000A:7B60   000B:7B50   000C:7B40   
  0047:7790   0048:7780   0049:7770   004A:7760   004B:7750   004C:7740   
  0077:7490   0078:7480   0079:7470   007A:7460   007B:7450   007C:7440   
  01FF:5C10   0200:5C00   0201:5BF0   0202:5BE0   0203:5BD0   0204:5BC0   
  07BB:0050   07BC:0040   07BD:0030   07BE:0020   07BF:0010   07C0:0000
    

事实上,最多可能有 4096 个不同的“段地址:段内偏移量”地址对指向内存中的同一个单元。对于绝对地址 0h 到FFEFh(0 到 65,519),指向同一单元的地址对数目可计算如下:将绝对地址除以十进制的 16(即将绝对地址右移一个十六进制位),去掉余数后再加 1。若已知此区间的“段地址:段内偏移量”地址对,则通过如下方法计算与此地址对指向相同内存单元的地址对数目:若段内偏移量小于等于 000Fh (15),则将段地址加 1 得到。例如,与0040:0000 指向相同内存单元的地址对数目为 41h (65)。对于绝对地址 7C00h,共有 7C00 / 10h --> 7C0 + 1 = 7C1 (1,985) 个地址对指向它。而对于绝对地址从FFF0h (65,520) 一直到FFFFFh (1,048,575),均有 4096 个地址对指向每个单元。大约 88% 多一些的内存可用地址对的方法访问。而用地址对可访问的最后的 65520 字节(即上方提到的超过 1MB 的那部分)称为高端内存区(HMA)。高端内存区的单元,每增加 16 字节,指向的地址对数目少一个。

鉴于指向每个内存单元的地址对数目不统一,大多数程序员达成共识,使用一种规范化方法(见下文的“规范化表示法”)。

 

重叠段的可视化

由于段内偏移量为 16 位,故每个段的大小为 216=65,536 字节。而在相对地址(分段的地址)转换为绝对地址时,是将段地址左移 4 位,所以两个相邻的段的起始位置总是相差 24=16 个字节而重叠在一起。在计算机术语中,将内存中连续的 16 个字节称为一个段落(paragraph)(注意与段(segment)的区别)。这样,内存中每向后 16 个字节,重叠的段的数目将增加 1,直至第一个段的末尾。从第一个段的末尾开始一直到 1MB 的位置,每个段落上都有 4,096 个段重叠在一起。下图 Figure0 所示为内存中最开始的五个段。其中四个角上分别为各个段 第一个段落的第一个字节、第一个段落的最后一个字节、最后一个段落的第一个字节、最后一个段落的最后一个字节 的地址(拗口不?)。

OSTaskStkInit_FPE_x86()--浮点仿真任务栈初始化函数(分段寻址的地址转换为线性地址)_第1张图片

而在图 Figure1 中,焦点放在了各个段的起始几个段落。请注意图中最开始的 16 个字节。此段落上只有一个段,没有其它段与之重叠。因此,内存中最开始的 16 个字节的段地址表示法是唯一的。即段地址为 0000,段内偏移量为 0000 到 000F。内存中接下来的 16 个字节(10h 到 1Fh)每个字节有 2 个不同的地址对可引用。依此类推。

OSTaskStkInit_FPE_x86()--浮点仿真任务栈初始化函数(分段寻址的地址转换为线性地址)_第2张图片

图 Figure1 的第二部分(蓝线以下)展示了从第一个段的最后一个段落(绝对地址为 FFF0h 到 FFFFh)到第二个段的第一个段落(绝对地址为 10000h 到 1000Fh)的过渡。注意段 0FFF: 的第一个段落(也是段 0000: 的最后一个段落)是内存中第一个有 4,096 个段重叠的段落,因此也有 4,096 个不同的地址对可引用之。

由图 Figure2 可见段 9000: 是整个段都位于所谓的常规内存区 Conventional Memory (内存的前 640KB 或 655,360 字节)中的最后一个段。段 A000: 的第一个段落是上端内存区 Upper Memory Area (UMA) 的起始。上端内存区包括 384KB 或 393,216 字节,也叫保留区。段 F000: 是整个段都位于上端内存区的最后一个段。常规内存区的 640KB 与 上端内存区的 384KB 加起来为 1024KB 即 1MB。

OSTaskStkInit_FPE_x86()--浮点仿真任务栈初始化函数(分段寻址的地址转换为线性地址)_第3张图片

可从另一个角度看内存的前 1MB 空间。就是只看从段 0000: 开始的所有连续(相邻而不重叠)的段。内存的第一个字节为 0000:0000 (绝对地址 00000h),由于每个段长度为 64KB,故与段 0000: 相邻而不重叠的段的第一个字节的绝对地址应该是 10000h,转换为段内偏移量为 0000h 的地址对应是 1000:0000,同理,下一个相邻而不重叠的段的第一个字节的绝对地址是 20000h, 而分段地址是 2000:0000。依此类推,内存的前 1MB 空间(共 1MB / 64KB = 16 个段)可以只用段 0000:, 10000:, 2000:, ... 9000:, A000:, B000:, ... F000: 再加上 65,536 个段内偏移量表示。

现在,将这种相邻而不重叠的段表示法扩展到高端内存区中,就可解释上文提到从 0000:0000 到 FFFF:FFFF 可寻址的内存大小为 17×64KB 再减去 16 个字节的计算方法。除了上述的 16 个段外,高端内存区已不到一个段,这部分内容与上端内存区正好重叠一个段落,故使用 17 个段长度减一个段落长度(16 个字节)的计算方法。见图 Figure3。

OSTaskStkInit_FPE_x86()--浮点仿真任务栈初始化函数(分段寻址的地址转换为线性地址)_第4张图片

图 Figure4 所示为高端内存区的最后几个段落的情况,可以看到重叠的段逐渐减少,直至最后一个段落,只能使用段 FFFF: 的最后一个段落来引用了。

OSTaskStkInit_FPE_x86()--浮点仿真任务栈初始化函数(分段寻址的地址转换为线性地址)_第5张图片

 

规范化表示法

规范化的“段地址:段内偏移量”表示法可使内存中的每个单元只有唯一的一个地址对与之对应。此地址对称为规范化地址指针

规范化表示法的原理就是将段内偏移量提取成只包含十六进制的 0h 到 Fh,也就是一个段落,然后再相应地设置段地址的值。将任意的段地址对转换为规范化地址或指针只需两步:

1. 将地址对转换为线性地址;
2. 在最后两个十六进制位之间插入一个冒号( :)

例如,规范化地址对 1000:1B0F 的步骤如下:


1000:1B0F -> 11B0Fh -> 11B0:F (或 11B0:000F)

由于规范化地址的段内偏移量部分总是有三个前导零,故经常省略这三个十六进制的零而写成这样:11B0:F

在 uC/OS-II 的 OSTaskStkInit_FPE_x86() 函数中,就有规范化的操作:

[code=cpp]
seg = (INT16U)(lin_bos >> 4); /* Get new 'normalized' segment */
[/code]

 

“段地址:段内偏移量”表示法可能导致的问题

在编写 BIOS 的引导代码时,大多数人使用的标准方法是将段地址设置为 0,因此引导程序的程序员应该假定所有的段地址都是 0 (代码段,数据段等)而只处理段内偏移量的值即可。因此,软驱的引导代码应该放在0000:7C00 位置处,用规范化表示法表示就是07C0:0(或 07C0:0000) 的位置。而一些 BIOS 厂商却不使用这一标准方法,而使用上述的规范化表示法(二者的适用范围不同),将软驱的引导代码放在 07C0:0000 处(使用 JMP 0700:0000 指令),这使得包括微软和 IBM 的操作系统启动出现问题。这就是为什么有些引导代码(如 GRUB 中的引导代码)使用额外的指令以确保正确设置了段地址寄存器的原因。GRUB 的一个作者在注释中说这个 Long Jump 指令是必须,“因为有些劣质的 BIOS 错误地跳转到了 07C0:0000 而不是正确的 0000:7C00。”

 

你可能感兴趣的:(OSTaskStkInit_FPE_x86()--浮点仿真任务栈初始化函数(分段寻址的地址转换为线性地址))