由于能力有限,不一定跟得上书本的进度,只能以自己的能力为限,一步一步写下去。


   第一天的内容是用二进制编辑工具写一个.img的文件,如图:

30天自制操作系统 笔记1_自制操作系统

   这是照着书上内容抄的。可以看到窗口右下方的数字1,474,560bytes,1474560/1024=1440,就是一张3寸盘的容量。

   我们用二进制编辑器,编写了一张可启动的软盘,通过虚拟机(qemu)加载这张“软盘”后可以启动并显示一些字符。

   说实在的,二进制文件不是很容易看懂,为了方便起见,我把对应的汇编语句贴出来,这样更方便理解。


1 00000000                                 ;hello-os
 2 00000000                                 ;TAB=8
 3 00000000                             
 4                                              ORG 0X7c00
 5 00007C00 EB 4E                               JMP ENTRY
 6 00007C02 90                                  DB  0X90
 7 00007C03 48 45 4C 4C 4F 49 50 4C             DB  "HELLOIPL"
 8 00007C0B 0200                                DW  512
 9 00007C0D 01                                  DB  1
10 00007C0E 0001                                DW  1
11 00007C10 02                                  DB  2
12 00007C11 00E0                                DW  224
13 00007C13 0B40                                DW  2880
14 00007C15 F0                                  DB  0XF0
15 00007C16 0009                                DW  9
16 00007C18 0012                                DW  18
17 00007C1A 0002                                DW  2
18 00007C1C 00000000                            DD  0
19 00007C20 00000B40                            DD  2880
20 00007C24 00 00 29                            DB  0, 0, 0X29
21 00007C27 FFFFFFFF                            DD  0XFFFFFFFF
22 00007C2B 48 45 4C 4C 4F 2D 4F 53 20 20       DB  "HELLO-OS   "
   00007C35 20
23 00007C36 46 41 54 31 32 20 20 20             DB  "FAT12   "
24 00007C3E 00 00 00 00 00 00 00 00 00 00       RESB    18
   00007C48 00 00 00 00 00 00 00 00
25 00007C50                             
26 00007C50                                 ENTRY:
27 00007C50 B8 0000                             MOV AX, 0
28 00007C53 8E D0                               MOV SS, AX
29 00007C55 BC 7C00                             MOV SP, 0X7C00
30 00007C58 8E D8                               MOV DS, AX
31 00007C5A 8E C0                               MOV ES, AX
32 00007C5C                                 
33 00007C5C BE 7C74                             MOV SI, MSG
34 00007C5F                                 
35 00007C5F                             
36 00007C5F                                 PUTLOOP:
37 00007C5F                             
38 00007C5F 8A 04                               MOV AL, [SI]
39 00007C61 83 C6 01                            ADD SI, 1
40 00007C64 3C 00                               CMP AL, 0
41 00007C66 74 09                               JE  FIN
42 00007C68 B4 0E                               MOV AH, 0X0E
43 00007C6A BB 0009                             MOV BX, 9
44 00007C6D CD 10                               INT 0X10
45 00007C6F EB EE                               JMP PUTLOOP
46 00007C71                             
47 00007C71                                 FIN:
48 00007C71 F4                                  HLT
49 00007C72 EB FD                               JMP FIN
50 00007C74                             
51 00007C74                                 MSG:
52 00007C74 0A 0A                               DB  0X0A, 0X0A
53 00007C76 48 65 6C 6C 6F 20 57 6F 72 6C       DB  "Hello World"
   00007C80 64
54 00007C81 0A                                  DB  0X0A
55 00007C82 00                                  DB  0
56 00007C83                             
57 00007C83 00 00 00 00 00 00 00 00 00 00       RESB    0X7DFE-$
   00007C8D 00 00 00 00 00 00 00 00 00 00
   00007C97 00 00 00 00 00 00 00 00 00 00
   00007CA1 00 00 00 00 00 00 00 00 00 00
   00007CAB 00 00 00 00 00 00 00 00 00 00
   00007CB5 00 00 00 00 00 00 00 00 00 00
   00007CBF 00 00 00 00 00 00 00 00 00 00
   00007CC9 00 00 00 00 00 00 00 00 00 00
   00007CD3 00 00 00 00 00 00 00 00 00 00
   00007CDD 00 00 00 00 00 00 00 00 00 00
   00007CE7 00 00 00 00 00 00 00 00 00 00
   00007CF1 00 00 00 00 00 00 00 00 00 00
   00007CFB 00 00 00 00 00 00 00 00 00 00
   00007D05 00 00 00 00 00 00 00 00 00 00
   00007D0F 00 00 00 00 00 00 00 00 00 00
   00007D19 00 00 00 00 00 00 00 00 00 00
   00007D23 00 00 00 00 00 00 00 00 00 00
   00007D2D 00 00 00 00 00 00 00 00 00 00
   00007D37 00 00 00 00 00 00 00 00 00 00
   00007D41 00 00 00 00 00 00 00 00 00 00
   00007D4B 00 00 00 00 00 00 00 00 00 00
   00007D55 00 00 00 00 00 00 00 00 00 00
   00007D5F 00 00 00 00 00 00 00 00 00 00
   00007D69 00 00 00 00 00 00 00 00 00 00
   00007D73 00 00 00 00 00 00 00 00 00 00
   00007D7D 00 00 00 00 00 00 00 00 00 00
   00007D87 00 00 00 00 00 00 00 00 00 00
   00007D91 00 00 00 00 00 00 00 00 00 00
   00007D9B 00 00 00 00 00 00 00 00 00 00
   00007DA5 00 00 00 00 00 00 00 00 00 00
   00007DAF 00 00 00 00 00 00 00 00 00 00
   00007DB9 00 00 00 00 00 00 00 00 00 00
   00007DC3 00 00 00 00 00 00 00 00 00 00
   00007DCD 00 00 00 00 00 00 00 00 00 00
   00007DD7 00 00 00 00 00 00 00 00 00 00
   00007DE1 00 00 00 00 00 00 00 00 00 00
   00007DEB 00 00 00 00 00 00 00 00 00 00
   00007DF5 00 00 00 00 00 00 00 00 00
58 00007DFE                             
59 00007DFE 55 AA                               DB  0X55, 0XAA

   上面这张图是书上的代码编译后的.lst文件。接下来分析一下这段代码(行号请以深色数字为准)。


5 00007C00 EB 4E                               JMP ENTRY
6 00007C02 90                                  DB  0X90

26 00007C50                                 ENTRY:
27 00007C50 B8 0000                             MOV AX, 0

   5行:是一个跳转,对应机器码是EB 4E。EB是跳转,4E则是相对的偏移量。跳转的目的地是ENTRY,我们可以看到26行是ENTRY的地址0X7C50。第6行的机器码是90,对应汇编代码就是个nop,空操作,这行的内存地址是0X7C02,4E这个相对偏移量就是ENTRY的内存地址减去紧跟在跳转语句后面的那条语句的内存地址,即 0x7c50 - 0x7c02 = 4e。

   7行到25行,都是一些软盘的信息,书上都写着,就不解释了。


27 00007C50 B8 0000                             MOV AX, 0
28 00007C53 8E D0                               MOV SS, AX
29 00007C55 BC 7C00                             MOV SP, 0X7C00
30 00007C58 8E D8                               MOV DS, AX
31 00007C5A 8E C0                               MOV ES, AX

   27到31行,是对几个寄存器的初始化,也没什么问题。


33 00007C5C BE 7C74                             MOV SI, MSG

   33行中的MSG是标号。这条语句把需要输出字符串的首地址(MSG标号的地址)保存到了SI寄存器中,后面会用这个SI寄存器来遍历字符串。


36 00007C5F                                 PUTLOOP:
37 00007C5F                              
38 00007C5F 8A 04                               MOV AL, [SI]
39 00007C61 83 C6 01                            ADD SI, 1
40 00007C64 3C 00                               CMP AL, 0
41 00007C66 74 09                               JE  FIN
42 00007C68 B4 0E                               MOV AH, 0X0E
43 00007C6A BB 0009                             MOV BX, 9
44 00007C6D CD 10                               INT0X10
45 00007C6F EB EE                               JMP PUTLOOP
46 00007C71                              
47 00007C71                                 FIN:  
48 00007C71 F4                                  HLT
49 00007C72 EB FD                               JMP FIN
50 00007C74        

   36行到50行,有两个标号,PUTLOOP和FIN,每一段的结尾都有一个JMP,算是两个循环。


   先看PUTLOOP

   38行:这条语句把SI内存中的值复制到了寄存器AL中。

   39行:SI加1,开始遍历字符串。

   40行:比较一下刚才复制到AL中的内容。

   41行:对40行的比较结果进行操作,若AL中的值为0(字符串结束),则跳转到FIN标号。若不为0则执行下一行。

   42~44行:调用BIOS的INT10中断,显示一个字符

   45行:若字符串未结束,继续循环

   再看FIN,这个简单,就两句,一个是HLT,让CPU处于睡眠状态。一个是JMP FIN,是个无线循环。


51 00007C74                                 MSG:
52 00007C74 0A 0A                               DB  0X0A, 0X0A
53 00007C76 48 65 6C 6C 6F 20 57 6F 72 6C       DB  "Hello World"
00007C80 64
54 00007C81 0A                                  DB  0X0A
55 00007C82 00                                  DB  0

   51行到55行,这里就是MSG,我们要输出的字符串。

   52行:两个0X0A,ascii码的回车。

   53行:赫赫有名的“hello world”字符串的ascii码。

   54行:又一个0X0A,回车。

   55行:0,ascii的0。我们再看一下40行的那段代码

40 00007C64 3C 00                               CMP AL, 0

   这里把AL与0比较,这个0就是我们55行手动放进去的一个0。C语言里,字符串最后会自带一个‘\0',这里我们手动加了一个0。


57 00007C83 00 00 00 00 00 00 00 00 00 00       RESB    0X7DFE-$

   57行:空出多少个0。因为要在一个扇区(512,0X200)的最后两字节存放0X55,0XAA两个字节,我们这段代码存放在0X7C00处,0X7C00+0X200=0X7E00。0X7E00是下一个扇区的起始位置,而我们这个扇区最后两个字节的位置分别是0X7DFE, 0X7DFF。$代表本行在内存中的位置即0X7C83,所以0X7DFE-0X7C83 = 0X17B(379)个0。


   好了,基本的分析结束了。我心里有个疑问,既然第一个扇区之后全都是0,那我还要后面那么多个0干吗?接下来我就开始修改原来的代码。


   我把后面的0都删了,只留下第一个扇区,再去掉了一些我觉得暂时没用的东西。这里我直接改了二进制文件,见图:

30天自制操作系统 笔记1_自制操作系统_02

   再看下加载后的结果:

30天自制操作系统 笔记1_自制操作系统_03

   能启动,也能输出字符,呵呵。    

   可以看到EB 4E还在,后面的关于软盘信息的东西我都给删了,因为4E没改,所以跳转入口的相对位置暂时不动。我只留下了PUTLOOP和FIN两个循环,MSG往后都用作字符串,直到。。。 直到最后的55AA。我发现我忘记给字符串结尾留一个0,结果程序把55AA也输出了。这样显得很不专业,我就把55AA之前的那个字节0X26(对应ascii码字符&)改成了0,这下就完美了。

30天自制操作系统 笔记1_自制操作系统_04


   人有多大胆,地有多大产,有了这次成功的经验,我决定再试一次,更彻底一点。当然了,问题也是难免的,见下图:

30天自制操作系统 笔记1_自制操作系统_05

以及启动后的截图:

30天自制操作系统 笔记1_自制操作系统_06


   这次改的确实比较彻底,我只留下了两个循环所用的代码,并且直接放到了程序的开头:

33 00007C5C BE 7C74                             MOV SI, MSG
34 00007C5F                                  
35 00007C5F                              
36 00007C5F                                 PUTLOOP:
37 00007C5F                              
38 00007C5F 8A 04                               MOV AL, [SI]
39 00007C61 83 C6 01                            ADD SI, 1
40 00007C64 3C 00                               CMP AL, 0
41 00007C66 74 09                               JE  FIN
42 00007C68 B4 0E                               MOV AH, 0X0E
43 00007C6A BB 0009                             MOV BX, 9
44 00007C6D CD 10                               INT0X10
45 00007C6F EB EE                               JMP PUTLOOP
46 00007C71                              
47 00007C71                                 FIN:
48 00007C71 F4                                  HLT
49 00007C72 EB FD                               JMP FIN
50 00007C74                              

   里面有三个JMP,但因为没有添加新的代码,所以相对的位移没有改变,所以EB(JMP),74(JE)后面的数值都不变。但有一点必须要注意:

33 00007C5C BE 7C74                             MOV SI, MSG

   这个BE 7C74,BE是操作码,0X7C74是地址,把字符串的首地址放到SI中,为后续操作所用。因为我把这段代码放到了最开头,并且也增加了字符串的内容,所以字符串的首地址已经有所变更,所以不能再使用原先的0X7C74了,至于用多少?自己算!但问题就出在了自己算上,看下面这张图

30天自制操作系统 笔记1_自制操作系统_07

   我们用的机子都是小端字节序,两字节以上的数值存放在二进制文件中时,是倒过来放的,也就是0X7C74,是先放74,再放7C。我刚开始改的时候,直接把7C给改了,结果就是能启动,但找不到字符串的首地址,无法输出字符。


   总算把这个扇区的东西给撸了一遍。感觉也可以把它看成是一个MBR,一个没有bootload,没有分区表,但能使CPU按照自己的想法执行并输出的“MBR”。


   我又做了一个空的扇区,512字节个0,加载后出现了这样的效果:

30天自制操作系统 笔记1_自制操作系统_08


   于是我把最后两字节改成了55AA

30天自制操作系统 笔记1_自制操作系统_09

   55AA真的很重要,没有它都无法引导。