x86汇编-从实模式到保护模式
注释
注释必须以分号“;”开始。
在屏幕上显示文字
文本模式和图形模式是显卡的两种基本工作模式,可以用指令访问显卡,设置它的显示模式。在不同的工作模式下,显卡对显存内容的解释是不同的。
为了访问显存,也需要使用逻辑地址,也就是采用“段地址:偏移地址”的形式
Intel的处理器不允许将一个立即数传送到段寄存器,它只允许这样的指令:
mov 段寄存器,通用寄存器 |
显示字符
mov byte [es:0x00],'L' |
显示标号的汇编地址
asm编译后,除了生成一个以“.bin”为扩展名的二进制文件,还会生成一个以“.lst”为扩展名的列表文件。这张表列出编译后生成的列表文件内容。
在编译阶段,每条指令都被计算并赋予了一个汇编地址,就像它们已经被加载到内存中的某个段里一样。实际上,当编译好的程序加载到物理内存后,它在段内的偏移地址和它在编译阶段的汇编地址是相等的。
源程序的编译是从上往下的,而内存地址的增长是从下往上的(从低地址往高地址方向增长)。
loop
loop指令的功能是重复执行一段相同的代码,处理器在执行它的时候会顺序做两件事: 将寄存器CX的内容减一; 如果CX的内容不为零,转移到指定的位置处执行,否则顺序执行后面的指令。
计算1到100的和
xor ax,ax |
分段、段的汇编地址和段内汇编地址
Intel处理器要求段在内存中的起始物理地址起码是16字节对齐的。这句话的意思是,必须是16的倍数,或者说该物理地址必须能被16整除。 |
实模式下的中断向量表
在实模式下,处理器要求将与该中断有关的程序(指令)的入口点集中存放到内存中从物理地址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的。
存储器的段描述符
段基地址可以是0~4GB范围内的任意地址,不过,还是建议应当选取那些16字节对齐的地址。尽管对于Intel处理器来说,允许不对齐的地址,但是,对齐能够使程序在访问代码和数据时的性能最大化。这一点,对于那些学过计算机原理,特别是了解内存芯片组织的人来说,是最清楚不过的。 |
安装存储器的段描述符并加载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中断都不能再用,因为它们是实模式下的代码。在重新设置保护模式下的中断环境之前,必须关中断。
cli ;保护模式下中断机制尚未建立,应 |
在保护模式下访问一个段时,传送到段选择器的是段选择子。它由三部分组成,第一部分是描述符的索引号,用来在描述符表中选择一个段描述符。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寄存器所能具有的最小值。即,堆栈操作时,必须符合条件:
ESP > 段界限×粒度值 |
对于描述符中G位是“0”的段来说,粒度值是1(字节);而对于G位是“1”的段来说,粒度值是4096(4KB)。
进入32位保护模式
;设置堆栈段和栈指针 |
内核的结构
内核分为四个部分,分别是初始化代码、内核代码段、内核数据段和内核例程段,主引导程序也是初始化代码的组成部分。
初始化代码用于从BIOS那里接管处理器和计算机硬件的控制权,安装最基本的段描述符,初始化最初的执行环境。然后,从硬盘上读取和加载内核的剩余部分,创建组成内核的各个内存段。
内核代码段用于分配内存,读取和加载用户程序,控制用户程序的执行。
内核数据段提供了一段可读写的内存空间,供内核自己使用。
内核例程段用于提供各种用途和功能的子过程以简化代码的编写。这些例程既可以用于内核,也供用户程序调用。
内核文件还包括一个头部,记录了各个段的汇编位置,这些统计数
据用于告诉初始化代码如何加载内核。