📝本文介绍
本文主要从系统系统的启动流程开始,中间介绍一些所用工具的使用方法,最后将完成一个启动区的制作。此次的启动区只涉及到汇编代码。
👋作者简介:一个正在积极探索的本科生
📱联系方式:943641266(QQ)
🚪Github地址:https://github.com/sankexilianhua
🔑Gitee地址:https://gitee.com/Java_Ryson
由于本人的知识所限,如果文章有问题,欢迎大家联系并指出,博主会在第一时间修正。
文章目录
- 📕系统的启动
- 📖BIOS是什么
- 📖如何启动
 
- 📙部分基础知识和工具的使用
- 📖各类重要寄存器
- 📖nasm使用
- 📖dd使用
- 📖qemu简单使用
- 📖Makefile编写
 
- 📘启动区的制作
- 📖完整代码
- 📖ipl的解析
- 📖boot_info解析
 
- 📗实际效果
开始之前,先跟大家说明:本文会涉及到较多地默认设置,也就是从最开始做计算机,做操作系统等等的前辈们留下来的。一些感到疑惑或者博主没有说清楚的地方可以搜索一下,说不定是固定设置或者遗留问题,也可以在评论区提问。
📕系统的启动
📖BIOS是什么
  BIOS是英文"Basic Input Output System"的缩略词,直译过来后中文名称就是"基本输入输出系统"。在IBM PC兼容系统上,是一种业界标准的固件接口。BIOS是个人电脑启动时加载的第一个软件。
   其实,它是一组固化到计算机内主板上一个ROM芯片上的程序,它保存着计算机最重要的基本输入输出的程序、开机后自检程序和系统自启动程序,它可从CMOS中读写系统设置的具体信息。 其主要功能是为计算机提供最底层的、最直接的硬件设置和控制。此外,BIOS还向作业系统提供一些系统参数。系统硬件的变化是由BIOS隐藏,程序使用BIOS功能而不是直接控制硬件。现代作业系统会忽略BIOS提供的抽象层并直接控制硬件组件。
   其实,计算机开始时,将执行的第一个程序,就可以认为是这个程序。(除了最开始的一个跳转指令)。
📖如何启动
  在讲述如何启动之前,或许需要先讲述我自己对于计算机硬件的理解:硬件是一台巨大的状态机。这里会涉及到一个状态机的概念。状态机实际上就是条件的改变可能会引起状态的改变。学过数逻应该会更加清楚一点。硬件实际上也差不多。根据pc寄存器的值,一条条从内存的地方取出指令,之后译码,去执行,根据所执行的指令,就会改变相应寄存器的状态(值),最终可以实现整个计算机像另一状态的变化。
   这个问题还会涉及到内存布局的问题,在实模式底下,到底内存如何分配呢?这里有篇博客,用来防止照片失效。

 在实模式底下,我们能操作的内存只有1M(为什么大家可以自行搜索)。
   现在,我们正式开始介绍系统启动的4个跳跃。
   第一跳 :系统启动时会先跳转到0xFFFF0这个位置。这里是BIOS程序的入口。
   第二条我们可以发现,这个位置到0xFFFFF只剩下16字节的空间,大概率只能放得下一条指令,所以我们用来跳转,跳到一个更加大的空间,去执行我们需要的任务。这里跳转指令会跳转到0xfe05b。
   第三跳:这里就开始执行一些硬件自检等活动,完成后,**加载(从外存中复制到内存)启动区的程序到0x7c00(规定)**并跳转到该位置。
   第四跳:启动区的程序主要是加载操作系统内核,之后会跳转到内核存储处开始执行。
   更加详细的内容,大家也可以查看这篇博客
📙部分基础知识和工具的使用
📖各类重要寄存器
8位寄存器:
- AL——累加寄存器低位(accumulator low)
- CL——计数寄存器低位(counter low)
- DL——数据寄存器低位(data low)
- BL——基址寄存器低位(base low)
- AH——累加寄存器高位(accumulator high)
- CH——计数寄存器高位(counter high)
- DH——数据寄存器高位(data high)
16位寄存器:
- AX——accumulator,累加寄存器
- CX——counter,计数寄存器
- DX——data,数据寄存器
- BX——base,基址寄存器
- SP——stack pointer,栈指针寄存器
- BP——base pointer,基址指针寄存器
- SI——source index,源变址寄存器
- DI——destination index,目的变址寄存器
32位寄存器是在16位寄存器前添加上e,如eax,ebx,ecx等.64位则变e为r,rax,rcx等。当然64位也有r0,r1,r2等等的写法,具体内容可自行了解。
📖nasm使用
nasm这里就只介绍最简单的用法目前来说足够本文使用。nasm主要是一些参数等的使用,用多了熟悉后会更加顺手。这就不是把一些参数贴上来能解决的。
nasm -o xxx.bin xxx.asm
或
nasm xxx.asm -o xxx.bin
这里的-o指的是将目标文件命名为什么。后面的asm文件就是我们所写的汇编文件。其余的一些参数可自行了解
📖dd使用
dd命令,可以往磁盘中写相应的数据。
dd if=? of=? seek=? bs=? count=?
if指向要输入的文件,of指向要输出的文件。(通俗就是把if的文件写入of)。seek是否要跳过前几个部分。如seek=1,就会跳过512个字节。bs用于指定块大小,默认情况下都为512字节。count指的是处理多少块数据。
📖qemu简单使用
  qemu的用法也很多,但我们这里主要用来模拟x86的64位系统就可以,当然其他系统可以启动应该也行。
   首先要介绍的是创建磁盘映像的功能。
qemu-img create -f xxx name(eg:os.raw) size
eg:qemu-img create -f raw os.raw 1440k
接下来是使用我们制作好的磁盘映像来启动
qemu-system-x86_64 os.raw
qemu-system-x86_64 -derive file=? if=floppy
如果我们制作的是软盘映像,最好指明是floppy,否则读取磁盘时可能会出现问题。(博主就因为这个问题困扰了很久)
📖Makefile编写
  这里涉及到另一个工具,make。 大家可以自行去网络上搜索下载一个,并如同之前一样,将其添加进环境变量。这样之后不论在哪个文件夹下都可以方便使用。这个工具是为了方便使用和规范化整理。
   使用这个工具涉及到了Makefile的书写,这里放上本文使用到的makefile,不够用正规,不过目前足够使用。
ipl.bin: ipl.asm 
	nasm -o ipl.bin ipl.asm
boot_info.bin:boot_info.asm
	nasm -o boot_info.bin boot_info.asm
		
os.raw:	ipl.bin boot_info.bin
	qemu-img create -f raw os.raw 1440k
	dd if=ipl.bin of=os.raw bs=512 count=1 
	dd if=boot_info.bin of=os.raw seek=1 bs=512 count=31 seek=1 
run:
	make -r os.raw
	make clean
	qemu-system-x86_64 -drive file=os.raw,if=floppy,index=0,media=disk,format=raw
clean:
	del ipl.bin
	del boot_info.bin
这样,我们在命令行时就无需每次都手打这些指令,直接make run就可以了,就会主动去执行这些指令。
   那么这个是怎么执行的呢?我们可以看到,run中首先要制作一个os.raw,但所以会去上面寻找os.raw的制作方法,需要什么?冒号后面可以看到,需要ipl.bin boot_info.bin,但目前没有,那就再找。找到这两个,分别需要其asm文件,这两个文件是我们一早就写好的,于是就开始制作两个bin,制作完成后,回来制作os.raw,先创建一个空映像,之后用dd将其写入。之后回来执行,clean,clean里主要是清除一些中间中间文件,让整个文件夹看起来清爽一些。由于是在window下,所以我们使用del命令。linux就使用rm。之后启动qemu就可以了。
   跟着这个不太规范的makefile写个一两次,大致理解之后就可以开始玩自己的makefile了。
📘启动区的制作
📖完整代码
  我们这里先放上一份完整的代码,再逐一去说明
 ipl.asm:
;告诉BIOS把启动区加载到内存的该位置。
	ORG 0x7c00
	CYLS equ 10
;调用BIOS 清屏
	mov ax,0x0600
	mov bx,0x700
	mov cx,0
	mov dx,0x184f
	int 0x10
;清屏完后,输出os介绍信息
entry:
	;先设置各种寄存器
	xor ax, ax
	mov ds, ax
	mov es, ax
	mov ss, ax               
	mov sp, 0x9000          
	mov si,msg
print_loop:
	mov al,[si]
	add si,1
	cmp al,0
	
	je read_disk
	mov ah,0x0e
	mov bx,15
	int 0x10
	jmp print_loop
	
read_disk:
	mov ax,0x0820
	mov es,ax
	mov ch,0	;磁道号
	mov cl,2	;扇区号
	mov dh,0	;磁头号
	mov bx,0	;读入内存哪块区域,同时需要看es的值
read_disk_loop:
	mov si,0	;记录失败次数
retry:
	;设置入口参数
	mov ah,0x02 ;设置功能号,0x02标识读,03为写
	mov al,1	;读入扇区数
	mov dl,0	;驱动器号
	int 0x13	;调用磁盘BIOS
	JNC next	;,没出错,就接着读下一个
	;JC 	error	
	mov ah,0x00	;能到这里就说明一定有出错
	mov dl,0x00
	int 0x13	;重置驱动器
	inc si
	cmp si,5
	jbe retry
	jmp error;;出错超过5次就跳转到error
next:
	mov ax,es
	add ax,0x0020 ; 512/16=32 这里寻址是[ES:BX] 所以原本512字节要除16
	mov es,ax
	add cl,1	
	cmp cl,18 	;一个道18个扇区
	jbe read_disk_loop
	mov cl,1	;重置扇区号
	add dh,1	;磁头号,两面
	cmp dh,2
	jb  read_disk_loop
	mov dh,0	;
	add ch,1
	cmp CH,CYLS ;总读取磁道数
	JB	read_disk_loop
	jmp 0x8200
	
fin:
	hlt
	jmp fin
error:
	mov si,error_info
error_loop:
	mov al,[si]
	add si,1
	cmp al,0
	
	je fin
	mov ah,0x0e
	mov bx,15
	int 0x10
	jmp error_loop
	
msg:
	DB	0x0a			;换行
	DB	"hello-os"
	DB	0	 
error_info:
	DB	0x0a			;换行
	DB	"read_disk_error"
	DB	0	 
	resb 510-($-$$);将剩下的空间用0填满
	DB 0x55,0xaa
boot_info.asm:
CYLS equ 0x0ff0
LEDS equ 0x0ff1
VMODE equ 0x0ff2
SCRNX equ 0x0ff4
SCRNY equ 0x0ff6
VRAM equ 0x0ff8
	org 0xc200 ; 这个程序将要被装载到内存的什么地方呢?	
	mov al,0x13 ; VGA显卡,320x200x8位彩色
	mov ah,0x00
	int 0x10
	mov byte[VMODE],8
	mov word[SCRNX],320
	mov word[SCRNY],200
	mov dword [VRAM],0x000a0000
	
	;用BOIS取得键盘上各种LED指示灯的状态
	mov ah,0x02
	int 0x16	;键盘BIOS
	mov [LEDS],al
fin:
	HLT
	JMP fin
📖ipl的解析
看完完整代码之后,我们再来逐一分析。
	ORG 0x7c00
	CYLS equ 10
ORG是Origin的缩写:起始地址,源。在汇编语言源程序的开始通常都用一条ORG伪指令来实现规定程序的起始地址。如果不用ORG规定则汇编得到的目标程序将从0000H开始。也就是cs:0处。
 第二条的CYLS实际上就只是一个常量的定义,equ实际上就是equal,也就是我们把CYLS定义为10这个数。
;调用BIOS 清屏
	mov ax,0x0600
	mov bx,0x700
	mov cx,0
	mov dx,0x184f
	int 0x10
这里和底下的一些具有int的代码片段都大致相同,根据调用BIOS程序的规定,设置好指定的寄存器值后,调用BIOS中断。int就是interrupt。
	;先设置各种寄存器
	xor ax, ax
	mov ds, ax
	mov es, ax
	mov ss, ax               
	mov sp, 0x9000          
这一段,实际上就是在初始化一些寄存器。xor异或,同一个数异或实际上就相当于清零。所以也有使用mov ax,0的写法。mov指令这个就相当于copy吧,或者用c语言的=来理解也可以,因为mov后原寄存器的值不会变化。
	mov si,msg
print_loop:
	mov al,[si]
	add si,1
	cmp al,0
	
	je read_disk
	mov ah,0x0e
	mov bx,15
	int 0x10
	jmp print_loop
	
msg:
	DB	0x0a			;换行
	DB	"hello-os"
	DB	0	 
  这一段就是输出一个字符串了。这里我们把msg拿上来一起看。db就是define byte,dw是define word。而dd 是define double word。使用DB作为数据类型的时候,字符串长度不受限制,默认字符串的每一个字符占一个字节,并且存储过程中,是按照一个字符占一个字节的方式,顺序依次存储的。这里讲msg赋值给si,那么si指向的就是该字符串的首地址。之后一个一个输出就可以了。同样的,设置好寄存器后,调用BIOS10号中断程序来处理。
   这里还有另一种方法,就是直接往显存中写数据。可以查看这篇博客,这里不再赘述。
read_disk:
	mov ax,0x0820
	mov es,ax
	mov ch,0	;磁道号
	mov cl,2	;扇区号
	mov dh,0	;磁头号
	mov bx,0	;读入内存哪块区域,同时需要看es的值
read_disk_loop:
	mov si,0	;记录失败次数
retry:
	;设置入口参数
	mov ah,0x02 ;设置功能号,0x02标识读,03为写
	mov al,1	;读入扇区数
	mov dl,0	;驱动器号
	int 0x13	;调用磁盘BIOS
	JNC next	;,没出错,就接着读下一个
	;JC 	error	
	mov ah,0x00	;能到这里就说明一定有出错
	mov dl,0x00
	int 0x13	;重置驱动器
	inc si
	cmp si,5
	jbe retry
	jmp error;;出错超过5次就跳转到error
next:
	mov ax,es
	add ax,0x0020 ; 512/16=32 这里寻址是[ES:BX] 所以原本512字节要除16
	mov es,ax
	add cl,1	
	cmp cl,18 	;一个道18个扇区
	jbe read_disk_loop
	mov cl,1	;重置扇区号
	add dh,1	;磁头号,两面
	cmp dh,2
	jb  read_disk_loop
	mov dh,0	;
	add ch,1
	cmp CH,CYLS ;总读取磁道数
	JB	read_disk_loop
	jmp 0x8200
  好了,到了启动区最重要的功能,读磁盘内容了。读磁盘有两种方式,一种是chs的方法,一种是使用lba地址的方法,两种方法需要设置的寄存器值有一些差别,大家要注意看其对应的要求。chs就是我们使用的物理结构,柱面,磁头,扇区。而lba就不管这个了,把磁盘看成一个大的空间,分成512个字节一小部分,计算时就是(柱面号*磁头数+磁头号)*扇区数+扇区编号-1。
   当然,读磁盘有可能出现各种问题导致某一次失败,所以我们需要让其有重来的机会。我们这里将其设定为5次,5次及之前,出现错误都会重置后跳转到retry继续读取,如果超过五次,就会跳转到error,打印read_disk_error。而next之后实际上就是要继续读取的写法了,1440kb的软盘结构:2面、80道/面、18扇区/道、512字节/扇区。所以才会有后面的那些判断。
   还有一个小问题,就是防止的地方,由于一次读进来一个扇区,512字节,所以每次之后都需要把内存防止位置在加上512字节,由于我们在es上操作,es实际计算地址时是[ES:BX],es需要左移4位也就是×16。(为什么,就是intel的遗留问题了,有兴趣可以搜索看看,没兴趣就算了。)所以512÷16=32,化为16进制就是0x20,所以才会每次给es加上0x20。但es又不能直接进行加减运算,所以只好先拿出来给ax,做完运算再放回去。
fin:
	hlt
	jmp fin
这里就是停止,hlt会让cpu停止,等待下一个操作,而不是一直运行着,浪费资源。
📖boot_info解析
	org 0xc200 ; 这个程序将要被装载到内存的什么地方呢?	
	mov al,0x13 ; VGA显卡,320x200x8位彩色
	mov ah,0x00
	int 0x10
	mov byte[VMODE],8
	mov word[SCRNX],320
	mov word[SCRNY],200
	mov dword [VRAM],0x000a0000
部分注释已经在上面,里面的一些参数依照如下来设置的:
设置显卡模式(video mode)
AH=0x00;
AL=模式:(省略了一些不重要的画面模式)
0x03:16色字符模式,80 × 25
0x12:VGA 图形模式,640 × 480 × 4位彩色模式,独特的4面存
储模式
0x13:VGA 图形模式,320 × 200 × 8位彩色模式,调色板模式
0x6a:扩展VGA 图形模式,800 × 600 × 4位彩色模式,独特的4
面存储模式(有的显卡不支持这个模式)
返回值:无
所以这实际上也只是一个设置好寄存器调用的问题。
📗实际效果
这里还没有把boot_info写入到磁盘,所以虽然跳转了,但是没有发生什么。主要看一下没有调用画面前的状态。
 
调用后,光标应是消失状态。
 



















