在本文的第一篇中,我主要对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.第一个描述符是不用的,只有从第二个描述符开始才有用.