x86汇编语言,作为学习操作系统前导知识,有必要学习一下。有幸在B站认识了up主李老师,引我走上学习操作系统内核之路。看了《x86汇编语言》这本书的前十章后,深觉有些纸上谈兵,摸不到头脑,在看保护模式时,突然发现,每章节会有一些学习目标,所以就以解决这些问题为目标进行学习吧。带着问题学习,才能更有收获。
INTEL8086处理器的通用寄存器和段地址加偏移地址的内存访问方式
首先有必要介绍一下INTEL8086处理器,它的诞生开启了x86架构的先河(至于为啥叫x86我就不知道了),直到现在,我们仍离不开它,或者说,在某些环境中,多多少少还能看到它的影子。
它是INTEL发布的第一款x86架构的16位微处理器。16位指的是它有16根数据总线,每次能处理至多2字节的数据。并且有20位外部地址总线,所以寻址空间为$2^{20}=1MB$。
INTEL8086处理器的通用寄存器
8086内部一共有8个通用寄存器,分别为AX、BX、CX、DX、SI、DI、BP、SP。这8个寄存器都是16位的,并且前4个又能分别拆成2个8位寄存器使用:
AL(AX Low)指的是低8位寄存器;AH(AX High)指的是高8位寄存器。操作低8位寄存器AL时,不影响高8位寄存器AH中的数据,反之也成立。
AX~DX寄存器比其他4个寄存器更加通用,从名字就能看出来。其余4个寄存器更加常用于以下工作:
- SI(Source Index):源变址寄存器
- DI(Destination Index):目的变址寄存器
- BP(Base Pointer):基指针寄存器
- SP(Stack Pointer):堆栈指针寄存器
这四个寄存器不能像AX~DX寄存器一样被分为高8位和低8位,什么时候需要使用了,我们再做分析。
段地址加偏移地址的内存访问方式
先介绍一下背景:
首先说一下我们为什么要使用内存,为什么不直接使用寄存器来存储数据。
这是因为寄存器造价太高,我们不得不使用价格稍微便宜,容量更大的内存来暂时存储数据。
在内存中有数据段和代码段,处理器并不知道什么是数据什么是指令,同样的数据在不同的空间中解释也不同。例如ADD指令,假如它翻译成的机器码是0100 1001,如果处理器将这段数据当作指令解释,那它就是ADD指令;而如果处理器将其解释为数据,那就是十进制的1+8+64=73。为了方便记忆这些操作码,人们发明了汇编语言,而编译器就充当了这个过程中的翻译官,当我们写完了汇编代码后,编译器会将程序翻译成对应的机器码,这样处理器就能理解了。
好了,说了这么多,我都等不及了,赶紧访问内存吧,怎么访问?起码我现在知道一点:内存至少分成两部分,可事实上,内存里乱的很。为啥呢?因为我们有时不止运行一个程序,所以内存里不止一套数据和代码,它们被加载到内存中的位置是随机的!
假如有个新手程序员写了一条指令,在内存中的0100(物理地址:内存的真实地址)处取一次数据,可0100处放的是别的程序的数据,我靠,这不就越俎(zǔ)代庖了吗?而且我可不想我程序的数据被别的程序读取,甚至修改!这其实是因为程序员想直接访问物理内存导致的结果。为了我们的程序能够总是正确的运行,我们不能直接使用物理地址了,这太不安全了,所以设计一套能够“自圆其说”的内存访问方式,这就是程序的重定向。
所以我们采用了内存分段策略,这个段可不是上边的数据段和代码段的段,而是使用“段:偏移”或“段地址:偏移地址”。(逻辑地址:段地址+偏移地址)这怎么理解呢?
可以这么理解:
把段地址想象成一个车技很好的大哥,偏移地址就是个特别听话的小弟。每次加载程序时,都可能在内存中的不同地方,段地址大哥的车技很好,就带着偏移地址小弟在内存中“漂移”,每次加载的时候位置都不一样,但偏移地址小弟只需要跟着段地址大哥飞就行了。
内存分段机制
为了在硬件级实现这样的功能,处理器需要两个段寄存器:代码段寄存器(Code Segment, CS)和数据段寄存器(Data Segment, DS)。加载程序时,CS和DS中的数据都会被重置,当代码段中的指令想要访问数据时,就需要将DS中的段地址与指令中的偏移地址相加,得到访问内存所需的物理地址。
分段机制对程序重定位的好处
从上边的叙述中就可以简单的概括出,使用分段机制,可以更简单的进行程序的重定位。在编写程序时,我们就不用真正考虑数据到底在内存中的什么地方,自有段地址为我们搞定。
INTEL8086处理器内存分段的本质及灵活性
受制于自身限制,INTEL8086的内存分段机制更加复杂。
INTEL8086的内存分段机制
8086内部有4个段寄存器:
- CS:代码段寄存器
- DS:数据段寄存器
- ES:(Extra Segment)附加段寄存器:程序中需要两个数据段时,ES可以为DS提供辅助
- SS:(Stack Segment)栈段寄存器
INTEL8086内存访问
之前说8086有20位外部地址总线,可以访问1MB的内存,可在CPU内部是如何实现的呢?
一共有3个部件参与这个工作。当加载程序时,代码段寄存器DS中会保存当前指令的段地址,IP寄存器会保存段内偏移地址,将段地址左移4位后,加上偏移地址,组成20位的物理地址,这个20位的物理地址不会保存在寄存器中(寄存器里当然也放不下),而是传给输入输出控制电路,然后再传输到地址总线上,从而到内存中寻址。找到代码段中的指令后,会有专门的电路用来执行指令,指令中如果有数据,那么它一定是数据段的偏移地址,我们还需要使用数据段寄存器DS来获得数据段的段地址,将两个地址相加,就可以得到数据的物理地址了。
8086访问内存使用了段地址加偏移地址的分段机制,一共有两种方式:
- 充分利用偏移地址,偏移地址寄存器有16位,内存有20位,那么内存就可以被分成$\frac{2^{20}}{2^{16}}=2^{4}=16$个段,那么每个段就有$2^{16}=64KB$大小,偏移地址寄存器的16位地址全被得到了利用
- 充分利用段地址,段地址寄存器有16位,那么就可以将内存分为$2^{16}=65536$个段,那么每个段就只有$2^{4}=16B$,此时段地址寄存器的16位地址全部得到了利用。
这两种方式肯定有各自的优缺点,这里先不做讨论。
灵活性
通过以上的分析,我们就知道8086的内存访问非常灵活了,那么灵活在哪呢?
我认为,正是由于分段机制的两种策略,再根据不同的使用场景,我们可以将内存分成一定数量的段,这这个数量是由我们决定的,我们可以分16~65536这个区间中的,这么多种不同的数量。(这里面可能涉及字对齐的问题,所以不是任意的数都可以取,假如你非要把内存分成17个段,那么肯定无法平均分配了,最后一块内存不够长了,就会有一定的bug)