美文网首页程序员
实现一个简单的64位操作系统 (0x02)编写一个简单的boot

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

作者: KernelThread | 来源:发表于2018-08-25 00:42 被阅读27次

    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

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/xexwiftx.html