这一节讲讲怎么x86是怎么boot的。
在进入到内核之前我们先得了解一下机器从上电到启动经历了什么。装过机器的人都知道机箱里面最重要的配件的是主板。CPU内存等等都在主板上。系统的启动对于CPU来讲就是不停的执行指令,至于指令是什么意义他是不管的,他只是不停地从存放指令的地方取出指令执行指令。一般的这个存放指令的地方是内存,但是内存都是掉电就会失去所有内容的,因此在上电之初内存是什么都没有的,所以CPU不可能是从内存获取指令的。另外一个问题是CPU执行指令的地址是由CPU内部一个叫cs和ip的寄存器决定的,他们的初始值决定了第一条指令在什么地方。这两个问题的解决方法是在上电之初cs:ip被强行初始化为一个特定的值,这个值指向的地址是主板上的一个可以在没电的情况下依然存储数据的flash,里面放着操作系统启动前需要执行的一些指令,以前是BIOS现在多是uefi,他们主管开机自检。这样机器上电之后马上开始执行的就是BIOS或uefi了。但是我们的操作系统基于假设flash里面执行的就是BIOS,uefi我没搞过不敢乱说。
当BIOS结束前将要把控制权交给操作系统时它还做了一件事。它把磁盘上的第一个扇区的内容copy到了内存的一个特殊的地址--0x7c00处。这看起来是个奇怪的地址,至于他是怎么来的很多人有过解释,有兴趣自行百度Google。这个地址非常重要,因为它影响到了我们boot程序的编写。如果不知道这一节你的程序大概率是不能启动的。
其次,BIOS在copy之前可是经过一番考察的,不是任何放在硬盘第一扇区上的指令流都会被copy,BIOS只认真正的启动代码。那么BIOS是靠什么辨认第一扇区中的代码是不是他要的呢?很简单,它只检查该扇区最后两个字节的值是不是0xaa55,只要满足这个条件它就会认为这是启动代码。所以我们的boot代码至少要注意两点,1、起始地址是0x7c00; 2、写满512字节,并以0xAA55结尾。
最后要了解内核最终是怎么运行起来的。上文讲过,BIOS只是把硬盘第一扇区的内容copy到内存,一个扇区只有512字节,肯定不是内核的全部甚至都不是内核。那么内核代码是怎么运行起来的呢?首先我们要知道内核代码也是放在硬盘上的,运行它必须先加载它,因此boot代码要做的就是加载内核到内存中,并jmp过去。在horos中,系统代码可以分成三个部分,这是参考了Linux0.11的内核源码,boot.asm,setup.asm和kernel.boot.asm负责加载另外两部分到内存并跳转到setup代码,setup.asm 负责设置实模式到保护模式、段描述符并跳转到kernel。
接下来我们要接触汇编代码了。不用怕,其实汇编是比较简单的,没什么大的难度,初次接触汇编可以看王爽的《汇编语言》我对汇编也只是知道很简单几个指令,但是无需太多就可以写出能够启动的代码。写汇编之前我们首先要知道汇编的一些基本常识,我在学汇编之前一直以为汇编只是跟CPU有关,其实没那么简单。首先不同的的指令集的汇编语言自然是不一样的,同时,汇编还跟编译器有关,不同的编译器也有自己独特的语法。流行的汇编器nasm、as、masm等各有不同,其中nasm语法简单,很受大家的欢迎,因为as是集成在gcc里面的,如果写嵌入汇编是不得不写as格式的汇编的。我的代码里面独立的汇编代码都是用nasm做汇编器的。
我的boot代码放在boot/arch/$(uname -m)/boot.asm下面,它的任务相当于bootloader,如果你搞过嵌入式,你肯定听说过它。它的作用就是把真正的kernel从硬盘中放入内存的指定位置并跳过去。我的系统可以分成三部分,这是从Linux0.11内核源码那里学来的下面直接看源码。
进入horos目录,
~/horos# vim boot/arch/$(uname -m)/boot.asm。
org 0x7c00 ;这是一条伪指令,告诉编译器这个程序的起始地址
setup_base_address equ 0x000009000 ;伪指令,相当于定义变量及初始化
head_segment_address equ 0x000000c00 - 0x8;同上
setup_sector equ 1 ;同上
head_sector equ 9; 同上
;设置栈指针在代码段之下,只要给栈找一段空闲的位置即可
mov ax, 0x7c00
mov ss,ax
mov sp,0
;使用BIOS中断获取内存大小
;get memory size from BIOS int 15
mov eax, 0
mov ds, eax
jmp get_mem_size
这段代码第一次看的话大部分都可以忽略,最重要的是org命令,它告诉编译器将本程序的起始地址放在何处,经过我的试验,它的作用是对程序中的常量是有作用的。因为经过编译常量都被替换成地址,如果你不告诉编译器程序的起始地址在哪里那么编译器默认把起始地址当做0,那么常量的地址就只是其在程序中的偏移地址。之前说过的,BIOS在将CPU控制权交给系统之前先将硬盘第一扇区也就是“boot.asm"里面的东西放到了内存中的0x7c00处,所以如果没有org语句,编译之后常量就是一个偏移地址,但是实际常量的存放地址是0x7c00加上偏移地址,于是访问常量就会出错。但是对于使用偏移地址的指令,比如call、jmp,org是无所谓的,因为在我的程序里没有使用常量,因此就算没有org指令程序也不会出错。
;loader will load setup.asm and kernel code to corresponding memory
loader:
mov eax,0x000 ;set ds
mov ds,eax
mov eax,0x7c0 ;set ss
mov ss,eax
xor esp,esp ;set esp to 0
;load setup code from sector 1 to 4 in hard disk
mov edi,setup_base_address ;set destination buffer address
mov eax,setup_sector
mov ebx,edi
mov ecx, 4
setup: call read_hard_disk_0
inc eax
loop setup
;load head code from 9 to 100 in hard disk
mov edi, 0
mov eax, head_sector
mov ebx, edi
mov ebp, head_segment_address
mov ds, ebp
mov ecx, 70
read_head:
call read_hard_disk_0
inc eax
loop read_head
mov ebp, 0
mov ds, ebp
;jmp to setup code
jmp 0x00:setup_base_address
;-------------------------------------------------------------------------------
read_hard_disk_0: ;read hard disk
;EAX=sector
;DS:EBX=destination memory address
;return EBX=EBX+512
push eax
push ecx
push edx
push eax
mov dx,0x1f2
mov al,1
out dx,al ;sectors amount number
inc dx ;0x1f3
pop eax
out dx,al ;LBA address 7~0
inc dx ;0x1f4
mov cl,8
shr eax,cl
out dx,al ;LBA address 15~8
inc dx ;0x1f5
shr eax,cl
out dx,al ;LBA address 23~16
inc dx ;0x1f6
shr eax,cl
or al,0xe0 ;LBA address 27~24
out dx,al
inc dx ;0x1f7
mov al,0x20 ;read
out dx,al
.waits:
in al,dx
and al,0x88
cmp al,0x08
jnz .waits ;ready to read
mov ecx,256 ;totall number of read
mov dx,0x1f0
.readw:
in ax,dx
mov [ebx],ax
add ebx,2
loop .readw
pop edx
pop ecx
pop eax
ret
;-------------------------------------------------------------------------------
;GDTR initial
pgdt dw 0
dd 0x00007e00 ;GDT base address
;-------------------------------------------------------------------------------
;get memory size from BIOS int 15
mem_size_para_address equ 0x00006000
get_mem_size:
mcr_number dd 0
mov ebx, 0
mov eax, 0x08
mov es, eax
mov di, mem_size_para_address
.loop mov eax, 0x0e820
mov ecx, 20
mov edx, 0x0534d4150
int 15h
jc $
add di, 20
inc dword [mcr_number+0x7c00]
cmp ebx, 0
jne .loop
jmp loader
;-------------------------------------------------------------------------------
times 510-($-$$) db 0
db 0x55,0xaa
这部分代码就是load setup代码和kernel到指定的内存地址。