【完整代码已经归档到 https://github.com/linzhanglong/mini_bootloader 】
引导内核调试了很久,终于调通了,这里主要关键点在于长跳转或者CPU模式切换时候段寄存器的初始化好,否则会跳不过去。现在看一下如何引导Linux内核。
首先我们看一下我们的内核编译的产物(摘自 https://www.slideshare.net/shimosawa/linux-kernel-booting-process-1-for-nlkb):
bzImage就是我们要引导的内核,我们需要了解一下bzImage构成和如何启动的。
bzImage有三部分组成:bootsector, setup.bin, vmlinux.bin。其中bootsector大小512字节,相当于前面的MBR,这个代码会加载到内存地址0x7c00。改代码会把自己移动到0x90000内存位置,并且加载了setup code代码到0x90200,把压缩的内核(vmlinix.bin头部自带解压戴安)代码vmlinu,bin加载到0x100000内存地址,最后跳掉setup code代码部分开始执行。目前bootsector的功能已经被bootloader替换了,我们需要他通过我们的bootloader把bzImage代码里面的bootsector + setup code代码到0x90000内存地址。把vmlinux,bin代码加载到0x100000内存地址。然后跳转到setup code就可以了。setup code代码要求CPU处于实模式下,代码会初始化硬件,进入保护模式,然后调转到0x100000地址去执行vmlinux.bin代码(vmlinux.bin头部自带解压后面的压缩代码)。这里具体setup code的代码实现那些功能可以查一下资料,这里没有详细研究。
现在我们要如何引导我们的内核?
首先我们简单理解就是把bootsector+ setup code加载到0x90000,并且把vmlinux.bin加载到0x10000,最后跳转到0x90200的setup code代码就可以引导内核了。但是这里需要处理两个问题:
1.把vmlinux.bin加载到0x10000,需要CPU处于保护模式,要不访问不了超过1M的内存地址空间;
2.我们需要知道setup code和vmlinux.bin的大小,其实这个存在的地方就在bootsector扇区,里面包含了内核头部信息的一部分,另一部分在setup code的:
这里的参数显示了一部分,具体有哪些参数信息,看https://www.kernel.org/doc/Documentation/x86/boot.txt的 THE REAL-MODE KERNEL HEADER
这里主要关注四个参数就可以了,其他有兴趣可以自行研究:
bzImage位置 启动协议版本 参数名字 参数用途
0228/4 2.02+ cmd_line_ptr 命令行设置,用于设置rootfs文件系统的地方,后面介绍我那件系统时候会用到
所以引导内核我们需要做的事情:
1.进入保护模式,这个上面文章有介绍了;
2.加载bzImage的启动扇区和setup到0x9000内存地址;
3.加载vmlinux.bin到0x100000内存地址;
4.设置内核参数
5. 退回实模式,然后跳转到setup code代码处执行(引导内核)
说了那么多废话,直接看代码实现:
文件名字common_protect_mode.asm,这个文件实现了几个函数接口,分别是 gdt描述符初始化,A20地址线开启,打印函数。代码如下:
;
;Data 2017/12/1
;Authon linzhanglong
;brief 初始化GDT表,启动A20地址线,进入保护模式
;***************初始化GDT表****************************
;第一条是空的描述符
[bits 16]
gdt_start:
dd 0x0
dd 0x0
;定义内核代码段访问权限
;Base_address-Base_address+4G代码段:权限只读,CPU处于ring0级别
gdt_system_cold:
dw 0xffff ;Limit
dw 0x0 ;Base adderess[15:0]
db 0x0 ;Base adderess[23:16]
db 10011010b ;P:1->描述符在内存中;DPL:00->ring0,S:1->存储段[代码数据段] 1010->代码段,权限是:执行,可读
db 11001111b ;G:1->Limit单位是4k,D/B:1->使用32位地址,未使用:00,Limit:1111
db 0x0 ;Base address[31:24]:0
;定义内核数据段访问权限
;Base_address-Base_address+4G数据段:权限读写,CPU处于ring0级别
gdt_system_data:
dw 0xffff ;Limit
dw 0x0 ;Base adderess[15:0]
db 0x0 ;Base adderess[23:16]
db 10010010b ;P:1->描述符在内存中;DPL:00->ring0,S:1->存储段[代码数据段] 010->数据段,权限是:读写
db 11001111b ;Base address[31:24]:0, G:1->Limit单位是4k,D/B:1->使用32位地址,未使用:00,Limit:1111
db 0x0 ;Base address[31:24]:0
;我们从保护模式切回实模式时候,需要对应修改一下段描述描述符
gdt_user_code:
dw 0xffff
dw 0x0
db 0x0
db 0x9a
db 0x0
db 0x0
gdt_user_data:
dw 0xffff
dw 0x0
db 0x0
db 0x92
db 0x0
db 0x0
gdt_end:
dgt_descriptors:
dw gdt_end - gdt_start -1
dd gdt_start
;进入保护模式之后,我们需要设置代码段将使用gdt_system_cold描述符,数据段将使用gdt_system_data描述符
GDT_SYSTEMCOLD_OFFSET equ gdt_system_cold - gdt_start ;在描述符表的偏移位置
GDT_SYSTEMDATA_OFFSET equ gdt_system_data - gdt_start ;在描述符表的偏移位置
GDT_USERCOLD_OFFSET equ gdt_user_code - gdt_start
GDT_USERDATA_OFFSET equ gdt_user_data - gdt_start
;向外提供的函数,初始化GDT描述符表
init_gdt:
pusha
lgdt [dgt_descriptors]
popa
ret
;******************启用A20*************
;摘自 http://mrhopehub.github.io/2014/12/26/enabling-the-A20-Gate.html
EnableA20_KB:
push ax ;Saves AX
mov al, 0xdd ;Look at the command list
out 0x64, al ;Command Register
pop ax ;Restore's AX
ret
;***************保护模式下的打印,不能通过BIOS,但是可以通过显卡映射的内存来操作*****
VIDEO_MEMERY_START equ 0xb8000
;@ brief 保护模式下的打印字符串
;@ param ebx 字符串的地址
[bits 32]
print_string_protect:
pusha
;先读取当前光标位置
;光标位置高8位
xor eax, eax
mov dx, 3D4H
mov al, 0xE
out dx, al ;设置索引:读取高8位
mov dx, 3D5H
in al, dx ;从VGA寄存器读取数据
mov ah, al
;光标位置低8位
mov al, 0xF
mov dx, 3D4H
out dx, al ;设置索引:读取低8位
mov dx, 3D5H
in al, dx ;从VGA寄存器读取数据
;设置光标对应的显卡内存位置,两个字节对应一个字符
mov edx, VIDEO_MEMERY_START
add edx, eax
add edx, eax
;保存光标的位置
mov ecx, eax
print_next_protect:
lodsb ;显示的字符
;判断是否为结束符
cmp al ,0
je print_ok_protect
mov ah, 0x0f ;显示的字符颜色和背景设置
mov [edx], ax
add edx, 2
inc ecx ;更新光标位置
jmp print_next_protect
print_ok_protect:
;设置光标显示到下一行,每一行有80个字符
;下一行的位置 = (当前的位置 + 79) / 80 * 80
;ecx 保存要设置的坐标位置
add ecx, 79
mov eax, ecx
mov ebx, 80
xor edx,edx
div ebx
mul ebx
mov ecx, eax
mov dx, 3D4H
mov al, 0xE
out dx, al ;设置索引:读取高8位
mov dx, 3D5H
mov al, ch
out dx, al ;从VGA寄存器写入数据
;光标位置低8位
mov al, 0xF
mov dx, 3D4H
out dx, al ;设置索引:读取低8位
mov dx, 3D5H
mov al, cl
out dx, al ;从VGA寄存器读取数据
popa
ret
;保护模式下打印ebx寄存器的数值
;这里把数值转为字符串,然后调用print_string_protect即可
[bits 32]
init_print_hex_pm:
pusha
mov ah, 0x0f
mov al, '0'
mov [edx], ax
add edx, 1
mov al, 'x'
mov [edx], ax
popa
add edx, 1
ret
;@ brief 直接把bl转为字符对应的ASCII码,例如3->'3',10->'a'
convert_bx_hex2str_pm:
;如果数字大于15,异常。打印?号
cmp bl, 15
jg _ERROR_pm
;如果数字大于10,那么转为A-F
cmp bl, 10
jge _LETTER_pm
;如果数字是0-10,就只加上'0',就可以把0-10转为对应数字的ascii码
add bl, '0'
jmp _EXIT_pm
_LETTER_pm:
;先把数字减去10,然后加上A,就可以把10-15的数字转为A-F
sub bl, 10
add bl, 'A'
jmp _EXIT_pm
_ERROR_pm:
mov bl, '?'
_EXIT_pm:
ret
;@ brief 把一个数字转为多位十六进制输出
;@ param bx保存要打印的数字
print_hex_pm:
pusha
;首先通过掩码和移位的方式,把数字以十六进制的方式从最后一位开始一个个压入堆栈
;然后输出时候才从堆栈里一个个取出来,这样就可以实现顺序打印
xor cx, cx
_NEED_PUSH_pm:
;取掩码获取数字以十六进制形式的最后一位,例如数字0x1234取出4,其中4保存到al,0x123保存到bx。cl计数
mov eax, ebx
and eax, 0x0f
shr ebx, 4
;入栈并且计数
push ax
inc cl
;如果bx数值为0,表示我们已经全部处理完
cmp ebx, 0
jne _NEED_PUSH_pm
mov eax,CONVERT_HEX2str
add eax, 2
;开始出栈,并且显示
_NEED_POP_pm:
pop bx
call convert_bx_hex2str_pm
mov [eax], bx
inc eax
dec cl
cmp cl, 0
jne _NEED_POP_pm
;开始打印
mov si, CONVERT_HEX2str
call print_string_protect
popa
ret
;8个字节,还有一个结束符
CONVERT_HEX2str: db '0','x',0,0,0,0,0,0,0,0,0
;brief 保护模式下读取磁盘数据
;param eax LBA扇区号
;param edi 保存的内存地址
;notes http://www.cnblogs.com/weiweishuo/archive/2013/05/26/3100254.html
; IDE的端口,写入的数据大小都是字节
[bits 32]
read_disk_onesector_pm:
pusha
;我们首先设置LBA扇区号
;因为后面我们会用到eax
mov edx, 0x1f3
out dx, al ;LBA[7:0]
mov edx, 0x1f4
shr eax, 8
out dx, al ;LBA[15:8]
mov edx, 0x1f5
shr eax, 8
out dx, al ;LBA[23:16]
mov edx, 0x1f6
shr eax, 8
and al, 0x0f ;LBA[27:24]
or al, 0xe0 ;LBA模式
out dx, al ;LBA[23:16]
;设置扇区数目,这个设置读取1扇区
mov dx, 0x1f2
mov al, 1
out dx, al
;设置读指令
mov dx, 0x1f7
mov al, 0x20
out dx, al
_not_ready:
;一次只能读取512字节
mov ecx, 256
in al, dx
and al, 0x88
cmp al, 0x08
jnz _not_ready
;开始读取数据
mov dx, 0x1f0
rep insw
popa
ret
文件名字stage2.asm,引导内核的入口代码如下:
;
; Date 2017/11/28
; Authon: linzhanglong
; notes: 这里是加载内核。
[org 0x9000]
;这个文件主要功能就是加载内核,需要分为几个主要步骤:
;step 1 初始化GDT表
;step 2 开始A20
;step 3 进入保护模式
;step 4 加载bzImage的启动扇区和setup到0x9000内存地址
;step 5 加载vmlinux到0x100000内存地址
;step 6 设置内核参数
;step 7 启动内核
cli
call init_gdt ;step 1 初始化GDT表
call EnableA20_KB ;step 2 启用A20
;step 3 进入保护模式
enter_protect:
mov eax, cr0
or eax, 0x1
mov cr0, eax
jmp GDT_SYSTEMCOLD_OFFSET:init_protect
[bits 32]
;开始进入保护模式了
init_protect:
;现在设置我们的数据段使用GDT_SYSTEMDATA_OFFSET描述符
mov ax, GDT_SYSTEMDATA_OFFSET
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
;设置堆栈s
mov ebp, PROTECT_STACK_MEMBASE
mov esp, ebp
mov esi, MSG_ENTER_PROTECT_OK
call print_string_protect
;step 4 加载bzImage的启动扇区和setup到0x10000内存地址
mov eax, BZIMAGE_DISKBASE
mov edi, BZIMAGE_BOOTSECTOR_MEMBASE
call read_disk_onesector_pm
;setup 扇区数目,1个字节
mov eax, BZIMAGE_BOOTSECTOR_MEMBASE
add eax, SETUP_SECTS
mov ebx, [eax]
mov byte [BZIMAGE_SETUP_SECTS], bl
;kernel的大小,16字节为单位
mov eax, BZIMAGE_BOOTSECTOR_MEMBASE
add eax, SYSSIZE
mov ebx, [eax]
shl ebx, 4 ;乘以16
shr ebx, 9 ;转为扇区单位
mov [BZIMAGE_SYSSIZE_SECTS], ebx
;加载setup到紧接着boot sector的内存地址
mov cl, [BZIMAGE_SETUP_SECTS]
mov eax, BZIMAGE_DISKBASE
add eax, 1
mov edi, BZIMAGE_BOOTSECTOR_MEMBASE
add edi, 512
_LOAD_SETUP_NEXT:
call read_disk_onesector_pm
;准备读取下一个扇区
add edi, 512
add eax, 1
dec cl
jnz _LOAD_SETUP_NEXT
;加载bzImage的vmlinux.bin到0x100000内存位置
mov edi, VMLINUX_BIN_BASENAME
;磁盘位置
mov eax, BZIMAGE_DISKBASE
add eax, 1
xor ecx, ecx
mov cl, [BZIMAGE_SETUP_SECTS]
add eax, ecx
;拷贝的扇区数目
mov ecx, [BZIMAGE_SYSSIZE_SECTS]
_LOAD_VMLINUX_NEXT:
call read_disk_onesector_pm
;准备读取下一个扇区
add edi, 512
add eax, 1
dec ecx
jnz _LOAD_VMLINUX_NEXT
_SETUP_PARAM:
;设置bootloader类型
mov eax, BZIMAGE_BOOTSECTOR_MEMBASE
add eax, TYPE_OF_LOADER
mov ebx, [eax]
or ebx, 0xff;Boot loader identifier,我们是自己的bootloader,所以设置为0xff
mov [eax], ebx
;设置setup代码执行完之后调转到内核的地址,这里就是 0x100000
;bzImage的vmlinux.bin代码的加载内存地址
mov eax, BZIMAGE_BOOTSECTOR_MEMBASE
add eax, CODE32_START
mov ebx, VMLINUX_BIN_BASENAME
mov [eax], ebx
;启用CAN_USE_HEAP
mov eax, BZIMAGE_BOOTSECTOR_MEMBASE
add eax, LOADFLAGS
mov ebx, [eax]
or ebx, 0x80
mov [eax], ebx
;0x0000-0x7fff Real mode kernel
;0x8000-0xdfff Stack and heap, heap_end = 0xe000 => heap_end_ptr = heap_end - 0x200;
;0xe000-0xffff Kernel command line
;stack/heap 设置
mov eax, BZIMAGE_BOOTSECTOR_MEMBASE
add eax, HEAP_END_PTR
mov ebx, [eax]
and ebx, 0xffff0000
or ebx, 0xde00
mov [eax], ebx
;设置命令行
mov eax, BZIMAGE_BOOTSECTOR_MEMBASE
add eax, CMD_LINE_PTR
mov ebx, 0xe000
mov [eax], ebx
mov si, cmd_line
mov di, 0xe000
mov cx, cmd_length
rep movsb
RUN_KERNEL:
;开始启动内核
;由于setup代码是在实模式下运行,我们需要退回实模式
;http://www.mouseos.com/arch/backto_real_mode.html
;切换回 real mode 的 segment
mov ax, GDT_USERDATA_OFFSET
mov ds, ax
mov es, ax
mov ss, ax
;更新 CS 寄存器
jmp GDT_USERCOLD_OFFSET:_ENTER_REAL_MODE
[bits 16]
_ENTER_REAL_MODE:
; 加载 real mode 下的 IDT 表
LIDT [IDT_POINTER16]
; 关闭 protected mode 和 paging 机制
mov eax, cr0
btr eax, 31 ; clear CR0.PG
btr eax, 0 ; clear CR0.PE
mov cr0, eax ; disable protected mode
;设置最终的 real mode 段
jmp 0:_INIT_REAL_MODE
_INIT_REAL_MODE:
;BZIMAGE_BOOTSECTOR_MEMBASE + 0x200
mov ax, BZIMAGE_BOOTSECTOR_MEMBASE_SEG
mov es, ax
mov ds, ax
mov es, ax
mov ss, ax
mov bp, STAGE1_STACK_MEMBASE ;使用stage1的堆栈就好了
mov sp, bp
jmp BZIMAGE_BOOTSECTOR_MEMBASE_SEG:512
;现在我们进入保护模式
;call enter_protect
%include "common.asm"
%include "common_protect_mode.asm"
%include "common_real_mode.asm"
IDT_POINTER16:
IDT_LIMIT16 dw 0xffff
IDT_BASE16 dd 0
MSG_ENTER_PROTECT_OK db 'Enter protect mode ok', 0
MSG_ENTER_REAL_MODE_OK db 'Enter real mode ok', 0
BZIMAGE_SETUP_SECTS db 0x00 ;setup的大小
BZIMAGE_SYSSIZE_SECTS db 0x00,0x00,0x00,0x00 ;The size of the 32-bit code in 16-byte paras
times 32766-($-$$) db 1
dw 0x4433
这已经引导成功内核了,后面文章在增加一个文件系统