动手编写 *** 作系统(2):系统引导过程——BIOSMBR(上)

动手编写 *** 作系统(2):系统引导过程——BIOSMBR(上),第1张

  上一讲,我们配置完Bochs环境后,还试着体验了一把开机,结果不出所料地被BIOS干了下来。接下来,我们就来探索开机之后的步骤,计算机是如何进入 *** 作系统的。

  本文将从传统的Legacy启动模式(MBR)探究 *** 作系统启动的基本过程以及部分细节,如有不严谨处请见谅并指出,欢迎指教。

启动步骤概述

  在计算机看来, *** 作系统本身也是一个运行在硬件平台上的软件,它的成功运行也脱不开装载和执行。那么,是谁唤醒了 *** 作系统?而那个唤醒 *** 作系统的又是谁?这一连串的问题看似无穷无尽,但从应用上又一定有一个“第一步”。下面,我们就以粗略的视角,概览一下从计算机启动电源到 *** 作系统完全启动的整个过程。下面描述的是Legacy启动模式的流程。

(上图中“主引导程序”指的是主引导记录中的程序部分)
位于主板上的BIOS系统对整个计算机的硬件设备进行自检BIOS按照引导优先顺序选择主引导记录(MBR),并移交控制权MBR搜索分区表中的活动分区,转交控制权给活动分区引导记录(DBR,也叫OBR,OS Boot Record)活动分区引导程序跳转到 *** 作系统引导程序 *** 作系统引导程序启动 *** 作系统内核 *** 作系统加载驱动程序与服务 *** 作系统启动

到这里, *** 作系统已经完整启动,登录账户后我们就可以便捷地享受 *** 作系统提供的强大功能了。大致的启动流程就是这样,但我们也就有了进一步的疑问:“BIOS是怎么加载和启动的?”“BIOS怎么找到MBR的?”……带着这些问题,我们继续向下,从更加“微观”的角度探究这个看起来”简单“但又不那么简单的启动流程。

BIOS

  BIOS(Basic Input & Output System),基本输入输出系统。是被静态写入主板上的系统。它仅有部分“基本输入输出”功能,体量也非常小,主要用于计算机硬件设备的自检和基本初始化,并建立中断向量表,最后找到MBR并转交控制权。

  Legacy BIOS工作在实模式下。实模式,是CPU复位(Reset)或刚上电(Power On)时的运行模式。在实模式下,CPU的内存寻址方式于8086相同,即使用20位地址总线,通过分段模型访问这1MB内存空间。地址的构造方式为: A d d r e s s = c s < < 4 + i p Address=cs<<4+ip Address=cs<<4+ip

  在实模式下,1MB内存空间被映射给各个硬件的ROM、显存、以及最重要的内存(DRAM)。后者大家都知道,内存是系统运行时存放数据的位置。硬件的ROM则存放了各个硬件的初始化代码,包括BIOS。程序运行时,也可以从这些位置读取指令,进行对应的硬件初始化 *** 作。

下面是实模式下,内存空间的分配情况:

  这1MB空间的最高64K(0xF0000 - 0xFFFFF)空间,就存放了BIOS。而最后16B存放BIOS的入口跳转指令,这就是计算机开机装载BIOS的关键。

  开机后,CS:IP寄存器被初始化为0xF000:0xFFFF0,这是硬件电路的功能。而这个位置,正是上面所说的BIOS入口跳转指令

jmp far f000h:e05bh

  这样,CPU就可以开始执行BIOS的工作了。

  之后,BIOS的主要工作是扫描、检查并初始化各个硬件,并填写中断向量表(IVT, Interrupt Vector Table)。BIOS将扫描从0xc00000xe0000的内存,这部分内存空间一开始被映射到硬件的ROM处,存放的是各个硬件的BIOS或初始化代码。如果搜索到0x55 0xaa魔数,并完成代码完整性校验,BIOS就会执行这部分代码,完成硬件初始化,并在中断向量表中填写对应功能的处理例程地址。

  硬件自检、初始化、以及建立中断向量表等一系列过程后,BIOS准备移交控制权限给MBR。首先,它将搜索磁盘的第一个逻辑扇区,也是CHS(Cylinder / Header / Sector 柱面/磁头/扇区)表示法的0磁头0道1扇区(扇区编号从1开始)。这里是磁盘主引导记录的位置。如果这个扇区的最后两字节为魔数0xaa55(也就是’0x55 0xaa’),BIOS认为该扇区就是MBR,将MBR加载至0x7c00开始的512B空间中,并跳转执行。(关于为什么跳转到0x7c00,还有个有意思的小推理,这里不再赘述,有兴趣的读者请参见《 *** 作系统真象还原》2.2节)


MBR

MBR(Master Boot Record,主引导记录),总大小512B,其中:

MBR引导程序:0h - 18Dh,总共446BDPT(Disk Partition Table 硬盘分区表):18Eh - 1FDh,共46B,4*16B,最大分区大小:4G Sector = 2TB结尾标志(魔数):55h AAh

  MBR的主要作用为,在其各个主分区中查找活动分区,并选择一个活动分区,将其OBR装入内存,并转交控制权。其中,起主要作用的是DPT硬盘分区表。

  关于这个分区表,相信大家在装新硬盘或是虚拟机等需要给硬盘分区的时候多少接触过一些。MBR分区表记录了最多4个主分区的信息。下面,我们用一个例子来了解一下分区表中的记录构成。

如上图所示:

Boot Indicator: 引导指示符,只有两个值: 0x80 活动分区,存在OBR;0x0 非活动分区开始扇区相关信息Partition Type: 分区类型指示符,每个数字代表一种分区类型。具体数值MBR与GPT分区不同。 0x07: NTFS 文件系统,或 exFAT文件系统0x82: Linux Swap分区0x83: Linux 原生系统分区,一般含有Linux系统的启动分区(即boot分区)其他分区类型参见:Partition types: List of partition identifiers for PCs (tue.nl) 结束扇区相关信息分区大小

具体细节有兴趣的读者可以自行研究,这不是我们今天的重点问题(还没有到寻找活动分区的部分,这一章我们只做到启动一个MBR引导程序)。

BIOS部分,我们已经讲到了BIOS将MBR加载到0x7c00位置,并跳转执行。接下来,我们就要构造一个能接下控制权,并有一定功能的主引导程序。


编写MBR主引导程序

  我们的目标是编写一个能够在屏幕上显示欢迎信息,然后什么也不干的“假的MBR”。选择活动分区并跳转执行是我们下一章的任务,这一章我们先通过打印欢迎信息初步接触BIOS提供的基础中断

  在实模式下,BIOS已经构造好了中断向量表(IVT)。如果进入保护模式,则会使用另外创建的中断描述符表(IDT,Interrupt Descriptor Table)。所以这里的中断向量表,一般就由MBR和后面的OBR使用。(中断向量表的内容见链接BIOS interrupt call - Wikipedia)

  使用中断时,只需要填写中断功能所需要的参数(保存在寄存器中),再使用int(interrupt)指令触发对应的中断号即可。某些中断会提供多个子功能,设置AH寄存器为子功能号即可选择。如下例所示,

 mov ah, 0x0e    ; function number = 0Eh : Display Character
 mov al, '!'     ; AL = code of character to display
 int 0x10        ; call INT 10h, BIOS video service

  在刚进入MBR时,还需要对各段寄存器(sregs)进行初始化。这里就来到了另一个重点。处理器中,没有将立即数放入寄存器中的硬件电路,不能将寄存器使用立即数赋值。但我们可以通过一个寄存器或者内存进行中转。除了cs:ip寄存器不能直接赋值,需要使用jmp指令外,其他寄存器都可以通过非立即数进行赋值。

mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov fs, ax 
mov sp, 0x7c00
其中,0x7c00以上是我们的程序存放位置。按照程序执行逻辑,此时0x7c00以下的内存位置是空的,可以给我们的栈使用。我们将栈指针移到这个位置,以初始化栈。

  接下来,需要进行清屏 *** 作,因为屏幕之前被BIOS输出过信息(一会儿我们可以通过下断点来看到),而进入MBR,我们翻开了 *** 作系统启动新的一页,自然屏幕输出也要用新的一页啦~ 清屏 *** 作调用BIOS0x10显示服务中断的0x06子功能,这通过设置AH寄存器实现。

; Clear Screen use 0x06 sub-func in interrupt 0x10(Video Service)
; function description: scroll up the window
; INPUTS:
; AH function_index = 0x06
; AL = rows to scroll up
; BH = scroll up property
; (CL, CH) = Top Left Corner of window
; (DL, DH) = Lower Right Corner of window
; No return
	mov ax, 0x0600		; also can mov ah, 6; mov ax, 0
	mov bx, 0x0700		; BH: Light Gray on Black
	mov cx, 0			; Top Left (0, 0)
	mov dx, 0x184f		; Lower Right: (80, 25) ((79, 14))
						; in VGA Text Module, only 80c in one line, max 25 lines
	int 0x10
AH:子功能号,0x06 = 上滚窗口AL:上滚行数,0 = 清屏(All)BH:颜色属性,0x07 = 前景浅灰背景黑;有关BIOS颜色属性,详见BIOS color attributes - WikipediaCX:(CH = row, CL = col)窗体左上角位置(序号)DX:(DH = row, DL = col)窗体右下角位置(序号)int 0x80 : 调用中断(有关详细信息,参见代码注释)

  这就是我们在窗口输出信息的第一步,接下来,我们需要在全新窗口的左上角打印欢迎信息,这需要使用该中断的另一个功能:0x13——Write String.

  在输出信息之前,我们需要先准备输出的字符串内容。(一般我们将它放在代码段,但鉴于程序短小和结构简单,我们直接将它放在本段的末尾)

	message db "Hello, world.", 0x0a, 0x0d, "This is a MBR."
	len db $ - message
在计算长度时,我们使用了NASM提供的伪指令$汇编器在汇编时,会将它转换为当前行的初始地址,而message为输出字符串开头的地址,这样就完成了长度的运算。构造字符串时,与AT&T语体不同,Intel语体不能使用转义符表示特殊符号,需要用十六进制单独写出。或者可能我不会用(

  然后开始编写输出调用,直接填充参数即可,没有硬难度

; print string
; function index = 0x13
	; set string
	mov bp, message		; es:bp = message to print
						; es is set as cs at line 4
	mov dx, 0			; move banner to top left corner
						;; we are using cursor pos in dx, abandon cx
	mov cx, [len]		; string length
	mov ax, 0x1301		; ah = 0x13, function index
						; al = 0x01, write mode
	mov bx, 0x0c		; bh = 0, page index
						; bl = 0x02; font color, green on black
	int 0x10
	
	jmp $				; pulse program
将需要输出字符串的地址赋值给es:bp寄存器(类似于将参数压入栈,es寄存器已经在段首完成初始化)DX: (DH=row, DL=col)将打印字符的初始坐标CX: 输出字符串长度,将len的值赋值给它AH: 调用号,0x13 = 窗口输出AL: 输出模式,设置光标跟随等jmp $:这个指令是一个死循环,将控制流约束在当前行,用于“暂停”程序

  到这里,我们的输出就算完成啦!如果你对这个视频服务的其他功能感兴趣,可以参考详细功能表试一试:INT 10H - Wikipedia

  最后,我们还需要组织一下MBR的结构,它是要让BIOS识别的,如果没有指定位置的魔数等信息,BIOS甚至没跳转到0x7c00就报错了(No Bootable Device)。回到第三部分MBR的结构,我们需要一个以这个程序为开头,总共512B,并以0x55 0xaa结尾的二进制文件。并且,它将被装载到0x7c00,这需要我们将程序中的地址进行定向,有一个段参数vstart可以帮我们很好地解决问题。

SECTION MBR vstart=0x7c00

  在段声明中,使用vstart参数,可以告知汇编器,本段将被加载到0x7c00偏移,汇编器会将本段中所有地址都加上这个偏移,保证程序被加载后正常运行。

	message db "Hello, world.", 0x0a, 0x0d, "This is a MBR."
	len db $ - message
;	times 510-($-$$) db 0
	db 510-($-$$) dup (0)
	db 0x55, 0xaa

  在段的末尾,使用times语句或者dup伪指令,可以构造重复数据,用于填充空位。(以上两行代码均可完成任务)最后使用0x55 0xaa填上魔数,文件构造完成。

总代码如下(中间有一些被注释掉的其他功能):

SECTION MBR vstart=0x7c00
	mov ax, cs
	mov ds, ax
	mov es, ax
	mov ss, ax
	mov fs, ax 
	mov sp, 0x7c00
	;mov ecx, 0		; initialize ecx

; Clear Screen use 0x06 sub-func in interrupt 0x10(Video Service)
; function description: scroll up the window
; INPUTS:
; AH function_index = 0x06
; AL = rows to scroll up
; BH = scroll up property
; (CL, CH) = Top Left Corner of window
; (DL, DH) = Lower Right Corner of window
; No return
	mov ax, 0x0600     ; also can mov ah, 6; mov ax, 0
	mov bx, 0x0700
	mov cx, 0          ; Top Left (0, 0)
	mov dx, 0x184f     ; Lower Right: (80, 25) ((79, 14))
					   ; in VGA Text Module, only 80c in one line, max 25 lines
	int 0x10

;; get cursor position
;; AH function index = 0x03
;; bh page number of cursor
;; return
;; ch: start row; cl: end row;
;; dh: row pos; dl: col pos;
	;mov ah, 3
	;mov bh, 0

	;int 0x10

; print string
; function index = 0x13
	; set string
	mov bp, message		; es:bp = message to print
						; es is set as cs at line 4
	mov dx, 0			; move banner to top left corner
						;; we are using cursor pos in dx, abandon cx
	mov cx, [len]		; string length
	mov ax, 0x1301		; ah = 0x13, function index
						; al = 0x01, write mode
	mov bx, 0x0c		; bh = 0, page index
						; bl = 0x02; font color, green on black
	int 0x10

	jmp $				; pulse program

	message db "Hello, world.", 0x0a, 0x0d, "This is a MBR."
	len db $ - message
	db 510-($-$$) dup (0)
	db 0x55, 0xaa

汇编并调试

  程序编写完成后,需要进行汇编,并将其装入我们之前创建的硬盘内。

汇编

nasm -o mbr.bin mbr.S
nasm汇编器,在上一章我们已经介绍过输出格式为其默认的binary格式,即没有任何其他数据的纯二进制代码-o 输出文件最后一个是我们的输入文件

可以使用hexdump观察一下我们编译成果的结构

hexdump -C mbr.bin

一切数据格式均符合预期

如果有兴趣的话,还可以使用IDA观察一下最终成品的内容

IDA提供了BIOS中断调用的参数说明,观察起来非常方便
我们也可以看到,所有的地址都被加上了0x7c00的偏移,如果没有这段偏移,我们的程序被加载后是无法运行的。

接下来使用dd命令,从数据层面将mbr.bin文件内容复制到disk.img文件的指定位置

dd if=mbr.bin of=disk.img bs=512 count=1 conv=notrunc
if: Input File 输入文件of: Output File 输出文件bs: bytes 一次读写大小,即块大小count: 读写的块数量conv: 转换方式 notrunc = 不要截断输出文件seek: 重要但没出现,在输出开始处,跳过指定的块数,不跳过就不写

输出完成后,我们的引导程序就被写入了硬盘镜像文件的首部。

RUN!

接下来,我们的程序就可以开始运行了!

(如果你还没有bochsrc,下面我再给出一份精简版的)

display_library: x

# BIOS ROM
romimage: file=/usr/share/bochs/BIOS-bochs-latest
vgaromimage: file=/usr/share/bochs/VGABIOS-lgpl-latest

# Memory
memory: guest=32, host=64

# Hard Disk
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata0-master: type=disk, mode=flat, path="disk.img"

# Boot mode
boot: disk

# log
log: /tmp/bochs.out

输入命令bochs,开始运行!

  一开始,会d出一个黑框,并且跳出命令行,这是Bochs进入调试状态的正常现象。还需要在终端中输入c(continue),才会开始运行。下面介绍部分Bochs简单调试命令。命令列表可以输入h查看。整体和gdb调试命令及其类似,读者应该都已经很熟悉了。

n: step over下一步(跳过)s: step into 下一步(进入)c: continue 继续,直到下一个断点b [address/symbol]: 添加断点x: 查看内存r: 查看(通用)寄存器sreg: 查看段寄存器q: 退出模拟watch: 添加监视 watch r [addr]读监视,对该地址有读 *** 作时中断watch w [addr]写监视

常用的命令也不多,多调试几下就会了

  下面给0x7c00下个断点看看

  果然,进入了我们熟悉的代码,接下来放开玩吧!

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/web/996465.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存