这次我们要用到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)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
博文把禁用中断、开启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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
运行结果: