实现一个简单的64位操作系统 (0x02)编写一个简单的boot

0x01 概述

在上一章中已经将环境搭建好了,接下来就开始进行操作系统编写的工作了。
编写操作系统的第一步当然是编写一个boot程序来将操作系统内核加载到系统中。
在BIOS刚将CPU初始化时,是没有操作系统带来的各种好处的。这时候没有文件系统、没有内存管理、没有任务调度,有的只是一个处于实模式的CPU,能做的就是使用这个处于实模式的CPU想办法讲操作系统内核加载到内存并执行。
早期的引导模式为Boot引导Loader,再由Loader加载操作系统,这是受当时的磁盘所限。现在一般用的都是以bootloader的方式直接加载操作系统。现在手机,甚至一般的单片机用的一般都是bootloader。但是为了从简到繁,还是要先学习boot + loader的方式来引导操作系统。

注:内容参考了《一个64位操作系统的设计与实现》,有兴趣的读者可以购买这本书阅读。

0x02 设计

这一章的目的是实现一个简单的boot程序,在屏幕上打印一个提示正在引导的字符串。
将使用Intel风格的汇编语言来实现,用NASM编译器编译,并将其写入到之前生成的boot.img中,使用bochs来模拟执行。

0x03 实现

首先是两个伪指令。

org 0x7c00

BaseOfStack equ 0x7c00

org指明程序的入口在0x7c00处,因为BIOS在初始化完CPU后会将CS:IP设置为0000:7c00,也就是从内存的0x7c00处开始执行。指定org 0x7c00之后,程序将会被装载到内存0x7c00处,否则将默认装载到0x0000处。至于地址长度问题,由于现在处于实模式,寻址是16位寻址,所以地址最高到0xffff。
BaseOfStack伪指令在后面用来初始化sp栈寄存器,让栈从0x7c00开始增长。由于栈是从高地址向低地址增长的,所以在正常情况下不用担心栈将代码覆盖的问题(在操作系统ring0层操作栈都得小心翼翼的,更何况在实模式下)。

之后对寄存器进行初始化。

; init registers
mov     ax, cs
mov     ds, ax
mov     es, ax
mov     ss, ax
mov     sp, BaseOfStack

这里的目的很简单,用cs段寄存器的值来初始化ds,es及ss,然后用0x7c00来初始化sp。

接下来就开始打印字符串了。
从上一章尝试运行bochs的时候能看出来,在bochs将控制权交到0x7c00之前,已经打印了很多信息了,屏幕上很乱。因此,在打印新的字符串之前,需要先将屏幕清空。清空屏幕的代码如下。

; clear screen
; AH = 06h roll pages
; AL = page num (0 to clear screen)
; BH = color attributes
; CL = left row, CH = left column
; DL = right row, DH = right column
mov     ax, 0600h
mov     bx, 0700h
mov     cx, 0
mov     dx, 184Fh
int     10h

这里用到的是0x10号中断,也就是int 10h。在没有操作系统的系统调用带来的好处时,各种功能的实现都需要用到中断,之后也会大量使用中断。各中断的用法可以查阅处理器对应的文档。
0x10号中断用AH来标记功能。这里用到的0x06是滚动屏幕的功能,同时也能用它来清除屏幕。AL传的值是滚动的页数,传0的话就能清除屏幕。BH传的是颜色信息。CL和DL制定行数,CH和DH制定列数。其实这CX和DX分别标记了左上角和右下角的点,以它们的连线为对角线的话就形成了一个矩形。
这里直接传0x0600给ax,这样AL=0x00,AH=0x06。传0x0700给bx,BL=0x00,BH=0x07,然后给cx传值0x0000,代表左上角坐标为(00,00),给DX传值0x184F,代表右下角坐标为(4F,18)。
传完参数后使用int 10h来触发中断。

接着,继续使用0x10号中断来设置光标。

; set focus
; AH = 02h set focus
; DL = row
; DH = column
; BH = page num
mov     ax, 0200h
mov     bx, 0000h
mov     dx, 0000h
int     10h

这里的AH为0x02,说明功能位设置光标。传的值及作用在注释里都提到了。

接下来就是打印字符串了。打印字符串用到的也是0x10号中断,传值情况如下。

; display boot string (int 10h)
; AH = 13h display a string
; AL = 01h display mode
; CX = StringLen
; DH = row, DL = column
; ES:BP = String adress
; BH = page num
; BL = text attributes

AH传的功能号为0x13,意思是打印字符串。AL传的是显示模式,模式分为cx指定单位是byte还是word、显示完之后光标显示在前面还是在后面。具体可以查阅文档。这里用到的0x01意思是cx传的单位是byte,打印完字符串后光标在字符串末尾。CX传的是字符串长度。DX制定行列号。BP传要打印的字符串地址。BH指定打印的页码,BL制定打印文字的属性。

由于要显示字符串的地方不少,我实现了一个简单地在制定行列数上打印白色字符串的函数,方便后续字符串的打印。

; Print a string on screen
; Parms:
; Stack: StringAddress, StringLength, ColRow
; Return:
; No return
Func_PrintString:

这里函数的实现模仿了C的实现,但是用的是stdcall,原因是预防调用时忘了清栈。函数名为Func_PrintSting(这里命名模仿了《一个64位操作系统的设计与实现》,我感觉这种命名方式非常好,用Func_前缀标记函数,用Label_前缀标记标签,这样就不会搞混),参数用栈传递,从栈底到栈顶分别为字符串地址、字符串长度及行列数。没有返回值。
下面将分段说明它的实现。

; construct stack frame
push    bp
mov     bp, sp

; StringAddress     = [bp + 4]
; StringLength      = [bp + 6]
; ColRow            = [bp + 8]

这里先形成一个栈帧(Stack Frame),方便后续对参数寻址。形成栈帧后,三个参数的地址如后面的注释所示(与32位不同的,一次入栈是2个Byes,所以一次加2)。

然后保护一下要用到的寄存器。

; protect registers
push    ax
push    bx
push    cx

保护完寄存器后开始触发中断打印字符串。

; protect BP
push bp

; print string
mov     ax, 1301h
mov     bx, 000fh
mov     cx, [bp + 6]
mov     dx, [bp + 8]
mov     bp, [bp + 4]
int     10h

; recover bp
pop bp

这里有两个地方要注意的,第一个是要注意对bp进行保护,因为中断用到了bp传值,会破坏bp原有的值。第二个是注意bp要最后赋值,因为要靠bp来对参数进行寻址。
按照上面的功能号为0x13的int 10h的说明传好值,然后开始进行int 10h中断调用。返回之后pop bp将bp恢复。

接着是恢复被保护的寄存器。

; recover registers
pop     cx
pop     bx
pop     ax

最后关闭栈帧并返回。

; close stack frame
mov     sp, bp
pop     bp
; return
ret     6h

这里的返回用的是ret 6h,意思是返回时讲sp降低6个bytes来平衡栈,就不需要调用者来清栈了。这是stdcall的调用约定。如果使用c call来实现,直接使用ret,则需要调用者使用add sp, 6h来清栈。
到这里,Func_PrintString就实现完了。

实现完打印字符串的函数后,就能很方便地打印字符串了。打印字符串实现如下。

; print boot message
push    0000h
push    16
push    StartBootMessage
call    Func_PrintString

这样调用Func_PrintString后,将在00行00列打印StartBootMessage处的16个字节的字符串。

打印完字符串后,开始循环等待。

; loop wait
jmp $

$表示的是当前地址,jmp $作用是一直往当前地址跳,也就是进入了死循环。这样会一直消耗CPU周期,但是不继续往下执行。

最后,是几个伪指令,用来定义消息字符串和填充空白,并且定义boot的签名。

; message string
StartBootMessage:   db  "Start Booting..."

; padding zero and set flag
times   510 - ($ - $$) db 0
dw      0xaa55

Boot扇区在第0扇区,大小为512个Bytes。BIOS在识别Boot扇区时回去识别第511和512个Byte是不是0x55和0xaa,如果是这两个值就认定Boot扇区是有效的。所以这里需要讲最后两个Bytes填充为制定的Signature。
times伪指令用来填充0,至于填充多少个0,由当前地址来决定。$ - $$表示当前地址与当前首地址的差值。$位当前地址,$$为块的首地址。用510 - ($ - $$)就能得到需要填充的bytes数,填充完成后下一个byte就是第511个byte了。然后用dw 0xaa55来写入签名。由于用的是Little-endian,所以高地位必须对应,0x55 0xaa就是0xaa55了。

到这里,一个简单的boot示例程序就编写完成了。

0x04 编译执行

为了方便之后对源代码和构建完的文件进行管理,源代码统一放到工程目录的"src"目录下,编译后的文件统一放到"build"目录下。这里将上面编写的代码保存到"src/bootloader/boot.asm"中,将生成的目标文件定为"build/boot.bin"。
并且,为了方便后续的编译,可以在工程根目录下新建一个makefile,内容如下。

build/boot.bin : src/bootloader/boot.asm
        nasm src/bootloader/boot.asm -o build/boot.bin

clean:
        rm build/*.bin

这样,每次增加需要编译的源文件和目标文件都直接添加到makefile中就能很方便地编译了。在根目录下直接执行make就能完成编译。
编译完成后,在build目录下会生成一个boot.bin。接下来要做的就是将其写入到之前生成的boot.img镜像中。这里也通过编写一个脚本来实现,避免每次繁琐地使用dd来写入。
下面是writeimage.sh脚本的实现。

#/bin/sh

dd if=build/boot.bin of=build/boot.img bs=512 count=1 conv=notrunc

这样,每次编译完之后,如果想将内容写入镜像中,只需要执行writeimage.sh就行。

接下来就开始编译、写入和执行了。分别执行make、writeimage.sh就能完成编译和写入。然后用Bochs来完成模拟(记得给writeimage.sh加执行权)。

make
./writeimage.sh

这样就完成了编译和写入。效果如下。


编译和写入

然后使用bochs模拟。

bochs -f bochsrc

bochsrc的配置在上一章已经完成了。
模拟效果如下。


Bochs模拟执行

能看到达到了预期效果。之前Bochs输出的内容已经被清空,并且打印了引导提示字符串。

到这里就完成了一个Boot示例程序的实现、编译和测试。

0x05 总结

这里的Boot程序无实际用途,仅仅用来熟悉Boot程序的编写和实模式下程序的编写。后续将会实现加载Loader的部分以及使用Loader加载内核。

项目已经托管到Github。链接:
Github-OperatingSystem

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容