学习王爽著汇编语言到了最后一阶段,完成了课程设计2,内容要求如下。
编写一个不需要在现有操作系统环境中运行的程序:
列出功能选项,让用户通过键盘进行选择功能,功能如下:
书中给出了系统启动相关的必备知识:
由此可知该程序的实现应该是通过向软盘写入引导程序,完成要求的功能,而软盘可以通过虚拟机模拟,具体方法可以参考我写的方法虚拟机创建软盘方法
具体的功能实现不是重点,该课程设计的难度主要是对整个系统的流程的掌握上,细分下可以分成,写入软盘程序、跳转程序、实际引导程序三个模块,因此着重关心程序框架。
通过调用int 13h的3号功能可以实现软盘写入
直接磁盘服务(Direct Disk Service——INT 13H)
功能03H
功能描述:写扇区
入口参数:AH=03H
AL=扇区数
CH=柱面
CL=扇区
DH=磁头
DL=驱动器,00H~7FH:软盘;80H~0FFH:硬盘
ES:BX=缓冲区的地址出口参数:CF=0——操作成功,AH=00H,AL=传输的扇区数,否则,AH=状态代码,参见功能号01H中的说明
;write_1.asm
assume cs:code
code segment
dcode:
;这里写要写入的程序
start:
mov ax,cs
mov es,ax
mov bx,offset dcode
mov dx,0
mov cx,1
mov ah,3
mov al,1
int 13h
mov ax,4c00h
int 21h
code ends
end start
这里是最重要的内容,也是我在编写该课程设计时候思考最久的内容,为什么不在软盘中直接写入需求的引导程序,而要加入一个跳转程序,主要原因在于偏移地址的校对。
在汇编语言程序的编写中,不可避免的要使用标号,而编译器在编译的时候,会根据标号在源程序中的位置转化为立即数,例如call,jmp near等。
要写入的程序(dcode)并不会在写入程序中被执行,仅仅只是写入到软盘中,而当在开机执行引导程序的时候,需要确保所有的跳转能正确执行,说起来可能非常抽象,可以看如下两小段程序。
;write1.asm
code segment
start:
int 13h;写入dcode到软盘
dcode:
jmp near fun
fun:
mov ax,1
code ends
end start
;write2.asm
code segment
dcode:
jmp near fun
fun:
mov ax,1
start:
int 13h;写入dcode到软盘
code ends
end start
重点在编译器对 jmp near fun 的编译上,为什么是用jmp near而不是jmp short,是因为jmp short记录的是jmp相对目标的偏移地址的差,而jmp near则和call等命令一样,记录的是绝对偏移地址。由于在源程序中的位置不同,因此两个程序看起来内容一样,但实际上在编译的exe文件中,对jmp near fun的编译结果是不同的。
那么,在编译的时候,偏移地址的编译结果是从code段第一行代码开始,从0开始往下。而在系统启动的时候,读取的内容目的地址是0:7c00h,然后从7c00h开始执行。这意味着,在写入的程序中,所有的子程序和跳转都不能正确跳转!
例如对于write.asm,fun的偏移地址是3(jmp near fun假设占3字节),那么在二进制文件中存储的就是0003h,而实际操作系统读取引导后,需要正确跳转的目的是7c00h+3=7c03h。也就是说需要在源程序中,给跳转的偏移地址加上7c00h即可。
但是,对于一个功能稍微复杂的程序,内容肯定不止一个子程序,不止一个跳转命令,若是要程序正确运行需要给每一个偏移加上7c00h,这样略显复杂,不如在读取后跳转到另一块内存,让偏移从0开始,并在源程序中把要写入的程序放在start之上,这样偏移就自然对齐了。
这也是为什么我在第1部分的源程序start在要写入的内容的下方的原理。
由于引导程序是在BIOS之上,操作系统之下,因此在编写的时候需要明确,通过使用INT13 AH=2从软盘读取出来的程序放在内存的哪个位置,虽然相比BIOS占用的内存,空闲区间很大,但仍然不是能随意设定的。我找到了这张图
于是完成的write_1.asm的子内容如下
dcode:
mov ax,50h
mov es,ax
mov bx,0
mov dx,0
mov cx,2
mov ah,2
mov al,5
int 13h
jmp bstart
stdin:
dw 0h,50h
nop
nop
bstart:
mov bx,offset stdin
add bx,7c00h
jmp dword ptr cs:[bx]
把主要的引导程序另外写入到软盘的从第2扇区开始的位置,并在读取后跳转到50:0的位置,这样就保证了跳转的正确性。
根据要求的4个功能,写出的源程序摘要如下
dcode:
mov ax,cs
mov ss,ax
mov sp,offset stkend
jmp guide
table dw sec1,sec2,sec3,sec4
guide:
mainlop:
call cls
call showgui
mov ah,0
int 16h
cmp al,31h
jb mainlop
cmp al,34h
ja mainlop
sub al,30h
mov bl,al
mov bh,0
sub bx,1
add bx,bx
call word ptr table[bx]
jmp mainlop
db 128 dup (?)
stkend:
nop
showgui:
jmp sguist
strt dw 0,gstr4,gstr3,gstr2,gstr1
gstr1:
db "1.reset pc "
gstr2:
db "2.start systeam"
gstr3:
db "3.clock "
gstr4:
db "4.set clock "
sguist:
;此处省略输出代码
ret
cls: ;清空屏幕子程序
push ax
push bx
push cx
push es
mov ax,0b800h
mov es,ax
mov bx,0
mov cx,4000
clslop:
mov byte ptr es:[bx],' '
add bx,2
loop clslop
pop es
pop cx
pop bx
pop ax
ret
sec1:
mov bx,offset rebot
jmp dword ptr cs:[bx]
rebot:
dw 0,0ffffh
sec2:
mov ax,0
mov es,ax
mov bx,7c00h
mov al,1
mov ch,0
mov cl,1
mov dh,0
mov dl,80h
mov ah,2
int 13h
mov bx,offset inbot
jmp dword ptr cs:[bx]
inbot:
dw 7c00h,0
sec3:
sec4:
由于两个复杂的子模块代码没有经过优化,较为冗余也没有注释就删掉了,留下前两个简单的功能。
由于是较为底层的程序,因此为了子程序的正确跳转,不能忘了手动分配栈空间,否则会错误。
前面的内容是基于王爽著汇编语言的内容写的方法,在其他书中看到了有这么一条伪指令。
ORG 表达式
表达式给出偏移地址,以表达式的值作为其后的程序段或数据块存放的起始地址的偏移量。
也即,在写入程序中给要被写入的引导程序前加上 org 7c00h,即可正确让引导程序执行。省略的繁琐的跳转步骤,这是汇编语言书中没有讲到的。