avatar

目录
x86汇编-从实模式到保护模式

x86汇编-从实模式到保护模式

注释

注释必须以分号“;”开始。

在屏幕上显示文字

文本模式和图形模式是显卡的两种基本工作模式,可以用指令访问显卡,设置它的显示模式。在不同的工作模式下,显卡对显存内容的解释是不同的。

为了访问显存,也需要使用逻辑地址,也就是采用“段地址:偏移地址”的形式

Intel的处理器不允许将一个立即数传送到段寄存器,它只允许这样的指令:

Code
mov 段寄存器,通用寄存器 
mov 段寄存器,内存单元

mov ax,0xb800 ;指向文本模式的显示缓冲区
mov es,ax

显示字符

Code
mov byte [es:0x00],'L'
mov byte [es:0x01],0x07 ;将属性值0x07传送到下一个内存单元
mov byte [es:0x02],'a'
mov byte [es:0x03],0x07
mov byte [es:0x04],'b'
mov byte [es:0x05],0x07

显示标号的汇编地址

asm编译后,除了生成一个以“.bin”为扩展名的二进制文件,还会生成一个以“.lst”为扩展名的列表文件。这张表列出编译后生成的列表文件内容。

在编译阶段,每条指令都被计算并赋予了一个汇编地址,就像它们已经被加载到内存中的某个段里一样。实际上,当编译好的程序加载到物理内存后,它在段内的偏移地址和它在编译阶段的汇编地址是相等的。

源程序的编译是从上往下的,而内存地址的增长是从下往上的(从低地址往高地址方向增长)。

loop

loop指令的功能是重复执行一段相同的代码,处理器在执行它的时候会顺序做两件事: 将寄存器CX的内容减一; 如果CX的内容不为零,转移到指定的位置处执行,否则顺序执行后面的指令。

计算1到100的和

Code
    xor ax,ax
mov cx,1
@f:
add ax,cx
inc cx
cmp cx,100
jle @f

分段、段的汇编地址和段内汇编地址

Code
Intel处理器要求段在内存中的起始物理地址起码是16字节对齐的。这句话的意思是,必须是16的倍数,或者说该物理地址必须能被16整除。
相应地,汇编语言源程序中定义的各个段,也有对齐方面的要求。具体做法是,在段定义中使用“align=”子句,用于指定某个SECTION的汇编地址对齐方式。比如说,“align=16”就表示段是16字节对齐的,“align=32”就表示段是32字节对齐的。
段的重定位是加载器的工作,它需要知道每个段在用户程序内的位置,即它们分别位于用户程序内的多少字节处。为此,需要在用户程序头部建立一张段重定位表。

实模式下的中断向量表

在实模式下,处理器要求将与该中断有关的程序(指令)的入口点集中存放到内存中从物理地址0x00000开始,到0x003ff结束,共1KB的空间内,这就是所谓的中断向量表(Interrupt Vector Table,IVT)。

初始化8259、RTC和中断向量表

当处理器执行任何一条改变堆栈段寄存器SS的指令时,它会在下一条指令执行完期间禁止中断。

绝大多数时候,对堆栈的改变是分两步进行的:先改变段寄存器SS的内容,接着又修改堆栈指针寄存器SP的内容。

软 中 断

int3和int 3不是一回事。前者的机器码为CC,后者则是CD 03,这就是通常所说的int n,其操作码为0xCD,第2字节的操作数给出了中断号。

into是溢出中断指令,机器码为0xCE,也是单字节指令。当处理器执行这条指令时,如果标志寄存器的OF位是1,那么,将产生4号中断。否则,这条指令什么也不做。

32位保护模式

32位Intel微处理器编程架构

80286和8086不一样的地方在于,它第一次提出了保护模式的概念。在保护模式下,段寄存器中保存的不再是段地址,而是段选择子,真正的段地址位于段寄存器的描述符高速缓存中,是24位的。因此,运行在保护模式下的80286处理器可以访问全部16MB内存。

在保护模式下,所有的32位处理器都可以访问多达4GB的内存,它们可以工作在分段模型下,每个段的基地址是32位的,段内偏移量也是32位的,因此,段的长度不受限制。在最典型的情况下,可以将整个4GB内存定义成一个段来处理,这就是所谓的平坦模式。在平坦模式下,可以执行4GB范围内的控制转移,也可以使用32位的偏移量访问任何4GB范围内的任何位置。32位保护模式兼容80286的16位保护模式。

在实模式下,用户程序对内存的访问非常自由,没有任何限制,随随便便就可以修改任何一个内存单元。

全局描述符表

为了让程序在内存中能自由浮动而又不影响它的正常执行,处理器将内存划分成逻辑上的段,并在指令中使用段内偏移地址。在保护模式下,对内存的访问仍然使用段地址和偏移地址,但是,在每个段能够访问之前,必须先进行登记。

和一个段有关的信息需要8个字节来描述,所以称为段描述符(Segment Descriptor),每个段都需要一个描述符。为了存放这些描述符,需要在内存中开辟出一段空间。在这段空间里,所有的描述符都是挨在一起,集中存放的,这就构成一个描述符表。
最主要的描述符表是全局描述符表(Global Descriptor Table,GDT),所谓全局,意味着该表是为整个软硬件系统服务的。在进入保护模式前,必须要定义全局描述符表。

描述符不是由用户程序自己建立的,而是在加载时,由操作系统根据你的程序结构而建立的,而用户程序通常是无法建立和修改GDT的。

存储器的段描述符

Code
段基地址可以是0~4GB范围内的任意地址,不过,还是建议应当选取那些16字节对齐的地址。尽管对于Intel处理器来说,允许不对齐的地址,但是,对齐能够使程序在访问代码和数据时的性能最大化。这一点,对于那些学过计算机原理,特别是了解内存芯片组织的人来说,是最清楚不过的。
20位的段界限用来限制段的扩展范围。因为访问内存的方法是用段基地址加上偏移量,所以,对于向上扩展的段,如代码段和数据段来说,偏移量是从0开始递增,段界限决定了偏移量的最大值;对于向下扩展的段,如堆栈段来说,段界限决定了偏移量的最小值。
G位是粒度(Granularity)位,用于解释段界限的含义。当G位是“0”时,段界限以字节为单位。此时,段的扩展范围是从1字节到1兆字节(1B~1MB),因为描述符中的界限值是20位的。相反,如果该位是“1”,那么,段界限是以4KB为单位的。这样,段的扩展范围是从4KB到4GB。
S位用于指定描述符的类型(Descriptor Type)。当该位是“0”时,表示是一个系统段;为“1”时,表示是一个代码段或者数据段(堆栈段也是特殊的数据段)。系统段将在以后介绍。
DPL表示描述符的特权级(Descriptor Privilege Level,DPL)。这两位用于指定段的特权级。共有4种处理器支持的特权级别,分别是0、1、2、3,其中0是最高特权级别,3是最低特权级别。刚进入保护模式时执行的代码具有最高特权级0(可以看成是从处理器那里继承来的),这些代码通常都是操作系统代码,因此它的特权级别最高。每当操作系统加载一个用户程序时,它通常都会指定一个稍低的特权级,比如3特权级。不同特权级别的程序是互相隔离的,其互访是严格限制的,而且有些处理器指令(特权指令)只能由0特权级的程序来执行,为的就是安全。
P是段存在位(Segment Present)。P位用于指示描述符所对应的段是否存在。一般来说,描述符所指示的段都位于内存中。但是,当内存空间紧张时,有可能只是建立了描述符,对应的内存空间并不存在,这时,就应当把描述符的P位清零,表示段并不存在。另外,同样是在内存空间紧张的情况下,会把很少用到的段换出到硬盘中,腾出空间给当前急需内存的程序使用(当前正在执行的),这时,同样要把段描述符的P位清零。当再次轮到它执行时,再装入内存,然后将P位置1。
P位是由处理器负责检查的。每当通过描述符访问内存中的段时,如果P位是“0”,处理器就会产生一个异常中断。通常,该中断处理过程是由操作系统提供的,该处理过程的任务是负责将该段从硬盘换回内存,并将P位置1。在多用户、多任务的系统中,这是一种常用的虚拟内存调度策略。当内存很小,运行的程序很多时,如果计算机的运行速度变慢,并伴随着繁忙的硬盘操作时,说明这种情况正在发生。
D/B位是“默认的操作数大小”(Default Operation Size)或者“默认的堆栈指针大小”(Default Stack Pointer Size),又或者“上部边界”(Upper Bound)标志。

安装存储器的段描述符并加载GDTR

处于实模式下,在GDT中安装描述符,必须将GDT的线性地址转换成段地址和偏移地址。

处理器规定,GDT中的第一个描述符必须是空描述符,或者叫哑描述符或NULL描述符。

保护模式下的内存访问

控制这达实模式和保护模式切换的开关是在一个叫CR0的寄存器。

CR0是处理器内部的控制寄存器(Control Register,CR)。之所以有个“0”后缀,是因为还有CR1、CR2、CR3和CR4控制寄存器,甚至还有CR8。
CR0是32位的寄存器,包含了一系列用于控制处理器操作模式和运行状态的标志位。它的第1位(位0)是保护模式允许位(Protection Enable,PE),是开启保护模式大门的门把手,如果把该位置“1”,则处理器进入保护模式,按保护模式的规则开始运行。

保护模式下的中断机制和实模式不同,因此,原有的中断向量表不再适用,而且,必须要知道的是,在保护模式下,BIOS中断都不能再用,因为它们是实模式下的代码。在重新设置保护模式下的中断环境之前,必须关中断。

Code
cli                                ;保护模式下中断机制尚未建立,应 
;禁止中断
mov eax,cr0
or eax,1
mov cr0,eax ;设置PE位

;以下进入保护模式... ...

在保护模式下访问一个段时,传送到段选择器的是段选择子。它由三部分组成,第一部分是描述符的索引号,用来在描述符表中选择一个段描述符。TI 是描述符表指示器(Table Indicator),TI=0 时,表示描述符在GDT 中;TI=1 时,描述符在LDT 中。LDT 也是一个描述符表,和GDT 类似。RPL 是请求特权级,表示给出当前选择子的那个程序的特权级别,正是该程序要求访问这个内存段。每个程序都有特权级别,

清空流水线并串行化处理器

即使是在实模式下,段寄存器的描述符高速缓存器也被用于访问内存,仅低20位有效,高12位是全零。当处理器进入保护模式后,不影响段寄存器的内容和使用,它们依然是有效的,程序可以继续执行。但是,在保护模式下,对段的解释是不同的,处理器会把段选择器里的内容看成是描述符选择子,而不是逻辑段地址。因此,比较安全的做法是尽快刷新CS、SS、DS、ES、FS和GS的内容,包括它们的段选择器和描述符高速缓存器。

在进入保护模式前,有很多指令已经进入了流水线。因为处理器工作在实模式下,所以它们都是按16位操作数和16位地址长度进行译码的,即使是那些用bits 32编译的指令。进入保护模式后,由于对段地址的解释不同,对操作数和默认地址大小的解释也不同,有些指令的执行结果可能会不正确,所以必须清空流水线。同时,那些通过乱序执行得到的中间结果也是无效的,必须清理掉,让处理器串行化执行,即,重新按指令的自然顺序执行。

使用32位远转移指令jmp或者远过程调用指令call。处理器最怕转移指令,遇到这种指令,一般会清空流水线,并串行化执行;另一方面,远转移会重新加载段选择器CS,并刷新描述符高速缓存器中的内容。唯一的问题是,这条指令必须在bits 16下编译,使得处理器能够在16位模式下正确译码;同时,还必须编译成32位操作数的指令,使处理器在刚进入保护模式时能正确执行。一个建议的方法是在设置了控制寄存器CR0的PE位之后,立即用jmp或者call转移到当前指令流的下一条指令上。

保护模式下的堆栈

堆栈是向下扩展的,因此,描述符中的段界限,和向上扩展的段含义不同。对于向上扩展的段,段内偏移量是从0开始递增,偏移量的最大值是界限值和粒度的乘积;而对于向下扩展的段来说,因为它经常用做堆栈段,而堆栈是从高地址向低地址方向推进的,故段内偏移量的最小值是界限值和粒度的乘积加一。在32位代码中,是用ESP作为堆栈指针的。因此,这里的段界限,用来和段粒度一起,决定ESP寄存器所能具有的最小值。即,堆栈操作时,必须符合条件:

Code
ESP > 段界限×粒度值

对于描述符中G位是“0”的段来说,粒度值是1(字节);而对于G位是“1”的段来说,粒度值是4096(4KB)。

进入32位保护模式

Code
;设置堆栈段和栈指针 
mov eax,cs
mov ss,eax
mov sp,0x7c00

;计算GDT所在的逻辑段地址
mov eax,[cs:pgdt+0x7c00+0x02] ;GDT的32位线性基地址
xor edx,edx
mov ebx,16
div ebx ;分解成16位逻辑地址

mov ds,eax ;令DS指向该段以进行操作
mov ebx,edx ;段内起始偏移地址

;创建0#描述符,它是空描述符,这是处理器的要求
mov dword [ebx+0x00],0x00000000
mov dword [ebx+0x04],0x00000000

;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
mov dword [ebx+0x08],0x0000ffff ;基地址为0,段界限为0xfffff
mov dword [ebx+0x0c],0x00cf9200 ;粒度为4KB,存储器段描述符

;创建保护模式下初始代码段描述符
mov dword [ebx+0x10],0x7c0001ff ;基地址为0x00007c00,512字节
mov dword [ebx+0x14],0x00409800 ;粒度为1个字节,代码段描述符

;创建以上代码段的别名描述符
mov dword [ebx+0x18],0x7c0001ff ;基地址为0x00007c00,512字节
mov dword [ebx+0x1c],0x00409200 ;粒度为1个字节,数据段描述符

mov dword [ebx+0x20],0x7c00fffe
mov dword [ebx+0x24],0x00cf9600

;初始化描述符表寄存器GDTR
mov word [cs: pgdt+0x7c00],39 ;描述符表的界限

lgdt [cs: pgdt+0x7c00]

in al,0x92 ;南桥芯片内的端口
or al,0000_0010B
out 0x92,al ;打开A20

cli ;中断机制尚未工作

mov eax,cr0
or eax,1
mov cr0,eax ;设置PE位

;以下进入保护模式... ...

内核的结构

内核分为四个部分,分别是初始化代码、内核代码段、内核数据段和内核例程段,主引导程序也是初始化代码的组成部分。

初始化代码用于从BIOS那里接管处理器和计算机硬件的控制权,安装最基本的段描述符,初始化最初的执行环境。然后,从硬盘上读取和加载内核的剩余部分,创建组成内核的各个内存段。

内核代码段用于分配内存,读取和加载用户程序,控制用户程序的执行。

内核数据段提供了一段可读写的内存空间,供内核自己使用。
内核例程段用于提供各种用途和功能的子过程以简化代码的编写。这些例程既可以用于内核,也供用户程序调用。

内核文件还包括一个头部,记录了各个段的汇编位置,这些统计数
据用于告诉初始化代码如何加载内核。

文章作者: kabeor
文章链接: https://kabeor.github.io/x86%E6%B1%87%E7%BC%96%E4%BB%8E%E5%AE%9E%E6%A8%A1%E5%BC%8F%E5%88%B0%E4%BF%9D%E6%8A%A4%E6%A8%A1%E5%BC%8F/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 K's House

评论