从零开始搭建环境编写操作系统 AT&T GCC (二)从实模式到保护模式

  上午有时间,继续上一篇文章,本篇的主要内容是如何启动保护模式,这样我们可以利用更大的内存来编程了。

一、我们创建一个顶层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位寄存器。
从零开始搭建环境编写操作系统 AT&T GCC (二)从实模式到保护模式_第1张图片
  其中0-15位是表限,就是这个表的大小,就是我这本书目录有多少,单位是字节
  16-47位是基址,就是这个表在哪,就是我这本书的目录在哪里,指向了我目录的首地址

  我们知道目录的位置了,接下来分析一下目录是怎么编排的,这个目录就叫做GDT,全局描述符表,GDT包含着一章又一章的标题,每一个标题我们称之为全局描述符GD,每一个全局描述符GD由64bit,8字节构成,连续向下排布,就好像书的目录中每一个标题的字数是相同的,看一下GD是怎么设计的,每一章的标题是怎么设计的:
从零开始搭建环境编写操作系统 AT&T GCC (二)从实模式到保护模式_第2张图片

  1. 基址:指向的内存首地值
  2. 段限:这一个段的大小,单位可以是byte,可以是4KB
  3. G标志位:段的长度单位,G=0单位位1字节,G=1,单位位4KB
  4. D/B标志位:为0表示16位段,为1表示32位段
  5. 0标志位:为64位CPU预留,这里把它置0
  6. AVL:留给程序员使用
  7. P标志位:为0此段无效,不能使用
  8. DPL:特权等级,00为最高等级
  9. S标志位:为1表示代码段或数据段,为0表示系统段(具体可以百度百科)
  10. TYPE:参数如下
      对于数据段来说, 这 4 位分别是 X、 E、 W、 A 位;而对于代码段来说,这 4 位则分别是 X、 C、 R、 A 位。如下表所示
      从零开始搭建环境编写操作系统 AT&T GCC (二)从实模式到保护模式_第3张图片
      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寄存器:辅助使用

      下面介绍一下段选择子寄存器的分布:
    从零开始搭建环境编写操作系统 AT&T GCC (二)从实模式到保护模式_第4张图片

      索引值指向了第几个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

你可能感兴趣的:(OranOS)