操作系统设计与编写教程: MBR编写(1)

前情提示: 该操作系统内存布局抄的linux0.11,整个系统仅为研究项目,开源,写的不好不要拷打我。

环境配置

nasm汇编编译器: Index of /pub/nasm/releasebuilds

安装教程: nasm编译器安装

bochs虚拟机: Bochs x86 PC模拟器下载 |SourceForge.net

安装教程: bochs的安装与使用 – 知乎

配置硬盘和bochsrc.bxrc:

硬盘写入工具:

dd硬盘写入工具

 

关于揭发计算机原理补充内容

计算机加电,BIOS完成自检后会加载硬盘第一扇区的mbr到0x00000:0x7c00处并跳转过去

寄存器: cpu里的“内存”断电后数据都不复存在,并且大小较小早期的cpu只有2字节,接着是4字节,到了现代的cpu上寄存器早拓展到8字节,你可以暂时把他理解为是一个变量。这里我们先说早期的cpu的常用寄存器。

以8086为例子:

这款cpu有14个寄存器,寄存器宽度为16位(2字节)

常用的寄存器有4个:AX、BX、CX、DX 他们啥都能存没有特殊用途

并且这些寄存器还可以拆成8位的来用AX能拆成AH(高八位),AL(低八位),其余寄存器同理只需要把开头的A,B,C,D加个H或L就行

4个段寄存器:CS、DS、SS、ES (这些是用来存放段基地址,看不懂是正常的,看下面写的解释内容即可)

2个用来取指令的寄存器:CS、 IP 这两就是整个cpu的心脏,用来不断的从内存中取指令运行

2个指针寄存器:BP,SP 这里提到一个数据结构叫栈,学过C++的可能知道,如果不知道可以看这个视频了解一下

【算法】数据结构中的栈有什么用?

这里的bp,sp是和前面的SS栈段寄存器一块搭配使用的(sp是强制的因为汇编指令中有push,pop必须用到ss和sp寄存器),虽然说bp是指向栈底,但是说实话bp这个寄存器更像是一个存储偏移地址用的寄存器(个人主观观点),而ss寄存器和sp寄存器,ss用来指定栈的基址,sp用来指定栈的顶端地址(决定栈的大小)

2个存偏移地址的寄存器: si,di

英特尔说他俩是存偏移地址的,但实际上我觉得更像是通用寄存器。

 

地址:假设你住在一个小区,跑外卖的想要精准的把外卖送到你家,就需要你家的地址,而内存的地址也是如此,只不过内存的地址没有复杂的主谓宾只有简单的阿拉伯数字。

例如:现在我们想要访问内存中的第一个字节(这里指的是实模式下的寻址,如果并不清楚实模式是啥可以看我下面写的),则是0x00000:0000,这里为啥是0x00000:0000而不是0x00000呢这样多方便,这个就牵扯到历史问题了,话说当年滚滚长江东逝水,浪花淘尽英雄…..诸公可否随我匡扶汉室(手动doge)。不扯远的了,当年英特尔设计了一款cpu名叫8086,小86很不给力,至少不如AE86,他的寄存器只有2字节大小,也就是16比特。因为cpu都是靠寄存器来寻址的,2字节能表示的最大数字是65535也就是64kb,也就是说这款cpu最大只能寻址到64kb,这点大小十分捉襟见肘,看片都不够用,但是也比当年美国登月用的内存阔气多了,为了解决内存不够用的缘故,英特尔的工程师想到一招,我们可以让地址在出厂的时候后面在加个0啊(左移4位),于是原本的地址0x0000被调成了0x00000,终于能访问到1mb的内存了,但是寄存器只能寻址64kb啊,你不能最后出来的地址全后面摆个零吧,那跟没有有啥区别。英特尔工程师又想到一招,我们可以把内存分成一个一个段,用段基址:段偏移地址不就能自由的访问每个段了因此最后的结果就是, 现在我给了ds寄存器0x0000的地址作为段基址,给了bx寄存器0x7c00作为偏移地址,首先ds寄存器的地址左移4位得到0x00000,加上bx寄存器中的地址得出0x07c00.可以说这是一个天才的方案,看来英特尔工程师的智商恐怕在我之上。

实模式和保护模式:严格意义上来说这俩玩意的最大区别就在于寻址方式上了,在实模式下寻址,全是物理地址,没有一点限制。到保护模式下,这片土地上突然就建了个统辖一切访问内存的事物的中央政府,之前随便进出的段,需要登记一下才能被访问,而且还得是通过规范的流程访问。

字节序:现在我要把0xAB72EF85存储到内存中,小端字节序会把高位存到高地址,地位存到低地址,而大端则相反具体可以看此图。

(图: 知乎 @小芯叽)

如果还是不能理解可以直接去看文章:大端序还是小端序,深入理解字节顺序

mbr要做的:

1. 显示个Hello,World.

2. 加载加载器程序(下篇文章)

显示Hello,World我们有两种方法

1. 直接写显存缓冲区

2.调用BIOS软中断显示

先科普一下中断的概念,中断分为内部中断,外部中断,软中断,现在举个例子大致了解一下

你在打游戏的时候,外卖员敲了你的门,于是你中断了你的游戏进程,去开门拿外卖。

计算机也是这样,操作系统正常运行程序的时候,外设传来了信号需要操作系统处理,操作系统不得不暂停当前手上的活,去处理外设的事。

 

这里先说一下软中断(软件中断)这个概念,你可以把他理解为一个全局性的所有应用程序都能调用的函数即可。调用中断需要依赖中断向量表,如图

\

(图:知乎 @万物皆有源)

中断向量表每项4字节,存储着中断处理程序的基地址和偏移地址。

cpu调用一个中断的流程如下:

要调用的中断号 * 4 = 中断号在中断向量表中对应的地址(这里仅限于实模式下的中断调用,保护模式下回建立个idt表取代中断向量表)

注意:BIOS会提供一些简单的接口(软中断)和硬件沟通用来减轻程序员开发压力(恩情还不完)

两个方法通用的编译运行方法 (bochsrc.bxrc,img文件都在同一目录)

nasm bootsect.asm -o bootsect.bin
dd if=bootsect.bin of=./qcos.img bs=512 count=1
bochs -dbg

这里我选择全写一遍做个示范

方法一(直接写):

; bootsect.asm

; 歪门邪道清屏法
mov ah,0x00
mov al,0x03
int 0x10

mov ax,0xb800
mov ds,ax  ;显存缓冲区位于0xb8000~0xbffff处,该段为32kb

mov si,0

; 循环打印message
mov cx,message_length
mov di,message

jmp print_text

print_text:
    ; 取文本内容
    mov al,[es:di+0x7c00]
    ; 扔到显存
    mov [si],al
    inc si
    mov byte [si],0x07
    inc si
    inc di
    loop print_text
    jmp $

message db 'Hello,World.'
message_length equ ($-message)

times 510-($-$$) db 0
db 0x55,0xaa

效果:

先讲一遍源码方便后面理解例子二

这里mov字面意思就是移动,在开头有这样几行代码

mov ah,0x00
mov al,0x03
int 0x10

分别是给ax寄存器中的ah,al寄存器分别赋值0x00和0x03,作为调用10号中断的参数设置。

顺带提一嘴10号中断是用来和显示器打交道的接口。方法二还会用到它。

mov ax,0xb800
mov ds,ax  ;显存缓冲区位于0xb8000~0xbffff处,该段为32kb

mov si,0

这三行代码前两行,为什么非要导个手先传给ax基地址再传给ds段寄存器,因为段寄存器不能直接赋值必须得通过别的寄存器来给他赋值。

然后si设置显存段偏移地址没啥好说的。

; 循环打印message
mov cx,message_length
mov di,message

接着是这两行,首先说message_length

message_length equ ($-message)

我们把message_length看作是一个常量,不过他是不占内存空间的,只会在编译阶段把自己所赋的值,替换掉所有写了这个常量名的地方。

这里的 $ 在nasm表示着当前的汇编(偏移?)地址,但我更愿意把这个汇编地址看成段内偏移地址。因为我的程序没有进行段声明,所以nasm编译器会自动把程序打包成一个段。所以这里的 $ 符号也可以看做是当前位置相对于段开头的偏移地址。

因为后面的print_text里有个汇编指令loop,这是循环指令,cx寄存器中则存储要循环的次数.

(我觉得把pirnt_text改成print_char更恰当)

然后是message,在源代码中他长这样。

message db 'Hello,World.'

message是一个标号可以在后面加个冒号,也可以不加,加个冒号可能看上去像个函数?标号的作用是表示指令或数据相对于程序开头的汇编地址,但是我们这里看成是表示指令或数据相对于程序开头的段内偏移地址即可。

di寄存器存储message偏移地址.

 

然后是db指令这是个伪指令,前面的equ也是个伪指令,他们都是没有相对应的机器码的。

db 用来在内存中声明字节类型的数据

dw 用来在内存中声明字(两个字节)类型的数据

dd 用来在内存中声明双字(四个字节)类型的数据

 

然后是print_text

jmp print_text

print_text:
    ; 取文本内容
    mov al,[es:di+0x7c00]
    ; 扔到显存
    mov [si],al
    inc si
    mov byte [si],0x07
    inc si
    inc di
    loop print_text
    jmp $

 注意:每个字符需要在显存占用1个字(两个字节)的大小

jmp指令,依旧字如其名: 转移

转移指令这一块,后面会写篇文章单独说,这里只需要知道他是跳转到print_text偏移处即可,而且不加上jmp也没事,因为print_text只是一个标号,只是表明mov al,[es:di+0x7c00]这条指令相对于程序开头的段内偏移地址,也是个类似于常量的存在。这里写上是为了直观点。(其实本来是打算写个call指令的,但是讲起来篇幅太长,写起来还要设置栈段,就先这样吧)

 取文本内容这里就涉及到了mov指令的第二种格式,第一种是 mov 寄存器,寄存器

并且nasm编译器下的mov指令赋值是从右往左的

第二种就是 mov 寄存器,[内存单元]

在mov al,[es:di+0x7c00]这里 es作为段基址(一般情况下es默认为0,除非BIOS程序特殊,或者你的电脑让宇宙辐射干了),di里头存的是message的偏移地址,但是要加上0x7c00因为mbr会被加载到0x07c00处,这里al存的是文本的ASCII码

然后是mov [si],al

这个是mov第三种格式mov [内存单元],寄存器

这里没有写成ds:si的格式,是因为当你不写段基址的时候,编译器会自动拿ds当段基址。

然后是inc si,这条指令很好理解,给si寄存器中的值+1,inc就是加一指令,而且还能给内存单元加一(不过我估计那就得加个前缀表明是要给字节类型数据加一,还是字类型数据加一),指向字符的属性单元。

然后是mov byte [si],0x07,这里加了个byte前缀,表示写入的是一个字节类型的数据,不加编译器会报错,只有制定了写入数据的类型才不会出现本来要写入0x07,结果写入成了0x00(高位),0x07(低位),0x07是字符的属性,意思是黑底白字,无闪烁,无高亮

然后是inc si指向下一个字符存储的地方

inc di,指向下一个要取的文本内容

最后loop回到print_text开头CX寄存器值 – 1,一直循环,直到cx寄存器为0

因为cx为0时跳出循环,就正好遇到jmp $程序就停在这了。

 

方法二(调用BIOS中断)

; bootsect.asm

; 歪门邪道清屏法
mov ah,0x00
mov al,0x03
int 0x10

; 循环打印message
mov cx,message_length
mov di,message

jmp print_text

print_text:
    mov ah,0xe
    ; 取文本内容
    mov al,[es:di+0x7c00]
    int 10h
    inc di
    loop print_text
    jmp $

message db 'Hello,World.'
message_length equ ($-message)

times 510-($-$$) db 0
db 0x55,0xaa

这个方法跟上个方法的不同就在于print_text的不同

将ah设置为0x0e,表明要调用10号中断的打印字符的功能.

然后在al中设置要打印的字符,调用10号中断打印.

效果:

这俩程序的区别就在于光标,BIOS中断程序打印完一个字符后会自动将光标后移,而直接写入显存缓冲区不会。

 

光看文章如果看不懂的,可以去多敲敲代码,做的过程就是在学。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
3 条回复 A文章作者 M管理员
  1. 原柠科技

    补充一下$$他表明从段开始位置的偏移地址,$改一下措辞应该说是当前行的偏移地址。$-$$就是前面所写程序的大小,510减去$-$$就是剩下空余的地方因为一个扇区512字节,并且mbr最后两字节要写入0x55,0xaa,因此是拿510去减,剩下的位置没有用所以填充0,最后两字节填充0x55,0xaa不填充计算机会认为这是一个无效的mbr