由于能力有限,不一定跟得上书本的进度,只能以自己的能力为限,一步一步写下去。
第一天的内容是用二进制编辑工具写一个.img的文件,如图:
这是照着书上内容抄的。可以看到窗口右下方的数字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
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
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都删了,只留下第一个扇区,再去掉了一些我觉得暂时没用的东西。这里我直接改了二进制文件,见图:
再看下加载后的结果:
能启动,也能输出字符,呵呵。
可以看到EB 4E还在,后面的关于软盘信息的东西我都给删了,因为4E没改,所以跳转入口的相对位置暂时不动。我只留下了PUTLOOP和FIN两个循环,MSG往后都用作字符串,直到。。。 直到最后的55AA。我发现我忘记给字符串结尾留一个0,结果程序把55AA也输出了。这样显得很不专业,我就把55AA之前的那个字节0X26(对应ascii码字符&)改成了0,这下就完美了。
人有多大胆,地有多大产,有了这次成功的经验,我决定再试一次,更彻底一点。当然了,问题也是难免的,见下图:
以及启动后的截图:
这次改的确实比较彻底,我只留下了两个循环所用的代码,并且直接放到了程序的开头:
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
里面有三个JMP,但因为没有添加新的代码,所以相对的位移没有改变,所以EB(JMP),74(JE)后面的数值都不变。但有一点必须要注意:
33 00007C5C BE 7C74 MOV SI, MSG
这个BE 7C74,BE是操作码,0X7C74是地址,把字符串的首地址放到SI中,为后续操作所用。因为我把这段代码放到了最开头,并且也增加了字符串的内容,所以字符串的首地址已经有所变更,所以不能再使用原先的0X7C74了,至于用多少?自己算!但问题就出在了自己算上,看下面这张图
我们用的机子都是小端字节序,两字节以上的数值存放在二进制文件中时,是倒过来放的,也就是0X7C74,是先放74,再放7C。我刚开始改的时候,直接把7C给改了,结果就是能启动,但找不到字符串的首地址,无法输出字符。
总算把这个扇区的东西给撸了一遍。感觉也可以把它看成是一个MBR,一个没有bootload,没有分区表,但能使CPU按照自己的想法执行并输出的“MBR”。
我又做了一个空的扇区,512字节个0,加载后出现了这样的效果:
于是我把最后两字节改成了55AA
55AA真的很重要,没有它都无法引导。