assume cs:code
code segment
org 7c00h;因为最终要加载到0:7c00h处,所以标记为7c00h开始的偏移
;
;由于初学没有一点经验,有的时候碰到问题总感觉程序写的对啊,怎么还是出错。
;就会怀疑是不是用的虚拟机不兼容,甚至怀疑书里讲的对不对啊之类的
;不过最终花了一个星期给写出来了!=.=,证明书里讲的都是正确的,使用的虚拟机也完全没有问题(用的是Bochs 2.3虚拟机)。
;大家在发现奇怪的问题的时候还是要看自己的代码是否正确
;
;个人觉得难点在功能3,在循环显示时钟的时候还要响应键盘。
;于是就重写了int9中断,在这个中断里判断用户如果按了esc键就返回到主选单
;返回主选单后还要退出显示时钟的循环,于是就定义了一个变量keyisesc,初始值为0,
;当用户按了esc键以后就设置为1,循环中检测到值为1了就跳出循环
;
;功能2也是一个难点,因为我们的程序被加载到0:7c00h处,在这里我们还要将C盘的
;引导扇区也加载到0:7c00h处。所以要特别注意加载C盘引导扇区的程序不要被覆盖掉了,还有使用的栈也不要被覆盖掉
;
;开始引导
rootstart:
;这一段是加载除第一个扇区之外的其它扇区
mov ax,cs
mov es,ax
mov ax,offset rootend - offset rootstart
mov dx,0
mov bx,512
div bx;用总字节/512(扇区的容量),al就记录的是要加载的扇区数
mov bx,startsystemaddress[0];7c00h
add bx,512;因为已经加载一个扇区了,所以就要加载到7c00h+512(一个扇区的长度)后面
mov ah,2
mov ch,0
mov cl,2;从第2个扇区开始加载
mov dh,0
mov dl,0
int 13h
mov ax,cs
mov ss,ax;设置栈段
mov sp,offset stack + 128;设置栈的起始偏移
;这个stack的栈定义在程序的最后端,这样做是为了防止功能2(从c盘加载引导扇区)将栈给覆盖掉,所以放到最后
call cleandisp;调用清理屏幕的子程序
call regnewint9;注册新的int9中断程序
;注意下面这2句
;首先showoption这个子程序是显示主选单的
;这个子程序会被新的int9中断调用,int9调用它的时候是一个段间调用,所以它要使用retf返回。
;但是在这里又不能使用 call far ptr showoption来进行段间调用,
;因为这样一来在编译后此处代码会变成 call 0aba:0123 带有段地址的代码
;那么在将当前程序往A盘写入后再被系统引导的时候调入,到这一步的时候就会发生错误
;(因为系统引导后当前的段地址是0,showoption的地址应该对应的是0:0123。于是在执行 call 0aba:0123的时候肯定就会出错)
;这里就采用模拟call far ptr的方式,先把段地址push到栈里,再使用段内调用
push cs
call showoption
jmp waitinput;跳转到等待用户输入的步骤
options dw option_1,option_2,option_3,option_4
option_1 db '1) reset pc',0
option_2 db '2) start system',0
option_3 db '3) clock',0
option_4 db '4) set clock',0
restaddress dd 0ffff0000h;reset pc 的时候要转到的地址
startsystemaddress dw 7c00h,0h
systemdate db '0','0','0','0','-','0','0','-','0','0',' ','0','0',':','0','0',':','0','0',0;日期按 2010:12:03 22:16:35 的格式来显示
keyisesc db 0;记录用户是否按了esc键
cursorrow db 0;当前光标所在的行号
cursorcolumn db 0;当前光标所在的列号
;这里是显示主选单的子程序
showoption:push ax
push bx
push cx
push di
push bp
call cleandisp;先清理一下屏幕
mov cx,4;有4个选项就循环4次
mov ah,3;从第3行开始绘制
mov di,0;从第一个选项开始绘制
mov bl,0;
showoption_s:
mov al,3;每一行都从第3列开始绘制
;将显示字符串需要的数据压到栈中
push bx
push ax
push options[di];要显示的选项字符串的起始处的偏移地址
push cs;要显示的选项的段地址
mov bp,sp
call drawstr;调用显示字符串的子程序
add bp,8
mov sp,bp;恢复栈顶的位置
add di,2;绘制下一个选项
inc ah;从下一行绘制
loop showoption_s
;设置光标的位置
inc ah
mov dh,ah
mov cursorrow,dh
mov dl,3
mov cursorcolumn,dl
mov ah,2
mov bh,0
int 10h
pop bp
pop di
pop cx
pop bx
pop ax
retf
;等待用户输入
waitinput:
mov ah,0
int 16h
mov keyisesc,0
cmp al,'1'
je option_1_handle;跳转到选项1(reset pc)的处理程序
cmp al,'2'
je option_2_handle;跳转到选项2(start system)的处理程序
cmp al,'3'
je option_3_handle;跳转到选项3(clock)的处理程序
cmp al,'4'
je option_4_handle;跳转到选项4(set clock)的处理程序
jmp short waitinput;如果不是1,2,3,4就继续等待用户输入
;引导扇区的最后2个字节必须是aa55h,所以在这里就要结束掉第一个扇区
shanqu1end:
db 510-(offset shanqu1end -offset rootstart) dup(0);将第一个扇区余下的字节设置为0
dw 0aa55h;最后的2个字节设置为aa55h
;选项1处理程序,直接跳转到0:ffff地址以实现重启动
option_1_handle:jmp restaddress
;选项2的处理程序放到了最下面,这样做是为了防止在将C盘读入内存的时候不会被覆盖掉
;选项3的处理程序
option_3_handle:
call cleandisp;清理屏幕
option_3_showdate:
call dispdatetime;调用显示当前时间的子程序
mov ax,002fh;为了防止过快的刷新时间,这里加了一个值为002f0000h的计数器,当值为0的时候才刷新
mov bx,0000h
option_3_handle_s:
cmp keyisesc,0
jne option_3_isback_handle;这里对keyisesc变量进行检测,如果不为0就跳出循环
sub bx,1;计数器-1
sbb ax,0
cmp ax,0
jne option_3_handle_s;当计数器不为0的时候就继续循环计数
je option_3_showdate;否则就刷新当前的时间
option_3_isback_handle:
jmp waitinput;当按下esc键,也就是keyisesc变量不为0的时候就继续等待用户的输入,这个时候已经返回主选单了(在新的int9中断里控制的)
;选项4的处理程序
option_4_handle:
call cleandisp;清理屏幕
call dispdatetime;显示时间
call option_4_inputkey_cursor_res;设置光标到显示的时间的起始位置,也就是年份那里
option_4_handle_waitinput:;然后就等待用户输入
mov ah,0
int 16h
cmp ah,01h;如果按下了esc键就返回主选单的等待用户输入程序,这个时候已经返回主选单了(在新的int9中断里控制的)
je waitinput
cmp ah,4bh;如果按的是左方向键,就将光标往左移动一位
je option_4_inputkey_cursor_before
cmp ah,4dh;如果按的是右方向键,就将光标往右移动一位
je option_4_inputkey_cursor_next
cmp al,0dh;如果按的是回车就保存改动,并返回主选单
je option_4_back
cmp al,30h;如果按的是除了0-9以外的其它键就继续等待用户输入
jb option_4_handle_waitinput
cmp al,39h
ja option_4_handle_waitinput
;一下这几句就是如果当前的光标处于非日期部分
;和日期部分的分隔符(如 2011-12-3 22:30:35 中的 - : 这些符号的时候
;不允许写入,继续等待输入
cmp cursorrow,10
jne option_4_handle_waitinput
cmp cursorcolumn,31
jb option_4_handle_waitinput
cmp cursorcolumn,49
ja option_4_handle_waitinput
cmp cursorcolumn,35
je option_4_handle_waitinput
cmp cursorcolumn,38
je option_4_handle_waitinput
cmp cursorcolumn,41
je option_4_handle_waitinput
cmp cursorcolumn,44
je option_4_handle_waitinput
cmp cursorcolumn,47
je option_4_handle_waitinput
;获取当前的光标位置,并把用户输入的数字写到当前光标位置处
mov bh,al;用户输入的字符
mov al,cursorcolumn;当前光标所在的列号
mov ah,cursorrow;当前光标所在的行号
push ax
push bx
mov bp,sp
call drawchar;调用显示字符子程序
add bp,4
mov sp,bp
;将用户输入的字符写入到systemdate变量的对应位置中
sub al,31;将当前光标所在的列数减去31就是在systemdate变量中对应的索引位置
;如当前光标在31处,减去31等于0,就是年份的千位在 systemdate变量的0位
mov ah,0
mov si,ax
mov systemdate[si],bh
;将光标移动到下一个位置
call inputkey_cursor_next
;如果下一个位置是日期的分隔符(如 2011-12-3 22:30:35 中的 - : 这些符号的时候 ),那就再往下移动一位
cmp cursorcolumn,35
je option_4_inputkey_cursor_next
cmp cursorcolumn,38
je option_4_inputkey_cursor_next
cmp cursorcolumn,41
je option_4_inputkey_cursor_next
cmp cursorcolumn,44
je option_4_inputkey_cursor_next
cmp cursorcolumn,47
je option_4_inputkey_cursor_next
;继续等待用户输入
jmp option_4_handle_waitinput
;光标移动到上一处并等待输入
option_4_inputkey_cursor_before:
call inputkey_cursor_before
jmp option_4_handle_waitinput
;光标移动到下一处并等待输入
option_4_inputkey_cursor_next:
call inputkey_cursor_next
jmp option_4_handle_waitinput
;设置光标的最初位置在日期年份的千位处
option_4_inputkey_cursor_res:
push ax
push bx
push dx
mov ah,2
mov bh,0
mov dh,10
mov cursorrow,dh
mov dl,31
mov cursorcolumn,dl
int 10h
pop dx
pop bx
pop ax
ret
;保存修改的日期并返回主选单
option_4_back:
;保存年份的千位和百位
mov al,32h;
mov bh,systemdate[0]
mov bl,systemdate[1]
call writecmosdate
;保存年份的十位和个位
mov al,9h;
mov bh,systemdate[2]
mov bl,systemdate[3]
call writecmosdate
;保存月份
mov al,8h;
mov bh,systemdate[5]
mov bl,systemdate[6]
call writecmosdate
;保存日
mov al,7h;
mov bh,systemdate[8]
mov bl,systemdate[9]
call writecmosdate
;保存小时
mov al,4h;
mov bh,systemdate[11]
mov bl,systemdate[12]
call writecmosdate
;保存分钟
mov al,2h;
mov bh,systemdate[14]
mov bl,systemdate[15]
call writecmosdate
;保存秒
mov al,0h;
mov bh,systemdate[17]
mov bl,systemdate[18]
call writecmosdate
push cs
call showoption
jmp waitinput
;将光标移动到下一位,如果达到最后一列会自动换行,如果达到最后一行的最后一列会返回到第一行的第一列
inputkey_cursor_next:
push ax
push bx
push dx
mov ah,2
mov bh,0
inc cursorcolumn
mov dl,cursorcolumn
cmp cursorcolumn,79
jna inputkey_cursor_next_int
inputkey_cursor_next_nextrow:
inc cursorrow
mov cursorcolumn,0
mov dl,0
cmp cursorrow,24
jna inputkey_cursor_next_int
mov cursorrow,0
inputkey_cursor_next_int:
mov dh,cursorrow
int 10h
pop dx
pop bx
pop ax
ret
;将光标移动到上一位,如果到第一列会自动换行到上一行。如果到底一行的第一列,会跳到最后一行的最后一列
inputkey_cursor_before:
push ax
push bx
push dx
mov ah,2
mov bh,0
cmp cursorcolumn,0
jna inputkey_cursor_before_beforerow
dec cursorcolumn
mov dl,cursorcolumn
jmp inputkey_cursor_before_int
inputkey_cursor_before_beforerow:
mov cursorcolumn,79
mov dl,cursorcolumn
cmp cursorrow,0
jna inputkey_cursor_before_row_max
dec cursorrow
jmp inputkey_cursor_before_int
inputkey_cursor_before_row_max:
mov cursorrow,24
inputkey_cursor_before_int:
mov dh,cursorrow
int 10h
pop dx
pop bx
pop ax
ret
;显示当前的日期
dispdatetime:push ax
push bx
push dx
;读取年份的千位和百位
mov al,32h;
call readcmosdate
mov systemdate[0],ah
mov systemdate[1],al
;读取年份的十位和个位
mov al,9
call readcmosdate
mov systemdate[2],ah
mov systemdate[3],al
;读取月份
mov al,8
call readcmosdate
mov systemdate[5],ah
mov systemdate[6],al
;读取日
mov al,7
call readcmosdate
mov systemdate[8],ah
mov systemdate[9],al
;读取小时
mov al,4
call readcmosdate
mov systemdate[11],ah
mov systemdate[12],al
;读取分钟
mov al,2
call readcmosdate
mov systemdate[14],ah
mov systemdate[15],al
;读取秒
mov al,0
call readcmosdate
mov systemdate[17],ah
mov systemdate[18],al
;将读取到的日期显示出来
mov ah,10;显示在第10行
mov al,31;显示在第31列
mov bl,0
;将要传递的参数压入栈
push bx
push ax
mov dx,offset systemdate;要显示的字符串的起始偏移量
push dx
push cs
mov bp,sp
call drawstr
add bp,8;恢复栈顶的位置
mov sp,bp
pop dx
pop bx
pop ax
ret
;将日期写入到cmos ram中
writecmosdate:
;bl和bh为要改变的日期部分的ASCII码
;要将他们转换为实际的数字,ASCII码减去30H就是实际的数字了
;如数字1的ASCII码是31H
sub bl,30h
sub bh,30h
;转换为BCD码,在书中281页有讲
;将十位数字左移4位,右边以0填充
shl bh,1
shl bh,1
shl bh,1
shl bh,1
and bl,00001111b;将个位数字的前4位设置为0
add bh,bl;然后相加就转换为BCD码了
out 70h,al;要对日期的哪个部分写入
mov al,bh
out 71h,al;写入日期bcd码
ret
;将日期从cmos ram中读取出来
readcmosdate:
out 70h,al;要对日期的哪个部分读取
in al,71h;读取的bcd码写入到al中
mov ah,al;复制一份bcd码到ah中
;右移4位获取十位的数字
shr ah,1
shr ah,1
shr ah,1
shr ah,1
and al,00001111b;将高4位设置为0获取个位数字
add ah,30h;将数字加上30H获取对应的ASCII码
add al,30h
ret
;清理屏幕
cleandisp:
push ax
push cx
push es
push di
mov ax,0b800h
mov es,ax
mov di,0
mov cx,2000
cleandisp_s:
mov byte ptr es:[di],' ';将所有内容设置为空格
mov byte ptr es:[di+1],00000111b;颜色设置为白色
add di,2
loop cleandisp_s
pop di
pop es
pop cx
pop ax
ret
;绘制字符串
drawstr:
push ax
push bx
push es
push di
push bp
;将压入栈的数据读取出来
mov es,ss:[bp];字符串所在的段地址
mov di,ss:[bp+2];字符串开头的偏移地址
mov ax,ss:[bp+4];ah为要在那一行显示,al为要在哪一列显示
mov bl,ss:[bp+7];要显示的颜色,这个不再使用了,随时可以按f1来更换颜色
drawstr_s:mov bh,es:[di]
cmp bh,0;读取到0表示到字符串的结尾了,就退出绘制
je drawstr_end
push ax
push bx
mov bp,sp
call drawchar;绘制字符
add bp,4
mov sp,bp
inc al;绘制完一个字符后将列数加1
cmp al,80;如果超过80列了就换一行
je drawstr_addrow_call
jmp drawstr_s_continue;否则就继续绘制
drawstr_addrow_call:call drawstr_addrow
drawstr_s_continue:inc di;继续绘制,将要绘制的字符偏移量加1
jmp drawstr_s
;换新行,列数重置为0
drawstr_addrow:
mov al,0
inc ah
ret
drawstr_end:
pop bp
pop di
pop es
pop bx
pop ax
ret
;绘制字符
drawchar:
push ax
push bx
push dx
push es
push di
mov ax,0b800h
mov es,ax
mov bx,ss:[bp]
mov ax,ss:[bp+2]
;根据行数和列数计算出位置偏移量
mov dl,al
mov al,ah
mov dh,160
mul dh
mov di,ax
mov al,2
mul dl
add di,ax
mov byte ptr es:[di],bh
;不绘制颜色了,按f1可随时修改颜色
;mov byte ptr es:[di+1],bl
pop di
pop es
pop dx
pop bx
pop ax
ret
;注册新的int9中断
regnewint9:
push ax
push ds
push si
push es
push di
push cx
;由于在新的int9中断里会监测esc键,如果按了esc键就要返回主选单,也就是要调用showoption子程序
;而int9中断和当前的程序没在一个段中。所以我们需要将showoption的段地址和偏移量记录下来供新的int9调用
mov si,offset showoptionaddress;这个是记录showoption子程序地址的变量,这个变量在新int9中定义
mov cs:[si],offset showoption;记录偏移量
mov cs:[si+2],cs;记录段地址
;在新的in9中断里如果监测到esc键以后会将keyisesc变量设置为1,
;这样功能3循环显示时钟的时候,如果看到keyisesc变量为1了就知道用户返回主选单里,从而退出循环
;同样由于int9中断和keyisesc变量没在一个段中,所以需要将keyisesc的段地址和偏移量记录下来供新的in9调用
mov si,offset keyisescaddress;这个是记录keyisesc子程序地址的变量,这个变量在新的in9中定义
mov cs:[si],offset keyisesc;记录偏移量
mov cs:[si+2],cs;记录段地址
mov ax,cs
mov ds,ax
mov si,offset newint9;将newin9所在的偏移地址记录下来
mov ax,0
mov es,ax
mov di,204h;将newin9写到0:204h中
mov cx,offset newint9_end - offset newint9;计算出newint9的长度
cld
rep movsb;开始复制
;将原来的int9中断的地址记录到0:200h以便调用
mov ax,es:[9*4]
mov word ptr es:[200h],ax
mov ax,es:[9*4+2]
mov word ptr es:[202h],ax
;将新的int9中段地址写到中段向量表中
cli;防止在写中断向量表的时候发生int9中断导致程序出错
mov word ptr es:[9*4],204h
mov word ptr es:[9*4+2],0
sti
pop cx
pop di
pop es
pop si
pop ds
pop ax
ret
;新的int9中断的主程序
newint9:jmp newint9start
showoptionaddress dd 0;定义记录showoption子程序的段地址和偏移量的变量
keyisescaddress dd 0;定义记录keyisescaddress子程序的段地址和偏移量的变量
newint9start:
push ax
push bx
push cx
push es
push di
in al,60h;从60h端口读取键盘输入的扫描码
;这是为了模拟调用老的int9中断
pushf
pushf
pop bx
and bx,11111100b;这几步应该是不需要的,因为系统中调用新的int9的时候已经把标志位if,tf设置为0了
push bx
popf
mov bx,0
mov es,bx
call dword ptr es:[200h];调用老的int9中断
cmp al,3bh;如果按了f1
je newint9_f1
cmp al,01h;如果按了esc
je newint9_esc
;中断返回
newint9_iret:
pop di
pop es
pop cx
pop bx
pop ax
iret
newint9_esc:
;如果按了esc键就根据showoptionaddress里的地址调用showoption子程序
;请注意这里为什么这样写,而不是直接 call dword ptr showoptionaddress
;因为新的in9中断是我们的程序复制过去的,如果 call dword ptr showoptionaddress,则这一句会被编译成 call 段地址:0abc 的固定地址
;这样一来我们在in9中断访问它的时候就是错误的
;那如何取得这个地址呢,首先可以通过 offset showoptionaddress - offset newint9 来获取showoptionaddress在新的newint9中断里的偏移量
;然后加上newint9中断偏移地址204h就能算出 showoptionaddress在当前段中的偏移量
;这一部分容易让人迷惑,我们主要要考虑到:新的中断是我们的程序在运行的时候复制过去的。
call dword ptr cs:[offset showoptionaddress - offset newint9+204h]
mov ax, cs:[offset keyisescaddress - offset newint9+204h+2]
mov es,ax
mov di,cs:[offset keyisescaddress - offset newint9 +204h];如果按了esc键就根据keyisescaddress里的地址将keyisesc变量设置为1
mov byte ptr es:[di],1
jmp newint9_iret
newint9_f1:
;如果按了f1就改变颜色,这里并没有指定颜色,而是根据当前的颜色+1产生新的颜色
mov ax,0b800h
mov es,ax
mov di,1
mov cx,2000
newint9_f1_s:
mov al,es:[di];获取当前的颜色
mov ah,al;将当前的颜色复制一份给ah
inc al;当前的颜色加1
;因为我们仅仅修改字体颜色,也就是颜色的低3位
and al,00000111b;将新的颜色的高5位设置0
and ah,11111000b;将就颜色的低3位设置为0
add ah,al;相加后就能将新的低3位的字体颜色修改到旧颜色中
mov es:[di],ah;将修改后的颜色写入显存中
add di,2
loop newint9_f1_s
jmp newint9_iret
newint9_end:nop
;设置128个字节的栈,之所以放到最后是防止在调用功能2(调用C盘扇区)的时候被覆盖掉
;这里我也想到一个问题就是如果C盘引导程序继续读取更多的扇区的时候可能也会覆盖这个栈区域
;不知道是否可以不设置栈,使用系统自动分配的栈,或者C盘引导程序会分配自己的栈空间,还请大家指教。
;
stack db 128 dup(0)
option_2_handle:
;功能2,调用C盘引导扇区,之所以放到这里也是为了防止被覆盖
;这里缺少一个功能,在从C盘引导后应该将新的int9中断给撤销,换回原来的int9中断
mov ax,0
mov es,ax
mov bx,7c00h
;加载到0:7c00h,我看有的朋友是加载的7e00h处然后执行
;我有点不太放心,C盘的引导扇区程序会不会也像我们一样将偏移地址的起始地址已经假设为 7c00h了或者其它的问题。
;当然这些朋友都已经测试过了应该是没问题,不过我还是老实将其加载到7c00h处吧=.=
mov dl,80h
mov dh,0
mov cl,1
mov ch,0
mov al,1
mov ah,2
int 13h
;模拟段间跳转开始执行C盘的引导程序
mov ax,0
push ax
mov ax,7c00h
push ax
retf
;给引导程序的结尾做个标记好计算引导程序的长度
rootend:nop
setup:
mov ax,cs
mov es,ax
//计算出引导程序的长度
mov ax,offset rootend - offset rootstart
mov dx,0
mov bx,512
div bx;长度/512计算出需要的扇区
inc ax;因为肯定有余数的,所以将需要的扇区数+1。这样就能保证完整保存
mov bx,offset rootstart;从引导程序的开始处读取
mov ah,3
mov ch,0
mov cl,1
mov dh,0
mov dl,0
int 13h;写入到软盘
;退出程序
mov ax,4c00h
int 21h
code ends
end setup
转载于:https://www.cnblogs.com/danqing/archive/2011/12/04/2275333.html