《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

我在/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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

運行結果:


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com