📝本文介绍
本文主要从系统系统的启动流程开始,中间介绍一些所用工具的使用方法,最后将完成一个启动区的制作。此次的启动区只涉及到汇编代码。
👋作者简介:一个正在积极探索的本科生
📱联系方式: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写入到磁盘,所以虽然跳转了,但是没有发生什么。主要看一下没有调用画面前的状态。
调用后,光标应是消失状态。