万物的起源--linux启动过程浅析(2)

在本文的第一篇中,我主要对bootsect.s进行了讲述. 在第二部分中,我将对setup.s进行描述,我将其视为是Linux启动的第二步骤.
操作系统的启动过程是一个漫长而有序的过程,各个阶段都有其不同的作用. boot;setup;init虽然看似很接近,但是却是完全不同的过程.他们各司其职,按部就班.boot比较准确的翻译应该是引导,而setup的翻译则是设置或者建立.这听上去可能有点微妙,不要怪我咬文嚼字,请看官少安毋躁,等你看完了setup.s的过程后,你就会有深刻的体会了.

这是本文的第二部分,我将接续第一部分的内容,直接从bootsect运行完毕,跳转到setup.s开始.
在开始之前,让我们先看看现在计算机处在什么样的状态.下图是一个假想的内存空间,当bootsect运行完毕后,计算机的物理内存分配情况如图所示:
内核的system模块现在处于0x10000起始的内存段中,bootsect.s的二进制码处于0x90000起始的内存段中,而现在即将运行的setup.s则被load在0x90200的内存段中. 需要说明的是,下图的运行结果是在0.11版本的Linux运行完毕之后的结果,如2.6这样的高版本,system模块可能已经相当大了,如果放在0x10000位置上,从0x10000到0x90000的内存空间已经容不下它.在这种情况下,system模块会被load至0x100000起始的地址空间中,这就是所谓的"高地址".当前的程序计数器指向0x90200位置上,既从setup.s开始运行.
     +--------+
      |           |
      |           |
      |           |
      |           | <---------0x100000 "高地址"
      |           |
      |           |
      |           |
      |---------|
      |           | 
      | setup|
      |           |
      |---------| <----0x90200  (程序计数器位置)
      |bootsect|
      |----------| <----0x90000
      |           |
      |           |
      |           |
      |           |
      |           |
      |---------|
      |           |
      |   S      |
      |   y       |
      |   s      |
      |   t       |
      |   e      |
      |   m     |
      |  模块  |
      |            |
      |           |
      |--------| <----0x10000 "低地址"
      |           |
      |           |
      |           |
      +--------+ <----0x00000

现在,让我们来看看setup.s究竟做了些什么工作.一下分析还是基于0.11版本的Linux为主,古老的版本更能清晰的展现出操作系统的原理.而如果直接拿高版本进行分析,反而经常会陷入其他方面的纠缠.
由于接下来的代码都会显得比较长,不会象bootsect.s那样简短(否则也不能称之为操作系统了,^_^).我将把一个文件分段讲述:

首先可以看到的是:
INITSEG  = 0x9000 ! we move boot here - out of the way
SYSSEG   = 0x1000 ! system loaded at 0x10000 (65536).
SETUPSEG = 0x9020 ! this is the current segment
这些符号定义你应该非常熟悉了,只需要使他们与bootsect.s中定义的一致就可以了. 在2.6中,他们都引用于同一个宏定义,更加确定了他们的一致性.

然后使程序的起始.应该说,bootsect.s运行完毕并且跳转的目的,就是下面的start:
entry start
start:

! ok, the read went well so we get current cursor position and save it for
! posterity.

!!下面几行代码的意义使使用0x10中断请求(int 0x10),ah=0x03,读取当前光标位置.
!!并且把结果存放在0x90000处.
 mov ax,#INITSEG ! this is done in bootsect already, but...
 mov ds,ax
 mov ah,#0x03 ! read cursor pos
 xor bh,bh
 int 0x10  ! save it in known place, con_init fetches
 mov [0],dx  ! it from 0x90000.  !!注意,在bootsect中,基地址已经设置为0x9000.
                                   !!所以起始使写倒了0x90000处,覆盖了原先的bootsect.s程序.
!!使用同样的方法取得内存大小.
! Get memory size (extended mem, kB)

 mov ah,#0x88
 int 0x15
 mov [2],ax

!!取显卡信息,呵呵,现在是不是觉得很无聊了.
! Get video-card data:

 mov ah,#0x0f
 int 0x10
 mov [4],bx  ! bh = display page
 mov [6],ax  ! al = video mode, ah = window width

!!...... 建议跳过此断,接下来的工作都是重复的,只为了取得机器参数.
!!写程序有时就是体力劳动.
! check for EGA/VGA and some config parameters

 mov ah,#0x12
 mov bl,#0x10
 int 0x10
 mov [8],ax
 mov [10],bx
 mov [12],cx

! Get hd0 data

 mov ax,#0x0000
 mov ds,ax
 lds si,[4*0x41]
 mov ax,#INITSEG
 mov es,ax
 mov di,#0x0080
 mov cx,#0x10
 rep
 movsb

! Get hd1 data

 mov ax,#0x0000
 mov ds,ax
 lds si,[4*0x46]
 mov ax,#INITSEG
 mov es,ax
 mov di,#0x0090
 mov cx,#0x10
 rep
 movsb

! Check that there IS a hd1 :-)

 mov ax,#0x01500
 mov dl,#0x81
 int 0x13
 jc no_disk1
 cmp ah,#3
 je is_disk1
no_disk1:
 mov ax,#INITSEG
 mov es,ax
 mov di,#0x0090
 mov cx,#0x10
 mov ax,#0x00
 rep
 stosb
is_disk1:

! now we want to move to protected mode ...

 cli   ! no interrupts allowed !

! first we move the system to it's rightful place

 mov ax,#0x0000
 cld   ! 'direction'=0, movs moves forward
这整整一段起始有些沉闷. 就是使用BIOS中断请求,读取机器的各种参数,并且放到目标地址上去.

取完了信息,现在开始更有趣的工作:
! now we want to move to protected mode ...

 cli   ! no interrupts allowed !  !!把中断禁了!

! first we move the system to it's rightful place
!!开始把system整块地往低位迁移,移动倒0x0000地址起始处地内存区域.
!!就是把整个system模块向低位移动了0x10000的距离.
 mov ax,#0x0000
 cld   ! 'direction'=0, movs moves forward
do_move:
 mov es,ax  ! destination segment
 add ax,#0x1000
 cmp ax,#0x9000
 jz end_move
 mov ds,ax  ! source segment
 sub di,di
 sub si,si
 mov  cx,#0x8000
 rep
 movsw
 jmp do_move

! then we load the segment descriptors

!!移动完成,接下来的工作很关键.
!!我们在正准备进入保护模式,所以需要在进入之前先设置GDT和LDT.
!!这里的设置也是暂时性的,只为了进入保护模式而做,之后我们会重新初始化这两张表.
!!对于不了解保护模式的读者,建议先找些资料了解一下.
end_move:
 mov ax,#SETUPSEG ! right, forgot this at first. didn't work :-)
 mov ds,ax
 lidt idt_48  ! load idt with 0,0                    !!设置IDT
 lgdt gdt_48  ! load gdt with whatever appropriate   !!设置GDT
其中idt的地址是0x00000000,可以参照setup.s后的符号定义:
idt_48:
 .word 0   ! idt limit=0
 .word 0,0   ! idt base=0L
而gdt_48的定义为:
gdt_48:
 .word 0x800  ! gdt limit=2048, 256 GDT entries
 .word 512+gdt,0x9 ! gdt base = 0X9xxxx
这里我们就可以看到lgdt的操作数了:
首先是GDT的长度定义:0x800
然后是GDT的起始地址,0x9是基地址,512+gdt是偏移量.其中512=0x0200,而基地址左移16位,0x90000+0x0200=0x90200,正好就是setup.s的起始地址.
再加上gdt的符号偏移,就得到了GDT的地址.
我们再看看gdt的符号定义处:
gdt:
 .word 0,0,0,0  ! dummy

 .word 0x07FF  ! 8Mb - limit=2047 (2048*4096=8Mb)
 .word 0x0000  ! base address=0
 .word 0x9A00  ! code read/exec
 .word 0x00C0  ! granularity=4096, 386

 .word 0x07FF  ! 8Mb - limit=2047 (2048*4096=8Mb)
 .word 0x0000  ! base address=0
 .word 0x9200  ! data read/write
 .word 0x00C0  ! granularity=4096, 386
这里其实定义了3个描述符,一个描述符长度位4字节,32bit.
第一行的.word 0,0,0,0表示第一个描述符全为0.事实上,我们不会使用它.
接着就定义了两个描述符,关于描述符的意义,读者可以参照相关书籍.现在,这两个描述符都指向了0x0000地址.

下面这段牵涉到了一个历史问题.
 call empty_8042
 mov al,#0xD1  ! command write
 out #0x64,al
 call empty_8042
 mov al,#0xDF  ! A20 on
 out #0x60,al
 call empty_8042
打开A20信号线,这里是一个很令人恼火的"历史"问题,即使知道现在,2.6版本中仍然保留着类似的功能.现在还有哪台机器内存没有超过1M的么?或者还有哪段程序会用到A20关闭时候的循环寻址特性么?在下才疏学浅,可能的确有这样的关键应用还在,只是不得而知了.不管怎样,这段代码应该不会影响到读者对Linux操作系统的理解,这个特性也是仅仅针对x86体系结构而存在的,对于考古学没有兴趣的读者大可跳过不看.

最后,这里的代码是关键的关键了:
 mov ax,#0x0001 ! protected mode (PE) bit
 lmsw ax  ! This is it!                     !!将EFLAGS的标志位置掉,这样我们就进入了i386保护模式了.
 jmpi 0,8  ! jmp offset 0 of segment 8 (cs)  !!由于进入了保护模式,跳转语句就有所不同了,这行代码意为跳转到0x0000处,从system的头开始执行.
这里就有点迷惑了,jmpi的第二个操作符8的意思是在GDT中的偏移量,是否还记得在上面讨论过的GDT的定义?一个描述符占用32bit,在jmpi操作中的长度为8,也就是说这句jmpi的意思是说选取第二个描述符,并且偏移量为0.第一个描述符是不用的,只有从第二个描述符开始才有用.

你可能感兴趣的:(万物的起源--linux启动过程浅析(2))