linux汇编学习(4)-----引导linux内核

【完整代码已经归档到    https://github.com/linzhanglong/mini_bootloader  】

     引导内核调试了很久,终于调通了,这里主要关键点在于长跳转或者CPU模式切换时候段寄存器的初始化好,否则会跳不过去。现在看一下如何引导Linux内核。

     首先我们看一下我们的内核编译的产物(摘自 https://www.slideshare.net/shimosawa/linux-kernel-booting-process-1-for-nlkb):

linux汇编学习(4)-----引导linux内核_第1张图片

    bzImage就是我们要引导的内核,我们需要了解一下bzImage构成和如何启动的。

linux汇编学习(4)-----引导linux内核_第2张图片
    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的代码实现那些功能可以查一下资料,这里没有详细研究。

linux汇编学习(4)-----引导linux内核_第3张图片

现在我们要如何引导我们的内核?

   首先我们简单理解就是把bootsector+ setup code加载到0x90000,并且把vmlinux.bin加载到0x10000,最后跳转到0x90200的setup code代码就可以引导内核了。但是这里需要处理两个问题:

   1.把vmlinux.bin加载到0x10000,需要CPU处于保护模式,要不访问不了超过1M的内存地址空间;

   2.我们需要知道setup code和vmlinux.bin的大小,其实这个存在的地方就在bootsector扇区,里面包含了内核头部信息的一部分,另一部分在setup code的:

linux汇编学习(4)-----引导linux内核_第4张图片

这里的参数显示了一部分,具体有哪些参数信息,看https://www.kernel.org/doc/Documentation/x86/boot.txt的 THE REAL-MODE KERNEL HEADER

这里主要关注四个参数就可以了,其他有兴趣可以自行研究:

bzImage位置   启动协议版本      参数名字    参数用途
01F1/1        ALL(1            setup_sects           setup code的大小,单位扇区
01F4/4        2.04+(2         syssize                   vmlinux.bin大小,单位16字节
0210/1        2.00+            type_of_loader     bootload类型
0211/1        2.00+            loadflags               可选参数,我们这里设置手动指定堆栈地址
0224/2        2.01+            heap_end_ptr      seup code后面有多少空间用于堆

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

因为我们的bzImage是放在磁盘的1M位置,所以这里的代码就是实现从磁盘1M位置加载bzImage,然后进行引导。效果图如下:

linux汇编学习(4)-----引导linux内核_第5张图片

这已经引导成功内核了,后面文章在增加一个文件系统



你可能感兴趣的:(linux汇编学习(4)-----引导linux内核)