《30天自制操作系统》——从汇编到C

这次我们要用到4个文件,分别是ipl.nas、asmhead.nas、func.nas和 bootpack.c。
ipl.nas是引导扇区中的16bit程序,用于从磁盘中加载数据并跳转到asmhead.nas中。
asmhead.nas也是16bit程序,用于加载全局变量表,切换cpu到32位的保护模式,并跳转到后面的程序。
bootpack.c用于改变屏幕颜色,func.nas为bootpack.c提供相应的一些函数。
那么我们现在就开始吧。

ipl.nas和上一篇文章中的程序几乎没有什么变化,我去掉了打印hello world的部分,同时在其中多加了一句MOV    [0x0ff0],CH来记录读了多少个柱面。

程序一、ipl.nas
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; hello-os
; TAB=8

    ORG    0x7C00

; 标准FAT格式软盘
start:
    JMP    entry
    DB    "HELLOIPL"    ; 启动区名称(8字节)
    DW    512        ; 扇区大小(512字节)
    DB    1        ; 簇大小(1扇区)
    DW    1        ; FAT起始位置 Reserved Sectors
    DB    2        ; FAT个数
    DW    224        ; 根目录(224项) Root Entries
    DW    2880        ; 磁盘大小(2880扇区) 2*80*18 Small Sectors
    DB    0xf0        ; 磁盘种类
    DW    9        ; FAT长度
    DW    18        ; 每个磁道扇区数
    DW    2        ; 磁头数
    DD    0        ; 隐藏扇区
    DD    2880        ; 重写一次磁盘大小 Large Sectors
    DB    0,0,0x29    ; Physical Drive Number, Current Head, Signature
    DD    0xffffffff    ; 可能是卷标号码 ID
    DB    "HELLO-OS   "    ; 磁盘名称(11字节) Volume Label
    DB    "FAT12   "    ; 格式名称(8字节) System ID
    RESB    18        ; 空出18字节
; 进入引导程序
entry:
    MOV    AX,0        ; 初始化寄存器
    MOV    SS,AX
    MOV    SP,0x7c00
    MOV    DS,AX
    MOV    ES,AX
; 读磁盘
    CYLS    EQU    10
    MOV    AX,0x0820
    MOV    ES,AX        ; ES:BX为内存缓存地址 ESx16+BX
    MOV    CH,0        ; 柱面0
    MOV    DH,0        ; 磁头0
    MOV    CL,2        ; 扇区2
readloop:
    MOV    SI,0        ; 记录失败次数

retry:
    MOV    AH,0x02        ; 读盘
    MOV    AL,1        ; 1个扇区
    MOV    BX,0
    MOV    DL,0x00        ; 0驱动器
    INT    0x13        ; 调用磁盘BIOS
    JNC    next        ; 没出错则读下一个扇区
    ADD    SI,1        
    CMP    SI,5        ; 比较SI与5
    JAE    error        ; 超过允许错误次数,跳转到error
    MOV    AH,0x00
    MOV    DL,0x00
    INT    0x13        ; 重置驱动器
    JMP    retry
next:
    MOV    AX,ES
    ADD    AX,0x0020    ; 把内存地址后移0x200
    MOV    ES,AX        ; 因为没有(ADD ES,0x20)
    ADD    CL,1
    CMP    CL,18
    JBE    readloop    ; 还没有读完一面
    MOV    CL,1
    ADD    DH,1        ; 读磁盘另一面
    CMP    DH,2
    JB    readloop
    MOV    DH,0
    ADD    CH,1
    CMP    CH,CYLS        ; 读CYLS个柱面
    JB    readloop

    MOV    [0x0ff0],CH    ; 记录读了多少个柱面

    jmp    0xc200     ; 调到主程序

error:
    MOV    SI,errmsg
errloop:
    MOV    AL,[SI]
    ADD    SI,1        ; 给SI加1
    CMP    AL,0

    JE    fin
    MOV    AH,0x0e        ; 显示一个文字
    MOV    BX,15        ; 指定字符颜色
    INT    0x10        ; 调用显卡BIOS
    JMP    errloop

fin:
    HLT
    JMP    fin
msg:
    DB    0x0a, 0x0a    ; 换行2次
    DB    "hello, world"
    DB    0x0a        ; 换行
    DB    0
errmsg:
    DB    0x0a, 0x0a    ; 换行2次
    DB    "disk error"
    DB    0x0a        ; 换行
    DB    0
marker:
    RESB    0x1fe-(marker-start)
    DB    0x55, 0xaa

end:
    RESB    1474560-(end-start)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

asmhead.nas可以参考这篇博文 http://blog.chinaunix.net/uid-28323465-id-3487762.html
博文把禁用中断、开启A20管脚、初始化段、开启保护模式、改变屏幕颜色都讲得非常清楚。
在全局变量表部分,我们有两个段条目,分别是:
DW        0xffff,0x0000,0x9200,0x00cf     ; 数据段
DW        0xffff,0x0000,0x9a28,0x0047     ; 代码段
因为内存是以小端方式存数据的,所以实际内存地址从小到大真正存储的数据为:
ff ff 00 00 00 92 cf 00
ff ff 00 00 28 92 47 00
数据段的起始位置为0x00000000,容量为0xfffff000,保护模式为0。
代码段的起始位置为0x00280000,容量为0xffff,保护模式为28。
关于全局变量表的具体内容可参看 http://wiki.osdev.org/GDT_Tutorial
在asmhead.nas程序中还有一句跳转指令
        MOV        ESP,0xffff    ; 设置栈地址
        JMP        DWORD 2*8:0x00000000    ; 跳转到0x280000
我们设置ESP为0xffff,这刚好为数据段的最大地址,因为栈的是向内存地址减小的方向增长的。
JMP     DWORD 2*8:0x00000000 是一个长跳转指令,因此要加DWORD把内存寻址范围变成32位,否则会被截断成20位的地址。2*8表示GDT(全局变量表)中的偏移地址,刚好是代码段,因此,代码跳转到0x280000。

程序二、asmhead.nas
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; haribote-os boot asm
; TAB=4

BOTPAK    EQU        0x00280000        ; 主程序地址
DSKCAC    EQU        0x00100000        ; 
DSKCAC0    EQU        0x00008000        ;

; 有关BOOT_INFO
CYLS    EQU    0x0ff0        ; 设定启动区
LEDS    EQU    0x0ff1        ; 键盘状态
VMODE    EQU    0x0ff2        ; 颜色位数
SCRNX    EQU    0x0ff4        ; 分辨率X (screen x)
SCRNY    EQU    0x0ff6        ; 分辨率Y (screen y)
VRAM    EQU    0x0ff8        ; 图像缓冲区开始地址,显卡内存

        ORG        0xc200        

; 设定Graphic Mode

        MOV        AL,0x13            ; VGA显卡,320x200x8位彩色    
        MOV        AH,0x00
        INT        0x10
        MOV        BYTE [VMODE],8    
        MOV        WORD [SCRNX],320
        MOV        WORD [SCRNY],200
        MOV        DWORD [VRAM],0x000a0000

; 返回键盘状态

        MOV        AH,0x02
        INT        0x16             ; keyboard BIOS
        MOV        [LEDS],AL

; PIC 可编程中断控制器 有两个PIC 每个PIC有8个输入0-7
; cli关闭所有中断,sti打开所有中断


        MOV        AL,0xff
        OUT        0x21,AL            ; PCI1 data
        NOP                    ; 太快可能会有问题
        OUT        0xa1,AL            ; PCI2 data

        CLI                    ; 关闭全部中断

; 蛋疼的键盘A20 address enable

        CALL    waitkbdout
        MOV        AL,0xd1
        OUT        0x64,AL
        CALL    waitkbdout
        MOV        AL,0xdf            ; enable A20
        OUT        0x60,AL
        CALL    waitkbdout    

; 切换到保护模式
        LGDT    [GDTR0]            
        MOV        EAX,CR0
        AND        EAX,0x7fffffff    ; 禁止分页
        OR        EAX,0x00000001    
        MOV        CR0,EAX
        JMP        pipelineflush
pipelineflush:
        MOV        AX,1*8        ; 取数据段偏移
        MOV        DS,AX        ; 数据段
        MOV        ES,AX        ; 数据段(字符操作目标)
        MOV        FS,AX        ; 数据段
        MOV        GS,AX        ; 数据段
        MOV        SS,AX        ; 栈段

; 主程序加载到0x280000

        MOV        ESI,bootpack    
        MOV        EDI,BOTPAK        
        MOV        ECX,512*1024/4
        CALL    memcpy

; boot程序加载到0x100000

        MOV        ESI,0x7c00        
        MOV        EDI,DSKCAC        
        MOV        ECX,512/4
        CALL    memcpy

        MOV        ESI,DSKCAC0+512    ; 跳过引导扇区    
        MOV        EDI,DSKCAC+512    ;
        MOV        ECX,0
        MOV        CL,BYTE [CYLS]
        IMUL        ECX,512*18*2/4    ; 扇区数量*512/4
        SUB        ECX,512/4    ; 去掉引导扇区
        CALL    memcpy

; 跳转主程序

        MOV        ESP,0xffff    ; 设置栈地址
        JMP        DWORD 2*8:0x00000000    ; 跳转到0x280000

waitkbdout:
        IN         AL,0x64
        AND         AL,0x02        ; cpu可向键盘写命令时为1
        JNZ        waitkbdout        ; 
        RET

memcpy:
        MOV        EAX,[ESI]
        ADD        ESI,4
        MOV        [EDI],EAX
        ADD        EDI,4
        SUB        ECX,1
        JNZ        memcpy    
        RET
; 全局变量表

        ALIGNB    16    ; 16字节对齐 bss段
GDT0:
        RESB    8                    ; 第一项为0,这是规定
        DW        0xffff,0x0000,0x9200,0x00cf    ; 数据段
        DW        0xffff,0x0000,0x9a28,0x0047    ; 程序段

        DW        0
GDTR0:
        DW        8*3-1        ; 表的大小(字节)减1
        DD        GDT0        ; 表的地址

        ALIGNB    16
bootpack:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

标签bootpack后面的程序被复制到了0x280000处,所以其实我们要跳转进入的程序就是在标签bootpack后面,因此我们只要把之后要跳转的程序放到到asmhead.nas就能够保证被执行。
在上面的程序里,我们设置了图像模式,320x200x8位彩色。
屏幕上的每个像素可由内存地址0xa0000-0xaffff对应。我们只要往对应的内存写入相应值就可以改变屏幕上每个点的颜色。

程序三、bookpack.c
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
extern void io_hlt(void);
extern void write_mem8(int addr, int data);

void HariMain(void) {
    int i;
    for (i=0xa0000; i<=0xaffff; i++) {
        write_mem8(i, i & 0x0f);
    }
    for (;;) {
        io_hlt();
    }
}
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

用c语言写果然直观很多,不过我们还需要调用两个函数,这两个函数是由汇编写的。
这里的汇编就是32位的了,需要在代码前面添加[BITS 32]来告诉nasm编译成32位代码,否则nasm会当作16位代码,并在出现32位寄存器的指令前面添加66、67之类的数字,cpu就没法好好执行了。

程序四、func.nas
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; func
; TAB=8
[BITS 32]        ; 制作32位模式用的机械语言

    GLOBAL    io_hlt    ; 程序中包含的函数名
    GLOBAL    write_mem8


[SECTION .text]        
io_hlt:    ; void io_hlt(void);
    hlt
    ret

write_mem8:    ; void write_mem*(int addr, int data);
    mov    ecx,[esp+4]
    mov    al,[esp+8]
    mov    [ecx],al
    ret
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

对有些连接器,需要在汇编程序中把c中用到的函数前面加_,而对于我所使用的ld是不需要加的,加了反而会报错,所以如果你不能通过链接的话就好好看看错误提示吧。

下面我们开始把所有程序编译并链接到一起。
首先是两个静态的汇编程序:
nasm -f bin ipl.nas -o ipl.img -l ipl.lst
nasm -f bin asmhead.nas -o asmhead.o -l asmhead.lst

我的电脑是苹果系统,而苹果系统中自带的gcc会把c编译成mach-o格式的目标文件。但是一个很大的问题是mach-o的二进制是fat binary或者叫universal binary,这种二进制是不能直接在cpu上跑的。所以我需要一个能编译成linux中目标文件格式elf的gcc,这种gcc又称为cross compiler,安装方法参见 http://www.danirod.es/blog/i386-elf-gcc-on-mac.html

我装的安装目录是/opt/local
《30天自制操作系统》——从汇编到C_第1张图片
我在/opt/local/bin中添加了ld、objcopy、objdump的符号引用i386-elf-ld、i386-elf-objcopy、i386-elf-objdump。同时我还把该目录添加到了path中。

我们需要把elf格式的目标文件链接到一起再提取出其中的二进制代码:
i386-elf-gcc -Wall -c bootpack.c -o bootpack.o
nasm -f elf func.nas -o func.o -l func.lst
i386-elf-ld bootpack.o func.o -o bootpack
i386-elf-objcopy -S -O binary bootpack bootpack.bin 

再把bootpack.bin添加到asmhead.o中:
cat bootpack.bin >> asmhead.o
 
下两步在上一篇文章中已经说明过了:
dd if=asmhead.o of=ipl.img bs=512 seek=33 conv=notrunc
qemu-system-i386 -fda ipl.img -boot a

整个过程的Makefile文件如下:

程序五、Makefile
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
default:
    make run

run: ipl.nas asmhead.nas func.nas bootpack.c
    nasm -f bin ipl.nas -o ipl.img -l ipl.lst
    nasm -f bin asmhead.nas -o asmhead.o -l asmhead.lst
    i386-elf-gcc -Wall -c bootpack.c -o bootpack.o
    nasm -f elf func.nas -o func.o -l func.lst
    i386-elf-ld bootpack.o func.o -o bootpack 
    i386-elf-objcopy -S -O binary bootpack bootpack.bin
    cat bootpack.bin >> asmhead.o 
    dd if=asmhead.o of=ipl.img bs=512 seek=33 conv=notrunc
    qemu-system-i386 -fda ipl.img -boot a
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

运行结果:
《30天自制操作系统》——从汇编到C_第2张图片

你可能感兴趣的:(操作系统)