上午有时间,继续上一篇文章,本篇的主要内容是如何启动保护模式,这样我们可以利用更大的内存来编程了。
一、我们创建一个顶层Makefile文件,方便之后我们的编译调试
OBJ := system.o loader.o
TOP_DIR := $(PWD)
OBJ_DIR := $(TOP_DIR)/obj
BIN_DIR := $(TOP_DIR)/bin
BIN := system.img
SUB_DIR := system loader
export OBJ_DIR BIN_DIR
all:CHECKDIR $(SUB_DIR)
CHECKDIR:
mkdir -p $(OBJ_DIR) $(BIN_DIR)
$(SUB_DIR) : ECHO
make -C $@
ECHO:
@echo $(SUB_DIR)
@echo begin compile
clean:
rm -rf $(OBJ_DIR)/*.o $(BIN_DIR)
简单说一下这个Makefile,前六行定义了六个变量,export会把变量传递到下一层makefile,当执行make命令的时候,默认执行第一个命令行,即all命令,all命令有两个依赖项,CHECKDIR 和 (SUBDIR),所以会分别执行,而 (SUB_DIR)中有多个项目,则每个项目名空格隔开,make -C的意思是执行下一层makefile,clean就很简单了,我们再看一下层makefile。
loader文件夹中makefile
run : loader.bin
dd if=$(BIN_DIR)/loader.bin of=$(BIN_DIR)/loader.img bs=512 count=1
dd if=$(BIN_DIR)/system.bin of=$(BIN_DIR)/system.img bs=1474048 count=1 conv=sync
dd if=$(BIN_DIR)/system.img of=$(BIN_DIR)/loader.img bs=512 seek=1
qemu-system-i386 -fda $(BIN_DIR)/loader.img -boot a
loader.bin : loader.o Makefile loader.lds
ld -M --oformat binary -m elf_i386 -o $(BIN_DIR)/loader.bin $(OBJ_DIR)/loader.o -T loader.lds
loader.o : loader.s Makefile
as --32 loader.s -o $(OBJ_DIR)/loader.o
system文件夹中的makefile
system.bin : system.o Makefile system.lds
ld -M -m elf_i386 -o $(BIN_DIR)/system $(OBJ_DIR)/system.o -T system.lds
objcopy -O binary $(BIN_DIR)/system $(BIN_DIR)/system.bin
system.o : system.s Makefile
as --32 system.s -o $(OBJ_DIR)/system.o
这两个Makefile的解释可以参考上一篇内容。
然后清理一下两个文件夹,都只留下makefile,lds,和汇编文件.s,回到主目录,执行make命令,成功啦,跟昨天的效果一样。
二、什么是保护模式
1、这是一个历史遗留问题,8086是当年intel的看家宝贝,但却只是一个16位单片机,所以它能寻找的内存地址仅限于0000_0000_0000_0000b~1111_1111 _1111_1111b之间,即64kb内存,64kb的内存也太小了,什么都做不了,于是intel脑洞大开,进行了这种操作,它增加了段寄存器,将内存访问引脚增加到20个,这样就可以访问1mb的空间啦,但是问题来了,随着以后技术的发展,CPU都发展到了32位,不需要段寄存器也可以访问4GB的内存,但是intel为了向下兼容,必须保留这个恶心的段寄存器,所以intel公司的大牛们绞尽脑汁,将段寄存器成功扩展为保护模式。而arm的cpu自从出生就是32位,所以就没有这么繁琐的寻址方式了。
2、现在大部分计算机启动时还是靠bios,而bios运行在16位模式下,它提供了大量的中断加快了计算机底层系统的开发,使得程序员不需要面向计算机硬件,连win7在开机时都不舍得丢弃int13,毕竟开发底层驱动还是耗时耗力。有人或许会问为什么现在32位64位电脑启动一定是16位,那是因为这32位64位这两种模式无法使用bios,我们姑且认为以后可能会是UEFI的天下,但究竟何时才会丢弃bios,我觉得遥遥无期吧,它太方便了。如今arm的bootloader如此的纷繁复杂,一个平台上一点问题都没有换个平台可能根本无法启动,而intel平台就不用担心这种问题,Intel向下兼容的作风和bios的引入使得程序非常容易在各个版本的平台上运行。
三、保护模式的启动流程
1、从实模式到保护模式有很规范的启动流程
1. 设置GDTR
2. 打开A20 GATE
3. 初始化中断
4. 设置CR0控制寄存器
你可能完全不知道我在说什么,没关系,一个一个来看。
2、什么是GDTR、GDT
顾名思义,Global Descriptor Table Regester,全局描述符表寄存器,虽然这个东西网上资料还是不少的,我感觉都比较难懂,我尽量讲的通俗一些,我们可以把这一套系统类比成一本书。GDTR这个寄存器保存着这本书目录的位置,比如说这本书还有前言巴拉巴拉一大堆,但是GDTR告诉了我们这本书的目录在第10页,目录有21页
GDTR是一个48位寄存器。
其中0-15位是表限,就是这个表的大小,就是我这本书目录有多少,单位是字节
16-47位是基址,就是这个表在哪,就是我这本书的目录在哪里,指向了我目录的首地址
我们知道目录的位置了,接下来分析一下目录是怎么编排的,这个目录就叫做GDT,全局描述符表,GDT包含着一章又一章的标题,每一个标题我们称之为全局描述符GD,每一个全局描述符GD由64bit,8字节构成,连续向下排布,就好像书的目录中每一个标题的字数是相同的,看一下GD是怎么设计的,每一章的标题是怎么设计的:
TYPE:参数如下
对于数据段来说, 这 4 位分别是 X、 E、 W、 A 位;而对于代码段来说,这 4 位则分别是 X、 C、 R、 A 位。如下表所示
X 表示是否可以执行( eXecutable)。数据段总是不可执行的, X=0;代码段总是可以执行的,因此, X=1。
对于数据段来说, E 位指示段的扩展方向。 E=0 是向上扩展的,也就是向高地址方向扩展的,是普通的数据段; E=1 是向下扩展的,也就是向低地址方向扩展的,通常是堆栈段。
W 位指示段的读写属性,或者说段是否可写, W=0 的段是不允许写入的,否则会引发处理器异常中断; W=1的段是可以正常写入的。
对于代码段来说, C 位指示段是否为特权级依从的( Conforming)。 C=0 表示非依从的代码段,这样的代码段可以从与它特权级相同的代码段调用,或者通过门调用; C=1 表示允许从低特权级的程序转移到该段执行。
R 位指示代码段是否允许读出。代码段总是可以执行的,但是,为了防止程序被破坏,它是不能写入的。至于是否有读出的可能,由 R 位指定。 R=0 表示不能读出,如果企图去读一个 R=0 的代码段,会引发处理器异常中断;如果 R=1,则代码段是可以读出的,即可以把这个段的内容当成 ROM 一样使用。
也许有人会问,既然代码段是不可读的,那处理器怎么从里面取指令执行呢?事实上,这里的R属性并非用来限制处理器, 而是用来限制程序和指令的行为。
数据段和代码段的 A 位是已访问位,用于指示它所指向的段最近是否被访问过。在描述符创建的时候,应该清零。之后,每当该段被访问时,处理器自动将该位置“ 1”。
现在我们知道了目录和目录中的标题怎么规定的了,接下来看一下怎么查目录,用到了段选择子寄存器。CS, SS, DS, ES 是不是很熟悉,在32位环境下,他们不是段寄存器了,他们被称为段选择子寄存器,他们各自有自己的对应关系,赘述一下吧
1. CS寄存器:与ip连用,构成cs:ip结构,ljmp和jmp的关系要搞清楚,后章有讲
2. SS寄存器:与sp连用,构成ss:sp结构
3. DS寄存器:除了sp使用ss默认段,其他全部使用默认ds段
4. ES寄存器:辅助使用
索引值指向了第几个GD,也就是说目录中的第几个标题,TI=0表示位全局,RPL=00权限最高,等以后用到详细说。
所以我们要用某一块内存的时候,先通过GDTR寻找到目录,然后通过段选择子寄存器看看我们要去找第几个标题,找到相应的标题,就可以读取这一部分内存了。
3、打开A20 GATE
这同样是个历史遗留问题,很多稀奇古怪的东西都是由于系统升级时,为了保持向下兼容而产生的,A20 Gate就是其中之一。
在8086/8088中,只有20根地址总线,所以可以访问的地址是2^20=1M,但由于8086/8088是16位地址模式,能够表示的地址范围是0-64K,所以为了在8086/8088下能够访问1M内存,Intel采取了分段的模式:16位段基地址:16位偏移。其绝对地址计算方法为:16位基地址左移4位+16位偏移=20位地址。
但这种方式引起了新的问题,通过上述分段模式,能够表示的最大内存为:FFFFh:FFFFh=FFFF0h+FFFFh=10FFEFh=1M+64K-16Bytes(1M多余出来的部分被称做高端内存区HMA)。但8086/8088只有20位地址线,如果访问100000h~10FFEFh之间的内存,则必须有第21根地址线。所以当程序员给出超过1M(100000H-10FFEFH)的地址时,系统并不认为其访问越界而产生异常,而是自动从重新0开始计算,也就是说系统计算实际地址的时候是按照对1M求模的方式进行的,这种技术被称为wrap-around。
到了80286,系统的地址总线发展为24根,这样能够访问的内存可以达到2^24=16M。Intel在设计80286时提出的目标是,在实模式下,系统所表现的行为应该和8086/8088所表现的完全一样,也就是说,在实模式下,80286以及后续系列,应该和8086/8088完全兼容。但最终,80286芯片却存在一个BUG:如果程序员访问100000H-10FFEFH之间的内存,系统将实际访问这块内存,而不是像过去一样重新从0开始。
为了解决上述问题,IBM使用键盘控制器上剩余的一些输出线来管理第21根地址线(从0开始数是第20根),被称为A20 Gate:如果A20Gate被打开,则当程序员给出100000H-10FFEFH之间的地址的时候,系统将真正访问这块内存区域;如果A20Gate被禁止,则当程序员给出100000H-10FFEFH之间的地址的时候,系统仍然使用8086/8088的方式。绝大多数IBM PC兼容机默认的A20Gate是被禁止的。由于在当时没有更好的方法来解决这个问题,所以IBM使用了键盘控制器来操作A20 Gate,但这只是一种黑客行为,毕竟A20Gate和键盘操作没有任何关系。在许多新型PC上存在着一种通过芯片来直接控制A20 Gate的BIOS功能。从性能上,这种方法比通过键盘控制器来控制A20Gate要稍微高一点。所以本次代码就直接使用OUT到某个地址来控制A20的开关。
4、CR0的介绍
CR0的位0是启用保护(Protection Enable)标志。当设置该位时即开启了保护模式;当复位时即进入实地址模式。这个标志仅开启段级保护,而并没有启用分页机制。若要启用分页机制,那么PE和PG标志都要置位
四、具体汇编如何实现
基础部分就到此为止了,开始写代码,注释写在代码里了
这是我写的最简单的实模式到保护模式的代码,包括没有初始化中断,直接关掉了中断,为了表现出大体思路,当然是可以运行的,可以用GDB观察到保护模式的成功启动,等有时间我再补一篇GDB的使用,调试内核非常的方便。
这一段程序不能单独使用,由上节内容加载到内存中才能运行,这段程序被放在了0x8200位置处。
.code16
.global _start
.section .text
########################################start 32
############set GDT
movw gdt_base, %ax
############0# empty GDT
movl $0x00, 0(%eax)
movl $0x00, 4(%eax)
############1# code GDT
movl $01ff, 8(%eax)
movl $0x00409800, 12(%eax)
############2# data GDT
movl $0x8000ffff, 16(%eax)
movl $0x0040920b, 20(%eax)
############3# stack GDT
movl $0x00007a00, 24(%eax)
movl $0x00409600, 28(%eax)
##############configuration
movw $31, gdt_size
lgdt gdt_size
#turn on A20
in $0x92, %al
or $0x02, %al
out %al, $0x92
#close interrupt
cli
#configure CR0
movl %cr0, %eax
or $1, %eax
mov %eax, %cr0
#protect mode start
ljmp $0x0008, $(start_protect-_start) #16bits descriptor, 32bits disp
start_protect:
.align 32
.code32
jmp start_protect
gdt_size: .word 0
gdt_base: .long 0x8400 #position of gdt
手打不易,转载请附加链接:http://blog.csdn.net/cheng7606535/article/details/76083210