Switernal's Blogs

看见每一种可能。

博客园 首页 新随笔 联系 订阅 管理

汇编实验4 8086标志寄存器及中断

实验任务1

源代码

功能:对128位的两个数字进行求和运算

点击查看代码
assume cs:code, ds:data

data segment
   x dw 1020h, 2240h, 9522h, 5060h, 3359h, 6652h, 2530h, 7031h
   y dw 3210h, 5510h, 6066h, 5121h, 8801h, 6210h, 7119h, 3912h
data ends
code segment 
start:
    mov ax, data
    mov ds, ax
    mov si, offset x    ; x -> si
    mov di, offset y    ; y -> di
    call add128

    mov ah, 4ch
    int 21h

add128:
    push ax         ; 依次保存寄存器的值
    push cx
    push si
    push di
    
    sub ax, ax  ; 主要是把CF(进位)标志位置NC(0),否则第一次adc会出问题
    
    ; 由于8086是小端模式,根据逻辑来说,高高低低
    ; 左边是低地址,应该是低位
    ; 从左加到右, 正好是从低位加到高位
    mov cx, 8       ; 8组数字
s:  mov ax, [si]    ; x中的数
    adc ax, [di]    ; 带进位的加法,加y中的数
    mov [si], ax    ; 结果送回x

    inc si  ; 如果换成add, 会影响CF的值,
    inc si  ; 本来应该有进位的, 如果用了add会导致CF = 0,
    inc di  ; 进位就失效了,
    inc di  ; 所以这里si和di只能用inc而不能用add.
    loop s
    
    pop di
    pop si
    pop cx
    pop ax
    ret
code ends
end start

实验问题解答

line34~line37的4条inc指令,能否替换成如下代码?

add si,2
add di,2

答案:

这段代码data段给的数据可以,但是如果换成其它数据就不一定可以

原因:

这题中给的128位数据,每个16位相加都恰好都没有产生进位,所以使用add即便修改了进位寄存器CF的值,也没有影响。在这题中是可以替换的。

但如果换成其他数据,若相加过程中产生了进位,则使用add会导致进位寄存器CF的值发生变化。

如果本来应当是有进位的,CF的值为CY(1),但是做了add操作后CF会变成NC(0),会对后面的位数加法产生影响。所以不能使用add

inc指令并不影标志寄存器CF的值。

事实上,根据 intel 白皮书中对inc的描述,可以很清楚的知道这一点(Intel® 64 and IA-32 Architectures Software Developer’s Manual,Vol. 2A3-493):

(翻译:给目标操作数加1,同时保留CF标志的状态。目标操作数可以是一个寄存器或一个内存位置。这条指令允许在不影响CF标志的情况下更新一个循环计数器。(使用即时操作数为1的ADD指令来执行更新CF标志的增量操作)。)

② 在debug中调试,观察数据段中做128位加之前,和,加之后,数据段的值的变化。

可以观察到数据被正确求和了。

观察

① add指令对标志寄存器中的零标志位ZF(Zero Flag)、进位标志位CF(Carry Flag)是否有影响?

答案:有影响

根据 Intel 白皮书(Vol. 2A 3-31)对ADD指令的描述:

(翻译:ADD指令执行整数加法。它对有符号和无符号整数操作的结果进行评估,并设置OF和CF标志,分别表示有符号或无符号结果中的进位(溢出)。SF标志表示有符号结果的符号。)

② inc指令对标志寄存器中的零标志位ZF(Zero Flag)、进位标志位CF(Carry Flag)是否有影响?

答案:对ZF有影响,而对CF无影响

根据 intel 白皮书中对inc的描述,可以很清楚的知道这一点(Intel® 64 and IA-32 Architectures Software Developer’s Manual,Vol. 2A3-493):

实验任务2

源代码

点击查看代码
assume cs:code, ds:data
data segment
        str db 80 dup(?)
data ends

code segment
start:  
        mov ax, data
        mov ds, ax
        mov si, 0
s1:        
        mov ah, 1       ; 调用int 21h的1号子程序
        int 21h         ; 从键盘接收输入
        mov [si], al    ; 把输入的字符放入 ds:[si]
        cmp al, '#'     ; 判断接收到的字符是否是# 
        je next         ; 如果是'#',执行next处操作
        inc si          ; 否则si + 1
        jmp s1          ; 返回s1继续读入下一个字符
next:
        mov ah, 2       ; 调用
        mov dl, 0ah     ; 0ah是换行符回车的ASCII码
        int 21h         ; 打印一个换行符
        
        mov cx, si      ; 由于si从0开始,si = 读入的字符个数-1,正好不会把'#'打印出来
        mov si, 0       ; si回到字符串开始的位置
s2:     mov ah, 2       ; 调用2号子程序
        mov dl, [si]    ; 打印字符
        int 21h         
        inc si                  
        loop s2         ; 打印字符串循环

        mov ah, 4ch     
        int 21h
code ends
end start

实验结果

实验分析

该程序的作用是:从键盘接收输入一串以#为结尾的字符串,然后将其打印在屏幕上

① 汇编指令代码line11-18,实现的功能是?

s1:        
        mov ah, 1       ; 调用int 21h的1号子程序
        int 21h         ; 从键盘接收输入
        mov [si], al    ; 把输入的字符放入 ds:[si]
        cmp al, '#'     ; 判断接收到的字符是否是# 
        je next         ; 如果是'#',执行next处操作
        inc si          ; 否则si + 1
        jmp s1          ; 返回s1继续读入下一个字符

总结:从键盘上读取输入的字符,并保存到ds:[si],每读入一个就判断是否为#,如果是则不保存,转跳至标号next处执行;如果不是则si + 1并继续读入下一个字符。

② 汇编指令代码line20-22,实现的功能是?

  mov ah, 2       ; 调用
  mov dl, 0ah     ; 0ah是换行符回车的ASCII码
  int 21h         ; 打印一个换行符

总结:打印一个回车(换行符)

③ 汇编指令代码line24-30,实现的功能是?

    mov cx, si      ; 由于si从0开始,si = 读入的字符个数-1,正好不会把'#'打印出来
    mov si, 0       ; si回到字符串开始的位置
s2: mov ah, 2       ; 调用int 21h的2号子程序
    mov dl, [si]    ; 打印字符
    int 21h         
    inc si                  
    loop s2         ; 打印字符串循环

总结:打印字符串,并且不会把#打出来

说明(实验指导里给的)

  1. DOS系统功能调用int 21h的1号子功能:

功能:从键盘上输入单个字符

入口参数:(ah) = 1

出口参数: (al)存放输入字符的ASCII码

即:

mov ah,1
int 21h ; (al) <-- 输入字符的ascII码
  1. DOS系统功能调用int 21h的2号子功能

功能:输出单个字符到屏幕上

入口参数:(ah) = 2, (dl) = 待输出的字符或其ascII码

出口参数:无

即:

mov ah, 2
mov dl, ×× ; ××是待输出的字符,或,其ascII码 
int 21h

实验任务3

题目描述

注:该任务是对实验3的任务3进行改进。

针对8086CPU,已知逻辑段定义如下:

data segment
		x dw 91, 792, 8536, 65521, 2021 
		len equ $ - x
data ends

编写8086汇编源程序task3.asm,在屏幕上以十进制形式输出data段中这一组连续的数据,数据和数据之间以空格间隔。

要求:

  • 编写子程序printNumber

    • 功能:以十进制形式输出一个任意位数的整数(整数范围0 ~ 65535)
    • 入口参数:寄存器ax(待输出的数据 --> ax)
    • 出口参数:无
  • 编写子程序printSpace

    • 功能:打印一个空格
    • 入口参数:无
    • 出口参数:无
  • 在主体代码中,综合应用寻址方式和循环,调用printNumber和printSpace, 实现题目要求。

源代码

点击查看代码
; 可以打印0~65535不定位数的数字
assume ds:data, cs:code, ss:stack

data segment
    x dw 91, 792, 8536, 65535, 2021, 0
    len equ $ - x
data ends

stack segment
    dw 8 dup(?)
stack ends

code segment
start:
    mov ax, data
    mov ds, ax

    mov ax, stack
    mov ss, ax
    mov sp, 16

    mov cx, len/2 ; 由于数据都是word型,所以len/2才是数据个数
    ; print循环: 依次打印所有数字
    print:
        mov ax, word ptr ds:[di]    ; 把数据放入al
        add di, 2                   ; di指针后移2字节

        push cx             ; 把cx保存起来, 子程序中会修改cx值

        call printNumber    ; 打印数字
        call printSpace     ; 打印空格

        pop cx              ; 恢复cx
    loop print
    
    mov ah, 4ch
    int 21h

; 子程序: printNumber
; 功能: 打印数字
;   入口参数: 
;       寄存器ax  (待输出的数据 --> ax)
;   局部变量说明: 
;       bx -> 存储数字字符个数
printNumber:
    mov bx, 0       ; 获取之前位数为0
    ; 逐位获取数字
    ; getEach循环: 获取每一位,然后压入栈中
    getEach:
        ; 除数放在16位寄存器bp中
        mov bp, 10      ; 除10运算
        mov dx, 0       ; 由于除数是16位寄存器,dx也是被除数一部分,需要置零      
        div bp          ; 数据除10

        push dx         ; 将数字压入栈中(余数在dx里)
        inc bx          ; 位数+1
        
        mov cx, ax      ; 除法商赋给cx, 如果商为0则说明所有位数都获取完了
        inc cx          ; 由于loop时会-1,这里先+1,防止出现负数

    loop getEach

    ; 打印数字
    mov cx, bx          ; 先把bx存的数字位数赋给cx
    ; printEach循环: 依次从栈中取出数字,逐位打印
    printEach:
        pop dx          ; 取出一位数
        add dl, 30h     ; dl是刚才除法的余数,也就是需要得到的位数,+30h是转成对应字符
        mov ah, 2       ; 调用int 21h的2号子程序打印
        int 21h
    loop printEach 

    ret

; 子程序: printSpace
; 功能: 打印空格
printSpace:
    mov ah, 2
    mov dl, 20h
    int 21h
    ret

code ends
end start

实验结果

该程序可以成功打印0~65535之间的任意数字。

实验说明

说明全部写在注释中。

实验任务4

题目描述

针对8086CPU,已知逻辑段定义如下:

data segment
		str db "assembly language, it's not difficult but tedious" 
		len equ $ - str
data ends

编写8086汇编源程序task4.asm,将data段中字符串里的小写字符转换成大写。

要求:

  • 编写子程序strupr
    • 功能:将包含任意字符的字符串中的小写字母变成大写
    • 入口参数
      • (ds:si) 字符串首地址的段地址和偏移地址分别送至ds和si
      • (cx) 字符串的长度
    • 出口参数:无
  • 在主体代码中,设置入口参数,调用strupr, 实现题目要求。

源代码

点击查看代码
assume cs:code,ds:data
data segment
    str db "assembly language, it's not difficult but tedious"
    len equ $ - str
data ends

stack segment
    db 8 dup(?)
stack ends

code segment
start: 
    mov ax, data
    mov ds, ax
    mov si, 0
    mov cx, len     ; 参数:字符串长度存在cx中
    call strupr     ; 调用转换子程序
    call printStr   ; 调用打印子程序

    mov ax, 4c00h
    int 21h

; 子程序 strupr
;   功能: 将包含任意字符的字符串中的小写字母变成大写
;   入口参数
;       (ds:si) 字符串首地址的段地址和偏移地址分别送至ds和si
;       (cx) 字符串的长度 
strupr:
    push cx
    push si         ; 先保存两个寄存器的值
    transform:
        mov al, ds:[si] ; 取出一个字符
        cmp al, 97      ; 判断ASCII码是否 >= 97
        jl continue     ; 小于97直接进入下次循环
        cmp al, 122     ; 判断ASCII码是否 <= 122
        jg continue     ; 大于122直接进入下次循环
        and al, 0dfh    ; 小写转成大写,原理详见实验2
        mov ds:[si], al ; 把转换后的字符送回data段
    continue:
        inc si          ; 指针后移
        loop transform  ; 循环

    pop si
    pop cx          ; 恢复寄存器的值
    ret             ; 返回

; 子程序 printStr
;   功能: 打印字符串
;   入口参数
;       (ds:si) 字符串首地址的段地址和偏移地址分别送至ds和si
;       (cx) 字符串的长度
printStr:
    push cx         ; 保存寄存器的值
    push si

    print:          ; 打印字符循环
        mov ah, 2
        mov dl, ds:[si]
        int 21h
        inc si
    loop print

    pop si          ; 恢复寄存器的值
    pop cx
    ret             ; 返回

code ends
end start

实验结果

可以看到,该程序成功将小写字母转成了大写。

实验任务5

源代码

点击查看代码
assume cs:code, ds:data

data segment
    str1 db "yes", '$'
    str2 db "no", '$'
data ends

code segment
start:
    mov ax, data
    mov ds, ax

    mov ah, 1   ; 调用int 21h的1号子程序进行单字符输入
    int 21h

    mov ah, 2       ; 调用BIOS中断例程int 10h的2号子功能
    mov bh, 0       ; 设置显示页为第0页
    mov dh, 24      ; 设置光标位置在第24行
    mov dl, 70      ; 设置光标位置在第70列
    int 10h         ; 设置光标位置

    cmp al, '7'     ; 把输入的字符和字符'7'进行比较
    je s1           ; 如果输入的字符是'7',就跳转s1处
    mov ah, 9       ; 调用int 21h的9号子程序显示str2
    mov dx, offset str2 ; 把标号str2的偏移量放入dx
    int 21h             ; 显示标号str2处的字符串(no)

    jmp over            ; 无条件跳转到over处

s1: mov ah, 9           ; 调用int 21h的9号子程序显示str1
    mov dx, offset str1 
    int 21h             ; 显示标号str1处的字符串(yes)
over:  
    mov ah, 4ch         ; 程序结束
    int 21h
code ends
end start

运行结果

输入7后,屏幕上倒数第2行右下角显示yes

输入其它字符,屏幕上倒数第2行右下角显示no

总结和理解

该程序的解释已经全部写在源代码的注释中。

总结一下,这个程序实现的功能是:

从键盘输入一个字符,判断输入的字符是否为7

如果为7,则从屏幕的第24行第70列开始显示yes

如果不为7,则从屏幕的第24行第70列开始显示no

实验任务6

运行42号中断程序

通过编译连接运行task6_1.asmtask6_2.asm后可以看到,屏幕底部出现了绿色的"welcome to 2049!",说明42号中断程序被成功调用。

自定义中断:36号中断程序

源代码

点击查看代码
assume cs:code
code segment
start:
    ; 36号中断
    mov ax, cs
    mov ds, ax
    mov si, offset int36   ; set ds:si

    mov ax, 0
    mov es, ax
    mov di, 200h        ; set es:di

    ; 复制中断程序到 es:[di]
    mov cx, offset int36_end - offset int36
    cld
    rep movsb

    ; 设置中断向量表
    mov ax, 0
    mov es, ax
    mov word ptr es:[36*4], 200h
    mov word ptr es:[36*4+2], 0

    ; 调用36号中断程序
    int 36

    ; 程序结束
    mov ah, 4ch
    int 21h

; 36号中断程序
int36: 
    jmp short int36_start
    stu_num db "201983290048 Li Qingyun"
    len1 equ $ - stu_num
    task db "Assembly Experiment 4 Interrupt No.36"
    len2 equ $ - task

    ; 中断程序指令部分
int36_start:
    mov ax, cs
    mov ds, ax
    mov di, 202h

    call printString
    iret

; 打印子程序:
;   参数说明:
;       学号字符串存储在 -> ds:[di]
printString:
    mov ax, 0b800h
    mov es, ax      ; 控制显存区域段指针
    mov si, 0       ; 显存区域列指针从0开始

; 先把屏幕前11行置位蓝色(第一行会被吃掉)
    mov al, 11      ; 全25行
    mov dl, 80     ; 每行160个字节文字和颜色都需要修改
    mul dl          ; 25*160, 获得需要修改的字节数

    mov cx, ax      
    printBlue1:
        mov byte ptr es:[si], 20h    ; 用一个空格填充文字位置
        inc si              ; 后移指针
        mov byte ptr es:[si], 17h    ; 填充颜色: 蓝底+白字:0 001 0 111 -> 17h
        inc si              ; 后移指针
    loop printBlue1

; 把屏幕中间5行置为白底蓝字
    mov cx, 5*80      
    printWhite:
        mov byte ptr es:[si], 20h    ; 用一个空格填充文字位置
        inc si              ; 后移指针
        mov byte ptr es:[si], 71h    ; 填充颜色: 白底+蓝字:0 111 0 001 -> 71h
        inc si              ; 后移指针
    loop printWhite

; 把屏幕最后10行置为蓝色
    mov cx, 10*80      
    printBlue2:
        mov byte ptr es:[si], 20h    ; 用一个空格填充文字位置
        inc si              				 ; 后移指针
        mov byte ptr es:[si], 17h    ; 填充颜色: 蓝底+白字:0 001 0 111 -> 17h
        inc si              ; 后移指针
    loop printBlue2


; 打印学号  
    mov si, 12*160        ; 从第12行开始打印
    mov dx, (80-len1)/2   ; 计算左右两边空格数    
    
    ; 调用打印空格的子程序, 打印学号左侧的空格
    mov cx, dx
    call printSpace 

    ; 打印学号字符串
    mov cx, len1
    printNumber:
        mov al, ds:[di]		        ; 低位是字符
        mov ah, 71h				    		; 高位是颜色
        mov word ptr es:[si], ax	; 按字放入
        inc di
        add si, 2
    loop printNumber

    ; 再次调用打印空格的子程序, 打印学号右侧的空格
    mov cx, dx
    call printSpace

; 打印下面一行文字
    mov si, 14*160          ; 从第14行开始打印
    mov dx, (80-len2)/2     ; 计算左右两边空格数 
    
    ; 调用打印空格的子程序, 打印文字左侧的空格
    mov cx, dx
    call printSpace 

    ; 打印文字字符串
    mov cx, len2
    printTask:
        mov al, ds:[di]		        ; 低位是字符
        mov ah, 71h				    		; 高位是颜色
        mov word ptr es:[si], ax	; 按字放入
        inc di
        add si, 2
    loop printTask

    ; 再次调用打印空格的子程序, 打印文字右侧的空格
    mov cx, dx
    call printSpace

    ret

; 子程序: 打印分隔符空格
;   参数: 长度 -> cx
;        位置 -> es:[si]
printSpace:
    mov al, 20h        ; 一个空格    
    mov ah, 71h
    mov word ptr es:[si], ax
    add si, 2
    loop printSpace
    ret


int36_end:
    nop


code ends
end start

运行结果

36号中断的运行效果是:

将屏幕背景变为蓝色,中间5行背景变为白色;

并居中打印"201983290048 Li Qingyun"和"Assembly Experiment 4 Interrupt No.36",表明这是36号中断程序。

所有说明都写在代码注释中。

总结与思考

  1. 标志寄存器会被多数指令修改,如addsubjmp等,而一些指令执行时也会使用标志寄存器的值来确定执行的方式,这可能会对操作结果造成影响。比如实验一中,inc不会对CF造成影响,但add却会,如果使用add再使用adc会对结果造成影响。
  2. movsbmovsw指令会使用方向寄存器的值,确定指针移动方向。
  3. cmp指令会对两个操作数做减法,然后将结果存在标志寄存器中,有条件跳转指令则会根据标志寄存器的值进行判断并跳转。
  4. 使用pushfpopf可以间接实现对标志寄存器的修改,但通常不需要这样操作。
  5. 内中断程序可以使CPU在执行到特殊情况时调用,中断处理程序可以让CPU从错误中恢复,比如除法中除0的错误。
  6. 中断向量表存储在0000:0000开始空间中,可以存一个字的中断程序地址。
posted on 2021-12-07 12:08  Switernal  阅读(391)  评论(4编辑  收藏  举报