前言
不少程序员都有一个系统梦,不管这个系统只能进行简单的交互还是只是个Hello World,都有很大的成就敢,这对加深计算机的了解也是必不可少的步骤,但是不少文章都是劝退型的,一堆理论,之后搞个模拟器,就完事了,很多人都不喜欢就此止于模拟器,我们更希望做到的是在实体电脑上直接通过硬盘启动,或者是U盘,所以下面会一点点的通过"手改扇区"的方式写一个Hello World系统,可以直接通过U盘启动。
而我们准备的工具只有两个,U盘、扇区编辑工具,扇区编辑工具可以使用DiskGenius,但是这玩意编辑扇区的时候是收费的,所以还要对他做一些不为人知的操作。
其实还有个更好的工具是HxD,功能远远不如DiskGenius强大,但是HxD对我们这个实验来说也够用了。
那为什么是改扇区方式呢?其实本来是需要汇编语言写的,然后在写入到U盘,但是很多人都没有开发环境,比如NASM,还需要安装,但是安装DiskGenius、HxD总比安装NASM快、简单,所以为了方便,直接使用DiskGenius。
实现过程
主引导
在此之前先说一下基本知识,我们以U盘启动为例,(首先得去BIOS中设置第一启动为U盘),在按下电源的时候,内存中肯定什么也没有,但是CPU却只能运行内存中的程序,没有办法直接执行U盘中的操作系统,所以得有个东西来进行加载,即将U盘中的系统加载到内存中,这个东西就是BIOS,BIOS首先会自检,就是检查硬件设备是否存在问题,如果没有问题,那么BIOS会根据启动顺序选择引导设备。
由于已经设置了第一启动是U盘,那么BIOS会检查U盘的第0磁头0磁道1扇区的结尾数值是不是0x55和0xaa,一个扇区能存储512字节的数据,所以如果最后两位是55aa,那么BIOS就会认为这个扇区是一个引导扇区,然后把此扇区的数据复制到物理内存地址的0x7c00,然后将处理器的执行权交给这段程序,也就是跳转至0x7c00地址执行。
55aa就好比一些文件的开头,用来确保是不是正确的文件,比如class的开头是0xCAFEBABE,png的开头是89504E470D0A1A0A,只不过他把标识号移到了末尾。
但是一个扇区只能存放512字节的数据,是没办法存下操作系统的,所以这里引导只是作为”一级助推器“,另外在执行引导加载程序期间,处理器以16位模式(实模式)运行,这意味着引导加载程序只能使用16位寄存器。
所以,我们只要用对16进制数据,直接修改U盘的扇区,从而打印出Hello World,就不需要先写汇编、编译、写入U盘了。
Hello World
至于Hello World的十六进制数据,也不是凭空就来的,得用NASM先编译出来,然后写入到U盘,先看段代码,这段代码的作用就是打印字符'a'到屏幕上,代码很少,只几个字节而已,但是每一行都具有很深的背景。
bits 16
org 0x7c00
mov al,'a'
mov ah, 0x0e
int 0x10
times 510 - ($-$$) db 0
dw 0xaa55
第一行是让NASM为以16位模式运行的CPU生成代码,org 0x7c00指令特别重要,用来指定程序的起始地址,如果没有他,编译器会把地址0x0000作为程序的起始地址,会影响绝对地址的寻址,org 0x7c00的意思就是将程序的起始地址设置在0x7c00处,至于为什么是0x7c00,想必只有制定人和鬼知道。
然后mov al,'a'和mov ah, 0x0e其实为int 0x10 所准备的"参数",这就要提到BIOS中断调用了,详细可以看一下en.wikipedia.org/wiki/BIOS_i…
这里的int 0x10 中断可以设置显示模式,输出字符等,调用时候,如果ah寄存器为0x0e,那么表示在TTY模式下写字符,将输出字符存放在al中。
最后的一行dw 0xaa55表示最后两个字节是0xaa55,但是这些些还凑不到512个字节,所以通过times 510 - ($-$$) db 0以0x00填充剩余的位置。
然后通过NASM编译,最后输出的字节如下。
nasm -f bin os.asm -o bootloader.bin
写入到U盘。
那么剩下工作就是把这些字节写入到U盘扇区了,在Linux下可以通过dd命令,但是很多人都不是Linux,可以使用其他工具,但是这里字节也不多,通过DiskGenius、或者Hxd就可以直接手改。
首先得把U盘格式化,然后打开Hxd,工具-读写硬盘,选择U盘,可能会出现如下数据(没有数据就是最好的数据)。
然后按照上图编译后的字节,更改前512个字节,最后如下。
但这不会最后一步,还需要在BIOS中更改一个东西,因为现在很多人电脑都是以UEFI启动的,如果是这种方式,那么需要把他改成Legacy,否则无法加载。至于怎么更改为Legacy,可以问问身边的同学。
之后进行最后一步,把装有人生第一个自制系统的U盘,插入的本本上,开机!
不出意外会出现如下结果,虽然他只打印了个'a',但是离成功更进了一步。
那么剩下就是通过一个循环,打印hello world了。
bits 16 ; 告诉NASM这是16位代码
org 0x7c00 ;告诉NASM起始地址0x7c00,
mov ax,0
mov ds,ax
boot:
mov si, hello ;把hello的标号地址移入si寄存器
mov ah, 0x0e ; 使用写字符模式
.loop:
lodsb ; 加载 [DS:SI]位置的数据到al寄存器
or al,al
jz exit ;如果al等于0,跳转到halt
int 0x10 ; BIOS 中断调用 0x10
jmp .loop
exit:
cli
hlt
hello: db "Hello world!",0
times 510 - ($-$$) db 0
dw 0xaa55
在上述代码中,lodsb是就是从ds:si寄存器中读取一个字节加载到AL,这也就是为什么事先要通过mov si, hello 告诉si寄存器数据的位置了。每次调用lodsb时si会自动下移。并且调用0x10进行打印,然后jmp进行跳转,这里就是个循环操作,但是要在合适的时候跳出,可以通过jz判断,如果零标志是1的话,它将跳转到指定位置。那么如何将零标志位置1呢?就是通过or指令。
or指令在两个操作数的对应位之间进行or运算,并将结果存放在目标操作数中,这里 or al,al 就是自己与自己或运算,如果al寄存器的值是0,那么0 or 0为0,此时就会设置零标志为1,从而就退出循环了。
编译上述代码,会得到如下字节,这回我们不能轻易手写了,需要借助工具,减少输入错误的可能,可以通过Rufus来进行写入,但手写也不是不可以,只要正确即可。
最终结果如下:
下载地址
打印字符a
www.houxinlin.com:6060/os/print_a.…
打印字符hello world