《操作系统真象还原》——2.3 让MBR先飞一会儿

简介: 虽说主引导记录mbr是咱们能够掌控的第一个程序,但这并不是让我们为之激动的理由。我们平时所写的程序都要依赖于操作系统,而我们即将实现的这个程序是独立于操作系统的,能够直接在裸机上运行,这才是让我们激动的理由,对咱们来说这无疑是历史性的一刻。

本节书摘来自异步社区《操作系统真象还原》一书中的第2章,第2.3节,作者:郑钢著,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.3 让MBR先飞一会儿

虽说主引导记录mbr是咱们能够掌控的第一个程序,但这并不是让我们为之激动的理由。我们平时所写的程序都要依赖于操作系统,而我们即将实现的这个程序是独立于操作系统的,能够直接在裸机上运行,这才是让我们激动的理由,对咱们来说这无疑是历史性的一刻。还记得当初我的MBR跑起来时,那可真是发自内心的高兴呀。

好了,不再抒情了,说正事要紧。MBR的大小必须是512字节,这是为了保证0x55和0xaa这两个魔数恰好出现在该扇区的最后两个字节处,即第510字节处和第511字节处,这是按起始偏移为0算起的。由于我们的bochs模拟的是x86平台,所以是小端字节序,故其最后两个字节内容是0xaa55,写到一起后似乎有点不认识了,不要怕,拆开就是0x55和0xaa。

**2.3.1 神奇好用的$和

$$ ,令人迷惑的section** $和 $$

是编译器NASM预留的关键字,用来表示当前行和本section的地址,起到了标号的作用,它是NASM提供的,并不是CPU原生支持的,相当于伪指令一样,对CPU来说是假的。

指令本来没有真伪之别,就像酒一样,因为有了假酒,所以才有了真酒之说。伪指令是相对于CPU可识别的指令来说的,它(伪指令)只是编译器定义的,CPU中并不存在这个指令,愣让CPU执行这些伪指令,CPU会抛出“UD(未定义的操作码)”异常。伪指令是编译器为了开发人员写代码方便而提供的一些符号,这些符号在编译时,会由编译器转换成CPU可识别的东西,如指令或地址等。

汇编语言中的标号是程序员“显式地”写在明处的,如:

……
code_start:
  mov ax, 0
……

code_start这个标号被nasm认为是一个地址,此地址便是“mov ax,0”这条指令所在的地址,即其指令机器码存放的内存位置是code_start。code_start只是个标记,CPU并不认识,和伪指令类似,它是假的,CPU不认。所以nasm会用为其安排的地址来替换标号code_start,到了CPU手中,已经被替换为有意义的数字形式的地址了。

$属于“隐式地”藏在本行代码前的标号,也就是编译器给当前行安排的地址,看不到却又无处不在,$在每行都有。或者这种说法并不是很正确,只有“显示地”用了$的地方,nasm编译器才会将此行的地址公布出来。如果上面的例子改为:

……
code_start:
  jmp $
……

这就和jmp code_start是等效的。$和code_start是同一个值。

$$ 指代本section的起始地址,此地址同样是编译器给安排的。 对于$和 $$

的意义,我强调过了,是编译器给安排的地址,默认情况下,它们的值是相对于本文件开头的偏移量。至于实际安排的是多少,还要看程序员同学是否在section中添加了vstart。这个关键字可以影响编译器安排地址的行为,如果该section用了vstart=xxxx修饰,

$$ 的值则是此section的虚拟起始地址xxxx。$的值是以xxxx为起始地址的顺延。如果用了vstart关键字,想获得本section在文件中的真实偏移量(真实地址)该怎么做?nasm编译器提供了这个方法。 section.节名.start。 如果没有定义section,nasm默认全部代码同为一个section,起始地址为0。 稍带说一下section。很多东西从名字上就能理解它的功能,毕竟名字不是乱起的。section也称为节、段,故名思义,是程序中的一小块,形象一点地说,就是用section这个关键字在程序中圈出一块地,并向编译器宣称,这块地我要做些规划,至于我用来干什么您就不用操心了,编译时请您合理安排。 为什么说合理安排呢,因为section是伪指令,是nasm提供的,具体解释权还是人家nasm说了算。比如以下代码:

section data
   var dd 0
section code
   jmp start
… ...

编译器一看这两个section,data中定义的是变量,code中是代码,于是把这两个section的内容分别归入最终的数据段和代码段。

有时候nasm并不会完全听您的,如改为下面的例子:

section data_a
  var dd 0
section code
  jmp start
section data_b
  var dd 1
  ……

虽然人为定义了三个section,但nasm发现data_a和data_b这两个section完全能够合并到一起,于是在编译阶段会被“合理”地安排到一起。

在第0章中有说明section和segment的区别。section是伪指令,CPU运行程序是不需要这个东西的,这个只是用来给程序员规划程序用的,有了section,就可以将自己的代码分成一段一段的,当然这只是在逻辑上的段,实际上编译出来的程序还是完整的一体。逻辑上划分成段的好处是方便开发人员梳理代码,方便管理。想像一下,把一大片农田按亩来划分成一个个的小段,一眼望去,是不是显得井然有序呢?单是简短的几行汇编代码是无法体现出这一优势的,就像如果农田本来就不大,还要划分成多个段,那自然是得不偿失的。当代码量上去的时候,会发现如果不在逻辑上将其拆分成几块,对一锅粥似的代码进行维护,代价还是很大的,可能一会儿脑子也像一锅粥了呢。

划分成section后,编译器便根据您的意图,将这些section中的内容安排位置,它被安排到哪里咱们是不需要关心的,咱们也不必管,因为程序内部的关联是通过地址实现的。想想看,无非是section被安排到A位置,其他用到此section中内容的相关指令,其操作数为A地址,若section被安排到B位置,操作数便是B地址,这些都是编译器安排的,它会帮您圆上的。

关于section地址更详细的说明,大家可以参照第3章,这里只是抛砖引玉。

总之,section是给开发人员逻辑上规划代码用的,只起到思路清晰的作用,最终还是在编译阶段由nasm在物理上的规划说了算。

2.3.2 NASM简单用法
在咱们的实际工程中只用到了nasm的一些简单功能,所以不必担心连操作系统的一句代码都没写呢,却先要为学习其他的东西而付出额外的精力。

nasm -f <format><filename> [-o <output>]
以上是nasm的基本用法,对咱们来说,够用了。注意我说的是“基本”,还有好多其他参数呢,不过咱们用不着。甚至,大多数时候连-f都不用呢。

-o 就是指定输出可执行文件的名称。

查看一下nasm的帮助,ok,执行man nasm回车,输出的信息太多了,我们只看-f的说明就行了。

-f format
   Specifies the output file format. 
To see a list of valid output formats, use the -hf option.

瞧,人家说啦,-f是用来指定输出文件的格式。要想知道有多少种有效的输出格式,用-hf选项。那咱们还是用nasm –hf来查看一下吧,见表2-2。
screenshot
screenshot

一共列出了21个,不过大部分格式和咱们关系不大,咱们只关注bin和elf格式就好啦。

既然bin是默认输出格式,也就是不用-f bin来明确指定了,所以以后咱们只在输出elf格式时才用-f指定。

bin是指纯二进制。二进制就二进制吧,还有不纯的?就像前面的拿酒举例一样,本来没有真酒之说,由于有了假酒的出现,才有了真的说法。纯二进制就是不掺杂其他的东西,直接给CPU后就能用,也就是可执行文件中什么样,内存中就什么样。我们平时所说的elf或pe格式的二进制可执行文件,那里面有好多和指令无关的东西,里面掺杂了程序的内存布局、位置等信息,这是给操作系统中的程序加载器用的,是属于操作系统规划的范畴了。

2.3.3 请下一位选手MBR同学做准备
有点不好意思了,说了好久,才说到实质性的东西,好了,赶紧说正题。

代码2-1 (c2/a/boot/mbr.S)

1 ;主引导程序
 2 ;------------------------------------------------------------
 3 SECTION MBR vstart=0x7c00
 4   mov ax,cs
 5   mov ds,ax
 6   mov es,ax
 7   mov ss,ax
 8   mov fs,ax
 9   mov sp,0x7c00
10
11 ; 清屏利用0x06号功能,上卷全部行,则可清屏。
12 ; -----------------------------------------------------------
13 ;INT 0x10  功能号:0x06  功能描述:上卷窗口
14 ;------------------------------------------------------
15 ;输入:
16 ;AH 功能号= 0x06
17 ;AL = 上卷的行数(如果为0,表示全部)
18 ;BH = 上卷行属性
19 ;(CL,CH) = 窗口左上角的(X,Y)位置
20 ;(DL,DH) = 窗口右下角的(X,Y)位置
21 ;无返回值:
22   mov   ax, 0x600
23   mov   bx, 0x700
24   mov   cx, 0        ; 左上角: (0, 0)
25   mov   dx, 0x184f      ; 右下角: (80,25),
26                   ; VGA文本模式中,一行只能容纳80个字符,共25行。
27                  ; 下标从0开始,所以0x18=24,0x4f=79
28   int   0x10        ; int 0x10
29
30 ;;;;;;;;;  下面这三行代码获取光标位置  ;;;;;;;;;
31 ;.get_cursor获取当前光标位置,在光标位置处打印字符。
32   mov ah, 3           ; 输入: 3号子功能是获取光标位置,需要存入ah寄存器
33   mov bh, 0          ; bh寄存器存储的是待获取光标的页号
34
35   int 0x10           ; 输出: ch=光标开始行,cl=光标结束行
36                   ; dh=光标所在行号,dl=光标所在列号
37
38 ;;;;;;;;;  获取光标位置结束  ;;;;;;;;;;;;;;;;
39
40 ;;;;;;;;;   打印字符串  ;;;;;;;;;;;
41   ;还是用10h中断,不过这次调用13号子功能打印字符串
42   mov ax, message
43   mov bp, ax          ; es:bp 为串首地址,es此时同cs一致,
44                 ; 开头时已经为sreg初始化
45
46   ; 光标位置要用到dx寄存器中内容,cx中的光标位置可忽略
47   mov cx, 5          ; cx 为串长度,不包括结束符0的字符个数
48   mov ax, 0x1301      ;子功能号13显示字符及属性,要存入ah寄存器,
49                  ; al设置写字符方式 ah=01: 显示字符串,光标跟随移动
50   mov bx, 0x2         ; bh存储要显示的页号,此处是第0页,
51                 ; bl中是字符属性,属性黑底绿字(bl = 02h)
52   int 0x10           ; 执行BIOS 0x10 号中断
53 ;;;;;;;;;   打字字符串结束  ;;;;;;;;;;;;;;;
54
55   jmp $            ; 使程序悬停在此
56
57   message db "1 MBR"
58   times 510-($-
$$
) db 0
59   db 0x55,0xaa

简短说一下代码功能,在屏幕上打印字符串“1 MBR”,背景色为黑色,前景色为绿色。

由于还没有给大家讲解显卡的使用方法,故本段代码中关于“打印显示”的操作都利用BIOS给我们建立好的例程就好了,这里第0x10号中断便是负责有关打印的例程。

0x10中断是最为强大的BIOS中断了,调用的方法是把功能号送入ah寄存器,其他参数按照BIOS中断手册的要求放在适当的寄存器中,随后执行int 0x10即可。我们不用太细致琢磨BIOS功能调用了,大家可以参数代码中的注释了解下即可,毕竟咱们这里用BIOS中断只是临时的,以后也用不到了。

第3行的“vstart=0x7c00”表示本程序在编译时,告诉编译器,把我的起始地址编译为0x7c00。

第4~8行是用cs寄存器的值去初始化其他寄存器。由于BIOS是通过jmp 0:0x7c00跳转到MBR的,故cs此时为0。对于ds、es、fs、gs这类sreg,CPU中不能直接给它们赋值,没有从立即数到段寄存器的电路实现,只有通过其他寄存器来中转,这里我们用的是通用寄存器ax来中转。例如mov ds:0x7c00,这样就错了。

第9行是初始化栈指针,在CPU上运行的程序得遵从CPU的规则,mbr也是程序,是程序就要用到栈。目前0x7c00以下暂时是安全的区域,就把它当作栈来用。

第11~28行是清屏。因为在BIOS工作中,会有一些输出,如检测硬件的结果信息。为了让大家看清楚我们在MBR中的输出字符串,故先把BIOS的输出清掉,这里演示的是BIOS中断int 0x10的用法。

第30~35行是做打印前的工作,先获取光标位置,目的是避免打印字符混乱,覆盖别人的输出。其实这是防君子不防小人的做法,万一别人不在光标处打印,自己打印的内容同样也会被别人覆盖。不管别人了,咱们做好自己的就行,老老实实地只在光标处打印。不知道这是否能提醒大家,字符打印的位置,不一定要在光标处,字符的位置只和显存中的地址有关,和光标是没关系的,这只是人为地加个约束,毕竟光标在视觉上告诉了我们当前字符写到哪里了,完全是为了好看,不要以为光标就是新打印字符的位置。更多细节,以后讲显卡时会提到。

这里还用到了页的概念,您看第33行,往bh寄存器中写入了0,这是告诉BIOS例程,我要获取第0页当前的光标。什么是页呢?

显示器有很多种模式,如图形模式、文本模式等,在文本模式中,又可以工作于8025和4025等显示方式,默认情况下,所有个人计算机上的显卡在加电后都将自己置为8025这种显示方式。8025是指一屏可以显示25行、每行80列的字符,也就是2000个字符。但由于一个字符要用两字节来表示,低字符是字符的ASCII编码,高字节是字符属性,故显示一屏字符需要用4000字节(实际上,分配给一屏的容量是4KB),这一屏就称为一页,0页是默认页。

第38~52行是往光标处打印字符。说一下第48行的mov ax,0x1301,13对应的是ah寄存器,这是调用0x13号子功能。01对应的是al寄存器,表示的是写字符方式,其低2位才有意义,各位功能描述如下。

(1)al=0,显示字符串,并且光标返回起始位置。

(2)al=1,显示字符串,并且光标跟随到新位置。

(3)al=2,显示字符串及其属性,并且光标返回起始位置。

(4)al=3,显示字符串及其属性,光标跟随到新位置。

第55行执行了个死循环,$是本行指令的地址,这属于伪指令,是汇编器在编译期间分配的地址。在最终编译出来的程序中,$会被替换为指令实际所在行的地址。jmp是个近跳转,$是jmp自己的地址,于是跳到自己所在的地址再执行自己,又是跳到自己所在的地址再继续执行跳转,这样便实现了死循环。可见CPU可乖了,它只会埋头做事,并不会觉得有什么不妥,靠谱,值得依赖。

第57行是定义打印的字符串。

第58行的

$$ 是指本section的起始地址,上面说过了$是本行所在的地址,故$- $$

是本行到本section的偏移量。由于MBR的最后两个字节是固定的内容,分别是0x55和0xaa,要预留出这2个字节,故本扇区内前512-2=510字节要填满,那到底要用多少字节才能填满此扇区呢。用510字节减去上面通过$-

$$ 得到的偏移量,其结果便是本扇区内的剩余量,也就是要填充的字节数。由此可见第50行的“times 510-($- $$

) db 0”是在用0将本扇区剩余空间填充。

代码说完了,可还有两件大事要做,1是编译,2是如何将编译后的文件存储到0盘0道1扇区中成为MBR,以供BIOS大神加载之用。

前面介绍了nasm的用法,咱们马上来编译汇编代码。

nasm -o mbr.bin mbr.S回车,您看,这样就编译成功了,我连-f都没有指定吧。按理说此文件大小是512字节,咱们用ls命令验证一下:ls -lb mbr.bin回车,以下是ls的输出。

-rw-rw-r--. 1 work work 512 7月 26 21:10 mbr.bin
用过Linux的同学对这个输出还是很熟悉的,若头一次用Linux的同学也不要慌张,这里面好多的信息并不重要,只要看看中间部分就好了,512,果然是512字节,这下心里踏实了,下一步是考虑如何将此文件写入0盘0道1扇区。

这里再给大家介绍另一个Linux命令:dd。dd是用于磁盘操作的命令,功能太强大了,有如穿甲弹一样,可以深入磁盘的任何一个扇区,无坚不摧。所以,它也可以删除Linux操作系统自己的文件,是把双刃剑。

还是先看帮助文件,man dd回车,为了节约大家的时间,我只把咱们今后用到的几个选项摘了出来,还是那句话,够用就行了,需要时再学。

if=FILE
read from FILE instead of stdin

此项是指定要读取的文件。

of=FILE
write to FILE instead of stdout

此项是指定把数据输出到哪个文件。

bs=BYTES
read and write BYTES bytes at a time (also see ibs=,obs=)

此项指定块的大小,dd是以块为单位来进行IO操作的,得告诉人家块是多大字节。此项是统计配置了输入块大小ibs和输出块大小obs。这两个可以单独配置。

count=BLOCKS
copy only BLOCKS input blocks

此项是指定拷贝的块数。

seek=BLOCKS
skip BLOCKS obs-sized blocks at start of output

此项是指定当我们把块输出到文件时想要跳过多少个块。

conv=CONVS
convert the file as per the comma separated symbol list

此项是指定如何转换文件。

append append mode (makes sense only for output; conv=notrunc suggested)

这句话建议在追加数据时,conv最好用notrunc方式,也就是不打断文件。

齐了,dd的介绍就到这了,赶紧试验一下这个神奇的工具吧。

dd if=/your_path/mbr.bin of=/your_path/bochs/hd60M.img bs=512 count=1 conv=notrunc

各位看官,请将上面命令行中的your_path替换为您自己的实际路径。

输入文件是刚刚编译出来的mbr.bin,输出是我们虚拟出来的硬盘hd60M.img,块大小指定为512字节,只操作1块,即总共1*512=512字节。由于想写入第0块,所以没用seek指定跳过的块数。

执行上面的命令后,会有如下输出。

记录了1+0 的读入

记录了1+0 的写出

512字节(512 B)已复制,0.313312 秒,1.6 KB/秒

这就说明命令执行成功了,mbr.bin已经写进hd60M.img的第0块了。借鉴美国宇航员阿姆斯特朗的一句话:虽然这只是简单的一小步,但却是实现我们自己系统的一大步。记得当初我可是非常激动呢。

启动bochs测试一下,我习惯到bochs安装目录下启动它,bin/bochs –f bochsrc.disk回车,接着会显示如图2-4所示的界面。

screenshot

默认是[6],开始模拟啦。回车。

由于咱们编译的是可调试的版本,所以会停下来,bochs等待咱们键入下一步的命令,如图2-5所示。

大家看到,这一下弹出了两个界面,前面的那个是bochs所模拟的机器,可以认为它就是台电脑了,不仅仅是电脑的显示器。后面的界面是bochs的控制台,咱们控制bochs运行就要在这里输入命令。现在激活后面的bochs控制台,输入字符c后,回车。bochs所模拟的机器就开始运行了。这里键入的c是continue,调试方法同gdb类似,详细的bochs操作方法咱们会在下一章中介绍。

screenshot

MBR运行起来后,就会出现下面的效果,如图2-6所示。

screenshot

相关文章
|
编译器
1.操作系统如何从BIOS到MBR的
1.操作系统如何从BIOS到MBR的
135 0
|
Linux C语言
《操作系统真象还原》——导读
以上情况对我们学习操作系统来说也同样存在,比如当老师介绍中断发生时的上下文保护时,我们更多的疑问不是如何保存CPU的上下文数据,而是想知道为什么在不同的特权级下会使用不同的栈,这背后的原理是什么,并且这是如何做到的。
1647 0
|
存储 内存技术 程序员
《操作系统真象还原》——2.2 软件接力第一棒,BIOS
Intel 8086有20条地址线,故其可以访问1MB的内存空间,即2的20次方=1048576=1MB,地址范围若按十六进制来表示,是0x00000到0xFFFFF。不知道硬件工程师当时设计的初衷是什么,总之人家有自己的理由,这1MB的内存空间被分成多个部分。
2100 0
|
存储
《操作系统真象还原》——第2章 编写MBR主引导记录,让我们开始 掌权 2.1 计算机的启动过程
所以,都在内存中运行程序,操作系统和硬件设计都省事了,这可能也是为了方式的统一吧,否则总不能出现某种存储介质后,操作系统和硬件就要付出额外努力去支持。当然,具体原因只有硬件工程师才知道,咱们在此先打住,继续咱们的内容。
1559 0
|
运维 Linux
《操作系统真象还原》——1.5 运行bochs
我们键入的是上面长方形框框中的部分:bochsrc.disk。由于我们刚刚把此文件放到了bochs的安装路径下,bochs找到了它并加载成功。紧接着下面给出的默认选项变成了[6],也就是Begin simulation选项,开始模拟x86硬件平台。
4260 0
|
NoSQL
《操作系统真象还原》——1.4 配置bochs
Bochs既然是模拟硬件的,它就得知道,您需要它模拟的计算机是什么样的,换句话说,在这个虚拟机中有哪些硬件,启动顺序是什么,是从软盘开始,还是从硬盘开始?人家也得像模像样地跟BIOS差不多才行。
2732 0
|
12天前
|
安全 Linux 数据安全/隐私保护
Vanilla OS:下一代安全 Linux 发行版
【10月更文挑战第30天】
31 0
Vanilla OS:下一代安全 Linux 发行版
|
15天前
|
人工智能 安全 Linux
|
1月前
|
Unix 物联网 大数据
操作系统的演化与比较:从Unix到Linux
本文将探讨操作系统的历史发展,重点关注Unix和Linux两个主要的操作系统分支。通过分析它们的起源、设计哲学、技术特点以及在现代计算中的影响,我们可以更好地理解操作系统在计算机科学中的核心地位及其未来发展趋势。
|
3月前
|
编解码 安全 Linux
基于arm64架构国产操作系统|Linux下的RTMP|RTSP低延时直播播放器开发探究
这段内容讲述了国产操作系统背景下,大牛直播SDK针对国产操作系统与Linux平台发布的RTMP/RTSP直播播放SDK。此SDK支持arm64架构,基于X协议输出视频,采用PulseAudio和Alsa Lib处理音频,具备实时静音、快照、缓冲时间设定等功能,并支持H.265编码格式。此外,提供了示例代码展示如何实现多实例播放器的创建与管理,包括窗口布局调整、事件监听、视频分辨率变化和实时快照回调等关键功能。这一技术实现有助于提高直播服务的稳定性和响应速度,适应国产操作系统在各行业中的应用需求。
109 3

热门文章

最新文章