Loader 引导加载程序

与Boot引导程序挥手作别后,此刻的处理器控制权已经移交给 Loader 引导加载程序。Loader 引导加载程序任重而道远,它必须在内核程序执行前,为其准备好一切所需数据,比如硬件检测信息、处理器模式切换、向内核传递参数等。

主要有几个事情要完成

  • 检测硬件信息:比如内存的大小
  • 处理器模式切换:从实模式切换到保护模式
  • 向内核传递数据:启动的一些参数传递

整体代码在 loader.asm

Loader 程序 #

org	10000h
	jmp	Label_Start

%include	"fat12.inc"

; 定义内核从 0x10000 开始
BaseOfKernelFile	equ	0x00
OffsetOfKernelFile	equ	0x100000

BaseTmpOfKernelAddr	equ	0x00
; 临时的内核转存空间
; BIOS在实模式下只支持上限为1 MB的物理地址空间寻址,所以必须先将内核程序读入到临时转存空间,然后再通过特殊方式搬运到1 MB以上的内存空间中。
OffsetTmpOfKernelFile	equ	0x7E00

MemoryStructBufferAddr	equ	0x7E00

A20 开启 #

A20 此项功能属于历史遗留问题。最初的处理器只有20根地址线,这使得处理器只能寻址1MB以内的物理地址空间,如果超过1 MB范围的寻址操作,也只有低20位是有效地址。随着处理器寻址能力的不断增强,20根地址线已经无法满足今后的开发需求。为了保证硬件平台的向下兼容性,便出现了一个控制开启或禁止1 MB以上地址空间的开关。当时的8042键盘控制器上恰好有空闲的端口引脚(输出端口P2,引脚P21),从而使用此引脚作为功能控制开关,即A20功能。如果A20引脚为低电平(数值0),那么只有低20位地址有效,其他位均为0。

;=======	open address A20
	; 通过 92 端口打开 A20
	push	ax
	in	al,	92h
	or	al,	00000010b
	out	92h,	al
	pop	ax

	; 禁用中断
	cli

	; 加载 GDT (全局描述符)
	db	0x66
	lgdt	[GdtPtr]	

	mov	eax,	cr0
	or	eax,	1
	mov	cr0,	eax

	mov	ax,	SelectorData32
	mov	fs,	ax
	mov	eax,	cr0
	; 关闭保护模式
	and	al,	11111110b
	mov	cr0,	eax

	sti

这里需要注意一点的是,在物理平台下,当段寄存器拥有这种特殊能力后,如果重新对其赋值的话,那么它就会失去特殊能力,转而变回原始的实模式段寄存器。但是Bochs虚拟机貌似放宽了对寄存器的检测条件,即使重新向FS段寄存器赋值,FS段寄存器依然拥有特殊能力。 这里可不是正确的路子

查找内核文件 #

;=======	search kernel.bin
	mov	word	[SectorNo],	SectorNumOfRootDirStart

Lable_Search_In_Root_Dir_Begin:

	cmp	word	[RootDirSizeForLoop],	0
	jz	Label_No_LoaderBin
	dec	word	[RootDirSizeForLoop]	
	mov	ax,	00h
	mov	es,	ax
	mov	bx,	8000h
	mov	ax,	[SectorNo]
	mov	cl,	1
	call	Func_ReadOneSector
	mov	si,	KernelFileName
	mov	di,	8000h
	cld
	mov	dx,	10h
	

Boot.bin 一样,找到内核加载进去,当加载成功之后,会打印一个 G

Label_File_Loaded:
		
	mov	ax, 0B800h
	mov	gs, ax
	mov	ah, 0Fh				; 0000: 黑底    1111: 白字
	mov	al, 'G'
	mov	[gs:((80 * 0 + 39) * 2)], ax	; 屏幕第 0 行, 第 39 列。

关闭磁盘马达 #

KillMotor:
	
	push	dx
	mov	dx,	03F2h
	mov	al,	0	
	out	dx,	al
	pop	dx

获取内存信息 #

;=======	get memory address size type

	mov	ax,	1301h
	mov	bx,	000Fh
	mov	dx,	0400h		;row 4
	mov	cx,	24
	push	ax
	mov	ax,	ds
	mov	es,	ax
	pop	ax
	mov	bp,	StartGetMemStructMessage
	int	10h

	mov	ebx,	0
	mov	ax,	0x00
	mov	es,	ax
	mov	di,	MemoryStructBufferAddr	

Label_Get_Mem_Struct:

	mov	eax,	0x0E820
	mov	ecx,	20
	mov	edx,	0x534D4150
	int	15h
	jc	Label_Get_Mem_Fail
	add	di,	20

	cmp	ebx,	0
	jne	Label_Get_Mem_Struct
	jmp	Label_Get_Mem_OK

获取 VGA 信息 #

;=======	get SVGA information

	mov	ax,	1301h
	mov	bx,	000Fh
	mov	dx,	0800h		;row 8
	mov	cx,	23
	push	ax
	mov	ax,	ds
	mov	es,	ax
	pop	ax
	mov	bp,	StartGetSVGAVBEInfoMessage
	int	10h

	mov	ax,	0x00
	mov	es,	ax
	mov	di,	0x8000
	mov	ax,	4F00h

	int	10h

	cmp	ax,	004Fh

	jz	.KO

模式切换 #

上面那些内容都属于硬件的 API 对我们掌握操作系统帮助有限,我们下面来看操作系统相关的东西,我们需要从实模式 -> 保护模式 -> IA-32e 模式

实模式 #

笔者认为,这事情对于软件工程师来说是和硬件的项目配合。

在处理器切换到保护模式前,还必须初始化GDTR寄存器、IDTR寄存器(亦可推迟到进入保护模式后,使能中断前)、控制寄存器CR1~4、MTTRs内存范围类型寄存器。

  • 系统数据结构:
    • 系统在进入保护模式前,必须创建一个拥有代码段描述符和数据段描述符的GDT(Globad DescriptorTable,全局描述符表)(第一项必须是NULL描述符),并且一定要使用LGDT汇编指令将其加载到GDTR寄存器。
    • 保护模式的栈寄存器SS,使用可读写的数据段即可,无需创建专用描述符。对于多段式操作系统,可采用LDT(Local DescriptorTable,局部描述符表)(必须保存在GDT表的描述符中)来管理应用程序,多个应用程序可独享或共享一个局部描述符表LDT。如果希望开启分页机制,
    • 则必须准备至少一个页目录项和页表项。(如果使用4 MB页表,那么准备一个页目录即可。)
  • 中断和异常
    • IDT由若干个门描述符组成,如果采用中断门或陷阱门描述符,它们可以直接指向异常处理程序;如果采用任务门描述符,则必须为处理程序准备TSS段描述符、额外的代码和数据以及任务段描述符等结构。如果处理器允许接收外部中断请求,那么IDT还必须为每个中断处理程序建立门描述符。
  • 分页机制
    • CR0控制寄存器的PG标志位用于控制分页机制的开启与关闭。在开启分页机制(置位PG标志位)前,必须在内存中创建一个页目录和页表(此时的页目录和页表不可使用同一物理页),并将页目录的物理地址加载到CR3控制寄存器(或称PDBR寄存器)。当上述工作准备就绪后,可同时置位控制寄存器CR0的PE标志位和PG标志位,来开启分页机制。
  • 多任务机制
    • 如果希望使用多任务机制或允许改变特权级,则必须在首次执行任务切换前,创建至少一个任务状态段TSS结构和附加的TSS段描述符。(当特权级切换至0、1、2时,栈段寄存器与栈指针寄存器皆从TSS段结构中取得。)在使用TSS段结构前,必须使用LTR汇编指令将其加载至TR寄存器,这个过程只能在进入保护模式后执行。
;=======	init IDT GDT goto protect mode 
; 初始化 GDT

	cli			;======close interrupt

	db	0x66
	lgdt	[GdtPtr]

;	db	0x66
;	lidt	[IDT_POINTER]

	mov	eax,	cr0
	or	eax,	1
	mov	cr0,	eax	

	jmp	dword SelectorCode32:GO_TO_TMP_Protect

IA-32e 模式 #

GO_TO_TMP_Protect:

;=======	go to tmp long mode

	mov	ax,	0x10
	mov	ds,	ax
	mov	es,	ax
	mov	fs,	ax
	mov	ss,	ax
	mov	esp,	7E00h

	call	support_long_mode
	test	eax,	eax

	jz	no_support
;=======	init temporary page table 0x90000

	mov	dword	[0x90000],	0x91007
	mov	dword	[0x90800],	0x91007		

	mov	dword	[0x91000],	0x92007

	mov	dword	[0x92000],	0x000083

	mov	dword	[0x92008],	0x200083

	mov	dword	[0x92010],	0x400083

	mov	dword	[0x92018],	0x600083

	mov	dword	[0x92020],	0x800083

	mov	dword	[0x92028],	0xa00083

;=======	load GDTR

	db	0x66
	lgdt	[GdtPtr64]
	mov	ax,	0x10
	mov	ds,	ax
	mov	es,	ax
	mov	fs,	ax
	mov	gs,	ax
	mov	ss,	ax

	mov	esp,	7E00h

;=======	open PAE

	mov	eax,	cr4
	bts	eax,	5
	mov	cr4,	eax

;=======	load	cr3

	mov	eax,	0x90000
	mov	cr3,	eax

;=======	enable long-mode

	mov	ecx,	0C0000080h		;IA32_EFER
	rdmsr

	bts	eax,	8
	wrmsr

;=======	open PE and paging

	mov	eax,	cr0
	bts	eax,	0
	bts	eax,	31
	mov	cr0,	eax

	jmp	SelectorCode64:OffsetOfKernelFile

;=======	test support long mode or not

support_long_mode:

	mov	eax,	0x80000000
	cpuid
	cmp	eax,	0x80000001
	setnb	al	
	jb	support_long_mode_done
	mov	eax,	0x80000001
	cpuid
	bt	edx,	29
	setc	al

Jump to Kernel #

jmp SelectorCode64:OffsetOfKernelFile

跳转到内核即可。 伴随着Loader引导加载程序最后一条指令(远跳转指令)的执行,处理器的控制权就移交到了内核程序手上。 此刻,Loader引导加载程序已完成了它的使命,其占用的内存空间可以释放或另作他用。目前系统虽已进入IA-32e模式, 但这只是临时中转模式,接下来的内核程序将会为系统重新创建IA-32e模式的段结构和页表结构。

comments powered by Disqus