目录

  • @[TOC](目录)
    • 通用寄存器
  • CS和IP
  • 修改CS、IP的指令
    • 小 结
  • 内存中的字的存储
    • 小结
  • SI和DI
  • [bx+si]和[bx+di]
  • [bx+si+idata]和[bx+di+idata]
  • 不同的寻址方式的灵活应用
  • 我们定义的描述性符号: reg和: sreg。
  • 汇编语言中数据位置的表达
  • 寻址方式
  • 指令要处理的数据有多长
  • div指令
  • 伪指令dd
  • dup
  • 转移指令
      • 8086cpu的转移行为有以下几类:
      • 8086cpu的转移指令有以下几类:
    • 操作符offset
      • 计算代码块所占字节数
    • jmp指令
    • 依据位移进行转移的jmp指令
    • 转移的目的地址在指令中的jmp指令
    • 转移地址在内存中的jmp指令
  • jcxz指令
  • loop指令
  • 如何在屏幕上显示信息?
  • CAll和RET指令
    • ret和retf
    • call指令
    • 依据位移进行转移的call指令
      • 检测点
    • 转移的目的地址在指令中的call指令
    • 转移地址在寄存器中的call指令
    • 转移地址在内存中的call指令
    • call和ret的配合使用
    • mul指令
  • 如何避免和子程序的寄存器冲突?
  • 标志寄存器(flag)
    • ZF标志(Zero Flag)
    • 奇偶标志PF(Parity Flag)
    • 符号标志SF(Sign Flag)
    • 进位标志CF(Carry Flag)
    • 溢出标志OF(Overflow Flag)
    • 追踪标志TF(Trap Flag)
    • 中断允许标志IF(Interrupt-enable Flag)
    • 辅助进位标志AF(Auxiliary Carry Flag)
    • 方向标志DF(Direction Flag)
      • 串传送指令
    • rep指令
  • 标志寄存器在Debug中的表示
  • adc指令
    • 为什么要加上CF的值呢?CPU为什么要提供这样一条指令呢?
    • 下面编写一个子程序,对两个128位数据进行相加。
  • sbb指令
  • cmp指令
  • 检测比较结果的条件转移指令
    • 补充条件转移指令
  • pushf 和 popf
  • 内中断
    • 内中断的产生
    • 中断向量表
    • 中断过程
    • 中断处理程序和iret指令
    • 设置中断向量
    • 单步中断
    • 响应中断的特殊情况
    • 编写内中断程序
  • int指令
    • BIOS和DOS所提供的中断例程
    • BIOS和DOS中断例程的安装过程
    • BIOS中断例程应用
      • 编程:在屏幕的5行12列显示3个红底高亮闪烁绿色的'a'。
    • DOS中断例程应用
      • 编程:在屏幕的5行12列显示字符串“Welcome to masm!"。
      • 总结
  • 端口
    • 端口的读写
      • 1.cpu执行内存访问指令时总线上的信息
      • 2.cpu执行端口访问指令时总线上的信息
    • CMOS RAM 芯片
    • shl 和 shr 指令
    • CMOS RAM 中存储的时间信息
      • 编程,在屏幕中间显示当前的月份
  • 外中断
    • 接口芯片和端口
    • 外中断信息
    • PC 机键盘的处理过程
    • 编写int 9 中断例程
  • 指令系统总结
  • 直接定址表
    • 描述了单元长度的标号
    • 在其他段中使用数据标号
    • 直接定址表
    • 程序入口地址的直接定址表
  • 使用BIOS进行键盘输入和磁盘读写
    • int9中断例程对键盘输入的处理
    • 使用int 16h中断例程读取键盘缓冲区
      • 在int 16h中断例程中,一定有设置IF=1的指令。”这种说法对吗?
    • 字符串的输入
    • 应用int 13h中断例程对磁盘进行读写
      • 编程: 将当前屏幕的内容保存在磁盘上。
      • 安装一个新的int7ch中断例程,实现通过逻辑扇区号对软盘进行读写。参数说明:

一个典型的CPU由运算器、控制器、寄存器等器件构成,这些器件靠内部总线相连。

内部总线实现CPU内部各个器件之间的联系,外部总线实现CPU和主板上其他器件的联系。

简单地说,在CPU中:

  • 运算器进行信息处理;
  • 寄存器进行信息存储:
  • 控制器控制各种器件进行工作;
  • 内部总线连接各种器件,在它们之间进行数据的传送。

8086CPU有14个寄存器。这些寄存器是: AX、BX、CX、DX、SI、 DI、SP、BP、IP、CS、SS、DS、ES、PSW。

通用寄存器

8086CPU的所有寄存器都是16位的,可以存放两个字节。AX、BX、CX、DX这4个寄存器通常用来存放一般性的数据,被称 为通用寄存器。

在进行数据传送或运算时,要注意指令的两个操作对象的位数应当是一致的。

例如

mov ax,bx -----正确
mov ax,bl -----错误

CS和IP

CS和IP是8086CPU中两个最关键的寄存器,它们指示了CPU当前要读取指令的地址。CS为代码段寄存器,IP 为指令指针寄存器,从名称上我们可以看出它们和指令的关系。
在8086PC机中,任意时刻,设CS中的内容为M,IP 中的内容为N, 8086CPU将从内存Mx16+N单元开始,读取一条指令 并执行。
也可以这样表述: 8086 机中,任意时刻,CPU将CS:IP指向的内容当作指令执行。

简述8086CPU的工作过程:

  1. 从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器;
  2. IP=IP+所读取指令的长度,从而指向下一条指令;
  3. 执行指令。转到步骤 1 ,重复这个过程。

在8086CPU加电启动或复位后(即CPU刚开始工作时)CS和IP被设置为CS一FFFFH, IP=0000H, 即在8086PC机刚启动时,CPU从内存FFFF0H单元中读取指令执行,FFFF0H 单元中的指令是8086PC机开机后执行的第一条指令。

修改CS、IP的指令

8086CPU中大部分寄存器的值都可以使用mov指令来改变,mov指令被称为传送指令

但是,mov指令不能设置CS、IP的值,8086CPU为CS、IP提供了 jmp指令来修改他们的值。能够改变 CS、IP的内容的指令被统称为转移指令

若想同时修改CS、IP的内容,可用形如“jmp 段地址:偏移地址”的指令完成,

# jmp 2AE3:3,执行后:CS = 2AE3H, IP = 0003H,CPU将从2AE33H处读取指令。
# jmp 3:0B16,执行后:CS = 0003H, IP = 0B16H,CPU将从00B46H处读取指令。

仅想修改IP的内容,可用形如“jmp 某一合法寄存器” 的指令完成,如

# jmp ax, 指令执行前:ax = 1000H , CS = 2000H , IP = 0003H
#               执行指令后:ax = 1000H , CS = 2000H , IP = 1000H
# jmp bx, 指令执行前:ax = 0B16H , CS = 2000H , IP = 0003H
#               执行指令后:ax = 0B16H , CS = 2000H , IP = 0B16H

小 结

  1. 段地址在8086CPU的段寄存器中存放。当8086CPU要访问内存时,由段寄存器提供内存单元的段地址。8086CPU有4个段寄存器,其中CS用来存放指令的段地址。
  2. CS存放指令的段地址,IP存放指令的偏移地址。
    8086机中,任意时刻,CPU将CS:IP指向的内容当作指令执行。
  3. 8086CPU的工作过程:
    ①从CS: IP指向的内存单元读取指令,读取的指令进入指令缓冲器;
    ②IP指向下一条指令;
    ③执行指令。(转到步骤①,重复这个过程。)
  4. 8086CPU提供转移指令修改CS、IP的内容。

内存中的字的存储

CPU中,用16 位寄存器来存储一个字。高8位存放高位字节,低8位存放低位字节。在内存中存储时,由于内存单元是:字节单元(一个单元存放一一个字节), 则一个字要用两个地址连续的内存单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。

字节型数据占8位也就是1个内存单元。

字形数据占16位也就是2个地址练血的内存单元来存放这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。

小结

(1)字在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
(2)用mov指令访问内存单元,可以在mov指令中只给出单元的偏移地址,此时,段地址默认在DS寄存器中。
(3) 【address】表示一个偏移地址为address 的内存单元。
(4)在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。

SI和DI

si和di是8086CPU中和bx功能相近的寄存器,si 和di不能够分成两个8位寄存器来使用。下面的3组指令实现了相同的功能。

mov bx,0
mov ax, [bx]mov si,0
mov ax, [si]mov di,0
mov ax, [di]

[bx+si]和[bx+di]

[bx+si]表示一个内存单元,它的偏移地址为(bx)+(si)(即bx中的数值加上si中的数值)。

数学化的描述为: (ax)= (ds) 16+(bx)+(si))*

该指令也可以写成如下格式(常用):

mov ax, [bx][si]

[bx+si+idata]和[bx+di+idata]

[bx+si+idata]表示一个内存 单元,它的偏移地址为(bx)+(si)+idata(即bx中的数值加上si中的数值再加上idata)。
指令mov ax,[bx+si+idata]的含义如下:

将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上si中的数值再加上idata,段地址在ds中。
*数学化的描述为: (ax)= (ds)16+(bx)+(si)+idata)

常用格式:

mov ax, [bx+200+si]
mov ax, [200+bx+si]
mov ax,200[bx] [si]
mov ax, [bx] .200[si]
mov ax, [bx] [si] .200

不同的寻址方式的灵活应用

如果我们比较一下前面用到的几种定位内存地址的方法(可称为寻址方式),就可以发现:
(1) [idata]用一个 常量来表示地址,可用于直接定位一一个内存单元;
(2) [bx]用一个变 量来表示内存地址,可用于间接定位一一个内 存单元; 8.5透回
(3)[bx+idata]用一个变量和常量表示地址,可在一-个起始地址的基础上用变量间接定位一一个内存单元;
(4) [bx+sij]用两 个变量表示地址;
(5) [bx+si+idata]用两 个变量和一个常量表示地址。
可以看到,从[idata]一 直到[bx+si+idata], 我们可以用更加灵活的方式来定位一一个内存单元的地址。这使我们可以从更加结构化的角度来看待所要处理的数据。下面我们通过一个问题的系列来体会CPU提供多种寻址方式的用意,并学习一些相关的编程技巧。

我们定义的描述性符号: reg和: sreg。

为了描述上的简洁,在以后的课程中,我们将使用两个描述性的符号reg 来表示一个寄存器,用sreg表示一个段寄存器。
reg的集合包括: ax、 bx、cx、dx、ah、al、bh、bl、ch、cl、dh、dl、sp、bp、si、di;
sreg的集合包括: ds、 ss、cs、es。

(1) 在8086CPU中, 只有这4个寄存器可以用在“[......]”中来进行内存单元的寻址。比如下面的指令都是正确的:

mov ax,[bx]
mov ax,[si]
mov ax,[di]
mov ax,[bp]

不正确的:

mov ax,[cx]
mov ax,[ax]
mov ax,[dx]
mov ax,[ds]

(2)在[.....]中, 这4个寄存器可以单个出现,或只能以4种组合出现: bx 和si、bx 和di、bp和si、bp和di。

比如下面的指令是正确的:

mov ax, [bx]
mov ax, [si]
mov ax, [di]
mov ax, [bp]
mov ax, [bx+si]
mov ax, [bx+di ]
mov ax, [bp+si]
mov ax, [bp+di]
mov ax, [bx+si+idata]
mov ax, [bx+di+idata]
mov ax, [bp+si+idata]
mov ax, [bp+di+idata]

下面的指令是错误的:

mov ax, [bx + bp]
mov ax, [si + di]

(3)只要在[.....]中使用寄存器bp,而指令中没有显性地给出段地址,段地址就默认在ss中

比如下面的指令:

mov ax, [bp]                ; 含义:(ax)=((ss)*16+(bp))
mov ax, [bp+idata]          ; 含义:(ax)=((ss)*16+(bp)+ idata)
mov ax, [bp+si]             ; 含义:(ax)=((ss)*16+(bp)+(si))
mov ax, [bp+si+idata]       ; 含义:(ax)=((ss)*16+(bp)+(si)+idata)
; 显性地给出段地址
mov ax, ds:[bp+si+idata]    ; 含义:(ax)=((ds)*16+(bp)+(si)+idata)

汇编语言中数据位置的表达

  1. 立即数 (idata)
    对于直接包含在机器指令中的数据(执行前在CPU的指令缓冲器中),在汇编语言中称
    为:立即数( idata),在汇编指令中直接给出。
  2. 寄存器
    指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名。
  3. 段地址(SA)和偏移地址(EA)
    指令要处理的数据在内存中,在汇编指令中可用[X]的格式给出EA,SA在某个段寄存器中

寻址方式

指令要处理的数据有多长

8086CPU的指令,可以处理两种尺寸的数据,byte和word。所以在机器指令中要指明,指令进行的是字操作还是字节操作。对于这个问题,汇编语言中用以下方法处理。

  1. 通过寄存器名

  2. 在没有寄存器名存在的情况下,用操作符Xpr指明内存单元的长度,X在汇编指令中可以为word或byte
    例如,下面的指令中,用 word ptr指明了指令访问的内存单元是一个字单元。

    mov word ptr ds: [0l,1
    inc word ptr [bx]
    inc word ptr ds: [01
    add word ptr [bx], 2
    

    下面的指令中,用 byte ptr指明了指令访问的内存单元是一个字节单元。

    mov byte ptr ds: [0],1
    inc byte ptr [bx]
    inc byte ptr ds: [01
    add byte ptr [bx], 2
    

    在没有寄存器参与的内存单元访问指令中,用 word ptr或 byte ptr显性地指明所要访问的内存单元的长度是很必要的。否则,CPU无法得知所要访问的单元是字单元,还是字节单元。

    mov word ptr [1000h],1     ; 指明是字单元
    mov byte ptr [1000h],1     ; 指明是字节单元
    
  3. push 指令只进行字操作

一般来说,我们可以用[bx+ data+si的方式来访问结构体中的数据。用bx定位整个结构体,用 idata定位结构体中的某一个数据项,用si定位数组项中的每个元素。为此,汇编语言提供了更为贴切的书写方式,如:
[bx] idata、[bx] idata[si]

div指令

dⅳ是除法指令,使用div做除法的时候应注意以下问题。

  1. 除数:有8位和16位两种,在一个reg或内存单元中。
  2. 被除数:默认放在AX或DX和AX中,如果除数为8位,被除数则为16位,默认在AX中存放;如果除数为16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位
  3. 结果:如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数;
    如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数

例子:

2568 / 300 = 商8余168

程序:

assume cs:codesg
codesg segmentmov ax,0a08h      ; 被除数 低16位存放在ax中(注意:a08h 程序中不能字母开头所以要在前面加0)mov dx,0          ; 被除数 高16位存放在dx中mov bx,300        ; 除数 因为除数300 大于8位寄存器最大值255 所以存放在16位寄存器ax中div bxmov ax,4c00hint 21h
codesg ends
end

结果:

伪指令dd

前面我们用db和dw定义字节型数据和字型数据。dd是用来定义dword(doubleword,双字)型数据的。

例如:

; 在data段中定义了3个数据
data segment db 1     ; 第一个数据为01H,在data:0处,占1个字节dw 1     ; 第二个数据为0001H,在data:1处,占1个字      dd 1     ; 第三个数据为0000001H,  在data:3处,占2个字。
data ends

dup

dup是一个操作符,在汇编语言中同db、dw、dd等一样,也是由编译器识别处理的符号。它是和db、dw、dd等数据定义伪指令配合使用的,用来进行数据的重复。比如:

db 3 dup (0)        ; 定义了3个字节,它们的值都是0,相当于db 0,0,0。
db 3 dup (0,1,2)    ; 定义了9个字节,它们是0、1、2、0、1、2、0、1、2,相当于db 0,1,2,0,1,2,0,1,2。

可见,dup的使用格式如下

  • db 重复的次数 dup (重复的字节型数据)
  • dw 重复的次数 dup (重复的字型数据)
  • dd 重复的次数 dup (重复的双字型数据)

转移指令

可以修改IP,或同时修改CS和IP的指令统称为转移指令。概括地讲,转移指令就是可以控制CPU执行内存中某处代码的指令。

8086cpu的转移行为有以下几类:

  1. 段内转移,只修改IP,比如:jmp ax,由于转移指令对IP的修改范围不同,

    段内转移又分为:

    • 短转移:IP的修改范围为-128~127
    • 近转移:IP的修改范围为-32768~32767
  2. 段间转移,同时修改CS和IP时,比如:jmp 1000:0

8086cpu的转移指令有以下几类:

  • 无条件转移指令(如: jmp)
  • 条件转移指令
  • 循环指令(如: loop)
  • 过程
  • 中断

操作符offset

操作符offset在汇编语言中是由编译器处理的符号,它的功能是取得标号的偏移地址。比如下面的程序:

assume cs : codesg
codesg segment
start:mov ax,offset start   ; 相当于mov ax, 0
s: mov ax, offset s      ; 相当于mov ax,3
codesg ends
end start

计算代码块所占字节数

mov ax,offset s1-offset s0 ; 计算s0代码块所占字节数
s0:………………
s1:nop

“-” 是编译器识别的运算符号,编译器可以用它来进行两个常数的减法。

汇编编译器可以处理表达式。
比如,指令:mov ax,(5+3)*5/10,被编译器处理为指令: mov ax,4

jmp指令

jmp为无条件转移指令,可以只修改IP,也可以同时修改CS和IP。

依据位移进行转移的jmp指令

jmp short标号(转到标号处执行指令)
这种格式的jmp指令实现的是段内短转移,它对IP的修改范围为-128~127,也就是说,它向前转移时可以最多越过128个字节,向后转移可以最多越过127 个字节。jmp指令中的“short” 符号,说明指令进行的是短转移。jmp 指令中的“标号”是代码段中的标号,指明了指令要转移的目的地,转移指令结束后,CS:IP 应该指向标号处的指令。

例如:

assume cs: codesg
codesg segment
start: mov ax, 0jmp short sadd ax, 1
s: inc ax
codesg ends
end start

在“jmpshort标号”指令所对应的机器码中,并不包含转移的目的地址,而包含的是转移的位移。这个位移,是编译器根据汇编指令中的“标号”计算出来的。

实际上,

“jmp short标号”的功能为: (IP)=(IP)+8 位位移。
(1)8位位移=标号处的地址-jmp指令后的第一个字节的地址;
(2) short 指明此处的位移为8位位移;
(3) 8位位移的范围为-128~127, 用补码表示(如果你对补码还不了解,请阅读附注2);
(4) 8 位位移由编译程序在编译时算出。
还有一种和“jmp short标号”功能相近的指令格式,jmp near p otr 标号,它实现的是段内近转移。
“jmp near ptr标号”的功能为: (IP)=(IP)+16 位位移。
(1)16位位移=标号处的地址-jmp指令后的第一个字节的地址;
(2) near ptr指明此处的位移为16位位移,进行的是段内近转移;
(3) 16 位位移的范围为-32768 ~32767,用补码表示;
(4) 16 位位移由编译程序在编译时算出。

转移的目的地址在指令中的jmp指令

“jmp far ptr标号”实现的是段间转移,又称为远转移。功能如下:

(CS)=标号所在段的段地址; (IP)=标 号在段中的偏移地址。

farptr指明了指令用标号的段地址和偏移地址修改CS和IP。

转移地址在内存中的jmp指令

  1. jmp word ptr 内存单元地址(段内转移)

    • 功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址。
  2. jmp dword ptr 内存单元地址(段间转移)

    • 功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址低地址处是转移的目的偏移地址

    • (CS)=(内存单元地址+2)

      (IP)=(内存单元地址)

jcxz指令

jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为:-128~127

  • 指令格式: jcxz 标号(如果(cx)=0,转移到标号处执行。)
  • 操作:当**(cx)=0** 时,(IP)=(IP)+8 位位移;
    8位位移=标号处的地址一jcxz指令后的第一个字节的地址;
    8位位移的范围为-128~127,用补码表示;
    8位位移由编译程序在编译时算出。
    当(cx)≠0时,什么也不做(程序向下执行)。
    我们从jcxz的功能中可以看出,“jcxz 标号” 的功能相当于:
    if((cx)==0)jmp short 标号;
    (这种用C语言和汇编语言进行的综合描述,或许能使你对有条件转移指令理解得更加清楚。)

loop指令

loop指令为循环指令,所有的循环指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为: -128~127。

  • 指令格式: loop 标号((cx)=(cx)-1,如果(cx)≠0,转移到标号处执行。)

  • 操作:

    1. (cx)=(cx)-l;

    2. 如果(cx)≠0, (IP)=(IP)+8 位位移。

    8位位移=标号处的地址-loop指令后的第一一个字节的地址;8位位移的范围为-128~127,用补码表示;
    8位位移由编译程序在编译时算出。
    如果(cx)=0,什么也不做(程序向下执行)。

    我们从loop的功能中可以看出,“loop 标号”的功能相当于:

    (cx)--; if((cx)≠0)jmp short 标号;

如何在屏幕上显示信息?

内存地址空间中,B8000H~BFFFFH 共32KB的空间,为80X25彩色字符模式的显示缓冲区。向这个地址空间写入数据,写入的内容将立即出现在显示器上。

在80x25彩色字符模式下,显示器可以显示25行,每行80个字符,每个字符可以有256种属性(背景色、前景色、闪烁、高亮等组合信息)。

共25行,每行80个字符,占160个字节

共80列,每列1个字符,占2个字节

在显示缓冲区中,偶地址存放字符,奇地址存放字符的颜色属性。

CAll和RET指令

call和ret 指令都是转移指令,它们都修改IP,或同时修改CS和IP。它们经常被共同用来实现子程序的设计。

ret和retf

ret指令用栈中的数据,修改IP的内容,从而实现近转移;
retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移。

CPU执行ret指令时,进行下面两步操作:

  1. (IP)-((ss)* 16+(sp))
  2. (sp)=(sp)+2
    CPU执行r etf指令时,进行下面4步操作:
  3. (IP)=(ss)* 16+(sp))
  4. (sp)=(sp)+2
  5. (CS-(ss)* 16+(sp))
  6. (sp)=(sp)+2

可以看出,如果我们用汇编语法来解释ret 和retf指令,则:

CPU执行ret指令时,相当于进行:
pop IP
CPU执行retf指令时,相当于进行:
pop IP
poP CS

call指令

CPU执行call指令时,进行两步操作:

  1. 将当前的IP或CS和IP压入栈中;
  2. 转移。

call指令不能实现短转移,除此之外,call 指令实现转移的方法和jmp指令的原理相同,下面的几个小节中,我们以给出转移目的地址的不同方法为主线,讲解call 指令的主要应用格式。

依据位移进行转移的call指令

call标号(将当前的IP压栈后,转到标号处执行指令)
CPU执行此种格式的call指令时,进行如下的操作:

  1. (sp)=(sp) -2
    ((ss)* 16+(sp))=(IP)
  2. (IP)=(IP)+16 位位移。

16位位移=标号处的地址call指令后的第一个字节的地址;
16位位移的范围为-32768 ~32767,用补码表示;
16位位移由编译程序在编译时算出。
从上面的描述中,可以看出,如果我们用汇编语法来解释此种格式的call指令,则:CPU执行“call标号”时,相当于进行:
push IP
jmp near ptr标号

检测点

下面的程序执行后,ax中的数值为多少?

内存地址             机器码                 汇编指令
1000: 0             b8 00 00            mov ax, 0
1000: 3             e8 01 00            call s
1000: 6             40                      inc ax
1000: 7             58                      s:pop ax

我们要知道CPU执行call指令时,进行两步操作:

  1. 将当前的IP或CS和IP压入栈中;
  2. 转移。

当执行call s 时 ,IP = 6,将 IP压入栈中,然后转移 (IP)=(IP)+16 位位移,执行pop ax,将栈中数据压出到ax中。

转移的目的地址在指令中的call指令

前面讲的call指令,其对应的机器指令中并没有转移的目的地址,而是相对于当前IP的转移位移。
“call far ptr标号”实现的是段间转移。
CPU执行此种格式的call指令时,进行如下的操作。

  1. (sp)=(sp)-2
    (ss)*16+(sp))=(CS)
    (sp)=(sp) 2
    (5s)*16+(sp))=(IP)
  2. (CS)=标 号所在段的段地址
    (IP)=标号在段中的偏移地址

从上面的描述中可以看出,如果我们用汇编语法来解释此种格式的call指令,则:CPU执行“call farptr标号”时,相当于进行:
push CS
push IP
jmp far ptr标号

转移地址在寄存器中的call指令

指令格式: call 16位reg
功能:
(sp)=(sp)-2
((ss)* 16+(sp))=(IP)
(IP)=(16位reg)
用汇编语法来解释此种格式的call 指令,CPU执行“call 16 位reg”时,相当于进行:
push IP
jmp 16 位reg

转移地址在内存中的call指令

转移地址在内存中的call指令有两种格式。

  1. call word ptr 内存单元地址
    用汇编语法来解释此种格式的call指令,则:
    CPU执行“call word ptr内存单元地址”时,相当于进行:
    push IP
    jmp word ptr内存单元地址

  2. call dword ptr内存单元地址
    用汇编语法来解释此种格式的call指令,则:
    CPU执行“call dword ptr内存单元地址”时,相当于进行:
    push CS
    push IP
    jmp dword ptr内存单元地址

call和ret的配合使用

从上面的讨论中我们发现,可以写一个具有一定功能的程序段,我们称其为子程序,在需要的时候,用call 指令转去执行。可是执行完子程序后,如何让CPU接着call指令向下执行? call指令转去执行子程序之前,call 指令后面的指令的地址将存储在栈中,所以可在子程序的后面使用ret指令,用栈中的数据设置IP的值,从而转到call指令后面的代码处继续执行。
这样,我们可以利用call和ret来实现子程序的机制。子程序的框架如下。
标号:
指令
ret
具有子程序的源程序的框架如下。

assume cs:code
code segmentmain:   ......call sub1          ; 调用子程序sub1......mov ax,4c00hint 21hsub1: ...                ; 子程序sub1开始...call sub2          ; 调用子程序sub2......ret                ; 子程序返回sub2: ...                ; 子程序sub2开始......ret                ; 子程序返回
code ends
end main

mul指令

mul指令是乘法指令,使用mul做乘法时需要注意以下两点:

  1. 两个相乘的数:两个相乘的数,要么都是8位,要么都是16位。如果是8位,一个默认放在AL中,另一个放在8位reg或内存字节单元中;如果是16位,一个默认在AX中,另一个放在16位reg或内存字单元中。
  2. 结果:如果是8位乘法,结果默认放在AX中;如果是16位乘法,结果高位默认在DX中存放,低位在AX中放
  • 格式如下:
    mul reg
    mul内存单元
    内存单元可以用不同的寻址方式给出,比如:
    mul byte ptr ds: [0]
    含义: (ax)=(al)((ds)* 16+0);
    mul word ptr [bx+si+8]
    含义: (ax)=(ax)*((ds)* 16+(bx)+(si)+8)结果的低16位。 (dx)=(ax)*((ds)* 16+(bx)+(si)+8)结果的高16位。

如何避免和子程序的寄存器冲突?

解决这个问题的简捷方法是,在子程序的开始将子程序中所有用到的寄存器中的内容都保存起来,在子程序返回前再恢复。可以用栈来保存寄存器中的内容。

capital:push CXpush si
change:mov cl, [si]mov ch, 0jcXZ okand byte ptr [si], 11011111binc sijmp short change
ok:pop sipop CXret

标志寄存器(flag)

作用:

  1. 用来存储相关指令的某些执行结果;
  2. 用来为CPU执行相关指令提供行为依据;
  3. 用来控制CPU的相关工作方式。

8086CPU的标志寄存器有16位,其中存储的信息通常位成为程序状态字(PSW)。这里的标志寄存器简称为flag。

8086CPU的标志寄存器的结构如图:

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
OF DF IF TF SF ZF AF PF CF

flag的1、3、5、12、13、14、 15位在8086CPU中没有使用,不具有任何含义。而0、2、4、6、7、8、9、10、11位都具有特殊的含义。

ZF标志(Zero Flag)

flag的第6位是ZF,零标志位。它记录相关指令执行后,其结果是否为0。如果结果为0,那么zf=1;如果结果不为0,那么zf=0。

注意,在8086CPU的指令集中,有的指令的执行是影响标志寄存器的,比如,add、sub、mul、 div、 inc、 or、 and等,它们大都是运算指令(进行逻辑或算术运算);有的指令的执行对标志寄存器没有影响,比如, mov、push、 pop等,它们大都是传送指令。在使用一条指令的时候,要注意这条指令的全部功能,其中包括,执行结果对标志寄存器的哪些标志位造成影响。

奇偶标志PF(Parity Flag)

flag的第2位是PF,奇偶标志位。它记录相关指令执行后,其结果的第八位bit位中1的个数是否为偶数。如果低八位1的个数为偶数,pf=1, 如果为奇数,那么pf=0。

注意:当bit位1的个数为0时,PF = 1

符号标志SF(Sign Flag)

flag的第7位是SF,符号标志位。它记录相关指令执行后,其结果是否为负。如果结果为负,sf=1; 如果非负,sf=0。

对于同一个二进制数据,计算机可以将它当作无符号数据来运算,也可以当做有符号数据来运算。

SF标志,就是CPU对有符号数运算结果的一种记录,它记录数据的正负。在我们将数据当作有符号数来运算的时候,可以通过它来得知结果的正负。如果我们将数据当作无符号数来运算,SF的值则没有意义,虽然相关的指令影响了它的值。

这也就是说,CPU在执行add等指令时,是必然要影响到SF标志位的值的。至于我们需不需要这种影响,那就看我们如何看待指令所进行的运算了。

进位标志CF(Carry Flag)

flag的第0位是CF,进位标志位。一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。
对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第N-1位,就是它的最高有效位,而假想存在的第N位,就是相对于最高有效位的更高位,如图所示。

  • CLC指令,表示清零CF

例如:

98H+98H将产生进位。

mov al,98H
add al,al    ; 执行后(al) = 30H, CF = 1, CF记录了从最高有效位向更高位的进位值

97H-98H将产生借位。

mov al,97H
sub al,98H   ; 执行后(al) = FFH, CF = 1, CF记录了向更高位的借位值

溢出标志OF(Overflow Flag)

flag的第11位是OF,溢出标志位。一般情况下,OF记录了有符号数运算的结果是否发生了溢出。如果发生溢出,OF=1;如果没有,OF=0。
一定要注意 CF和OF的区别: CF是对无符号数运算有意义的标志位,而OF是对有符号数运算有意义的标志位。

对于8位的有符号数据,机器所能表示的范围就是-128~127。同理,对于16位有符号数据,机器能表示的范围是-32768~32767

OF和CF的区别:

OF的8位有符号数据的范围为 :[ -128 ~ 127]

CF的8位无符号数据的范围为 :[ 0 ~ 255 ]

正加正得负,负加负得正,肯定溢出
一正一负相加肯定不会溢出
(进行正加正,负加负运算时,可以全部转为十进制来看,如果得到正加正得正,负加负得负,则需看他们结果是否在可表示范围内)

追踪标志TF(Trap Flag)

TF=1,表示控制CPU进入单步工作方式。在这种方式下,CPU每执行完一条指令就自动产生一次内部中断,这在调试程序过程中非常有用。TF的设定和清除没有专用的指令,但可用编程间接达到目的。

中断允许标志IF(Interrupt-enable Flag)

flag的第9位是IF,中断允许标志位。可屏蔽中断是CPU可以不响应的外中断。CPU是否响应可屏蔽中断,要看标志寄存器的IF位的设置。当CPU检测到可屏蔽中断信息时,如果IF=1,则CPU在执行完当前指令后响应中断,引发中断过程;如果IF=0,则不响应可屏蔽中断

  • sti,设置IF = 1
  • cli,设置IF = 0

辅助进位标志AF(Auxiliary Carry Flag)

AF=1,表示运算结果的8位数据中,低4位向高4位有进位(加法运算时)或有借位(减法运算时),这个标志位只在十进制中有用。

方向标志DF(Direction Flag)

flag的第10位是DF,方向标志位。在串处理指令中,控制每次操作后si、di的增减。

df=0每次操作后 si、di递增;
df=1每次操作后 si、di 递减。

8086CPU提供下面两条指令对df位进行设置。

  • cld指令:将标志寄存器的df位置0
  • std指令:将标志寄存器的df位置1

串传送指令

  • 格式:movsb

  • 功能:执行movsb 指令相当于进行下面几步操作

    1. ((es)*16+(di)) = ((ds)*16+(si))

    2. 如果df = 0 则:(si) = (si)+1

      (di) = (di)+1

      如果df = 1 则:(si) = (si)-1

      (di) = (di)-1

用汇编语法描述movsb的功能:

mov es: [di],byte ptr ds: [si] ; 8086并不支持这样的指令,这里只是个描述

如果df=0:
inc si
inc di
如果df=1:
dec si
dec di

可以看出,movsb的功能是将ds:si指向的内存单元中的字节送入es:di中,然后根据标志寄存器df位的值,将si和di递增或递减。

也可以传送一个字

  • 格式:movsw
  • 功能:movsw的功能是将ds:si 指向的内存字单元中的字送入es:di中,然后根据标志寄存器df位的值,将si和di递增2或递减2。

rep指令

movsb和movsw 进行的是串传送操作中的一个步骤,一般来说,movsb和movsw都和rep配合使用,格式如下:
rep movsb
用汇编语法来描述rep movsb的功能就是:
s :movsb loop s

可见,**rep的作用是根据cx的值,重复执行后面的串传送指令。**由于每执行一次movsb指令si和di都会递增或递减指向后一个单元或前一个单元,则rep movsb就可以循环实现(cx)个字符的传送。

标志寄存器在Debug中的表示

标志 意义 值为1的标记 值为0的标记
OF (overflow flag) 溢出标志位 OV (OVerflow) NV(No oVerflow)
DF (direction flag) 方向标志位 DN (DowN) UP
IF (interrupt flag) 中断允许标志位 EI (Enable Interrupt) DI (Disable Interrupt)
TF (Trace flag) 追踪标志位
SF (symbol flag) 符号标志位 NG (NeGative) PL (PLus)
ZF (zero flag) 零标志位 ZR (ZeRo) NZ(Not Zero)
AF (auxiliary carry flag) 辅助进位标志位 AC (Auxiliary Carry) NA (Not Auxiliary carry)
PF (parity flag) 奇偶标志位 PE (Parity Even) PO(Parity Odd)
CF (carry flag) 进位标志位 CY (CarrY) NC (Not Carry)

adc指令

adc 是带进位加法指令,它利用了CF位上记录的进位值。

  • 格式:adc 操作对象 1, 操作对象 2
  • 功能:操作对象1 = 操作对象 1 + 操作对象 2 + CF

为什么要加上CF的值呢?CPU为什么要提供这样一条指令呢?

​ 先来看一下CF的值的含义。在执行adc指令的时候加上的CF的值的含义,是由adc指令前面的指令决定的,也就是说,关键在于所加上的CF值是被什么指令设置的。显然,如果CF的值是被sub指令设置的,那么它的含义就是借位值;如果是被add指令设置的,那么它的含义就是进位值。我们来看一下两个数据: 0198H 和0183H如何相加的:

可以看出,加法可以分两步来进行:

  1. 低位相加;
  2. 高位相加再加上低位相加产生的进位值。

下面的指令和add ax,bx 具有相同的结果:
add al,bl adc ah,bh

看来CPU提供adc指令的目的,就是来进行加法的第二步运算的。adc指令和add指令相配合就可以对更大的数据进行加法运算。

例子:

编程,计算1E F000 1000H + 20 1000 1EF0H,结果放在ax(最高16位),bx(次高 16位),cx(低16位)中。
计算分3步进行:
(1) 先将低16位相加,完成后,CF中记录本次相加的进位值;
(2)再将次高16位和CF(来自低16 位的进位值)相加,完成后,CF中记录本次相加的进位值;
(3)最后高16位和CF(来自次高16位的进位值)相加,完成后,CF中记录本次相加的进位值。
程序如下。

mov ax,001EH
mov bx,0F000H
mov cx,1000H
add cx,1EF0H
adc bx,1000H
adc ax,0020H

下面编写一个子程序,对两个128位数据进行相加。

名称: add128
功能:两个128位数据进行相加。
参数: ds:si 指向存储第一个数的内存空间,因数据为128 位,所以需要8个字单元,由低地址单元到高地址单元依次存放128 位数据由低到高的各个字。运算结果存储在第一个数的存储空间中。
ds:di指向存储第二个数的内存空间。
程序如下。

add128:push axpush sipush dipush cxsub ax,ax    ; 将CF设置为0mov cx,8s:mov ax,[si]adc ax,[di]mov [si],axinc siinc siinc diinc diloop spop cxpop dipop sipop axret

sbb指令

sbb是带借位减法指令,它利用了CF位上记录的借位值。

  • 格式:sbb 操作对象 1, 操作对象 2
  • 功能:操作对象1 = 操作对象 1 + 操作对象 2 - CF

sbb和adc是基于同样的思想设计的两条指令,在应用思路上和adc类似。

cmp指令

cmp是比较指令,cmp的功能相当于减法指令,只是不保存结果。cmp指令执行后,将对标志寄存器产生影响。其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。

  • 格式:cmp 操作对象1,操作对象 2
  • 功能: 计算操作对象1-操作对象2但并不保存结果,仅仅根据计算结果对标志寄存器进行设置。

比如,指令cmp ax,ax, 做(ax)-(ax)的运算,结果为0,但并不在ax中保存,仅影响flag的相关各位。指令执行后: zf=1, pf=1, sf=0, cf=0, of=0

通过cmp指令执行后,相关标志位的值就可以看出比较的结果。

cmp ax,bx
如果(ax)=(bx)则(ax)-(bx)=0, 所以: zf=1;
如果(ax)≠(bx)则(ax)-(bx)≠0,所以: zf=0;
如果(ax)<(bx)则(ax)-(bx)将产生借位,所以: cf=1;
如果(ax)≥(bx)则(ax)-(bx)不必借位,所以: cf=0;
如果(ax)>(bx)则(ax)-(bx)既不必借位,结果又不为0,所以: cf=0 并且zf=0;
如果(ax)≤(bx)则(ax)-(bx)既可能借位,结果可能为0,所以: cf=1 或zf=1。

现在我们可以看出比较指令的设计思路,即:通过做减法运算,影响标志寄存器,标志寄存器的相关位记录了比较的结果。反过来看上面的例子。

指令 cmp ax,bx的逻辑含义是比较ax和bx中的值,如果执行后:zf=1,说明(ax)=(bx)
zf=0,说明(ax)≠(bx)
cf=1,说明(ax)<(bx)
cf=0,说明(ax)≥(bx)
cf=0并且zf=0,说明(ax)>(bx)
cf=1或zf=1, 说明(ax)≤(bx)

同add、sub 指令一样,CPU在执行cmp指令的时候,也包含两种含义:进行无符号数运算和进行有符号数运算。所以利用cmp 指令可以对无符号数进行比较,也可以对有符号数进行比较。上面所讲的是用 cmp 进行无符号数比较时,相关标志位对比较结果的记录。下面我们再来看一下如果用cmp来进行有符号数比较时,CPU用哪些标志位对比较结果进行记录。我们以cmp ah,bh为例进行说明。

cmp ah, bh
如果(ah)=(bh)则(ah)-(bh)=0, 所以: zf=1;
如果(ah)≠(bh)则(ah)-(bh)≠0,所以: zf=0;
所以,根据cmp指令执行后zf的值,就可以知道两个数据是否相等。
我们继续看,如果(ah)<(bh)则 可能发生什么情况呢?
对于有符号数运算,在(ah)<(bh)情况 下,(ah)-(bh)显然可能引起sf=1,即结果为负。比如:
(ah)=1,(bh)=2; 则(ah)- (bh)=0FFH,0FFH 为-1的补码,因为结果为负,所以sf=1。
(ah)=0FEH,(bh)=0FFH; 则(ah)-(bh)= -2-(-1)=0FFH,因为结果为负,所以sf=1。

通过上面的例子,我们是不是可以得到这样的结论:cmp 操作对象1,操作对象2指令执行后,sf=1, 就说明操作对象1<操作对象2?
当然不是。

我们应该在考查sf(得 知实际结果的正负)的同时考查of(得知有没有溢出),就可以得知逻辑上真正结果的正负,同时就可以知道比较的结果。

下面,我们以cmp ah,bh为例,总结一下CPU执行cmp指令后,sf和of的值是如何来说明比较的结果的。

  1. 如果sf=1,而of=0
    of=0,说明没有溢出,逻辑上真正结果的正负=实际结果的正负;
    因sf=1,实际结果为负,所以逻辑上真正的结果为负,所以(ah)<(bh)。
  2. 如果sf=1, 而of=1:
    of=1,说明有溢出,逻辑上真正结果的正负≠实际结果的正负;
    因sf=1,实际结果为负。
    实际结果为负,而又有溢出,这说明是由于溢出导致了实际结果为负,简单分析一下,就可以看出,如果因为溢出导致了实际结果为负,那么逻辑上真正的结果必然为正。这样,sf=1, of=1, 说明了(ah)> (bh)。
  3. 如果sf=0,而of=1
    of=1,说明有溢出,逻辑上真正结果的正负≠实际结果的正负;
    因sf=0,实际结果非负。而of=1说明有溢出,则结果非0,所以,实际结果为正。实际结果为正,而又有溢出,这说明是由于溢出导致了实际结果非负,简单分析一下, 就可以看出, 如果因为溢出导致了实际结果为正,那么逻辑上真正的结果必然为负。这样,sf=0, of=1, 说明了(ah)<(bh)。
  4. 如果sf=0, 而of=0
    of=0,说明没有溢出,逻辑上真正结果的正负=实际结果的正负;
    因sf=0,实际结果非负,所以逻辑上真正的结果非负,所以(ah)≥(bh)。

检测比较结果的条件转移指令

  • 所有条件转移指令的转移位移都是[-128,127]。

下面是常用的根据无符号数的比较结果进行转移的条件转移指令。

指令 含义 检测的相关标志位
je 等于则转移 zf=1
jne 不等于则转移 zf=0
jb 低于则转移 cf= 1
jnb 不低于则转移 cf=0
ja 高于则转移 cf=0且zf=0
jna 不高于则转移 cf=1或zf=1

这些指令比较常用,它们都很好记忆,它们的第一个字母都是 j,表示jump;后面的字母表示意义如下。

  • e: 表示 equal [ˈiːkwəl] 等于;比得上
  • ne: 表示 not equal
  • b: 表示 below [bi’ləu] 低于
  • nb: 表示 not below
  • a: 表示 above [ə’bʌv] 高于
  • na: 表示 not above

编程实现如下功能:
如果(ah)=(bh)则(ah)=(ah)+(ah),否则(ah)=(ah)+(bh)。

 cmp ah,bhje sadd ah,bhjmp short ok
s:add ah,ah
ok: ……

补充条件转移指令

转移指令 条件 意义 英文助记
jz/je ZF = 1 相减结果等于0/ 相等时转移 jump if Zero/Equal
jnz/jne ZF = 0 不等于0 / 不相等时转移 Jump if Not Zero / Not Equal
js SF = 1 负数时转移 Jump if Sign
jns SF = 0 整数时转移 Jump if Not Sign
jo OF = 1 溢出时转移 Jump if Overflow
jno OF = 0 未溢出时转移 jump if Not Overflow
jp/jpe PF = 1 低字节中有偶数个1时转移 jump if Parity/Parity Even
jnp/jpo PF = 0 低字节中有奇数个1时转移 jump if Not Parity/Parity Odd
jbe/jna CF = 1或ZF = 1 小于等于/不大于时转移 jump if Below or Equal/Not Above
jnbe/ja CF = ZF = 0 不小于等于/大于时转移 Jump if Not Below or Equal/ Above
jc/jb/jnae CF = 1 进位/小于/不大于等于时转移 Jump if Carry/ Below /Not Above Equal
jnc/jnb/jae CF = 0 未进位/不小于/大于等于时转移 Jump if Not Carry /Not Below /Above Equal
jl/jnge SF != OF 小于/不大于等于时转移 Jump Less /Not Great Equal
jnl/jge SF = OF 不小于/ 大于等于时转移 Jump if Not Less /Great Equal
jle/jng ZF != OF或ZF = 1 小于等于/不大于 Jump if Less or Equal /Not Great
jnle/jg SF = OF且ZF = 0 不小于等于/ 大于时转移 Jump Not Less Equal /Great
jcxz CX 寄存器值 = 0 cx寄存器值为0时转移 Jump if register CX’s value is Zero

pushf 和 popf

pushf的功能是将标志寄存器的值压栈,而popf 是从栈中弹出数据,送入标志寄存器中。
pushf和popf,为直接访问标志寄存器提供了一种方法。

内中断

​ 任何一个通用的CPU,比如8086, 都具备-种能力, 可以在执行完当前正在执行的指令之后,检测到从CPU外部发送过来的或内部产生的一种特殊信息, 并且可以立即对所接收到的信息进行处理。这种特殊的信息,我们可以称其为:中断信息。中断的意思是指,CPU不再接着(刚执行完的指令)向下执行,而是转去处理这个特殊信息。中断信息可以来自CPU的内部和外部。

内中断的产生

对于8086CPU,当CPU内部有下面的情况发生时,将产生相应的中断信息。

  1. 除法错误,比如,执行div指令产生的除法溢出;
  2. 单步执行;
  3. 执行into指令;
  4. 执行int指令。

用处理不同中断信息,CPU首先要知道,所接受到的中断信息的来源。所以中断信息中必须包含识别来源的编码。

8086CPU用称为中断类型码的数据来标识中断信息的来源。

中断类型码为一个字节型数据,可以表示256种中断信息的来源,中断信息的来源简称为中断源。

8086CPU中的中断类型码如下:

  1. 除法错误:0
  2. 单步执行:1
  3. 执行into指令:4
  4. 执行int指令,该指令的格式为int n,指令中的n为字节型立即数,是提供给CPU的中断类型码。

中断向量表

CPU用8位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址。

中断向量表就是中断向量的列表,中断向量表在内存中保存,其中存放着256个中断源所在的中断处理程序的入口。

CPU只要知道了中断类型码,就可以将中断类型码作为中断向量表的表项号,定位相应的表项,从而得到中断处理程序的入口地址。

中断向量表在内存中存放,对于8086PC机,中断向量表指定放在内存地址0处。从内存0000:0000到0000:03FF的1024 个单元中存放着中断向量表。能不能放在别处呢?不能,如果使用8086CPU,中断向量表就必须放在0000:0000 ~ 0000:03FF单元中,这是规定,因为8086CPU就从这个地方读取中断向量表。

对于8086CPU中断向量表中一个表项占两个字,高地址存放段地址,低地址存放偏移地址。

8086系统在存储器的最低1KB区域(00000H~003FFH)建立一个中断向量表,存放256个中断类型的中断向量。这1024个单元被分成256组,每组包括4个字节单元,存储一个中断向量的段基址和段内偏移地址,高两个字节用来存放段地址,低两个字节用来存放段内偏移地址。

存储N号中断源对应的中断处理程序入口的段地址的内存单元的地址为: 4N+2、偏移地址的内存单元的地址为: 4N

中断过程

  • 中断过程的主要任务就是用中断类型码在中断向量表中找到中断处理程序的入口地址,设置CS和IP。

下面是8086CPU在收到中断信息后,所引发的中断过程。

  1. (从中断信息中)取得中断类型码;——取得中断类型码N;
  2. 标志寄存器的值入栈(因为在中断过程中要改变标志寄存器的值,所以先将其保存在栈中);——pushf
  3. 设置标志寄存器的第8位TF和第9位IF的值为0(为了避免陷入一个永远不能结束的循环,CPU永远执行单步中断处理程序的第一条指令);——TF=0, IF=0
  4. CS 的内容入栈;——push CS
  5. IP的内容入栈;——push IP
  6. 从内存地址为中断类型码*4和中断类型码*4+2的两个字单元中读取中断处理程序的入口地址设置IP和CS。——(IP)=(N*4), (CS)=(N*4+2)

中断处理程序和iret指令

中断处理程序的编写方法和子程序的比较相似,下面是常规的步骤:

  1. 保存用到的寄存器;
  2. 处理中断;
  3. 恢复用到的寄存器;
  4. 用iret指令返回。

iret指令的功能用汇编语法描述为:

pop IP pop CS popf

​ iret通常和硬件自动完成的中断过程配合使用。可以看到,在中断过程中,寄存器入栈的顺序是标志寄存器、CS、IP,而iret 的出栈顺序是IP、CS、标志寄存器,刚好和其相对应,实现了用执行中断处理程序前的CPU现场恢复标志寄存器和CS、IP的工作。iret指令执行后,CPU回到执行中断处理程序前的执行点继续执行程序。

设置中断向量

0号表项的地址为0:0,其中0:0 字单元存放偏移地址,0:2 字单元存放段地址。

mov ax,0
mov es,ax
mov word ptr es:[0*4],200h ; 存放偏移地址
mov word ptr es:[0*4+2],0  ; 存放段地址

单步中断

如果检测到标志寄存器的TF位为1,则产生单步中断,引发中断过程。单步中断的中断类型码为1,它引发的中断过程如下。

  1. 取得中断类型码1;
  2. 标志寄存器入栈,TF、IF 设置为0;
  3. CS、IP 入栈;
  4. (IP)=(1*4), (CS)=(1*4+2)。

响应中断的特殊情况

​ 一般情况下,CPU在执行完当前指令后,如果检测到中断信息,就响应中断,引发中断过程。可是,在有些情况下,CPU在执行完当前指令后,即便是发生中断,也不会响应。对于这些情况,我们不一一列举,只是用一种情况来进行说明。

​ 在执行完向ss寄存器传送数据的指令后,即便是发生中断,CPU也不会响应。这样做的主要原因是,ss:sp 联合指向栈顶,而对它们的设置应该连续完成。如果在执行完设置ss的指令后,CPU响应中断,引发中断过程,要在栈中压入标志寄存器、CS和IP的值。而ss改变,sp 并未改变,ss:sp 指向的不是正确的栈顶,将引起错误。所以CPU在执行完设置 的指令后,不响应中断。这给连续设置ss和sp指向正确的栈顶提供了一个时机。即,我们应该利用这个特性,将设置ss和sp的指令连续存放,使得设置sp的指令紧接着设置ss 的指令执行,而在此之间,CPU不会引发中断过程。比如,我们要将栈顶设为1000:0

; 应该
mov ax,1000h
mov ss,ax
mov sp,0
; 不应该
mov ax,1000h
mov ss,ax
mov ax,0
mov sp,0

编写内中断程序

  1. 编写中断安装程序

    例如:安装do0,((es)*16+(di)) = ((ds)*16+(si))

    assume cs:code
    code segment
    start:  mov ax,cs           ; 设置ds:si指向源地址mov ds,axmov si,offset do0mov ax,0            ; 设置es:di指向目的地址mov es,axmov di,200h; 设置cx为传输长度mov cx,offset do0end-offset do0; 设置传输方向为正,df标志位为0cld; std设置方向为负,df标志位为1rep movsb ; movsb的功能:将ds:si指向的内存单元中的字节送入es:di中,然后根据标志寄存器df位的值,将si和di递增或递减设置中断向量表mov ax,4c00hint 21h
    do0:        ……do0start: ……do0end:   nop
    code ends
    end start
    
  2. 设置中断向量表

    将编写的中断程序的入口地址写入到中断向量表中,

    例如:设置do0

    assume cs:code
    code segment
    start:  安装程序; 设置中断向量表mov ax,0mov es,axcli                                                ; 设置IF = 0 ,不响应可屏蔽中断mov word ptr es:[0*4],200h ; 存放偏移地址mov word ptr es:[0*4+2],0  ; 存放段地址sti                                               ; 设置IF = 1 ,CPU在执行完当前指令后可以响应中断mov ax,4c00hint 21h
    do0:        ……do0start: ……do0end:   nop
    code ends
    end start
    
  3. 编写doN程序

    例如:编写0号中断的处理程序,使得在除法溢出发生时,在屏幕中间显示字符串“divideerror!” ’,然后返回到DOS。

    assume cs:code
    code segment
    start:  安装程序设置中断向量表mov ax,4c00hint 21h
    ; 编写0号中断的处理程序,使得在除法溢出发生时,在屏幕中间显示字符串“divideerror!” ’,然后返回到DOS。
    do0:    jmp short do0startdb "divide error!"      ; 使用一部分内存存储字符串do0start: mov ax,csmov ds,axmov si,202h           ; 设置ds:si指向字符串mov ax,0b800hmov es,axmov di,12*160+33*2 ; 设置es:di指向显存空间的中间位置mov cx,13           ; 长度不同,循环次数和内存中的位置不同
    s:    mov al,[si]mov es:[di],almov byte ptr es:[di+1],2    ; 给字符增加颜色inc siadd di,2         loop smov ax,4c00hint 21hdo0end:    nop
    code ends
    end start
    

int指令

  • 格式:int n ,n为中断类型码
  • 功能:引发中断过程

CPU执行int n指令,相当于引发一个n号中断的中断过程,执行过程如下。

  1. 取中断类型码n;
  2. 标志寄存器入栈,IF=0,TF=0;
  3. CS、IP 入栈;
  4. (IP)=(n*4), (CS)=(n*4+2)。

​ 一般情况下,系统将一些具有一定功能的子程序,以中断处理程序的方式提供给应用程序调用。我们在编程的时候,可以用int指令调用这些子程序。当然,也可以自己编写-些中断处理程序供别人使用。以后,我们可以将中断处理程序简称为中断例程。

​ int指令的最终功能和call指令相似,都是调用一段程序 。int指令的最终功能和call指令相似,都是调用一段程序 。

BIOS和DOS所提供的中断例程

在系统板的ROM中存放着一套程序,称为BIOS(基本输入输出系统),BIOS 中主要包含以下几部分内容。

  1. 硬件系统的检测和初始化程序;
  2. 外部中断和内部中断的中断例程;
  3. 用于对硬件设备进行I/O操作的中断例程;
  4. 其他和硬件系统相关的中断例程。

​ BIOS和DOS在所提供的中断例程中包含了许多子程序,这些子程序实现了程序员在编程的时候经常需要用到的功能。程序员在编程的时候,可以用int 指令直接调用BIOS和DOS提供的中断例程,来完成某些工作。和硬件设备相关的DOS中断例程中,-般都调用了BIOS的中断例程。

BIOS和DOS中断例程的安装过程

  1. 开机后,CPU 一加电,初始化(CS)=0FFFFH, (IP)=0, 自动从FFF:0单元开始执行程序。FF:0处有一条转跳指令,CPU执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序。
  2. 初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。注意,对于BIOS所提供的中断例程,只需将入口地址登记在中断向量表中即可,因为它们是固化到ROM中的程序,一直在内存中存在。
  3. 硬件系统检测和初始化完成后,调用int 19h进行操作系统的引导。从此将计算机交由操作系统控制。
  4. DOS启动后,除完成其他工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量。

我们无法变成改变FFFF:0处的指令,因为FFFF:0处是属于ROM内存,只能读取,不能改变。

BIOS中断例程应用

int 10h 中断例程是BIOS 提供的中断例程,其中包含了多个和屏幕输出相关的子程序
一般来说,一个供程序员调用的中断例程中往往包括多个子程序,中断例程内部用传递进来的参数来决定执行哪一一个 子程序。BIOS和DOS提供的中断例程,都用ah来传递内部子程序的编号

int 10h 中断例程的设置光标位置功能:

mov ah,2 ; 置光标
mov bh,0    ; 第0页
mov dh,5    ; dh中放行号
mov dl,12   ; dl中放列号
int 10h

(ah)=2表示调用第10h号中断例程的2号子程序,功能为设置光标位置,可以提供光标所在的行号(8025字符模式下: 0~24)、 列号(8025字符模式下: 0~79), 和页号作为参数。
(bh)=0,(dh)=5, (dl)=12, 设置光标到第0页,第5行,第12列。

bh中页号的含义:内存地址空间中,B8000H~BFFFFH共32kB的空间,为80*25彩色字符模式的显示缓冲区。一屏的内容在显示缓冲区中共占4000个字节。
显示缓冲区分为8页,每页4KB(≈4000B),显示器可以显示任意一页的内容。 一般情况下,显示第0页的内容。也就是说,通常情况下,B8000H- B8F9FH中的4000 个字节的内容将出现在显示器上。

再看一下int 10h中断例程的在光标位置显示字符功能:

mov ah, 9        ; 在光标位置显示字符
mov al,'a'    ; 字符
mov bl, 7       ; 颜色属性
mov bh, 0       ; 第0页
mov cx, 3       ; 字符重复个数
int 10h

(ah)=9表示调用第10h 号中断例程的9号子程序,功能为在光标位置显示字符,可以提供要显示的字符、颜色属性、页号、字符重复个数作为参数。

bl中的颜色属性的格式如下。

编程:在屏幕的5行12列显示3个红底高亮闪烁绿色的’a’。

assume cs:code
code segmentmov ah,2    ; 置光标mov bh,0   ; 页号mov dh,5    ; 行号mov dl,12   ; 列号int 10hmov ah,9     ; 在光标位置显示字符mov al,'a'     ; 字符mov bl,11001010b ; 颜色mov bh,0   ; 页号mov cx,3    ; 字符重复个数int 10hmov ax,4c00hint 21H
code ends
end

DOS中断例程应用

int 21h中断例程是DOS提供的中断例程,其中包含了DOS提供给程序员在编程时调用的子程序。
我们前面一直使用的是int 21h中断例程的4ch号功能,即程序返回功能,如下:

mov ah, 4ch  ; 程序返回
mov al, 0   ; 返回值
int 21h

(ah)=4ch表示调用第21h号中断例程的4ch号子程序,功能为程序返回,可以提供返回值作为参数。
我们前面使用这个功能的时候经常写做:

mov ax, 4c00h
int 21h

我们看一下int21h中断例程在光标位置显示字符串的功能:

ds :dx 指向字符串  ;要显示的字符串需用"$"作为结束符
mov ah, 9                   ;功能号9,表示在光标位置显示字符串
int 21h

(ah)=9表示调用第21h号中断例程的9号子程序,功能为在光标位置显示字符串,可以提供要显示字符串的地址作为参数。

编程:在屏幕的5行12列显示字符串“Welcome to masm!"。

assume cs:code
data segmentdb 'Welcome to masm','$'
data endscode segment
start:mov ah,2      ; 置光标mov bh,0       ; 第0页mov dh,5       ; 第5行mov dl,12      ; 第12列int 10hmov ax,datamov ds,axmov dx,0       ; ds:dx 指向字符串的首地址 data:0mov ah,9int 21hmov ax,4c00hint 21h
code ends
end start

上述程序在屏幕的5行12列显示字符串“Welcome to masm!",直到遇见“”(“”(“”(“”本身并不显示,只起到边界的作用)。

如果字符串比较长,遇到行尾,程序会自动转到下一行开头处继续显示;如果到了最后一行,还能自动上卷一行。

总结

  • int 10h:

    • 2号子程序:(ah)= 2 置光标,(bh)是页号,(dh)是行号,(dl)是列号
    • 9号子程序:(ah)= 9 在光标位置显示字符,(bl)是颜色,(al)是字符,(bh)是页号,(cx)是重复次数
  • int 21h:
    • 9号子程序:(ah) = 9 打印字符到"$"停止,(dx)指向字符的首地址

端口

​ 各种存储器都和CPU的地址线、数据线、控制线相连。CPU在操控它们的时候,把它们都当作内存来对待,把它们总地看做一个由若干存储单元组成的逻辑存储器,这个逻辑存储器我们称其为内存地址空间。

在PC机系统中,和CPU通过总线相连的芯片除各种存储器外,还有以下3种芯片。

  1. 各种接口卡(比如,网卡、显卡)上的接口芯片,它们控制接口卡进行工作;
  2. 主板上的接口芯片,CPU通过它们对部分外设进行访问;
  3. 其他芯片,用来存储相关的系统信息,或进行相关的输入输出处理。

​ 在这些芯片中,都有一组可以由CPU读写的寄存器。这些寄存器,它们在物理上可能处于不同的芯片中,但是它们在以下两点上相同。

  1. 都和CPU的总线相连,当然这种连接是通过它们所在的芯片进行的;
  2. CPU对它们进行读或写的时候都通过控制线向它们所在的芯片发出端口读写命令。

​ 可见,从CPU的角度,将这些寄存器都当作端口,对它们进行统一编址, 从而建立了一个统一的端口地址空间。每一个端口在地址空间中都有一个地址。

CPU可以直接读写以下3个地方的数据。

  1. CPU 内部的寄存器;
  2. 内存单元;
  3. 端口。

端口的读写

​ 在访问端口的时候,CPU通过端口地址来定位端口。因为端口所在的芯片和CPU通过总线相连,所以,端口地址和内存地址一样,通过地址总线来传送。在PC系统中,CPU最多可以定位64KB个不同的端口。则端口地址的范围为0~65535。

对端口的读写不能用mov、push、 pop等内存读写指令。端口的读写指令只有两条:

  • in :用于从端口读取数据。
  • out:用于往端口写入数据。

1.cpu执行内存访问指令时总线上的信息

mov ax,ds:[8] ; 假设执行前(ds)=0

执行时与总线的相关操作如下:

  1. CPU通过地址线将地址信息8发出;
  2. CPU通过控制线发出内存读命令,选中存储器芯片,并通知它,将要从中读取数据;
  3. 存储器将8号单元中的数据通过数据线送入CPU。

2.cpu执行端口访问指令时总线上的信息

in al, 60h ; 从60h号端口读入一个字节

执行时与总线的相关操作如下:

  1. CPU通过地址线将地址信息60h发出;
  2. CPU通过控制线发出端口读命令,选中端口所在的芯片,并通知它,将要从中读取数据;
  3. 端口所在的芯片将60h端口中的数据通过数据线送入CPU。

注意,在in和out指令中,只能使用ax或al来存放从端口中读入的数据或要发送到端口中的数据。访问8位端口时用al,访问16位端口时用ax。

对0~255以内的端口进行读写时:

in al, 20h       ; 从20h端口读入一个字节
out 20h,al      ; 往20h端口写入一个字节

对256~65535的端口进行读写时,端口号放在dx中:

mov dx, 3f8h     ; 将端口号3f8h送入dx
in al,dx                ; 从3f8h端口读入一个字节
out dx,al               ; 向3f8h端口写入一个字节

只允许直接寻址和寄存器间接寻址。直接寻址时,I/O端口地址限制为8位。寄存器间接寻址时,IO端口地址为16位,只能用DX执行寄存器间接寻址。

CMOS RAM 芯片

PC机中,有一个CMOS RAM芯片,一般简称为CMOS。此芯片的特征如下。

  1. 包含一个实时钟和一一个有128个存储单元的RAM存储器(早期的计算机为64个字节)。
  2. 该芯片靠电池供电。所以,关机后其内部的实时钟仍可正常工作,RAM中的信息不丢失。
  3. 128个字节的RAM中,内部实时钟占用0 0dh单元来保存时间信息,其余大部分单元用于保存系统配置信息,供系统启动时BIOS 程序读取。BIOS 也提供了相关的程序,使我们可以在开机的时候配置CMOS RAM中的系统信息。
  4. 该芯片内部有两个端口,端口地址为70h和71h。 CPU通过这两个端口来读写CMOS RAM。
  5. 70h 为地址端口,存放要访问的CMOS RAM单元的地址; 71h 为数据端口,存放从选定的CMOS RAM单元中读取的数据,或要写入到其中的数据。可见,CPU对CMOS RAM的读写分两步进行,比如,读CMOS RAM的2号单元:
    1. 将2送入端口70h;
    2. 从端口71h读出2号单元的内容。

shl 和 shr 指令

shl和shr是逻辑移位指令

  • shl是逻辑左移指令,它的功能为:

    1. 将一个寄存器或内存单元中的数据向左移位;
    2. 将最后移出的一位写入CF中;
    3. 最低位用0补充。

    指令:

    mov al,01001000b

    shl al,1 ; 将al中的数据左移一位

    执行后(al) = 10010000b, CF = 0

    如果移动位数大于1时,必须将移动位数放在cl

    指令:

    mov al,01010001b

    mov cl,3

    shl al,cl

    执行后(al) = 10001000b,因为最后移出的一位是0,所以CF = 0

    可以看出,将X逻辑左移一位,相当于执行 X = X*2。

    比如:

    mov al,00000001b     ; 执行后(al) = 00000001b = 1
    shl al,1                        ; 执行后(al) = 00000010b = 2
    shl al,1                        ; 执行后(al) = 00000100b = 4
    shl al,1                        ; 执行后(al) = 00001000b = 8
    mov cl,3
    shl al,cl                       ; 执行后(al) = 01000000b = 64
    
  • shr是逻辑右移指令,它和shl所进行的操作刚好相反

    1. 将一个寄存器或内存单元中的数据向右移位;
    2. 将最后移出的一位写入CF中;
    3. 最高位用0补充。

    指令:

    mov al,10000001b

    shr al,1 ; 将al中的数据右移一位

    执行后(al) = 01000000b,CF = 1

    如果移动位数大于1时,必须将移动位数放在cl

    指令:

    mov al,01010001b

    mov cl,3

    shr al,cl

    执行后(al) = 00001010b,因为最后移出的一位是0,所以CF = 0

    可以看出,将X逻辑右移一位,相当于执行 X = X/2。

CMOS RAM 中存储的时间信息

  • 在CMOSRAM中,存放着当前的时间:年、月、日、时、分、秒。这6个信息的长度都为1个字节,存放单元为:

    • 秒: 0 分:2 时: 4 日: 7 月: 8 年: 9
    • 这些数据以BCD码的方式存放。
  • BCD码值=十进制数码值,则 BCD码值+30h=十进制数对应的ASCII码

编程,在屏幕中间显示当前的月份

  1. 从CMOSRAM的8号单元读出当前月份的BCD码。

    mov al,8
    out 70h,al
    in al,71h
    
  2. 将用BCD码表示的月份以十进制的形式显示到屏幕上。

    • 将从CMOS RAM的8号单元中读出的一个字节,分为两个表示BCD码值的数据

      mov ah,al                 ; al中为从CMOSRAM的8号单元中读出的数据
      mov cl, 4
      shr ah,cl                   ; ah中为月份的十位数码值
      and al,00001111b    ; al中为月份的个位数码值
      
    • 显示(ah)+30h和(al)+30h对应的ASCII码字符。

      add ah,30h
      add al,30hmov bx,0b800h
      mov es,bx
      mov byte ptr es:[160*12+40*2],ah
      mov byte ptr es:[160*12+40*2+2],al
      

完整代码:

assume cs:code
code segmentmov al,8out 70h,alin al,71hmov ah,almov cl,4shr ah,cland al,00001111badd ah,30Hadd al,30Hmov bx,0B800Hmov es,bxmov byte ptr es:[160*12+40*2],ahmov byte ptr es:[160*12+40*2+2],almov ax,4c00hint 21h
code ends
end

外中断

接口芯片和端口

PC系统的接口卡和主板上,装有各种接口芯片。这些外设接口芯片的内部有若干寄存器,CPU将这些寄存器当作端口来访问。

外设的输入不直接送入内存和CPU,而是送入相关的接口芯片的端口中; CPU向外设的输出也不是直接送入外设,而是先送入端口中,再由相关的芯片送到外设。CPU还可以向外设输出控制命令,而这些控制命令也是先送到相关芯片的端口中,然后再由相关的芯片根据命令对外设实施控制。
可见,CPU通过端口和外部设备进行联系

外中断信息

CPU 如何及时得知并进行处理外设的输入事件?

​ CPU内部有需要处理的事情发生,将产生中断信息,引发中断过程,这种中断信息来自CPU的内部;当CPU外部有需要处理的事情发生的时候,比如说,外设的输入到达,相关芯片将向CPU发出相应的中断信息。CPU在执行完当前指令后,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入。

在PC系统中,外中断源一共有以下两类。

  1. 可屏蔽中断

    可屏蔽中断是CPU可以不响应的外中断。CPU是否响应可屏蔽中断,要看标志寄存器的IF位的设置。当CPU检测到可屏蔽中断信息时,如果IF=1,则CPU在执行完当前指令后响应中断,引发中断过程;如果IF=0,则不响应可屏蔽中断。

    我们回忆一下内中断所引发的中断过程:

    1. 取中断类型码n;
    2. 标志寄存器入栈,IF=0, TF=0;
    3. CS、IP入栈;
    4. (IP)=(n*4), (CS)=(n*4+2)

    由此转去执行中断处理程序。

    可屏蔽中断所引发的中断过程,除在第1步的实现上有所不同外,基本上和内中断的中断过程相同。因为可屏蔽中断信息来自于CPU外部,中断类型码是通过数据总线送入CPU的;而内中断的中断类型码是在CPU内部产生的。

    现在,我们可以解释中断过程中将IF置为0的原因了。将IF置0的原因就是,在进入中断处理程序后,禁止其他的可屏蔽中断。

    当然,如果在中断处理程序中需要处理可屏蔽中断,可以用指令将IF置1。8086CPU提供的设置IF的指令如下:

    sti,设置IF=1

    cli,设置IF=0

  2. 不可屏蔽中断

    不可屏蔽中断是CPU必须响应的外中断。当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后,立即响应,引发中断过程。

    对于8086CPU,不可屏蔽中断的中断类型码固定为2,所以中断过程中,不需要取中断类型码。则不可屏蔽中断的中断过程为:

    1. 标志寄存器入栈,IF=0,TF=0;
    2. CS、IP入栈;
    3. (IP)=(8),(CS=(0AH)。

    **几乎所有由外设引发的外中断,都是可屏蔽中断。**当外设有需要处理的事件(比如说键盘输入)发生时,相关芯片向CPU发出可屏蔽中断信息。不可屏蔽中断是在系统中有必须处理的紧急情况发生时用来通知CPU的中断信息。

PC 机键盘的处理过程

  • 键盘输入

    • 键盘上的每一个键相当于一个开关,按下一个键时,开关接通,该芯片就产生一个扫描码,扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为60h。松开按下的键时,也产生一个扫描码,扫描码说明了松开的键在键盘上的位置。松开按键时产生的扫描码也被送入60h端口中。
    • 一般将按下一个键时产生的扫描码称为通码,松开一个键产生的扫描码称为断码。扫描码长度为一个字节,通码的第7位为0,断码的第7位为1,即:断码=通码+80h
  • 引发9号中断

    键盘的输入到达60h端口时,相关的芯片就会向CPU发出中断类型码为9的可屏蔽中断信息。CPU检测到该中断信息后,如果IF=1,则响应中断,引发中断过程,转去执行int9中断例程。

  • 执行int 9 中断例程

    BIOS提供了int9中断例程,用来进行基本的键盘输入处理,主要的工作如下:

    1. 读出60h端口中的扫描码;
    2. 如果是字符键的扫描码,将该扫描码和它所**对应的字符码(**即ASCⅡ码)送入内存中的BlOS键盘缓冲区;如果是控制键(比如Cr)和切换键(比如 CapsLock)的扫描码,则将其转变为状态字节(用二进制位记录控制键和切换键状态的字节)写入内存中存储状态字节的单元;
    3. 对键盘系统进行相关的控制,比如说,向相关芯片发出应答信息。

    BIOS键盘缓冲区是系统启动后,BIOS用于存放int9中断例程所接收的键盘输入的内存区。该内存区可以存储15个键盘输入,因为int9中断例程除了接收扫描码外,还要产生和扫描码对应的字符码,所以在BIOS键盘缓冲区中,一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。

    0040:17单元存储键盘状态字节,该字节记录了控制键和切换键的状态。键盘状态字节各位记录的信息如下。

    0:   右 shift状态,置1表示按下右 shift键;
    1:  左 shift状态,置1表示按下左 shift键;
    2:  Ctrl状态,置1表示按下Ctrl键
    3:  AIt状态,置1表示按下Alt键;
    4:  ScrolIlock状态,置1表示 Scroll指示灯亮;
    5:  NumLock状态,置1表示小键盘输入的是数字;
    6:  CapsLock状态,置1表示输入大写字母;
    7:  Insert状态,置1表示处于删除态。
    

编写int 9 中断例程

键盘输入的处理过程:

  1. 键盘产生扫描码
  2. 扫描码送入60h端口
  3. 引发9号中断
  4. CPU执行int 9 中断例程处理键盘输入

上面的过程中,第1、2、3步都是由硬件系统完成的。我们能够改变的只有int 9中断处理程序。我们可以重新编写int 9中断例程,按照自己的意图来处理键盘的输入。但我们编写新的键盘中断处理程序不包括一些硬件细节,我们可以使用BIOS提供的int 9 中断例程来处理这些硬件细节。我们只要在自己编写的中断例程中调用BIOS的int 9中断例程就可以了。

指令系统总结

8086CPU提供一下6大类指令:

  1. 数据传送指令

    比如, mov, push, pop, pushf, popf, xchg等都是数据传送指令,这些指令实现寄存器和内存、寄存器和寄存器之间的单个数据传送。

  2. 算术运算指令

    比如, add, sub, adc, sbb, inc, dec. cmp, imul, idiv, aa等都是算术运算指令,这些指令实现寄存器和内存中的数据的算数运算。它们的执行结果影响标志寄存器的sf. z, of, cf,pf. af位。

  3. 逻辑指令

    比如, and,or, not, xor, test, shl, shr, sal, sar, rol, ror, rel,rer等都是逻辑指令。除了not指令外,它们的执行结果都影响标志寄存器的相关标志位。

  4. 转移指令

    可以修改IP,或同时修改CS和IP的指令统称为转移指令。转移指令分为以下几类。

    • 无条件转移指令,比如, jmp;
    • 条件转移指令,比如, joxz, je. jb, ja, jnb, jna等;
    • 循环指令,比如, loop;
    • 过程,比如, call, ret. retf;
    • 中断,比如, int, iret.
  5. 处理机控制指令

    这些指令对标志寄存器或其他处理机状态进行设置,比如, cld, std,cli, sti,nop, clc,cmc,stc, hlt, wait, esc, lock等都是处理机控制指令。

  6. 串处理指令

    这些指令对内存中的批量数据进行处理,比如,movsb、 movsw、 cmps、scas、lods、stos等。若要使用这些指令方便地进行批量数据的处理,则需要和rep、repe、repne等前缀指令配合使用。

直接定址表

描述了单元长度的标号

在之前的学习中,我们一直在代码段中使用标号来标记指令、数据、段的起始地址

assume cs : code
code segmenta: db 1,2,3,4,5,6,7,8b: dw 0
start:mov si,offset amov bx,offset bmov CX, 8
s: mov al,cs: [si]mov ah, 0add cs: [bx] , axinc siloop smov ax,4c00hint 21h
code ends
end start

程序中, code、a、 b、start、 s都是标号。这些标号仅仅表示了内存单元的地址。

现在我们学习一种新的标号,这种标号不但表示内存单元的地址,还表示了内存单元的长度,即表示在此标号处的单元,是一个字节单元,还是字单元,还是双字单元。

上面的程序还可以写成这样:

assume cs:code
code segmenta db 1,2,3,4,5,6,7,8b dw 0
start:mov si,0mov CX, 8
s:mov al,a[si]mov ah,0add b,axinc siloop smov ax,4c00hint 21h
code endsend start

​ 在code段中使用的标号a、b后面没有“:” 它们是同时描述内存地址和单元长度的标号。标号a,描述了地址code:0,和从这个地址开始,以后的内存单元都是字节单元;而标号b描述了地址code:8,和从这个地址开始,以后的内存单元都是字单元。

​ 因为这种标号包含了对单元长度的描述,所以在指令中,它可以代表一个段中 的内存单元。比如,对于程序中的“b dw 0”:
指令: mov ax,b
相当于: mov ax,cs:[8]
指令: mov b,2
相当于: mov word ptr cs:[8],2
指令: inc b
相当于: inc word ptr cs:[8]
在这些指令中,标号b代表了一个内存单元,地址为code:8,长度为两个字节。

下面的指令会引起编译错误:
mov al,b
因为b代表的内存单元是字单元,而al 是8位寄存器。

使用这种包含单元长度的标号,可以使我们以简洁的形式访问内存中的数据。以后,我们将这种标号称为数据标号,它标记了存储数据的单元的地址和长度。

在其他段中使用数据标号

一般来说,我们不在代码段中定义数据,而是将数据定义到其他段中。在其他段中,我们也可以使用数据标号来描述存储数据的单元的地址和长度。

注意,在后面加有“:”的地址标号,只能在代码段中使用,不能在其他段中使用。

​ **如果想在代码段中直接用数据标号访问数据,则需要用伪指令 assume 将标号所在的段和一个段寄存器联系起来。否则编译器在编译的时候,无法确定标号的段地址在哪一个寄存器中。**当然,这种联系是编译器需要的,但绝对不是说,我们因为编译器的工作需要,用 assume 指令将段寄存器和某个段相联系,段寄存器中就会真的存放该段的地址。我们在程序中还要使用指令对段寄存器进行设置。
比如,在上面的程序中,我们要在代码段code中用data段中的数据标号a、b访问数据,则必须用assume 将一个寄存器和data 段相联。在程序中,我们用ds寄存器和data段相联,则编译器对相关指令的编译如下。

可以将标号当作数据来定义,此时,编译器将标号所表示的地址当作数据的值。

比如:

data segmenta db 1,2,3,4,5,6,7,8b dw 0c dw a,b
data ends

数据标号c处存储的两个字型数据为标号a、b的偏移地址。相当于:

data segmenta db 1,2,3,4,5,6,7,8b dw 0c dw offset a, offset b
data ends

再比如:

data segmenta db 1,2,3,4,5,6,7,8b dw 0c dd a,b
data ends

数据标号c处存储的两个字型数据为标号a、b的偏移地址。相当于:

data segmenta db 1,2,3,4,5,6,7,8b dw 0c dw offset a,seg a, offset b,seg b
data ends

直接定址表

​ 利用表,在两个数据集合之间建立一种映射关系,使我们可以用查表的方法根据给出的数据得到其在另一集合中的对应数据。这样做的目的一般来说有以下3个。

  • 为了算法的清晰和简洁;
  • 为了加快运算速度;
  • 为了使程序易于扩充

通过依据数据,直接计算出所要找的元素的位置的表,我们称其为直接定址表。

程序入口地址的直接定址表

我们可以将这些功能子程序的入口地址存储在-一个表中,它们在表中的位置和功能号相对应。对应关系为:功能号*2=对应的功能子程序在地址表中的偏移。程序如下:

setscreen:   jmp short settable dw sub1,sub2,sub3
set:                push bxcmp ah,3 ; ja sretmov bl,ahmov bh,0add bx,bx ;call word ptr table[bx] ;
sret:               pop bxret
sub1:               ……
sub2:               ……
sub3:               ……

也可以将子程序setscreen如下实现。

setscreen:   cmp ah,0je do1cmp ah,1je do2cmp ah,2je do3jmp short sret
do1:call sub1jmp short sret
do2:call sub2jmp short sret
do3:call sub3jmp short sret
sret:ret
sub1:               ……
sub2:               ……
sub3:               ……

使用BIOS进行键盘输入和磁盘读写

int9中断例程对键盘输入的处理

我们已经讲过,键盘输入将引发9号中断,BIOS 提供了int 9中断例程。CPU在9号中断发生后,执行int 9中断例程,从60h端口读出扫描码,并将其转化为相应的ASCII码或状态信息,存储在内存的指定空间(键盘缓冲区或状态字节)中。
一般的键盘输入,在CPU执行完int 9中断例程后,都放到了键盘缓冲区中。键盘缓冲区中有16个字单元,可以存储15个按键的扫描码和对应的ASCII码

下面, 我们通过下面几个键:A、B、C、D、E、ShiftA、A 的输入过程,简要地看一下int 9中断例程对键盘输入的处理方法。

  1. 初始状态下,没有键盘输入,键盘缓冲区空,此时没有任何元素。

  2. 按下A键,引发键盘中断;CPU执行int9中断例程,从60h端口读出A键的通码;然后检测状态字节,看看是否有 Shift、Ctrl等切换键按下;发现没有切换键按下,则将A键的扫描码leh和对应的ASCI码,即字母“a”的ASCⅡ码6lh,写入键盘缓冲区。缓冲区的字单元中,高位字节存储扫描码,低位字节存储ASCI码。此时缓冲区中的内容如下。

  3. 按下B、C、D、E键后,缓冲区中的内容如下。

  4. 按下左Shift 键,引发键盘中断; int 9中断例程接收左Shift 键的通码,设置0040:17处的状态字节的第1位为1,表示左Shift键按下。

  5. 按下A键,引发键盘中断; CPU 执行int 9中断例程,从60h端口读出A键的通码;检测状态字节,看看是否有切换键按下;发现左Shift 键被按下,则将A键的扫描码1Eh和Shift_ A对应的ASCII码,即字母“A”的ASCII码41h,写入键盘缓冲区。此时缓冲区中的内容如下。

  6. 松开左Shift 键,引发键盘中断; int 9中断例程接收左Shift 键的断码,设置0040:17处的状态字节的第1位为0,表示左Shift键松开。

  7. 按下A键,引发键盘中断; CPU执行int 9中断例程,从60h端口读出A键的通码;然后检测状态字节,看看是否有切换键按下;发现没有切换键按下,则将A键的扫描码1Eh和A对应的ASCII码,即字母“a”的ASCII码61h,写入键盘缓冲区。此时缓冲区中的内容如下。

使用int 16h中断例程读取键盘缓冲区

BIOS提供了int16h中断例程供程序员调用。int16h中断例程中包含的一个最重要的功能是从键盘缓冲区中读取一个键盘输入,该功能的编号为0。下面的指令从键盘缓冲区中读取一个键盘输入,并且将其从缓冲区中删除:

mov ah,0

int 16h

结果: (ah)= 扫描码,(al)=ASCII 码。

int 16h中断例程检测键盘缓冲区,发现缓冲区空,则循环等待,直到缓冲区中有数据。

int 16h中断例程的0号功能,进行如下的工作:

  1. 检测键盘缓冲区中是否有数据;
  2. 没有则继续做第1步;
  3. 读取缓冲区第一个字单元中的键盘输入;
  4. 将读取的扫描码送入ah, ASCII码送入al;
  5. 将已读取的键盘输入从缓冲区中删除。

BIOS的 int 9中断例程和 int 16h 中断例程是一对相互配合的程序,int 9中断例程向键盘缓冲区中写入,int 16h中断例程从缓冲区中读出。它们写入和读出的时机不同,int 9中断例程是在有键按下的时候向键盘缓冲区中写入数据;而int 16h 中断例程是在应用程序对其进行调用的时候,将数据从键盘缓冲区中读出。

在int 16h中断例程中,一定有设置IF=1的指令。”这种说法对吗?

在int 16h中断例程中,一定有设置IF=1的指令,因为当键盘缓冲区为空时,如果设置IF=0,int 9中断无法执行,循环等待会死锁

字符串的输入

用户通过键盘输入的通常不仅仅是单个字符而是字符串。下面我们讨论字符串输入中的问题和简单的解决方法。

最基本的字符串输入程序,需要具备下面的功能。

  1. 在输入的同时需要显示这个字符串;
  2. 一般在输入回车符后,字符串输入结束;
  3. 能够删除已经输入的字符。

对于这3个功能,我们可以想象在DOS中,输入命令行时的情况。

编写一个接收字符串输入的子程序,实现上面3个基本功能。因为在输入的过程中需要显示,子程序的参数如下:

(dh)、(dI)= 字符串在屏幕上显示的行、列位置;
ds:si指向字符串的存储空间,字符串以0为结尾符。

下面我们进行分析。

  1. 字符的输入和删除

    每个新输入的字符都存储在前一个输入的字符之后,而删除是从最后面的字符进行的,我们看下面的过程。

    空字符串:
    输入“a’ :a
    输入“b”: ab
    输入“c”: abc
    输入“d”: abcd
    删除一个字符: abc
    删除一个字符: ab
    删除一个字符: a
    删除一个字符:

    ​ 可以看出在字符串输入的过程中,字符的输入和输出是按照栈的访问规则进行的,即后进先出。这样,我们就可以用栈的方式来管理字符串的存储空间,也就是说,字符串的存储空间实际上是一个字符栈字符栈中的所有字符,从栈底到栈顶,组成一个字符串

  2. 在输入回车符后,字符串输入结束。

    输入回车符后,可以在字符串中加入0,表示字符串结束。

  3. 在输入的同时需要显示这个字符串。

    每次有新的字符输入和删除一个字符的时候,都应该重新显示字符串,即从字符栈的栈底到栈顶,显示所有的字符。

  4. 程序的处理过程

    现在我们可以简单地确定程序的处理过程如下。

    ① 调用int 16h读取键盘输入;

    ② 如果是字符,进入字符栈,显示字符栈中的所有字符;继续执行①;

    ③ 如果是退格键,从字符栈中弹出一个字符,显示字符栈中的所有字符;继续执行①;

    ④ 如果是Enter 键,向字符栈中压入0,返回。

    从程序的处理过程中可以看出,字符栈的入栈、出栈和显示栈中的内容,是需要在多处使用的功能,我们应该将它们写为子程序。

    #子程序:字符栈的入栈、出栈和显示。
    #参数说明: (ah)=功能号, 0表示入栈,1表示出栈,2表示显示;
    #               ds:si指向字符栈空间;
    #               对于0号功能: (al)=入栈字 符;
    #               对于1号功能: (al)=返回的字符;
    #               对于2号功能: (dh)、 (dI)=字符串 在屏幕上显示的行、列位置。
    
    charstact: jmp short charstart
    table   dw charpush,charpop,charshow
    top     dw 0                                                    ; 栈顶
    charstart:  push bxpush dxpush dipush escmp ah,2ja sretmov bl,ahmov bh,0add bx,bxjmp word ptr table[bx]charpush:        mov bx,topmov [si][bx],alinc topjmp sret
    charpop:        cmp top,0je sretdec topmov bx,topmov al,[si][bx]jmp sret
    charshow:       mov bx,0b800hmov es,bxmov al,160mov ah,0mul dhmov di,axadd dl,dlmov dh,0add di,dxmov bx,0charshows: cmp bx,topjne noemptymov byte ptr es:[di],' 'jmp sret
    noempty:        mov al,[si][bx]mov es:[di],almov byte ptr es:[di+2],' 'inc bxadd di,2jmp charshows
    sret:               pop espop dipop dxpop bxret
    

    上面的子程序中,字符栈的访问规则如下

    1. 栈空

    2. “a” 入栈

    3. “b” 入栈

    另外一个要注意的问题是,显示栈中字符的时候,要注意清除屏幕上上一次显示的内容。
    我们现在写出完整的接收字符串输入的子程序,如下所示。

    getstr:      push ax
    getstrs:    mov ah,0int 16hcmp al,20hjb nochar                  ; ASCII码小于20h,说明不是字符mov ah,0call charstack       ; 字符入栈mov ah,2call charstack        ; 显示栈中的字符jmp getstrs
    nochar:     cmp ah,0eh              ; 退格键的扫描码je backspacecmp ah,1ch             ; Enter键的扫描码je enterjmp getstrs
    backspace:mov ah,1call charstack        ; 字符出栈mov ah,2call charstack        ; 显示栈中的字符jmp getrtrs
    enter:      mov al,0mov ah,0call charstack      ; 0入栈mov ah,2call charstack     ; 显示栈中的字符pop axret
    

应用int 13h中断例程对磁盘进行读写

这里我们以3.5英寸软盘为例:

3.5英寸软盘分为上下两面,每面有80个磁道,每个磁道又分为18 个扇区,每个扇区的大小为512个字节。

则: 2面*80磁道*18扇区*512字节= 1440KB≈1.44MB

磁盘的实际访问由磁盘控制器进行,我们可以通过控制磁盘控制器来访问磁盘。只能以扇区为单位对磁盘进行读写。在读写扇区的时候,要给出面号、磁道号和扇区号。面号和磁道号从0开始,而扇区号从1开始。

BIOS提供了对扇区进行读写的中断例程,这些中断例程完成了许多复杂的和硬件相关的工作。

BIOS提供的访问磁盘的中断例程为int13h。读取0面0道1扇区的内容到0:200的程序如下所示。

mov ax,0
mov es,ax
mov bx,200hmov al,1 ; 读取的扇区数
mov ch,0 ; 磁道号
mov cl,1 ; 扇区号
mov dl,0 ; 驱动器号
mov dh,0 ; 磁头号
mov ah,2 ; 读取
int 13h

入口参数:

  • (ah) = int 13h的功能号(2表示读扇区)
  • (al) = 读取的扇区数
  • (ch) = 磁道号
  • (cl) = 扇区号
  • (dh) = 磁头号(对于软盘即面号,因为一个面用一个磁头来读写)
  • (dl) = 驱动器号 软驱从0开始,0:软驱A,1:软驱B;硬盘从80h开始,80h: 硬盘C, 81h:硬盘D

es:bx指向接收从扇区读入数据的内存区

返回参数:

  • 操作成功: (ah)=0, (al)=读入的扇区数

  • 操作失败: (ah)=出错代码

将0:200中的内容写入0面0道1扇区。

mov ax,0
mov es,ax
mov bx,200hmov al,1
mov ch,0
mov cl,1
mov dl,0
mov dh,0mov ah,3 ; 写入
int 13h

入口参数:

  • (ah) = int 13h的功能号(3表示写扇区)

  • (al) = 写入的扇区数

  • (ch) = 磁道号

  • (cl) = 扇区号

  • (dh) = 磁头号(面)

  • (dl) = 驱动器号 软驱从0开始,0: 软驱A,1: 软驱B;

    ​ 硬盘从80h开始,80h: 硬盘C, 81h:硬盘D

es:bx 指向将写入磁盘的数据

返回参数:

  • 操作成功: (ah)=0, (al)=写入的扇区数

  • 操作失败: (ah)=出错代码

注意,下面我们要使用int 13h 中断例程对软盘进行读写。直接向磁盘扇区写入数据是很危险的,很可能覆盖掉重要的数据。如果向软盘的0面0道1扇区中写入了数据,要使软盘在现有的操作系统下可以使用,必须要重新格式化。在编写相关的程序之前,必须要找一-张空闲的软盘。在使用int 13h中断例程时一定要注意驱动器号是否正确,千万不要随便对硬盘中的扇区进行写入。

编程: 将当前屏幕的内容保存在磁盘上。

分析:1屏的内容占4000个字节,需要8个扇区,用0面0道的1~8扇区存储显存中的内容。程序如下。

assume cs:code
code segment
start:mov ax,0b800hmov es,axmov bx,0mov al,8mov cl,1mov ch,0mov dh,0mov dl,0mov ah,3int 13hmov ax,4c00hint 21h
code ends
end start

我们可以看出,用面号、磁道号、扇区号来访问磁盘不太方便。可以考虑对位于不同的磁道、面上的所有扇区进行统一编号。编号从0开始,一直到2879,我们称这个编号为逻辑扇区编号。

逻辑扇区号=(面号*80+磁道号)*18+扇区号-1

那么如何根据逻辑扇区号算出物理编号呢?可以用下面的算法。

  • int ( ):描述性运算符,取商
  • rem ( ): 描述性运算符,取余数
  • 逻辑扇区号=(面号*80+磁道号)*18+扇区号-1
  • 面号=int(逻辑扇区号/1440)
  • 磁道号=int(rem(逻辑扇区号/1440)/18)
  • 扇区号= rem(rem(逻辑扇区号/1440)/18)+ 1

安装一个新的int7ch中断例程,实现通过逻辑扇区号对软盘进行读写。参数说明:

  1. 用ah寄存器传递功能号: 0表示读,1表示写;
  2. 用dx寄存器传递要读写的扇区的逻辑扇区号;
  3. 用es:bx指向存储读出数据或写入数据的内存区。
; al = 扇区数
; ch = 磁道号
; cl = 扇区号
; dh = 面号
; dl = 驱动器号 --- 0、1 表示软盘 --- 80h、81h 表示硬盘
assume cs:code
code segment
start:mov ax,0mov es,axmov di,200hpush cspop dsmov si,offset do7cmov cx,offset do7c_end-offset do7ccldrep movsbmov ax,0mov es,axmov word ptr es:[0*4],200hmov word ptr es:[0*4+2],0mov ax,4c00hint 21h
do7c:push cxpush dxpush bxpush axpush axmov ax,dxmov dx,0mov bx,1440div bxpush axmov bl,18mov ax,dxdiv blinc ahpop dxmov cl,8shl dx   ; dh = 面号mov dl,0mov cl,ah ; 扇区号mov ch,al ; 磁道号pop axadd ah,2int 13hpop axpop bxpop dxpop cxiretdo7c_end:nopcode ends
end start

汇编语言笔记(王爽)相关推荐

  1. 《汇编语言》——王爽

    <汇编语言>--王爽 第1章 基础知识 汇编语言是直接在硬件之上工作的编程语言. PC机及CPU物理结构和编程结构的全面研究--<微机原理与接口> 计算机的一般结构.功能.性能 ...

  2. 汇编语言(王爽第三版) 实验5

    汇编语言(王爽第三版) 实验5 由图可见: 第一问:cpu执行程序,程序返回前,ds一直未变,所以data段中的数据不变. 第二问:cpu执行程序,程序返回前,cs=1CD5,SS=1CD4,DS=1 ...

  3. 学习汇编语言 -王爽,自已完成的一道课程设计题 (5)

    课程设计1 (材料详见书上211页) 题目描述: 以下是我解答的完整的代码: :>-------------------------------------------------------- ...

  4. 【汇编语言】王爽 - 内中断复习

    0 前言 基于王爽<汇编语言>和Coursera的<计算机组成>课程. 1 中断分类 CPU在执行指令的过程中,产生了一个异常/中断,因为CPU只能同时执行一条指令,所以需要暂 ...

  5. 【汇编语言】王爽实验5(5)(6)的解答 建立数据类型匹配的观念

    0 前言 本文解答王爽<汇编语言>实验5的(5)(6)题 同时给出一些常见问题的解答 以及给出最易犯错的地方:数据类型不匹配的解决方案 1 题目解答 1.1 实验5(5) 1.1.1 题目 ...

  6. 汇编语言(王爽第三版) 实验5编写、调试具体多个段的程序

    参考:http://blog.sina.com.cn/s/blog_171daf8e00102xclx.html 汇编语言实验答案 (王爽):https://wenku.baidu.com/view/ ...

  7. 汇编语言答案-王爽第三版

    汇编语言答案(王爽) 检测点1.1 (1)1个CPU的寻址能力为8KB,那么它的地址总线的宽度为 13位. (2)1KB的存储器有 1024 个存储单元,存储单元的编号从 0 到 1023 . (3) ...

  8. 汇编语言答案(王爽版)

    王爽汇编语言答案(本答案是自己做的 所有题目已在deubg中调试:但难免有差错,发现的提醒我 email:maokaijiang1211@163.com  谢谢) 第一章 检测点1.1 1) 13 ( ...

  9. 《汇编语言》王爽(第四版) 第十章 实验10

    文章目录 前言 一.子程序1 显示字符串 1.实验任务 2.分析 (1)如何在指定位置显示 (2)如何显示指定颜色 (3)保存子程序中用到的寄存器 3.代码 二.子程序2 解决除法溢出的问题 1.实验 ...

  10. 《汇编语言》王爽(第四版) 第十二章 实验12

    文章目录 前言 一.思路分析 1.安装 2.设置中断向量 3.do0程序 4.测试 5.优化 二.最终成果 1.完整代码 2.效果图 总结 前言 本文是王爽老师<汇编语言>(第四版) 第十 ...

最新文章

  1. 如何利用SEO做好网站推广
  2. header python 环境信息_Python开发必备:如何建立一个完美的项目工程环境
  3. 控制台ui_设计下一代控制台UI
  4. 免费网络研讨会:Java应用程序中的吞咽异常
  5. 剑指offer-JZ82 二叉树中和为某一值的路径(一)(附区分DFS和回溯)
  6. 一张图看懂开源许可协议,开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别...
  7. Leetcode 刷题笔记(二) —— 数组类型解题方法二:双指针法
  8. java并发编程实战看哪几章,附源代码
  9. c语言汉诺塔课设计报告,汉诺塔游戏的设计
  10. Re:从零开始的鸿蒙开发教程
  11. 实现调用本地office打开在线文档功能
  12. android led弹幕,LED弹幕手持字幕
  13. RK3568-SPI
  14. 二代测序技术之illumina测序技术原理简介
  15. 新手做网站只需要4个步骤
  16. Sping +hibernate+JTA 注解配置
  17. 蓝桥杯每日一练:芯片检测
  18. 学校宽带被远程计算机终止,宽带连接被远程计算机终止是什么意思
  19. 精心整理的超好的共享文库
  20. Android自定义心率图表

热门文章

  1. 关于wi-fi无线局域网的若干问题
  2. 神操作 用 Python 操作 xmind 绘制思维导图
  3. Win10重装win7时一直显示windows启动中,不要慌
  4. 静态网页制作HTML学习笔记
  5. itext生成页眉页脚
  6. excel添加自定义名称
  7. 读书笔记:软件工程(11) - 传统方法学 - 软件需求分析
  8. C语言一维数组的定义与常见用法
  9. [作业] 六角填数问题
  10. 记一次-更新win10版本到2004