《汇编语言》课程设计

一、目的

    知行合一,就是说学以致用。《汇编语言》这本书从13年春节开始看,中间由于众多事物中断,知道14年春节才看完。但是,百看不如一练,借着刚看完书,对汇编语言还很热的感觉,决定对书中最后的课程设计冲刺一下,作为这本书的结业课。

二、课程设计内容

1、题目

阅读下面的材料: 

开机后,CPU自动进入到FFFF:0单元处执行,此处有一条跳转指令。CPU执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序。 

初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。 

硬件系统检测和初始化完成后,调用int 19h进行操作系统的引导。 

如果设为从软盘启动操作系统,则int 19h将主要完成以下工作。 

(1)控制0号软驱,读取软盘0道0面1扇区的内容到0:7c00; 

(2)将CS:IP指向0:7c00。 

软盘的0道0面1扇区中装有操作系统引导程序。int 19h将其装到0:7c00处后,设置CPU从0:7c00开始执行此处的引导程序,操作系统被激活,控制计算机。 

如果在0号软驱中没有软盘,或发生软盘I/O错误,则int 19h将主要完成以下工作。 

(1)读取硬盘C的0道0面1扇区的内容到0:7c00; 

(2)将CS:IP指向0:7c00。 

这次课程设计的任务是编写一个可以自行启动的计算机,不需要在现有操作系统环境中运行的程序。 

该程序的功能如下: 

(1)列出功能选项,让用户通过键盘进行选择,界面如下 

   1) reset pc          ;重新启动计算机 

   2) start system      ;引导现有的操作系统 

   3) clock             ;进入时钟程序 

   4) set clock         ;设置时间 

(2)用户输入"1"后重新启动计算机(提示:考虑ffff:0单元) 

(3)用户输入"2"后引导现有的操作系统(提示:考虑硬盘C的0道0面1扇区)。 

(4)用户输入"3"后,执行动态显示当前日期、时间的程序。 

显示格式如下:年/月/日 时:分:秒 

进入此项功能后,一直动态显示当前的时间,在屏幕上将出现时间按秒变化的效果(提示: 循环读取CMOS)。 

当按下F1键后,改变显示颜色;按下Esc键后,返回到主选单(提示:利用键盘中断)。 

(5)用户输入"4"后可更改当前的日期、时间,更改后返回到主选单(提示:输入字符串)。

2、程序设计思路 

   将安装程序分为三个段, 

1、第一个段为安装程序,负责将第二个段写入第一扇区,第三个段写入2-11扇区; 

2、第二个段是主引导程序,存在于软盘第一扇区,由BIOS的19h中断读取到0:7c00开始的内存单元中,并执行0:7c00的第一行代码。这一段的任务是将软盘2-11扇区的数据读入内存,并执行。(这里读入到了2000h:0h开始的内存中) 

3、第三个段是系统程序,存放所有引导所需的程序和子程序

3、程序实现功能

 程序实现的功能有:

   1) reset pc          ;重新启动计算机 

   2) start system      ;引导现有的操作系统 

   3) clock             ;进入时钟程序 

4、实现程序 

;安装程序代码段
assume cs:code
code segment

start:    
    mov ax,initsg ;将初始化代码段写入软盘的第1扇区
    mov es,ax
    mov bx,0
    mov al,1
    mov ch,0
    mov cl,1
    mov dl,0
    mov dh,0
    mov ah,3
    int 13h
    
    mov ax,mainshell ;将主界面代码段写入软盘的2-11扇区
    mov es,ax
    mov bx,0
    mov al,10
    mov ch,0
    mov cl,2
    mov dl,0
    mov dh,0
    mov ah,3
    int 13h
    
    mov ax,4c00h
    int 21h
code ends

;初始化代码段
;运行地址:0:7c00h
assume cs:initsg
initsg segment

;这部分注释掉的代码为错误代码
;initsgstart: jmp near ptr initstart
;    initsgstack: db 128 dup(0)    

;initstart: 
    ;mov ax,7c0h
    ;mov ss,ax
    ;mov sp,(offset initsgstack)+128
    
initsgstart:    
    call loadmainshell
    
    ;将2000h:0h存入堆栈,用retf指令跳转到该处去运行
    mov ax,2000h
    push ax
    mov ax,0
    push ax
    retf

;加载主界面代码段到内存的2000h:0h位置    
loadmainshell:    
    mov ax,2000h
    mov es,ax
    mov bx,0
    
    mov al,10
    mov ch,0
    mov cl,2
    mov dl,0
    mov dh,0
    mov ah,2
    int 13h
    ret
initsg ends

;主界面代码段
;运行地址:2000h:0h
assume cs:mainshell
mainshell segment     

;这部分注释掉的代码为错误代码
;mainshellstart:    
;    jmp near ptr shellstart
;    mainshellstack: db 128 dup(0)    ;主界面的堆栈区

;shellstart: 
;        mov ax,cs
;        mov ss,ax
;        mov sp,(offset mainshellstack)+128 ;设置堆栈
        
mainloop:    
        call menudisplay ;显示主界面
        
        call getonechar  ;读取一个字符的输入,al返回输入的字符 
        
        mov al,al        ;传递参数
        call funsecl     ;根据不同的输入执行不同功能的函数
        
        jmp short mainloop ;跳到主循环运行

;函数功能:根据不同的输入执行不同功能的函数
;函数输入参数:al
funsecl: 
    jmp short funseclect
    funtable dw 0,sysrest,sysstart,clockdisplay ;子函数的地址
    
funseclect: 
    push bx
    
    ;先判断输入的字符是否在允许范围内
    cmp al,'4'    
    ja funret
    cmp al,'1'
    jb funret
    
    ;根据不同的输入执行不同功能的函数
    mov bl,al
    mov bh,0
    sub bx,'0'
    add bx,bx
    call word ptr funtable[bx]
    
funret: pop bx
    ret

;函数功能:系统复位
sysrest:    mov ax,0ffffh
    push ax
    mov ax,0
    push ax
    retf

;函数功能:启动操作系统    
sysstart: 
    call clearscreen
    call loadsyskernel

    add sp,4    ;为了使跳出本软盘程序时的堆栈和进入本软盘程序开始时的堆栈相同
                ;调整sp+4,因为之前执行了两个call指令,但是都还没有用ret指令返回
                
    ;将0h:7c00h存入堆栈,用retf指令跳转到该处去运行
    mov ax,0
    push ax
    mov ax,7c00h
    push ax
    retf
    
loadsyskernel:    ;从硬盘c加载操作系统
    mov ax,0
    mov es,ax
    mov bx,7c00h
    
    mov al,1
    mov ch,0
    mov cl,1
    mov dl,80h
    mov dh,0
    mov ah,2
    int 13h
    ret 
     
;函数功能:时钟显示,按下ESC键退出显示,按下F1键更改颜色
;参考《汇编语言》280页例程
clockdisplay: 
    jmp short clockstart
    escflag dw 0     ;是否按下esc键的标志,0:未按下;1:按下
    int9back dw 0,0    ;保存int9中断向量的空间
clockstart:        
    push ds
    push es
    push bx
    
    mov ax,cs
    mov ds,ax
    
    mov ax,0
    mov es,ax
    
    ;备份int9中断向量到int9back中
    mov bx,offset int9back 
    push es:[9*4]
    pop ds:[bx]
    push es:[9*4+2]
    pop ds:[bx+2]
    
    ;更改int9的中断向量为int9函数地址
    mov word ptr es:[9*4],offset int9
    mov es:[9*4+2],cs
    
    call clearscreen     ;清屏
    
    mov escflag,0       ;将escflag清0
    mov bx,0b800h        
    mov es,bx

;时钟主循环为不断的更新时间,并显示出来    
clockloop:     mov byte ptr es:[160*12+28*2],32h
    mov byte ptr es:[160*12+29*2],30h    
    
    mov al,9
    call readpara
    mov byte ptr es:[160*12+30*2],ah
    mov byte ptr es:[160*12+31*2],al
    mov byte ptr es:[160*12+32*2],'/'
    
    mov al,8
    call readpara
    mov byte ptr es:[160*12+33*2],ah
    mov byte ptr es:[160*12+34*2],al
    mov byte ptr es:[160*12+35*2],'/'

    
    mov al,7
    call readpara
    mov byte ptr es:[160*12+36*2],ah
    mov byte ptr es:[160*12+37*2],al
    mov byte ptr es:[160*12+38*2],' '
    
    mov al,4
    call readpara
    mov byte ptr es:[160*12+39*2],ah
    mov byte ptr es:[160*12+40*2],al
    mov byte ptr es:[160*12+41*2],':'

    mov al,2
    call readpara
    mov byte ptr es:[160*12+42*2],ah
    mov byte ptr es:[160*12+43*2],al
    mov byte ptr es:[160*12+44*2],':'
    
    mov al,0
    call readpara
    mov byte ptr es:[160*12+45*2],ah
    mov byte ptr es:[160*12+46*2],al
    
    cmp escflag,0 ;判断是否按下ESC
    jne exit      ;如果是,就跳出循环
    jmp clockloop ;如果不是,继续显示
    
exit:    
    call clearscreen ;清屏
    mov ax,0
    mov es,ax
    
    ;将原int9中断向量复原
    mov bx,offset int9back
    push ds:[bx]
    pop es:[9*4]
    push ds:[bx+2]
    pop es:[9*4+2]
    
    pop bx
    pop es
    pop ds
    ret

;函数功能:读取参数(年、月、日、时、分、秒)
;函数输入参数:al
;函数输出参数:ax    
readpara: push cx
    out 70h,al
    in al,71h
    
    mov ah,al
    mov cl,4
    shr ah,cl
    and al,00001111b
    
    add ah,30h
    add al,30h
    
    pop cx
    ret

;函数功能:按下ESC键更改escflag标志,按下F1键更改颜色
int9:    push ax
    push bx
    push es
    
    in al,60h
    push ax
    
    pushf
    pushf
    pop bx
    and bh,11111100b
    push bx
    popf
    
    mov bx,2000h
    mov ds,bx
    mov bx,offset int9back
    call dword ptr ds:[bx]
    
    pop ax
    cmp al,3bh
    je changecolor     ;按下F1键更改颜色
    cmp al,01h
    je quit            ;按下ESC键退出
    
    pop es
    pop bx
    pop ax
    iret
    
changecolor:    push es
    push bx
    
    mov ax,0b800h
    mov es,ax
    mov bx,0
    
    mov cx,19
changeloop:    inc byte ptr es:[160*12+28*2+1][bx]
    add bx,2
    loop changeloop
    
    pop bx
    pop es
    jmp short int9ret
    
quit:    mov escflag,1    ;按下ESC键更改escflag标志
int9ret:    pop es
    pop bx
    pop ax
    iret

;函数功能:清屏
clearscreen: push bx
    push es
    push cx
    
    mov bx,0b800h
    mov es,bx
    mov bx,0
    
    mov cx,2000
clearloop:    mov byte ptr es:[bx],' '
    mov byte ptr es:[bx+1],07h
    add bx,2
    loop clearloop
    
    pop cx
    pop es
    pop bx
    ret 

;函数功能:主界面显示    
menudisplay:    jmp short display
    table1: db '1) reset pc',0
    table2: db '2) start system',0
    table3: db '3) clock',0
    table4: db 'Enter your selection:   ',0
    
display:    push dx
    push ax
    push ds
    push si
    
    mov dh,10
    mov dl,30
    mov ax,cs
    mov ds,ax
    mov si,offset table1
    call stringsshow
    
    mov dh,11
    mov dl,30
    mov ax,cs
    mov ds,ax
    mov si,offset table2
    call stringsshow
    
    mov dh,12
    mov dl,30
    mov ax,cs
    mov ds,ax
    mov si,offset table3
    call stringsshow
    
    mov dh,13
    mov dl,30
    mov ax,cs
    mov ds,ax
    mov si,offset table4
    call stringsshow
    
    pop si
    pop ds
    pop ax
    pop dx
    ret

;函数功能:显示一个字符串    
stringsshow:    push bx
    push es
    push ax
    push di
    
    mov bx,0b800h
    mov es,bx
    mov al,160
    mov ah,0
    mul dh
    mov di,ax
    add dl,dl
    mov dh,0
    add di,dx
    
menucharshow:    mov al,ds:[si]
    cmp al,0
    je menusret ;如果字符为0,就结束显示
    mov es:[di],al
    add si,1
    add di,2
    jmp menucharshow
    
menusret:    pop di
    pop ax
    pop es
    pop bx
    ret

;子程序:字符栈的入栈、出栈和显示。
;参数说明:(ah)=功能号,0表示入栈,1表示出栈,2表示显示;
;          ds:si指向字符栈空间;
;          对于0号功能:(al)=入栈字符;
;          对于1号功能:(al)=返回的字符;
;          对于2号功能:(dh)、(dl)=字符串在屏幕上显示的行、列位置。

charstack:jmp short charstart
table dw charpush,charpop,charshow
top   dw 0          ;栈顶(字符地址、个数记录器)

charstart:push bx
     push dx
     push di
     push es
     cmp ah,2      ;判断ah中的功能号是否大于2
     ja sret            ;功能号>2,结束
     mov bl,ah
     mov bh,0
     add bx,bx     ;计算对应子程序在table表中的偏移
     jmp word ptr table[bx] ;调用对应的功能子程序 

charpush:mov bx,top
     mov [si][bx],al
     inc top
     jmp sret

charpop:cmp top,0
     je sret            ;栈顶为0(无字符),结束
     dec top
     mov bx,top         ;//保存数据,其它作用不详
     mov al,[si][bx]        ;//保存数据,其它作用不详
     jmp sret  

charshow:mov bx,0b800h
     mov es,bx
     mov al,160
     mov ah,0
     mul dh             ;dh*160
     mov di,ax
     add dl,dl     ;dl*2
     mov dh,0
     add di,dx     ;di=dh*160+dl*2,es:di指向显存
     mov bx,0      ;ds:[si+bx]指向字符串首地址

charshows:cmp bx,top        ;判断字符栈中字符是否全部显示完毕
     jne noempty        ;top≠bx,有未显示字符,执行显示
     mov byte ptr es:[di],' ';显示完毕,字符串末加空格
     jmp sret

noempty:mov al,[si][bx]     ;字符ASCII码赋值al
     mov es:[di],al         ;显示字符
     mov byte ptr es:[di+2],' '  ;字符串末加空格
     inc bx             ;指向下一个字符
     add di,2      ;指向下一显存单元
     jmp charshows

sret:    pop es
     pop di
     pop dx
     pop bx
     ret


;子程序:读取一个字符的输入
;读取到的输入:al    

getonechar:   jmp short getonecharstart
    char db 8 dup(0) ;存输入的位置
getonecharstart: push ds
     push si
     push dx
     
     mov char,0 ;输入存储单元清零
     mov top,0  ;将输入计数器清零
     
     mov ax,cs
     mov ds,ax
     mov si,offset char
     
     ;输入显示的位置是13行52列
     mov dh,13
     mov dl,52
     call getstr
     mov al,char
     
     pop dx
     pop si
     pop ds
     ret

getstr:  push ax
getstrs:mov ah,0
     int 16h
     cmp al,20h
     jb nochar     ;ASCII码小于20h,说明不是字符 
     
     cmp top,0     
     jne getstrs  ;如果有一个输入,就等待Enter键按下结束输入或者Backspace键删除
                  ;如果没有输入,就将这个输入的字符保存起来
                  
     mov ah,0
     call charstack         ;字符入栈
     mov ah,2
     call charstack         ;显示栈中字符
     jmp getstrs

nochar:  cmp ah,0eh         ;退格键的扫描码
     je backspace
     cmp ah,1ch             ;Enter键的扫描码
     je enter
     jmp getstrs

 

backspace:mov ah,1
     call charstack         ;字符出栈
     mov ah,2
     call charstack         ;显示栈中字符
     jmp getstrs

enter:   mov al,0
     mov ah,0
     call charstack         ;0入栈
     mov ah,2
     call charstack         ;显示栈中字符
     pop ax
     ret
     
mainshell ends
end start
code

3、测试方法

软件介绍:

  仿真虚拟软驱(点击下载):运行在windows上,像真实的软驱一样可以支持虚拟镜像的读写

  vFloppy 1.5(点击下载):脱离与windows运行,其镜像可以像真实软盘那样在PC启动时运行

(1)虚拟机+虚拟软驱镜像(vfloppy.img)

测试使用工具:VMware虚拟机、仿真虚拟软驱

a)安装仿真虚拟软驱,并加载镜像(vfloppy.img)后,“我的电脑”中会模拟出一个“3.5 软盘 (A:)”,可以像真的软盘一样对其进行读写等。

    说明:虚拟软驱就相当于物理软驱,虚拟镜像就相当于软盘,加载镜像就相当于将软盘插入软驱


b)在windows命令行下对程序进行编译、连接,执行后引导代码写入到软盘中

    说明:写入软盘实际上就是写入虚拟镜像(vfloppy.img)
c)首先卸载虚拟镜像(vfloppy.img),然后新建一个虚拟机,使用虚拟镜像(vfloppy.img)引导,启动虚拟机。

说明:卸载虚拟镜像的目的是因为,虚拟光驱占用着vfloppy.img时,虚拟机就不能使用这个文件了。

d) 虚拟机上的运行效果

(2)物理机+虚拟软驱镜像(vfloppy.img)(实机操作,慎用)

测试使用工具:物理机、仿真虚拟软驱vFloppy 1.5

a) 制作vfloppy.img方法同方式(1)的前两个步骤一样

b) 使用vFloppy 1.5制作“启动软盘”

 c) 选择刚才制作好的虚拟镜像vfloppy.img,然后点击“应用”。

     此时,可以查看c盘隐藏的系统文件boot.ini相应发生的改变。

d)重启就可以看到启动菜单(显示文本)。选择从“显示文本”项启动,就可以看到vfloppy中程序的运行效果。

 

 三、调试过程中遇到的问题及解决方法

1、代码的位置无关性

(1)概念

 位置有关性代码:

  它的链接地址在编译的时候已经确定,也就是说将来运行时候要将这部分代码搬移到指定的位置(链接位置)才能正确运行。例如:

;段代码功能:sum单元加1
asscume cs:code code segment sum dw 0 start: mov ax,2000h mov ds,ax inc word ptr ds:[0] mov ax,4c00h int 21h code ends end start

    如果要实现sum单元加1,就必须使代码加载到2000h:0的位置才能正确执行。

 位置无关性代码:

  这些代码放在任何位置都能正确运行,与链接地址无关。例如相对跳转指令,jmp short 标号。还是上边的程序,稍作修改就变成位置无关代码了。

;段代码功能:sum单元加1
assume cs:code

code segment
    sum dw 0

start:
    mov ax,cs           ;把2000h换成cs
    mov ds,ax
    inc word ptr ds:[0]

    mov ax,4c00h
    int 21h

code ends
end start

  实际上,这个程序还不是完美的位置无关代码,在更改CS的情况下,能正确执行。但是在变更IP的情况下,还会出错。

 (2)我的结论

  X86架构的指令在设计的时候,即是位置有关的,也是位置无关的。CS是位置无关的,IP是位置有关的。

解释CS的位置无关性

  假设程序段中没有使用跨段跳转指令和含绝对段地址的指令,再假设程序链接地址是2000h:0h,实际运行地址是7c0h:0h,程序是能正常运行的。因为内部的指令的链接地址都是相对于段首(段首IP为0)而来,当运行的时候,我们的指令只更改IP,不更改CS,自然能够正确运行。

解释IP的位置有关性

  一个代码段,在链接时候,连接地址都是相对于段首(IP为0)。假设程序链接地址是2000h:0h,而程序被加载到了0:7c00h处去执行。也就是说,链接的时候IP的首址为0,执行的时候是7c00h。程序就不能正确执行。举例说明。测试程序还是上边的代码为:

;段代码功能:sum单元加1
assume cs:code

code segment
    sum dw 0

start:
    mov ax,cs           ;把2000h换成cs
    mov ds,ax
    inc word ptr ds:[0]

    mov ax,4c00h
    int 21h

code ends
end start

     sum单元的地址是程序加载的物理地址偏移量为0。程序被加载到0h:7c00h处去执行,所以sum单元的物理地址是7c00h。但是,我们看程序中sum的访问地址ds:[0],程序执行后是0h:0h。这显然不是我们想要的sum位置。

解释真正的位置无关代码

    真正意义上的位置无关代码,不受IP的限制(CS的位置无关性),放在何处运行都正确。例如下边的程序:

 ;段代码功能:sum单元加1
assume cs:codesg
codesg segment
    sum dw 0
codesgstart:
    call getip ;获得下一条指令的运行地址
pos:           
    mov bx,ax
    mov ax,offset pos 
    sub bx,ax    ;获得sum的真实物理地址
        
    mov ax,cs          
    mov ds,ax
    inc word ptr ds:[bx]

    mov ax,4c00h
    int 21h
    
;========================================================
;函数功能:获得当前的IP,也即下一条指令的地址
;函数返回:ax
;========================================================
getip:
    push bx
    mov ax,ss
    mov ds,ax
    mov bx,sp
    mov ax,ds:[bx+2]
    pop bx
    ret
    

如果想测试这段代码,请看详细版本的代码:

;========================================================
;段代码功能:加载codesg段到2000h:7f00h位置处,并且将cs:ip
;设定到2000h:7f02处去执行,以测试codesg段是否是真正意义的
;位置无关代码
;======================================================== 
assume cs:setupsg,ss:stack
stack segment
    db 128 dup(0)
stack ends

setupsg segment
start:
    mov ax,stack
    mov ss,ax
    mov sp,128
    
    mov ax,codesg
    mov ds,ax
    mov si,0 ;设置ds:si指向源地址
    
    mov ax,2000h
    mov es,ax
    mov di,7f00h ;设置es:di指向目标地址
    mov cx,offset codesgend-offset codesgstart+2
    cld
    rep movsb
    
    mov ax,2000h
    push ax
    mov ax,7f02h ;codesg段的第一条地址是偏移量为2的地方
    push ax
    retf
    
setupsg ends 
 
;========================================================
;段代码功能:sum单元加1
;======================================================== 
assume cs:codesg
codesg segment
    sum dw 0
codesgstart:
    call getip ;获得下一条指令的运行地址
pos:           
    mov bx,ax
    mov ax,offset pos 
    sub bx,ax    ;获得sum的真实物理地址
        
    mov ax,cs          
    mov ds,ax
    inc word ptr ds:[bx]

    mov ax,4c00h
    int 21h
    
;========================================================
;函数功能:获得当前的IP,也即下一条指令的地址
;函数返回:ax
;========================================================
getip:
    push bx
    mov ax,ss
    mov ds,ax
    mov bx,sp
    mov ax,ds:[bx+2]
    pop bx
    ret
    
codesgend:
        nop
codesg ends
end start
View Code

(3)本课程设计程序中遇到的位置有关代码问题

    程序一开始没有mainshell段,这部分代码紧随initsg段。编译好后,被写入到了虚拟镜像。等于说是,虚拟镜像上的程序就只有一个代码段。开机时,虚拟镜像的程序被加载到了0h:7c00h出去执行。但是initsg段在编译后的段首址的IP值为0。程序不能正常运行。

    解决办法就是设置两个段,一个是mainshell段,是真正的代码段,这个段含有位置有关代码,必须放在IP为0的位置处运行。一个是initsg段,负责将mainshelll段搬运到2000h:0h处,并且设置cs:ip到该处去运行,这个段是位置无关代码。

(4)如何设计位置无关代码的程序

    位置无关代码的程序必须不能使用绝对地址,而要使用相对地址。相对跳转就是为设计位置无关代码来服务的。

    我认为,x86汇编指令用cs:ip这样的指令指针(pc)方式,引入代码段的概念,也就是为设计位置无关代码考虑的。

2、中断程序不能随意返回

    中断返回的地址不能随意更改,必须是从哪里断的返回到哪里。

    因为,假设从调用了n次“call 标号”后的子程序中断的,而更改了返回地址跳到别处去。这个时候,即使中断程序写的很好,保证了进入和出去中断时sp的不变(堆栈的不变)。但是,由于返回的不是原断点处,就有很大的可能堆栈不能完全恢复(因为还需要n次ret指令来恢复中断),导致错误。

3、堆栈设置问题

    这个课程设计程序的堆栈是不需要我们来设置的,因为BIOS在启动的时候已经设置过了。倘若,我们重新指定堆栈的位置,就有可能导致错乱。

    堆栈的使用必须小心,保证进出函数时堆栈的不变。因为堆栈里边存放了寄存器的值,变量的值,还有返回地址,所以必须小心。

参考资料:王爽汇编语言课程设计2

原文地址:https://www.cnblogs.com/amanlikethis/p/3566133.html