从零写一个操作系统之booting

这一节讲讲horos是怎么boot的。首先我们要知道x86是怎么boot的。

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是怎么boot起来的

在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内核源码那里学来的,分别为boot.bin , setup.bin和kernel.bin,打开run.sh可以看到这三部分分别放在磁盘的位置。下面直接看源码。

进入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,setup和read_head负责将放在磁盘中的setup.bin和kernel.bin加载到指定的内存中,使用read_hard_disk_0完成
loader:                                  
         mov eax,0x000                    ;set ds
         mov ds,eax
         
         ;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_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

;-------------------------------------------------------------------------------
	;get memory size from BIOS int 15
;通过BIOS中断读内存大小
	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

;-------------------------------------------------------------------------------                             

这部分代码就是load setup代码和kernel到指定的内存地址。其实这些内存地址是要精心安排的,防止程序相互覆盖。x86的CPU初始时是在实模式,只能访问1M以下的内存,因此你不能将程序放在0x100000以上的地方,这中间还有显示映射内存区域,中断向量表区。

0x7c00-0x7dff是boot.bin的区域,我们隔开一段内存将0x9000存放setup.bin,将kernel.bin放在0xc000处,kernel.bin大概有30kb,因此需要读几十个扇区。在程序的尾部,我们将程序最后两个字节设为0xaa55。

;将程序的最后两个字节设为0xaa55
         times 510-($-$$) db 0
         db 0x55,0xaa

times一句的意思是写填充0至510字节,$代表当前地址,$$代表本段代码起始地址,他们的差就是已经程序已经写入了多少字节,这样编译器就会在目标代码里填充0至510字节,最后一句将最后两个字节写成0xaa55,因为是小端,所以先写0x55后写0xaa。times和db都是伪指令,CPU是不执行的,只是告诉编译器要做什么。

boot.bin要做的最后一件事是jmp。

jmp  0x00:setup_base_address

既然setup.bin已经放到内存地址setup_base_address处,那就跳过去就行了,接着程序就会转入setup环节。

至此,boot的代码我们就讲完了。

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值