操作系统实验一
从零开发能使用的操作系统需要的可能不只是C语言,还需要汇编的支持。我没有接触过X86汇编,不过我学过MIPS的汇编,两者其实有很多相似之处,主要的差别应该是在X86的寄存器上。除此之外其实X86还有串操作的语句,方便我们直接操作字符串或数组。这里我们做一个练手的项目,设计一个能让字符串在屏幕上按照左上、左下、右上、右下四种方向自动弹动的程序。
另外,我们还希望能够通过把这段汇编程序的可执行二进制程序装入软盘,来实现裸机控制权的接管。这也可以说成是一个最简单的操作系统。
打印字符串
main:
org 07c00h
mov ax, cs
mov ds, ax
mov es, ax
call DispStr
jmp $
DispStr:
mov ax, BootMessage
mov bp, ax
mov cx, [Strlen] ;字符串长
mov ax, 01301h ;写模式
mov bx, 000fh ;页号0,黑底白字
mov dl, 0 ;行=列=0
int 10h ;10h号接口
ret
BootMessage: db "huangwx8 17310031"
Strlen db 17
times 510-($-$$) db 0
dw 0xaa55
这段代码其实相当简洁,最开始有一行org 07c00h,这个指令会让编译器把代码装载到0x7c00,如果我们把它编译成BIN文件,在PC上运行时,PC识别到扇区末的0xaa55代号,认为它是引导扇区;然后它会把这个扇区放到7c00h的地址处,把程序计数器设为7c00h,从这里开始执行程序,从此bios不再控制PC,而是我们的程序接管PC。
然后我们把ds和es指向与cs相同的段,是为了后面偏移寻址找数据不会出错。
DispStr部分,我们会把Message(一个指向字符串的地址)塞到es:bp,然后我们会为ax赋一个值,高8位是13h,定义01h的功能为写字符串,低8位是01h,定义写模式。然后为bx赋一个值,高8位是页号,这里设为0;低8位是控制颜色,高4位是底色,低四位是字色。0fh的意义就是黑底白字。cx是字符串长度,dx控制位置。
我们call int 10h,10h中断号就是call系统的17号中断向量。然后屏幕上就会打印出字符串了。
最后ret,然后 jmp 是“当前行被汇编后的地址”。$$是”程序被编译后的首地址”.因此jmp $是一个死循环,相当于halt。而times 510-($-$$) db 0则是为了填满内存从开头到其后的510个字节为0,再加上0xaa55的2字节,以重置512字节即一个扇区的空间。方便我们直接把这个东西烧到软盘里,让bios识别并执行这段引导程序。
我们用nasm编译器编译这段代码,就能得到一个512字节的bin程序。然后我们把这段程序复制到虚拟机所搭载的启动软盘的第一个扇区,虚拟机会执行程序逻辑,我们就能在屏幕上看见这样的字符输出啦,我的姓名和学号。
任意位置打印字符
上面说过了,设置dh和dl的数值就能控制打印位置。我们只需要用一个循环,在几个不同的位置打印字符串,观察一下输出怎么样。
main:
org 07c00h
mov ax, cs
mov ds, ax
mov es, ax
call DispStr
jmp $
DispStr:
dec word[count] ; 递减计数变量
jnz DispStr ; >0:跳转;
mov word[count],delay
dec word[dcount] ; 递减计数变量
jnz DispStr
mov word[count],delay
mov word[dcount],ddelay
;用简单的双循环控制时延,这里时延50000x580个时间单位
mov ax, BootMessage
mov bp, ax
mov cx, [Strlen] ;字符串长
mov ax, 01301h ;写模式
mov bx, 000fh ;页号0,黑底白字
mov dh, [boundary] ;行=b
mov dl, [boundary] ;列=b
int 10h ;10h号接口
dec byte[boundary]
jnz DispStr
ret
BootMessage: db "huangwx8 17310031"
Strlen dw 17
delay equ 50000
ddelay equ 580
count dw delay
dcount dw ddelay
boundary db 10
times 510-($-$$) db 0
dw 0xaa55
正如我们希望的,字符串在对角线方向被打印了10次。
弹球逻辑
我们刚学C语言的时候做过的最简单的逻辑大概就是类似这种的逻辑了。这里我们设置初始方向是向右下方,一旦触壁就改变方向。即初始的速度vx、vy为1,位置x、y为0,0,除了上面的打印函数要调整以外,还要设置位置更新函数和边界判断函数;每次打印前判断边界并更新速度,打印时按照位置打印字符串,打印后更新位置。
org 07c00h
mov ax, cs
mov ds, ax
mov es, ax
loop1:
dec word[count] ; 递减计数变量
jnz loop1 ; >0:跳转;
mov word[count],delay
dec word[dcount] ; 递减计数变量
jnz loop1
mov word[count],delay
mov word[dcount],ddelay
; 以上是用一个二重循环实现时延50000*580个单位时间
jmp Entrance ;进行一个周期的工作
jmp $ ;halt
Entrance:
jmp BoundaryCheckx
DispStr:
mov ax, BootMessage ;打印字符串
mov bp, ax
mov cl, byte[Strlen] ;字符串长
mov ch, 0
mov ax, 01301h ;写模式
mov bx, 000fh ;页号0,黑底白字
mov dh, byte[x] ;行=x
mov dl, byte[y] ;列=y
int 10h ;10h号接口
Updatexy:
mov al, byte[x]
add al, byte[vx]
mov byte[x], al
mov al, byte[y]
add al, byte[vy]
mov byte[y], al
jmp loop1 ;无限循环
BoundaryCheckx:
mov al, byte[x]
add al, byte[vx] ;预测下一刻的x
cmp al, byte[upper] ;如果x小于上边界
jl Changevx ;更新vx
cmp al, byte[lower] ;如果x大于下边界
jg Changevx ;更新vx
BoundaryChecky:
mov al, byte[y]
add al, byte[vy]
cmp al, byte[left] ;如果y小于左边界
jl Changevy ;更新vy
add al, byte[Strlen];预测下一刻的yr=y+字符串长
cmp al, byte[right] ;如果yr大于下边界
jg Changevy ;更新vy
jmp DispStr ;如果不需要更新vx vy就继续打印流程
Changevx:
neg byte[vx]
jmp BoundaryChecky
Changevy:
neg byte[vy]
jmp DispStr
BootMessage: db "Welcome!"
Strlen db 8
delay equ 50000
ddelay equ 580
count dw delay
dcount dw ddelay
x db 0
y db 0
vx db 1
vy db 1
left db 0
upper db 0
right db 75
lower db 24
boundary db 10
times 510-($-$$) db 0
dw 0xaa55
对于比较复杂的逻辑要使用跳转语句来实现简单的递归调用。上面用了jg和jl作为逻辑判断语句,没有用call和ret而是用了jmp实现递归调用和返回。总体上还是一个死循环(检测-打印-更新位置),结果如图。
字符串持续打印并按照一个速度运动,一旦遇到边界就会让一个方向的速度反转。
小结
这个实验难度并不大,尽管我没有x86基础,但经过了两个小时的学习和两个小时的编程实验,这个项目也很快被完成了。实验的目的主要是为了让我们理解PC开机后,硬件软件做了什么;以及让我们熟悉x86汇编编程,为以后更复杂的操作系统开发打下基础。