首页 > 代码库 > bootloader实现

bootloader实现

上篇文章我们完成了一个简单的bootloader,与其说是bootloader,不如说是boot,本篇我们完成loader部分功能loader部分是在boot部分基础上,通过到约定好的启动盘位置上读数据载入内存,达到loader的目的。到启动盘读数据是bios提供的功能调用.

1. 铺垫

(1)我们这次的程序分两个部分,一个部分是bootloader,boot和loader功能;一个是head程序,这个程序什么也不做,简单的几条指令,我们只是要加载它执行它而已.

(2)bootloader是as86+ld86的产物,语法遵从as86语法;head是gnu汇编器语法,使用gcc编译ld链接,是32位的程序.

(3)默认bootloader程序会被放在软盘的引导扇区,就是虚拟软盘的前512Byte;head程序则放在从第二个512Byte开始处和以后的地方.

(4)bootloader的任务是加载head,执行head;注意,我们如果还是简单的加载as86汇编程序,实模式下跳转,哪有什么意思呢?我们需要更近一步,进入保护模式,同时跳转到head程序,为以后的AB任务切换做准备.

BOOTSEG=0x07c0
SYSSEG=0x1000
SYSLEN=4

entry start
start:
	jmpi go,#BOOTSEG
go:
	mov ax,cs
	mov ds,ax
	mov es,ax
	mov ss,ax
	mov sp,#0x400

	mov ax,#0x0600
	mov cx,#0x0000
	mov dx,#0xFFFF
	int 0x10

	mov cx,#10
	mov dx,#0x0000
	mov bx,#0x000c
	mov bp,#msg
	mov ax,#0x1301
	int 0x10

load_system:
	mov dx,#0x0000
	mov cx,#0x0002
	mov ax,#SYSSEG
	mov es,ax
	xor bx,bx
	mov ax,#0x200+SYSLEN
	int 0x13
	jnc ok_load
	mov dx,#0x0000
	mov ax,#0x0000
	int 0x13
	jmp load_system

ok_load:
	cli
	mov ax,#SYSSEG
	mov ds,ax
	xor ax,ax
	mov es,ax
	mov cx,#0x1000
	sub si,si
	sub di,di
	rep 
	movw

	mov ax,cs
	mov ds,ax

	lidt idt_48
	lgdt gdt_48

	mov ax,#0x0001
	lmsw ax
	jmpi 0,8

msg:
	.ascii "Loading..."
	.byte 13,10

gdt:
	.word 0,0,0,0

	.word 0x07FF
	.word 0x0000
	.word 0x9A00
	.word 0x00C0

	.word 0x07FF
	.word 0x0000
	.word 0x9200
	.word 0x00C0

idt_48:
	.word 0
	.word 0,0

gdt_48:
	.word 0x7FF
	.word 0x7c00+gdt,0
.org 510
	.word 0xAA55

2.代码分析

BOOTSEG = 0x07c0
SYSSEG = 0x1000
SYSLEN = 4

entry start
start:
    jmpi go,#BOOTSEG
go:
    mov ax,cs
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov sp,#0x400

只有mov sp,#0x400需要说一下,大家都知道sp是堆栈指针,对堆栈的操作会引起sp的变化,这里简单留出一段空间即可.

    ! clear screen
    mov ax,#0x0600
    mov cx,#0x0000
    mov dx,#0xFFFF
    int 0x10

    ! show "Loading..."
    mov cx,#10
    mov dx,#0x0000
    mov bx,#0x000c
    mov bp,#msg
    mov ax,#0x1301
    int 0x10

bios程序0x10的两段程序,分别是清屏幕和写字符串.

load_system:
    mov dx,#0x0000
    mov cx,#0x0002
    mov ax,#SYSSEG
    mov es,ax
    xor bx,bx
    mov ax,#0x200+SYSLEN
    int 0x13
    jnc ok_load
    mov    dx,#0x0000
    mov    ax,#0x0000
    int    0x13
    jmp    load_system

这段程序是真正的loader部分了,也是对bios功能的调用,准备参数如下:

    mov dx,#0x0000 - dh磁头是0,dl是0表示软盘
    mov cx,#0x0002 - ch柱面是0,cl开始扇区为2
    mov ax,#SYSSEG
    mov es,ax - es:bx = 0x1000:0x0 表示目的地址,0x13中断把扇区读到此位置.
    xor bx,bx - 清零bx.
    mov ax,#0x200+SYSLEN - ah对应int 0x13调用功能号02表示读扇区,al对应扇区个数.
    int 0x13

整体的意思是把0柱面,0磁头,从2扇区开始的4个扇区读到内存0x1000:0x0处.

    jnc ok_load
    mov    dx,#0x0000
    mov    ax,#0x0000
    int    0x13
    jmp    load_system

这段代码意思是如果出错了,就反复读,直到读出来正确为止,正确后跳转到ok_load标号处.

ok_load:
    cli
    mov ax,#SYSSEG
    mov ds,ax
    xor ax,ax
    mov es,ax
    mov cx,#0x1000
    sub si,si
    sub di,di
    rep
    movw

上边的代码目标是把读出来的代码移动到0x0处,为什么要移动到0x0处呢,就在0x1000:0x0处执行不成?实际上是可以的,只是需要和gdt描述符配合使用.

    mov ax,cs
    mov ds,ax
    
    lidt idt_48
    lgdt gdt_48

上边代码看似复杂,其实没什么,lidt是指令,idt_48是操作数;lgdt是指令,gdt_48是操作数;意思是加载中断描述符表和全局描述符表,为啥要加载这两个表呢?

因为保护模式下,cpu取指令和数据不再是0x7c00:0x0这样的方式了,而是根据你给定的0x1:0x0来找一个表,之后通过这个表找到具体的物理地址.这个过程中,

可以检查点权限什么的,起到保护作用.关于保护模式的问题慢慢理解即可,无需急于求成.mov ds,ax这句实际是提供数据段位置以便找到正确的变量地址.

    mov ax,#0x0001
    lmsw ax
    jmpi 0,8

以上几句看似神奇,其实也很简单,通过设置寄存器的值,让cpu进入保护模式,保护模式无非就是寻址方式变了而已,理解就好.寻址方式变了之后,注意jmpi 0,8不再是跳转到0x8段:0x0偏移处了,而8是gdt表的选择符,0是偏移,8是选择符在gdt表中的偏移,这个时候cs就会被赋值8,但是不会从0x8:0x0处取地址,而是从gdt+8这个描述符定义的物理基地址+0x0物理地址处取指令噢!

至此,跳转到了head程序里了。head程序被我们加载到0x1000:0x0处,后被移动到0x0:0x0处,我们判断8对应的gdt里的描述符定义的物理基地址是0x0,下面重点分析gdt表定义。

gdt:
    .word 0,0,0,0 - .word定义了一个字,就是两个字节,此处首先定义了0,0,0,0 8个空的字节,系统规定保留.一个段描述符就是8个字节哦.

    .word 0x07FF - 可以想到前边的jmpi 0,8中的8(前边有了8个字节,偏移分别是0^7)指的就是接下来的8个字节定义的这个段. 0x07FF表示段限制长度,就是说这个段有多长,0x07FF十进制是2047,这里还不能确定是2047*1B还是2047*4KB,要看后边的定义.后边定了颗粒度为4KB,表示段限长度为8M
    .word 0x0000 - 表示段基地址的0-15位
    .word 0x9A00 - 00表示基地址的16-23位,9A为0x10011010分别表示代码段可读、执行
    .word 0x00C0 - 00表示基地址的24-31为,C为0x1100表示颗粒度为4KB等.

    .word 0x07FF - 此段为数据段描述符,意义基本同上.
    .word 0x0000
    .word 0x9200 - 00表示基地址的16-23位,92为0x10010010分别表示数据段可读写
    .word 0x00C0


综上分析,我们实现了加载head程序,进入保护模式,同时最后跳转到了0x00000000物理地址处开始执行,我们知道head程序的代码就在那.

3. head程序

.globl startup_32
.text
startup_32:
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
die:
	jmp die
head程序没什么功能,只是测试是否能够进入保护模式,并跳转到head程序过来.


4. 编译组合

(1)编译bootloader

as86 -0 -a -o boot.o boot.s

ld86 -0 -s -o boot boot.o

(2)编译head

gcc -m32 -g -Wall -O2 -fomit-frame-pointer -fno-stack-protector -traditional -c head.s

此句用gcc编译head.s生成head.o,实际上gcc会调用as来汇编head.s

ld head.o -m elf_i386 -Ttext 0 -e startup_32 -o system

ld把head.o链接成system,启动代码段偏移从0开始,且把startup_32作为第一条指令.

objcopy -O binary -R .note -R .comment system kernel

ld生成的实际上是有文件头的文件,使用objcopy -O binary可以去掉文件头,同时-R去掉了文件中的指定段,生成kernel文件

至此,kernel是head.s生成的纯二进制代码.

(3)组合bootloader和head

dd if=boot of=boot.img bs=32 skip=1

生成boot到boot.img中,读写Block大小为32Byte,跳过输入文件的1个Block,也就是跳过了文件头.

dd if=kernel of=boot.img bs=512 seek=1

生成kernel到boot.img中,读写Block大小为512Byte,跳过输出文件的1个Block,也就是保留了boot.img中boot程序的512Byte,从512Byte后写入head程序kernel.

至此,boot.img就是集合了bootloader和head的启动盘了,其中bootloader在前512Byte,head紧挨着bootloader.

(4)执行

bochs即可

疑问解答:

1.我在首次看到这段的代码的时候,对mov ax,cs;mov ds,ax这两句的功能不了解,以为可以省略,但是经过代码调试发现,并不能省略。

lidt idt_48,在bochs调试对应的形式:


我们知道idt_48是一个偏移值,所以0x9a就是这个标号在代码段的偏移量(这个程序代码和数据放在同一个段中),所以lidt指令对应了一种寻址方式,段寄存器默认是ds,要知道现在还没进入保护模式,也就是在实模式下面,所以设置ds是非常重要的。

2.gdt_48:

.word 0x7FF

.word 0x7c00+gdt,0

根据调试,gdt是82,所以第二行相当于:.word 0x7c82,0,而lgdt执行完毕,GDTR中base(基址)的值是:0x00007c82,段长度是0x7FF。根据我的估计,我的计算机小段模式,所以要颠倒顺序。

参考:

http://www.cnblogs.com/linucos/archive/2012/04/01/2428402.html