分段寄存器的使用

6

我正在努力理解低级别的内存管理,有几个问题:

1)Kip R. Irvine编写的汇编语言书籍中提到,在实模式下,程序启动时前三个段寄存器将装入代码、数据和堆栈段的基地址。这对我来说有点含糊不清。这些值是手动指定的还是汇编器生成指令以将这些值写入寄存器?如果是自动进行的,那么它如何找出这些段的大小呢?

2)我知道Linux使用平面线性模型,即非常有限地使用分段。此外,根据Daniel P. Bovet和Marco Cesati所著的《深入理解Linux内核》一书,GDT中有四个主要段:用户数据段、用户代码段、内核数据段和内核代码段。所有四个段的大小和基地址都相同。我不明白为什么需要四个段,如果它们只在类型和访问权限上有所不同(它们都产生相同的线性地址,不是吗?)。为什么不只使用其中一个并将其描述符写入所有段寄存器?

3)不使用分段的操作系统如何将程序划分为逻辑段?例如,如何在没有段描述符的情况下区分堆栈和代码。我读过使用页面处理此类事情的方法,但不理解如何实现。

3个回答

5
  1. 您一定读了许多旧书,因为没有人再真正编写 real-mode 的程序了;-) 在 real-mode 中,您可以通过 physical address = segment register * 0x10 + offset 来获取内存访问的物理地址,其中偏移量是通用寄存器中的值。由于这些寄存器宽度为16位,因此一个段将有64kb长,并且由于没有属性,你无法改变其大小!通过 * 0x10 的乘法,可以提供1mb内存,但取决于您放置在 segment registersaddress registers 中的内容,存在重叠的组合。我没有为 real-mode 编译过任何代码,但我认为操作系统应该在二进制加载期间设置 segment registers,就像加载ELF二进制文件时加载器会分配一些页面一样。然而,我已经编译了裸机内核代码,需要自己设置这些寄存器。

  2. 由于架构约束,平面模型中必须有四个段。在 protected-mode 中,segment registers 不再包含段基址,而是包含了一个基于GDT的 segment selector。根据 segment selector 的值,CPU 将处于给定的特权级别中,这是CPL(当前特权级)。 segment selector 指向一个segment descriptor,该描述符具有DPL(描述符特权级别),如果填充了此选择器,则最终为CPL(至少对于代码段选择器)。因此,您需要至少一对segment selectors来区分内核和用户空间。此外,段既可以是代码段也可以是数据段,因此您最终会在GDT中拥有四个segment descriptors

  3. 我没有任何严肃操作系统使用分段机制的例子,只是因为分段机制仍然存在于向后兼容性。使用平面模型方法只是摆脱它的一种方式。无论如何,您是正确的,分页更有效率、更灵活,并且几乎适用于所有架构(至少概念上如此)。我无法在这里解释分页内部原理,但您需要知道的所有信息都在优秀的英特尔手册中: Intel® 64 和 IA-32 架构 软件开发人员手册 卷3A: 系统编程指南,第1部分


嗯,1)所以当二进制文件被加载时,段寄存器就被设置了?但是如果没有操作系统,我们正在使用原始硬件,例如如果我编写类似BIOS的东西呢。2)如果我理解正确,内核和用户空间的段必须能够在硬件级别上切换特权。但我不明白为什么数据和代码需要不同的段。例如,如果cs和ds寄存器都指向GDT中的同一条目,会发生什么。 - Anton Frolov
  1. 在裸机上,您必须使用“ljmp”指令手动加载CS,并使用“mov”指令加载DS、ES、FS、GS。这些必须在程序的最开始进行初始化。
  2. 因为一个段描述符只描述代码或数据段,而不是代码+数据段,所以您必须为代码段和数据段分别设置段描述符。
- Benny
2
“现在没有人再为实模式编程”这种说法有些夸张了。 :) - Alexey Frunze
2
“线程本地存储”是一个例子,展示了在现代操作系统中如何仍然使用x86段,尽管几乎其他所有方面都使用“平坦内存模型”。 - Alexey Frunze
@Alex 感谢您指出这一点!确实TLS利用FS/GS数据段。我在这篇博客上找到了很棒的相关信息。 - Benny

4

扩展 Benoit对第3个问题的回答...

将程序划分为代码、常量数据、可修改数据和堆栈等逻辑部分是由不同的代理在不同的时间点完成的。

首先,您的编译器(和链接器)创建了可执行文件,其中指定了这种划分。如果您查看一些可执行文件格式(PE、ELF等),您会发现它们支持某种节或段或任何您想称之为的东西。除了地址、大小和文件内位置外,这些节还具有属性,告诉操作系统这些节的目的,例如此节包含代码(并且这是入口点),此节-已初始化的常量数据,那 - 未初始化的数据(通常不占用文件中的空间),这里关于堆栈的一些内容,在那里是依赖项列表(例如DLL)等。

接下来,当操作系统开始执行程序时,它会解析文件,以查看程序需要多少内存,每个部分需要哪里和什么内存保护。后者通常通过页表完成。代码页面标记为可执行和只读,常量数据页面标记为不可执行和只读,其他数据页面(包括堆栈的页面)标记为不可执行和可读写。这通常是正常情况下的做法。

通常,程序需要读写和可执行区域以用于动态生成的代码或修改现有代码。这种组合的RWX访问可以在可执行文件中指定或在运行时请求。

还可以有其他特殊页面,例如用于动态堆栈扩展的保护页面,它们放置在堆栈页面旁边。例如,您的程序从分配足够64KB堆栈的页面开始,然后当程序尝试访问超出该点时,操作系统会截获对那些保护页面的访问,为堆栈分配更多页面(最大支持大小)并将保护页面移动。这些页面不需要在可执行文件中指定,操作系统可以自行处理它们。文件应仅指定堆栈大小和可能的位置。

如果硬件或操作系统中没有区分代码内存和数据内存的代码或执行内存访问权限的代码,那么划分就非常形式化。16位实模式DOS程序(COM和EXE)并没有以某种特殊方式标记代码、数据和栈段。COM程序将所有内容放在一个公共的64KB段中,并以IP=0x100和SP=0xFFxx开始,代码和数据的顺序可以在内部任意交织。DOS EXE文件仅指定了起始CS:IP和SS:SP位置,除此之外,代码、数据和栈段对于DOS来说是不可区分的。它所需要做的只是加载文件,执行重定位(仅适用于EXE),设置PSP(程序段前缀,包含命令行参数和一些其他控制信息),加载SS:SP和CS:IP。它无法保护内存,因为实地址模式中不提供内存保护,因此16位DOS可执行文件格式非常简单。

@AntonFrolov 如果你的问题已经得到了答案,请不要忘记接受最佳答案。 - Alexey Frunze

-1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接