linux-0.11中保护模式建立过程的分析[1]

来源:互联网 发布:交通数据 编辑:程序博客网 时间:2024/04/30 21:36

学习linux有一段时间了。在学习过程中,发现赵烔博士编著《linux内核完全注释》是一本很好的linux入门书。该书主要是以linux-0.11的源代码为线索来讲述linux-0.11的实现原理。linux-0.11Linus写的比较老的一个linux版本,该版本的linux利用了很多80x86的特性,特别是保护模式的一些相关特性。理解保护模式的工作细节是理解linux-0.11的关键。我在理解这一部分内容时,也花了很长时间,很懊悔当初没有很好的学习《微机原理》这门课啊。在这方面赵烔博士编著《linux内核完全注释》已经写得很详细了,不过我觉得关键细节的地方不够突出,也许这不是《linux内核完全注释》要讲解的重点。所以我从我学习的角度并结合linux-0.11的三个文件bootsect.ssetup.shead.s来说说80x86实模式和保护模式的一下工作细节。

 

版权说明:作者保留本电子书籍的修改和正式出版的所有权利。读者可以自由传播本书的全部和部分内容,但需要注明出处。由于本人的水平有限,难免有错误和不足的地方,希望读者能给予批评指出或者建议。本人电子邮箱:lzhengwen@gmail.com

 

作者建议读者在阅读本文之前先看看80x86保护模式下编程方面的资料。这方面的资料太多了,作者并没有在这方面做一个系统的介绍,但是阅读本文又需要这方面的知识。读者最好手里有一份linux-0.11的源代码并对汇编指令有一定的了解。给读者推荐一个软件:bochs80x86的仿真软件。它可以跟踪调试底层的代码,对学习linux-0.11有很大的帮助。

CPU刚上电时, CR0寄存器的值为0x00000010,此时CPU是工作在实模式。实模式下,处理器是根据段寄存器CS和寄存器iP的值来取指令。刚上电时,CS的值为0xf000iP的值为0xfff0,所以CPU此时是从地址[0xffff0] 0xf000:0xfff0处取得的第一条指令,这也是BIOS存放的第一条指令处,该指令通常是一条跳转指令。此时也就把CPU的控制权交给了BIOS。通过bochs软件,刚上电时CPU各寄存器的值如图1

BIOS在经过一些自检测和初始化以后,就会根据BIOS的设置得到引导驱动器的顺序(这些引导驱动器包括软盘、硬盘、光盘或者一些USB储存设备),然后依次检查,直到找一个可用来引导的驱动器。那么BIOS是根据什么来确定该驱动器是可以用来作为引导的呢?在BIOS识别可引导驱动器过程中,它会把驱动器第一扇区的内容读到[0x07c00] 0x0000:0x7c00处,如果个扇区的最后两个字节是“55 AA”,那么这就是一个引导扇区,这个磁盘也就是一块可引导盘。通常这个大小为512B 的程序就称为引导程序(boot)。如果最后两个字节不是“55 AA”,那么BIOS 就检查下一个磁盘驱动器。

    BIOS找到可引导的驱动器后,该驱动器引导扇区的内容也就读到了[0x07c00] 0x0000:0x7c00处,BIOS就会利用段间跳转指令跳到[0x07c00] 0x0000:0x7c00处开始执行引导代码,也就是BIOSCPU的控制权交给了引导代码。此时,CPU还是工作在实模式下。CPU个寄存器的值如图2所示:

通常我们的代码就是从这个地方开始运行的。通过bochs软件,可以看出,这时CPU各寄存器的值和刚上电时的没太多的不一样。所以我们要想让程序工作在一个完整的32位保护模式环境,那么我们就要亲自一步一步的建立该环境。下面结合linux-0.11来说明:

段的划分,有利用于应用程序的定位。当一个程序是从0x0000地址开始编译链接的时候,假如程序没有用到地址无关的编程技术,那么在没有出现分段

                   图一:刚上电时,CPU各寄存器的值

                         2:运行完BIOS时的CPU

技术时(更别说后来出现的MMU技术),该程序就只能是从物理地址0x0000开始运行,但是有了分段技术以后,该程序就可以加载到任意位置去去执行,只要设置好段寄存器CS就可以了。在这里看来,编译链接的所用的地址(逻辑地址)都是段寄存器的一个偏移量了。所以要对逻辑地址进行准确的寻址就要先设置好段寄存器。段寄存器CS是一个特殊的段寄存器,因为它是用来寻址代码指令的,在一边寻址加载代码指令,另一边又在修改寻址参数(段寄存器的值),这是比较危险的行为。所以段寄存器CS一般是不通过直接加载的方式来修改,在要跨段寻址取指令的时候就要用段间跳转指令或者call指令来完成,这些指令会要求处理器刷新段寄存器CS。下面先分析linux-0.11bootsect.s的部分代码:

从图2可以知道,运行完BIOS时,段寄存器CS的值的为:0x0000,寄存器ip的值为:0x7c00,所以cpu会在0x07c00处取得引导扇区的第一条指令。从这两个寄存器(CSip)可以知道这时的CPU不是从一个段的偏移量为0的地址开始寻址的(偏移量是0x7c00),所以引导代码的程序代码一般来说是应该从偏移地址0x7c00开始编译链接。该程序要是想从偏移地址0x0000开始编译链接,那么就要使用地址无关编程技术或者改变段寄存器CS的值,改变段寄存器CS的值的最好办法就是使用段间跳转指令,我想这也许是Linus要把bootsect.s的代码从0x0000:0x7c00搬到0x9000:0x0000的原因吧。因为bootsect.s的代码是从逻辑地址0x0000开始编译链接的。bootsect.s的头几条指令首先为搬运代码设置好段寄存器的值接着就把自身(512字节)搬运到指定的地方,(这几条指令是跟地址无关的)

45 entry start
46 start:
47         mov     ax,#BOOTSEG
48         mov     ds,ax
49         mov     ax,#INITSEG
50         mov     es,ax
51         mov     cx,#256
52         sub     si,si
53         sub     di,di
54         rep
55         movw

在搬完自身代码以后,Linus利用了段间跳转指令来改变了段寄存器CS的值。

56         jmpi    go,INITSEG

在运行这条指令之前,段寄存器CS值一直是0x0000的,在运行这条指令之后,段寄存器CS值就变成了0x9000了,这样就解决了bootsect.s的代码是从逻辑地址0x0000开始编译链接的问题了。

    代码跳转到0x9000:$go以后,Linus首先把几个段寄存器的值设置成了跟段寄存器CS的值是一样的,这是因为bootsect.s的代码数字段和代码段是重叠的(这跟编译链接有关),

57 go:     mov     ax,cs
58         mov     ds,ax
59         mov     es,ax

接着就是设置栈寄存器sssp,为栈留出空间,这样就可以使用call等等要用到栈的指令了。

60 ! put stack at 0x9ff00.
61         mov     ss,ax
62         mov     sp,#0xFF00              ! arbitrary value >>512

接着bootsect.s利用BIOS中断(int指令,第72行代码)把setup.s代码从引导盘读到指定的位置,这里是[0x90200] 0x9020:0x0000,紧跟着bootsect.s

67 load_setup:
68         mov     dx,#0x0000              ! drive 0, head 0
69         mov     cx,#0x0002              ! sector 2, track 0
70         mov     bx,#0x0200              ! address = 512, in INITSEG
71         mov     ax,#0x0200+SETUPLEN     ! service 2, nr of sectors
72         int     0x13                    ! read it
73         jnc     ok_load_setup           ! ok - continue
74         mov     dx,#0x0000
75         mov     ax,#0x0000              ! reset the diskette
76         int     0x13
77         j       load_setup
上面提到了BIOS中断,那什么是BIOS中断呢?BIOS中断是BIOS为用户提供程序资源时提供的通讯接口。BIOS通讯的机制有点像linux系统的系统调用。这种通讯方式可以解决两个独立模块中(是指两个独立编译链接可运行的程序)一个模块“调用”另一个模块函数的寻址问题。一般来说,程序中调用函数的地址确定是由编译器和连接器来完成的,但是当两个程序是分开来独立编译和链接的话,那么就没办法来通过这种办法来确定函数的地址了(当然,通过符号表还是可以用这种办法来实现的,但是这样就很麻烦了)。所以BIOS为了让用户可以使用BIOS的程序资源,就编制了一套中断服务程序给用户使用(因为中断寻址是通过硬件实现的,不存在编译链接问题了)。在用户程序中主要使用中断指令就可以准确的调用相应的中断服务程序了,所以也可以说,BIOS提供的程序资源就是一些中断服务程序。当我们想利用BIOS提供的程序资源时,就人为地产生相关中断。 
接着就是要得到磁盘驱动器的一些信息,并把每道的扇区数量保存在变量sectors中,该变量在第241行申明。
83         mov     dl,#0x00
84         mov     ax,#0x0800       ! AH=8 is get drive parameters
85         int     0x13
86         mov     ch,#0x00
87         seg cs
88         mov     sectors,cx
89         mov     ax,#INITSEG
90         mov     es,ax
 
241 sectors:
242         .word 0
Linus为什么要在这里得到磁盘驱动器的信息呢?这里先卖个关子,往后再回答这个问题。跟着就是一个很重要的环节,bootsect.s要把system模块从磁盘读到指定的内存位置,这里是[0x10000]0x1000:0x0000。它先是在显示屏上打印"Loading system ..."这些字样然后再读盘。
94         mov     ah,#0x03                ! read cursor pos
95         xor     bh,bh
96         int     0x10
97         
98         mov     cx,#24
99         mov     bx,#0x0007       ! page 0, attribute 7 (normal)
100         mov     bp,#msg1
101         mov     ax,#0x1301              ! write string, move cursor
102         int     0x10
 
107         mov     ax,#SYSSEG
108         mov     es,ax           ! segment of 0x010000
109         call    read_it
 
244 msg1:
245         .byte 13,10
246         .ascii "Loading system ..."
247         .byte 13,10,13,10
当读者阅读setup.s时,发现setup.s又把system模块从该位置搬到0x00000处。也许你就会问,为什么不在bootsect.s就直接把system模块搬到0x00000处呢?这跟前面提到的BIOS中断有关,因为这时0x00000处放着BIOS的中断向量表或者说是中断描述符表,要是把system模块放在这里就会把BIOS的中断向量表覆盖,那么用户就没办法跟BIOS通讯了。因为setup.s还要通过BIOS中断来取得硬件相关的信息,等到不需要用到BIOS中断时,setup.s就会把system模块搬到0x00000处。那为什么bootsect.ssystem模块放在[0x10000] 0x1000:0x0000是合适的呢?这就要看看BIOSCPU控制权交给bootsect.s时前1M的内存(因为这时处理器是处于实模式也就是8086模式,该模式下,处理器只能寻址1M的内存空间)空间使用情况了。用图3说明:使用情况一目了然。
关于中断描述符表,这里不多说,因为这方面的资料很多,只是简单说明从产生中断到调用服务程序的过程。跟中断描述符表(IDT表)有着密切关系的寄存器是IDTR寄存器。该寄存器与GDTR的数据结构一样,共6个字节,前4个字节存放着IDT表的基地址,后两个字节存放的是该表的限长。IDT表中每8个字节作为一项,对应着一个中断,存放的是该中断服务程序首地址的相关信息和保护信息。当中断产生时,处理器就会从IDTR寄存器处得到IDT表的基地址,并结合中断号查表,得到服务程序的首地址后就跳转到该服务程序中去执行。
system模块顺利的从磁盘中读到内存时,bootsect.s就把软盘驱动器关掉,为有可能要把软盘更换成根文件系统盘做好准备。
110         call    kill_motor
接着bootsect.s就要确定根文件系统设备了,也就是回答前面提出的问题的时候了。把每磁道的扇区数保存在变量sectors中是为寻找根文件系统设备做准备。根文件系统是完整linux系统的一部分,linux-0.11允许为内核指定根文件系统设备,由第43行代码指定。
43 ROOT_DEV = 0x306
 
31M内存的使用情况
这里的0x306是指第二个硬盘的第一分区,当年Linus就是在自己电脑第二个硬盘上安装了根文件系统。如果没有指定,ROOT_DEV = 0x00时,那么linux就会根据变量sectors的数值来确定目前的A驱动器是1.2M还是1.44M
117         seg cs
118         mov     ax,root_dev
119         cmp     ax,#0
120         jne     root_defined
121         seg cs
122         mov     bx,sectors
123         mov     ax,#0x0208              ! /dev/ps0 - 1.2Mb
124         cmp     bx,#15
125         je      root_defined
126         mov     ax,#0x021c              ! /dev/PS0 - 1.44Mb
127         cmp     bx,#18
128         je      root_defined
129 undef_root:
130         jmp undef_root
同是A驱动器,为什么要这样区分呢?在没有指定根文件系统设备时,系统就会默认根文件系统设备为A驱动器,也就是软驱。这软驱有两种规格,1.2M1.44M的,并且这两种软驱在linux中是表现成两种不同的驱动设备,所以在这里要把他们区分清楚。之后就会把确定的根文件系统设备号写到指定的位置,这里是引导扇区第508509两个字节处。
131 root_defined:
132         seg cs
133         mov     root_dev,ax
 
249 .org 508
250 root_dev:
251         .word ROOT_DEV
到这里,引导程序bootsect.s的任务已经完成了。由第253行代码知道这是一个可用来引导的引导程序。上面说到只要第一扇区最后两个字节的值是0x55AA,那么该扇区就是引导扇区。也许你会问,这里写着是0xAA55啊。这是因为80x86处理器使用的是大端模式。至于什么是大端小端模式,读者可以查询相关资料。
249 .org 508
250 root_dev:
251         .word ROOT_DEV
252 boot_flag:
253         .word 0xAA55
接着bootsect.s就通过一条段间跳转指令跳转到0x9020:0x0000处(setup.s代码的存放处),把CPU的控制权交给了setup.s
139         jmpi    0,SETUPSEG
    只有512字节的引导程序干不了很多事情。bootsect.s在这里只做了三件事情,就是把setup.ssystem模块从磁盘读到指定的位置并且找到根文件系统设备号,为加载根文件系统做好准备。如果一切顺利的话,现在整个linux内核都已经在内存上,那么软盘驱动器上系统盘就可以卸载了。为在软盘驱动器上更换根文件系统盘做好准备。
原创粉丝点击