汇编语言_汇编语言指令 - CSDN
汇编语言 订阅
汇编语言(Assembly Language)是任何一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。特定的汇编语言和特定的机器语言指令集是一一对应的,不同平台之间不可直接移植。 [1] 展开全文
汇编语言(Assembly Language)是任何一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。特定的汇编语言和特定的机器语言指令集是一一对应的,不同平台之间不可直接移植。 [1]
信息
产生年代
20世纪50年代
外文名
Assembly Language
编译方式
汇编
中文名
汇编语言
学    科
软件工程
汇编语言简介
汇编语言, 即第二代计算机语言,用一些容易理解和记忆的字母,单词来代替一个特定的指令,比如:用“ADD”代表数字逻辑上的加减,“ MOV”代表数据传递等等,通过这种方法,人们很容易去阅读已经完成的程序或者理解程序正在执行的功能,对现有程序的bug修复以及运营维护都变得更加简单方便。当计算机的硬件不认识字母符号,这时候就需要一个专门的程序把这些字符变成计算机能够识别的二进制数。因为汇编语言只是将机器语言做了简单编译,所以并没有根本上解决机器语言的特定性,所以汇编语言和机器自身的编程环境息息相关,推广和移植很难,但是还是保持了机器语言优秀的执行效率,因为他的可阅读性和简便性,汇编语言到现在依然是常用的编程语言之一。 [2]  汇编语言不像其他大多数的程序设计语言一样被广泛用于程序设计。在今天的实际应用中,它通常被应用在底层,硬件操作和高要求的程序优化的场合。驱动程序、嵌入式操作系统和实时运行程序都需要汇编语言。 [1] 
收起全文
精华内容
参与话题
  • 汇编语言笔记(全)

    千次阅读 多人点赞 2019-08-01 11:40:05
    汇编语言 最近系统的学了下汇编语言,下面是学习笔记,用的书是清华大学出版社出版的汇编语言第三版,作者王爽(最经典的那版)。 汇编语...

    汇编语言

    最近系统的学了下汇编语言,下面是学习笔记,用的书是清华大学出版社出版的汇编语言第三版,作者王爽(最经典的那版)。

    基础知识

    汇编语言指令组成
    • 汇编指令:机器码的助记符,有对应的机器码。
    • 伪指令:没有对应的机器码,编译器执行,机器不执行。
    • 其他符号:如+-*/有编译器识别,无对应机器码。
    CPU与外部器件交互需要
    • 存储单元地址(地址信息)
    • 器件选择,读写命令(控制信息)
    • 数据(数据信息)
    总线

    总线就是一根根导线的集合,分为

    • 地址总线,越宽(数量越多)代表可以寻址的范围越大
    • 数据总线,越宽代表一次性读写的数据越多(8根1字节)
    • 控制总线,越宽代表对器件控制操作越多
    小结

    汇编指令和机器指令一一对应

    每一种cpu都有自己的汇编指令集

    在存储器中指令和数据都是二进制,没有任何区别

    CPU可以直接使用的信息存放在存储器中(内存)

    接口卡

    CPU无法直接控制显示器,键盘等的外围设备,但CPU通过直接控制这些外围设备在主板上的接口卡来控制这些设备。

    存储器

    随机存储器(RAM):带电存储,关机丢失,可读可写

    • 用于存放CPU使用的绝大部分程序和数据,主随机存储器由装在主板上的RAM和扩展插槽的RAM组成。
    • 其他接口卡上也可能有自己的RAM

    只读存储器(ROM):关机不丢,只能读取

    • 主板上的ROM装有系统的BIOS(基本输入输出系统)。

    • 其他接口卡上也可能有自己的ROM,一般装着相应的BIOS。

    (P10图)

    内存地址空间

    以上这些内存都和CPU总线相连,CPU都通过控制总线向他们发出内存读写命令。所以CPU都把他们当内存对待,看做一个一个由若干存储单元组成的逻辑存储器,即内存地址空间(一个假想的逻辑存储器P11图)。

    内存地址空间中的各个不同的地址段代表不同的存储设备,内存地址空间大小收到CPU地址总线长度限制。

    寄存器

    内部总线

    之前讨论的总线是CPU控制外部设备使用的总线,是将CPU和外部部件连接的。而CPU内部由寄存器,运算器,控制器等组成,由内部总线相连,内部总线负责连接CPU内部的部件。

    通用寄存器

    8086CPU寄存器都是16位的,一共14个,分别是AX,BX,CX,DX,SI,DI,SP,BP,IP,CS,SS,DS,ES,PSW。其中AX,BX,CX,DX四个寄存器通常存放一般性的数据,称为通用寄存器。

    而且为了兼容上一代的8位寄存器,这四个寄存器可以拆开成两个8位的寄存器来使用。称为AH,AL,BH,BL,CH,CL,DH,DL。低八位(编号0-7)构成L寄存器,高八位构成H寄存器。

    8086CPU可以处理以下两种数据

    • 字节byte,8位
    • 字word,连个字节,16位。分别称为高位字节和低位字节。
    简单的汇编指令
    指令 操作 高级语言
    mov ax,18 将18存入AX寄存器 AX=18
    add ax,8 将AX寄存器中的数加8 AX=AX+8
    mov ax,bx 将BX中的数据存入AX AX=BX
    add ax,bx 将AX中的数据和BX中的数据相加存入AX AX=AX+BX

    汇编指令或寄存器名称不区分大小写。

    注:AX寄存器当做两个8位寄存器al和ah使用的时候,CPU就把他们当做两个8位寄存器使用,而不会看成是一个16未分开,即如果al进行加法运算C5+93=158,即add al,93,al会变成58,ax则是0058而不是0158。

    CPU位结构

    16位结构的CPU指的是运算器一次最多处理16位数据,寄存器宽度16,寄存器和运算器之间通路也是16位。

    CPU表示物理地址

    如果物理总线宽度超过寄存器宽度,CPU寻址方法是两个寄存器输出一个地址,当地址总线宽度20的时候,P21图。一个寄存器输出短地址,另一个输出偏移地址。然后通过地址加法器合并为一个20位的地址,然后通过内部总线送给控制电路,控制电路通过地址总线送给内存。

    公式:物理地址=段地址x16+偏移地址(这里的x16其实就是左移四位,P21图)

    虽然这么表示,但内存并没有被分为一段一段的,是CPU划分的段。段地址x16称为基础地址,所以我们可以根据需求把任意的基础地址加上不超过一个寄存器表示的最长(64KB)的偏移地址来表示地址。而且一个实际地址往往可以有各种不同的方法表示,通常我们表示21F60H这个地址通过下面方法:

    • 2000:1F60
    • 2000H段中的1F60单元中
    段寄存器与指令指针寄存器

    8086CPU有四个段寄存器:CS,DS,SS,ES

    除此之外,IP寄存器称为指令指针寄存器,所以任意时刻可以读取从CSx16+IP单元开始,读取一条指令执行。也就是说,CPU将IP指向的内容当做指令执行。

    P26图,CPU执行一段指令。另外,8086CPU开机时CS被置为FFFFH,IP被置为0000H,也就是说刚开机的第一条指令从FFFF0H开始读取执行。

    CPU将CS:IP指向的内存中的内容当做指令,一条指令被执行了,那一定被CS:IP指向过。

    修改CS,IP

    CS和IP寄存器不可以使用传送指令mov来改变,而能改变CS,IP内容的指令是转移指令。

    jmp指令用法:

    • jmp 段地址:偏移地址 同时修改CS和IP的值 如jmp 2AE3:3 结果CS=2AE3H IP=0003H
    • jmp 某一合法寄存器 只修改IP的值 如jmp ax,将IP的值置为AX中的值(AX不变)
    小结

    8086CPU有四个段寄存器,CS是用来存放指令的段地址的段寄存器

    IP用来存放指令的偏移地址

    CS:IP指向的内容在任意时刻会被当做指令执行

    使用转移指令修改CS和IP的内容

    实验

    Debug命令:

    • R:查看,改变CPU寄存器内容
      • 直接-r查看寄存器内容
      • -r 寄存器名,改变寄存器内容
    • D:查看内存中内容
      • -d直接查看
      • -d 段地址:偏移地址 查看固定地址开始的内容
      • -d 段地址:偏移地址 结尾偏移地址 查看指定范围内存
    • E:改写内存中内容
      • -e 起始地址 数据 数据 数据 …
      • 提问方式修改 -e 段地址:偏移地址 从这个地址开始一个一个改,空格下一个,回车结束
      • 也可以写入字符 ‘a’
    • U:将内存中的机器指令翻译成汇编指令
      • -u 段地址:偏移地址
    • T:执行一条机器指令
      • -t 执行cs:ip指向的命令
    • A:以汇编指令格式在内存中写入一条机器指令
      • -a 段地址:偏移地址 从这个地址开始一行一行的写入汇编语句

    寄存器(内存访问)

    内存到寄存器的储存

    寄存器是16位的,可以存放一个字即两个字节,而内存中的一个存储单元是一字节。所以一个寄存器可以存两个存储单元的内容,高地址存储单元存在高位字节中,低地址存储单元存在低位字节中。

    字单元:存放一个字型数据的两个地址连续的内存单元。

    DS寄存器

    与CS类似,DS寄存器存放的是要从内存中读取的数据的段地址。我们想要使用mov指令从内存10000H(1000:0)中的数据送给AL时,如下:

    mov al,[0]

    后面的[0]指的是内存的偏移地址是0,CPU会自动从DS寄存器中提取段地址,所以应该首先将段地址1000H写入DS寄存器中。但却不能直接使用mov ds,1000指令,只能从其他寄存器中转传入DS寄存器。所以完整命令如下:

    mov bx,1000
    mov ds,bx
    mov al,[0]
    • 1
    • 2
    • 3

    当然,从AL寄存器中将数据送入内存只要反过来使用mov就可以了,mov [0],al

    如果需要传输字型数,只要使用对应的16位寄存器就可以了,传输的是以相应地址开始的一个字型数据(连续两个字节)。如mov [0],cx。

    mov,add,sub

    mov常见语法:

    mov 寄存器,数据       mov ax,8
    mov 寄存器,寄存器     mov ax,bx
    mov 寄存器,内存单元    mov ax,[0]
    mov 内存单元,寄存器    mov [0],ax
    mov 段寄存器,寄存器    mov ds,ax
    mov 寄存器,段寄存器    mov ax,ds
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    add,sub常见语法:

    add 寄存器,数据        add ax,8
    add 寄存器,寄存器      add ax,bx
    add 寄存器,内存单元    add ax,[0]
    add 内存单元,寄存器    add [0],ax
    subadd一样
    • 1
    • 2
    • 3
    • 4
    • 5

    注意,add,sub不可以操作段寄存器。

    栈是一种后进先出的存储空间,从栈顶出栈入栈。LIFO(last in first out)

    入栈指令:push ax ax中的数据送入栈顶

    出栈指令:pop ax 栈顶送入ax

    入栈和出栈指令都是以字为单位的。P58图

    栈寄存器SS,SP与push,pop

    CPU通过SS寄存器和SP寄存器来知道栈的范围,段寄存器SS存放的是栈顶的段地址,SP寄存器存放的是栈顶的偏移地址。所以,任意时刻SS:SP指向栈顶元素。

    指令push ax执行过程:

    1. SP=SP-2,SP指针向前移动两格代表新栈顶
    2. AX中的数据送入SS:SP目前指向的内存字单元,P59图

    所以栈顶在低地址,栈底在高地址。初始状态下,SP指向栈底的下一个单元。

    反之pop ax执行过程相反。

    8086CPU并不会自己检测push是否会超栈顶,pop是否会超栈底。

    push和pop可以加寄存器,段寄存器,内存单元(直接偏移地址[address])

    指定栈空间通常通过指定SS来进行,如:

    指定10000H~1000FH为栈空间
    mov ax,1000
    mov ss,ax
    mov sp 0010
    • 1
    • 2
    • 3
    • 4

    注:将一个寄存器清零 sub ax,ax 两个字节,mov ax,0 三个字节

    注:若设定一个栈段为10000H~1FFFFH,栈空的时候SP=0(要知道入栈操作先SP-2,然后再送入栈)

    实验

    Debug中的t命令一次执行一条指令,但如果执行的指令修改了ss段寄存器,下一条命令也会紧跟着执行(中断机制)。

    简单编程

    一个汇编语言程序
    1. 编写
    2. 编译(masm5.0)
    3. 连接
    一些伪指令功能
    assume cs:codesg
    
    codesg segment
    
    mov ax,0123
    mov bx,0456
    add ax,bx
    add ax,ax
    
    mov ax,4c00
    int 21
    
    codesg ends
    
    end
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    涉及到的一些知识:

    • XXX segment···XXXends
      • segment和ends成对出现,代表一个段的开始和结束。
      • 一个汇编程序可以有多个段,代码,数据和栈等,至少要有一个段。
    • end
      • end代表一个汇编程序结束,遇到end编译器停止编译。
    • assume
      • assume 假设,假设某一个段寄存器和程序中的一个段关联。
      • 可以理解为将这个段寄存器指向程序段的段地址
    • 标号(codesg)
      • 一个标号代表一个地址
    • 程序返回mov ax,4c00 int 21
      • 暂时记住这两条指令代表程序返回

    编译和连接方法,P83。

    注:编译器只能发现语法错误而无法发现逻辑错误。

    CPU执行一个程序,需要有另一个程序将它加载进内存(即将CS:IP指向它),一般情况下我们通过DOS执行这个.exe,所以是DOS程序将它加载进入内存。当这个程序运行结束,再返回DOS程序继续执行。如果是DOS调用Debug调用.exe,那么先返回Debug再返回DOS。

    DOS加载一个.exe时,先在内存中找到一段内存,起始段地址SA,然后分配256字节的PSP区域,用来和被加载程序通信。在之后的段地址SA+10就是程序开始的段地址。CS:IP指向它,DS=SA。

    注:在Debug中,最后的int 21指令要使用P命令执行。

    [BX]和loop指令

    内存单元的描述

    内存单元可以使用[数字]表示,当然也可以使用[寄存器]表示,如[bx],mov ax,[bx],mov al,[bx]

    为了表示方便,使用()来表示一个内存单元或寄存器中的内容,如(ax),(20000H),或((dx)*16+(bx))表示ds:bx中的内容,但不可写为(1000:0),((dx):0H)。而(X)中的内容由具体寄存器名或运算来决定。

    我们使用idata来表示常亮。所以以下语句可以这么写:mov ax,[idata] mov ax,idata。

    loop指令

    loop指令格式:loop 标号。

    loop指令通常用来实现循环功能,当执行loop指令时,CPU进行两步操作:

    1. (cx)=(cx)-1
    2. (cx)不为零则跳至标号处执行程序。

    所以CX中存放的是循环次数,一个简单的例子如下(计算2^12):

    assume cs:code
    code segment
    
    mov ax,2
    
    mov cx,11
    s:add ax,ax
    loop s
    
    mov ax,4c00h
    int 21h
    
    code ends
    end
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    所以使用loop注意三点:

    1. 先设置cx的值 mov cx,循环次数
    2. 设置标号与执行循环的程序段 s:执行程序段
    3. 在程序段最后写loop loop

    注:在汇编语言中,数据不能以字母开头,所以大于9fffH的数据,要在开头加0,如0A000H

    注:debug中G命令 g 0012表示CPU从当前CS:IP开始一直执行到0012处暂停。P命令可以将loop部分一次执行完毕,直到(CX)=0,或使用g loop的下一条命令。

    Debug和masm编译器对指令的不同处理

    mov ax,[0]这条指令在Debug和masm中有着不同的解释,Debug是将DS:0内存中的数据送给AX,而masm中则是mov ax,0,即将0送入AX。

    解决方法1:先将偏移地址送入BX,然后再使用mov ax,[bx]

    解决方法2:直接显式给出地址,如mov al,ds:[0] (相应的段寄存器还有CS,SS,ES这些在汇编语言中可以称为“段前缀”)当然,这种写法通过编译器之后会变成Debug中的mov al,[0]

    注:inc bx bx值加一

    安全的编程空间

    在之前没有提到的一个问题,如果在写程序之前不看一眼要操作的内存,就直接开始使用的话,万一改写了内存中重要的系统数据,可能会引起系统崩溃。所以我们一般在一个安全的内存空间中操作。一般操作系统和合法程序都不会使用0:200~0:2ff这256字节的空间,所以我们可以在这里操作。

    学习汇编语言的目的就是直接和硬件对话,而不理会操作系统,这在DOS(实模式)下是可以做到的,但在windows或Unix这种运行与CPU保护模式的操作系统上却是不可能的,因为这种操作系统已经将CPU全面严格的管理了。

    段前缀的使用

    将ffff:0~ffff:b中的数据转存入0:200~0:20b中:

    assume cs:code
    code segment
    
    mov ax,0ffffh
    mov ds,ax
    
    mov ax,0020h
    mov es,ax
    
    mov bx,0
    
    mov cx,12
    s:mov dl,[bx]
    mov es:[bx],dl
    inc bx
    loop s
    
    mov ax,4c00h
    int 21h
    
    code ends
    end
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    [bx]直接使用的时候默认段前缀是ds,但要使用其他的段前缀,如es就要在前面加上。

    程序的段

    数据段

    一般一个程序想要使用内存空间,有两种方法,在程序加载的时候系统分配或在需要使用的时候向系统申请,我们先考虑第一种情况。所以我们应事先将所需的数据存入内存中的某一段中,但我们又不可以随意的指定内存地址,以下面的求8个数据累加和的代码为例:

    assume cs:code
    code segment
    
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
    
    mov bx,0
    mov ax,0
    
    mov cx,8
    s:add ax,cs:[bx]
    add bx,2
    loop s
    
    mov ax,4c00h
    int 21h
    
    code ends
    end
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    代码第一行的dw是定义字类型数据,define word的意思。这里定义了8个字类型数据,占16字节。由于是在程序最开始定义的dw,所以数据段的偏移地址为0,也就是说第一个数据0123h的地址是CS:[0]第二个0456h的地址是CS:[2]以此类推。

    所以这个程序加载之后CS:IP指向的是数据段的第一个数据,我们要是想成功执行,需要把IP置10,指向第一条指令mov bx,0,所以我们想要直接执行(不在Debug中调整IP)的话,需要指定程序开始的地方:

    ···
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
    
    start:mov bx,0
    ···
    code ends
    end start
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在第一条指令前加start,后面的end变成end start,end除了通知编译器程序在哪里结束之外,也可以通知程序的入口在哪,也就是第一条语句,在这里编译器就知道了mov bx,0是程序的第一条指令。也就是说,我们想要CPU从何处开始执行程序,只要在源程序中使用end 标号指定就好了。

    所以有如下框架:

    assume cs:code
    code segment
    ···数据···
    start:
    ···代码···
    code ends
    end start
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    栈段

    看下面一段使8个数逆序存放的代码:

    assume cs:codesg
    codesg segment
    
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
    dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    
    start:mov ax,cs
    mov ss,ax
    mov sp,30h
    
    mov bx,0
    mov cx,8
    s:push cs:[bx]
    add bx,2
    loop s
    
    mov bx,0
    mov cx,8
    s0:pop cs:[bx]
    add bx,2
    loop s0
    
    mov ax,4c00h
    int 21h
    
    codesg ends
    end start
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在定义了8个字型数据之后,又定义了16个取值为0的字型数据,用作栈空间。所以dw这个定义不仅仅用来定义数据,也可以用来开辟内存空间留给之后的程序使用。

    数据,代码,栈的程序段

    在8086CPU中,一个段的长度最大为64KB,所以如果我们将数据或栈空间定义的比较大,就不能像前面一样编程了。我们需要将代码,数据,栈放入不同的段中:

    assume cs:code,ds:data,ss:stack
    data segment
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
    data ends
    
    stack segment
    dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    srack ends
    
    code segment
    start:mov ax,stack
    mov ss,ax
    mov sp,20h
    
    mov ax,data
    mov ds,ax
    
    mov bx,0
    
    mov cx,8
    s:push [bx]
    add bx,2
    loop s
    
    mov bx,0
    
    mov cx,8
    s0:pop [bx]
    add bx,2
    loop s0
    
    mov ax,4c00h
    int 21h
    
    code ends
    end start
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    我们可以这样在写代码时就将程序分为几个段,这段代码中,mov ax,data的意思是将data段的段地址送入ax寄存器。但我们不可以使用mov ds,data这样是错误的,因为在这里data被编译器视为一个数值。

    在这里将数据命名为data,代码命名为code,栈命名为stack只是为了方便阅读,CPU并不能理解,和start,s,s0一样,只在源程序中使用。而assume cs:code,ds:data,ss:stack这段代码也并不能让CPU的cs,ds,ss指向对应的段,因为assume是伪指令,CPU并不认识,它是由编译器执行的。源程序中end start语句指明了程序的入口,在这个程序被加载后,CS:IP被指向start处,开始执行第一条语句,这样CPU才会将code段当做代码执行。而当CPU执行

    mov ax,stack
    mov ss,ax
    mov sp,20h
    • 1
    • 2
    • 3

    这三条语句后才会将stack段当做栈空间开使用。也就是说,CPU如何区分哪个段的功能,全靠我们使用汇编指令对ds,ss,cs寄存器的内容设置来指定。

    灵活定位内存地址

    and和or指令

    and:逻辑与指令,按位与运算,如:

    mov al,01100011B
    and al,00111011B
    • 1
    • 2

    执行结果是al=00100011B,所以我们想要把某一位置零的时候可以使用and指令。

    or:逻辑或指令,按位或运算,如:

    mov al,01100011B
    or al,00111011B
    • 1
    • 2

    执行结果是al=01111011B,or指令可以将相应位置1。

    ASCII码和字符形式的数据

    在汇编语言中我们可以使用’···’的方式指明数据是以字符形式给出的,编译器会自动将它们转化为ASCII码。例如:

    assume cs:code,ds:data
    data segment
    db 'unIX'
    db 'foRK'
    data ends
    code segment
    start:mov al,'a'
    mov bl,'b'
    mov ax,4c00h
    int 21h
    code ends
    end start
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    db和dw类似,只不过定义的是字节型数据,然后通过’unIX’相继在接下来四个字节中写下75H,6EH,49H,58H即unIX的ASCII值。同理,mov al,’a’也是将’a’的ASCII值61H送入al寄存器。

    使用and和or指令改变一串字符串字母的大小写,将第一串全变为大写,第二串全变为小写:

    首先分析ASCII码:

    大写  十六进制    二进制         小写  十六进制    二进制
     A      41      01000001        a       61     01100001
     B      42      01000010        b       62     01100010
     C      43      01000011        c       63     01100011
    • 1
    • 2
    • 3
    • 4

    可见,只有第5位(从右往左数,从0开始计数)在大写和小写的二进制中是不一样的,所以我们只要把所有字母的二进制第五位置零,那就是大写,置1就是小写。代码如下:

    assume cs:codesg,ds:datasg
    
    datasg segment
    db 'BaSiC'
    db 'iNfOrMaTiOn'
    datasg ends
    
    codesg segment
    start:mov ax,datasg
    mov ds,ax
    mov bx,0
    
    mov cx,5
    s:mov al,[bx]
    and al,11011111B
    mov [bx],al
    inc bx
    loop s
    
    mov bx,5
    
    mov cx,11
    s0:mov al,[bx]
    or al,00100000B
    mov [bx],al
    inc bx
    loop s0
    
    mov ax,4c00h
    int 21h
    
    codesg ends
    end start
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    [bx+idata]的内存表示方法与数组处理

    除了使用[bx]来表示一个内存单元外,我们还可以使用[bx+idata]来表示一个内存单元,他表示的意思是偏移地址为(bx)+idata(bx中的数值加idata)的内存单元。当然也可写为[idata+bx],除此之外还可写为,200[bx],[bx].200。

    既然有了这种表示方法,我们就可以使用这种方法来操作数组,刚才将两个字符串改变大小写的代码的循环部分可以如下优化:

    ···
    s:mov al,[bx]
    and al,11011111B
    mov [bx],al
    mov al,[5+bx]
    or al,00100000B
    mov [5+bx],al
    inc bx
    loop s
    ···
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    当然也可写为0[bx]和5[bx],注意这种写法和C语言中数组的相似之处:C语言中数组表示为a[i],汇编语言中表示为5[bx]。

    SI和DI寄存器

    SI和DI功能和BX相似,但不可以拆分为两个8位寄存器。也就是说下面代码等价:

    mov bx|si|di,0
    mov ax,[bx|si|di]
    mov ax,[bx|si|di+123]
    • 1
    • 2
    • 3

    所以在这里可以使用更方便的方式:[bx+si]和[bx+di],这两个式子表示偏移地址为(bx)+(si)的内存单元,使用方法如:mov ax,[bx+si]等价于mov ax,[bx][si]。

    当然,有了这些表示方法,自然就有[bx+si+idata]和[bx+di+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
    • 2
    • 3
    • 4
    • 5

    那我们总结一下这些内存寻址方法:

    • [idata]用一个常量表示偏移地址,直接定位一个内存单元
    • [bx]用一个变量表示偏移地址,定位一个内存单元
    • [bx+idata]用一个常量和一个变量表示偏移地址,可在一个起始地址的基础上间接定位一个内存单元
    • [bx+si]用两个变量表示偏移地址
    • [bx+si+idata]用两个变量和一个常量表示偏移地址

    使用双循环,使用一个寄存器暂存cs的值,如:

    ···
    mov cx,4
    s0:mov dx,cx
    mov si,0
    
    mov cx,3
    s:mov al,[bx+si]
    and al,11011111b
    mov [bx+si],al
    inc si
    loop s
    
    add bx,16
    mov cx,dx
    loop s0
    ···
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    假如循环比较复杂,没有多余的寄存器可用,我们可以使用内存暂存cx或其他数据:

    ···
    dw 0
    ···
    mov cx,4
    s0:mov ds:[40H],cx
    mov si,0
    
    mov cx,3
    s:mov al,[bx+si]
    and al,11011111b
    mov [bx+si],al
    inc si
    loop s
    
    add bx,16
    mov cx,ds:[40H]
    loop s0
    ···
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    这么使用的话注意需要在数据段声明用来暂存的内存,好在程序加载时分配出来。当然,在需要暂存的地方,还是建议使用栈:

    ···
    dw 0,0,0,0,0,0,0,0
    ···
    mov ax,stacksg
    mov ss,ax
    mov sp,16
    ···
    mov cx,4
    s0:push cx
    mov si,0
    
    mov cx,3
    s:mov al,[bx+si]
    and al,11011111b
    mov [bx+si],al
    inc si
    loop s
    
    add bx,16
    pop cx
    loop s0
    ···
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    数据处理的两个基本问题

    两个基本问题
    1. 处理的数据在什么地方
    2. 要处理的数据有多长

    接下来的讨论中,使用reg来表示一个寄存器,使用sreg来表示一个段寄存器。所以:

    • reg:ax,bx,cx,dx,ah,al,bh,bl,ch,cl,dh,dl,sp,bp,si,di
    • sreg:ds,ss,cs,es
    bx,si,di和bp

    在8086CPU中,只有这四个寄存器可以使用[···]来进行内存寻址,可以单个出现,或以下面组合出现(常数可以随意出现在这些表示方法中):

    • bx+si/di
    • bp+si/di

    注:如果使用了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)+idata)
    • 1
    • 2
    • 3
    数据的位置

    绝大部分机器指令都是用来处理数据的,基本可分为读取,写入,运算。在机器指令这个层面上,并不关心数据是什么,而关心指令执行前数据的位置。一般数据会在三个地方,CPU内部,内存,端口。

    汇编语言中使用三个概念来表示数据的位置:

    • 立即数(idata)
      • 对于直接包含在机器指令中的数据,在汇编语言中称为立即数
      • 例:mov ax,1 add bx,2000h
    • 寄存器
      • 指令要处理的数据在寄存器中,在汇编指令中给出相应寄存器名
      • 例:mov ax,bx mov ds,ax
    • 段地址(SA)和偏移地址(EA)
      • 指令要处理的数据在内存中,在指令中使用[X]方式给出,SA在某个段寄存器中
      • 例:mov ax,[0] mov ax,[di]

    总结一下寻址方式:

    寻址方式 含义 名称
    [idata] EA=idata;SA=(DS) 直接寻址
    [bx|si|di|bp] EA=(bx|si|di|bp);SA=(DS) 寄存器间接寻址
    [bx|si|di|bp+idata] EA=(bx|si|di|bp+idata);SA=(DS) 寄存器相对寻址
    [bx|bp+si|di] EA=(bx|bp+si|di);SA=(DS|SS) 基址变址寻址
    [bx|bp+si|di+idata] EA=(bx|bp+si|di+idata);SA=(DS|SS) 相对基址变址寻址
    数据的长度

    8086CPU中可以指定两种尺寸的数据,byte和word,所以在使用数据的时候要指明数据尺寸。

    • 在有寄存器参与的时候使用寄存器的种类区分
      • 字:mov ax,1
      • 字节:mov al,1
    • 在没有寄存器参与的时候,使用X ptr指明内存单元长度,X是word或byte
      • 字:mov word ptr ds:[0],1 add word ptr [bx],2
      • 字节:mov byte ptr ds:[0],1 add byte ptr [bx],2
    • 其他默认指明处理类型的指令
      • push [1000H],push默认只进行字操作

    灵活使用寻址方式的例子,修改下面内存空间中的数据:

    段seg:60

    起始地址 内容
    00 ‘DEC’
    03 ‘Ken Oslen’
    0C 137
    0E 40
    10 ‘PDP’
    ···
    mov ax,seg
    mov ds,ax
    mov bx,60h
    
    mov word ptr [bx].0ch,38    ;第三字段改为38
    
    add word ptr [bx].0eh,70    ;第四字段改为70
    
    mov si,0
    mov byte ptr [bx].10h[si],'v'   ;修改最后一个字段的三个字符
    inc si
    mov byte ptr [bx].10h[si],'A'
    inc si
    mov byte ptr [bx].10h[si],'X'
    ···
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这段代码中地址的使用类似c++中结构体的使用。[bx].idata.[si],就类似与c++中的dec.cp[i]。dec是结构体,cp是结构体中的字符串成员,[i]表示第几个字符。

    div指令

    div是除法指令,需要注意以下三点:

    • 除数:8位或16位,在一个reg或内存单元中
    • 被除数:默认在AX或DX中,如果除数8位,被除数则为16位,放在AX中;如果除数16位,则被除数32位,在DX和AX中,DX存放高16位,AX放低16位。
    • 结果,除数8位,结果(商)存放在AL中,AH存放余数;如果除数16位,则AX存放商,DX存放余数

    格式:div reg或div 内存单元,所以div byte ptr ds:[0]表示:

    (al)=(ax)/((ds)*16+0)的商;
    (ah)=(ax)/((ds)*16+0)的余数;
    • 1
    • 2

    div word ptr es:[0]表示:

    (al)=[(dx)*10000H+(ax)]/((es)*16+0)的商
    (ah)=[(dx)*10000H+(ax)]/((es)*16+0)的余数
    • 1
    • 2

    例:计算100001/100,因为100001(186A1H)大于65535,则需要存放在ax和dx两个寄存器,那么除数100只能存放在一个16位的寄存器中,实现代码:

    mov dx,1
    mov ax,86A1H
    mov bx,100
    div bx
    • 1
    • 2
    • 3
    • 4

    执行之后(ax)=03E8H(1000),(dx)=1。

    伪指令dd

    dd是一个伪指令,类似dw,但dd是用来定义dword(double word,双字),如:

    dd 1  ;2字,4字节
    dw 1  ;1字,2字节
    db 1  ;1字节
    • 1
    • 2
    • 3

    将data段中第一个数据除以第二个数据,商存入第三个数据:

    ···
    data segment
    dd 100001
    dw 100
    dw 0
    data ends
    ···
    mov ax,data
    mov ds,ax
    mov ax,ds:[0]
    mov dx,ds:[2]
    div word ptr ds:[4]
    mov ds:[6],ax
    ···
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    总结一下div相关:

    • div后面跟的是除数
    • 被除数位数是除数两倍
    • 被除数存在ax中或ax+dx(ax低,dx高)
    • 商在ax或al中,余数在ah或dx中(高余数,低商)
    dup

    dup是一个操作符,由编译器识别,和db,dw,dd配合使用,如:

    db 3 dup (0)表示定义了三个值是0的字节,等价于db 0,0,0

    db 3 dup (1,2,3)等价于db 1,2,3,1,2,3,1,2,3 共九个字节

    db 3 dup (‘abc’,’ABC’)等价于db ‘abcABCabcABCabcABC’

    综上,db|dw|dd 重复次数 dup (重复内容)

    转移指令原理

    转移指令

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

    • 转移行为:
      • 只修改IP,称为段内转移,如jmp ax
      • 同时修改CS和IP,称为段间转移,如jmp 1000:0
    • 修改范围(段内转移):
      • 短转移:修改IP范围-128~127
      • 近转移:修改IP范围-32768~32767
    • 转移指令分类:
      • 无条件转移:jmp
      • 条件转移
      • 循环指令
      • 过程
      • 中断
    offset操作符

    offset是由编译器处理的符号,它能去的标号的偏移地址,如:

    start:mov ax,offset start
    s:mov ax,offset s
    • 1
    • 2

    这里就是将start和s的偏移地址分别送给ax,也就是0和3

    jmp指令

    jmp是无条件转移指令,可以只修改IP也可以同时修改CS和IP,只要给出两种信息,要转移的目的地址和专一的距离。

    依据位移的jmp指令:jmp short 标号(转到标号处执行指令)。这个指令实现的是段内短转移,对IP修改范围是-128~127,指令结束后CS:IP指向标号的地址,如:

    0BBD:0000   start:mov ax,0  (B80000)
    0BBD:0003   jmp short s   (EB03)
    0BBD:0005   add ax,1    (050100)
    0BBD:0008   s:inc ax    (40)
    • 1
    • 2
    • 3
    • 4

    执行之后ax值为1,因为跳过了add指令。

    还应注意的是,jmp short短转移指令并不会在机器码中直接写明需要转移的地址(0BBD:0008),jmp的机器码是EB03并没有包含转移的地址,这里的转移距离是相对计算而出的地址,来看下面的执行过程:

    1. (CS)=0BBDH,(IP)=0006H,CS:IP指向EB03(jmp short s)
    2. 读取指令EB03进入指令缓冲器
    3. (IP)=(IP)+指令长度,即(IP)=(IP)+2=0008H,之后CS:IP指向add ax,1
    4. CPU指向指令缓冲器中的指令EB03
    5. 执行之后(IP)=000BH,指向inc ax

    在jmp short s的机器码中,包含的并不是转移的地址,而是转移的位移,这里的位移是相对计算出来的,用8位一字节来表示,所以表示范围是-128~127,用补码表示。计算方法如是,8位位移=标号处地址-jmp下一条指令的地址。当然还有一种类似的指令是jmp near ptr 标号,是近转移,原理一样,只是表示位移的是字类型16位,表示范围-32768~32767。

    jmp+地址远转移

    jmp far ptr 标号实现的是段间转移,也就是远转移,它的机器码中指明了转移的目的地址的CS和IP的值,如下面例子:

    0BBD:0000   start:mov ax,0    (B80000)
    0BBD:0003   mov bx,0    (BB0000)
    0BBD:0006   jmp far ptr s    (EA0B01BD0B)
    0BBD:000B   db 256 dup (0)    
    0BBD:010B   s:add ax,1    
    0BBD:010X   inc ax
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可以看出,jmp的机器码中明确指明了跳转位置s的地址0BBD:010B,在低位的是IP的值,高位的是CS的值。

    jmp+寄存器|内存转移

    jmp+寄存器:jmp 16位reg,实现的是(IP)=(16位reg),之前讨论过,直接修改IP的值为寄存器中的值。

    jmp+内存:jmp加内存使用的时候有两种用法:

    • jmp word ptr 内存单元地址(段内转移)
      • 从内存单元地址处开始存放一个座位转移目的的偏移地址的字
      • 内存单元支持任何寻址方式
      • 如jmp word ptr ds:[0],执行后(IP)=0123H(ds:[0]中的值是123H)
    • jmp dword ptr 内存单元地址(段间转移)
      • 从内存单元地址处开始存放两个字,高位存放段地址,低位偏移地址作为转移的目的地址
      • (CS)=(内存单元地址+2),(IP)=(内存单元地址),支持任一种寻址方式
      • 如jmp dword ptr [bx]跳转到0:123H
    jcxz指令

    jcxz指令为条件转移指令,所有的条件转移指令都是短转移,转移范围是-128~127。使用格式是jcxz 标号,功能是如果(cx)=0则跳转到标号处执行;如果(cx)!=0,那么什么也不做继续执行代码。

    loop指令

    loop为循环指令,所有的循环指令都是短转移,转移范围是-128~127。使用格式是loop 标号,功能是如果(cx)!=0那么跳转到标号处执行;如果(cx)=0那么什么也不做继续执行程序。

    根据位移进行转移的指令总结

    下面几条指令是根据位移进行转移(相对计算转移位置,而不是直接提供转移目的的IP和CS的值)

    • jmp short 标号
    • jmp near ptr 标号
    • jcxz 标号
    • loop 标号

    这些指令之所以是间接计算标号的位置,是为了方便在代码中浮动装配,使得循环体或这些指令的代码段在任何位置都可以执行(不要超跳转范围)。而编译器会对跳转的范围进行检测,如果跳转超过了范围,编译器会报错。

    注:jmp 2100:0是debug使用的汇编指令,编译器并不认识。

    call和ret指令

    ret和retf

    ret和call都是转移指令,都是修改IP的值,或同时修改CS和IP。

    ret指令用栈中的数据修改IP,实现的是近转移;retf指令用栈中的数据修改CS和IP的值,实现远转移。格式:直接用 ret。

    ret执行步骤:

    1. (IP)=((SS)*16+(SP))
    2. (SP)=(SP)+2

    retf执行步骤:

    1. (IP)=((SS)*16+(SP))
    2. (SP)=(SP)+2
    3. (CS)=((SS)*16+(SP))
    4. (SP)=(SP)+2

    所以ret指令相当于 pop ip,执行retf指令相当于执行pop ip,pop cs。

    call指令

    call指令也是一个转移指令,执行格式:call 目标(具体使用接下来说明),call的执行步骤:

    1. 将当前的IP或CS和IP入栈
    2. 转移

    call不能实现短转移,但它实现转移的原理和jmp相同。

    根据位移转移:call 标号,近转移,16位转移范围,也是使用相对的转移地址。

    执行步骤:

    1. (SP)=(SP)-2
    2. ((SS)*16+(SP))=(IP)
    3. (IP)=(IP)+16

    所以执行这条命令相当于执行push ip,jmp near ptr 标号。

    直接使用地址进行(远)转移:call far ptr 标号,执行步骤:

    1. (SP)=(SP)-2
    2. ((SS)*16+(SP))=(CS)
    3. (SP)=(SP)-2
    4. ((SS)*16+(SP))=(IP)
    5. (CS)=标号所在的段的段地址
    6. (IP)=标号的偏移地址

    所以执行call far ptr 标号相当于执行push cs,push ip,jmp far ptr 标号

    使用寄存器的值作为call的跳转地址:call 16位reg

    1. (SP)=(SP)-2
    2. ((SS)*16+(SP))=(IP)
    3. (IP)=(16为reg)

    相当于执行push ip,jmp 16位reg

    使用内存中的值作为call的跳转地址:call word ptr 内存单元地址,当然还有call dword ptr 内存单元地址,这样进行的就是远转移。

    联合使用ret和call

    联合使用ret和call实现子程序的框架:

    assume cs:code
    code segment
    main:
    ···
    call sub1
    ···
    mov ax,4c00h
    int 21h
    
    sub1:
    ···
    call sub2
    ···
    ret
    
    sub2:
    ···
    ret
    code ends
    end main
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    mul指令

    mul是乘法指令,使用时应注意,两个相乘的数,要么都是8位,要么都是16位,如果是8位,那么其中一个默认放在al中,另一个在一个8位reg或字节内存单元中;若是16位,则一个默认在ax中,另一个在16位reg或字内存单元中。如果是8位乘法, 则结果放在ax中,结果是16位;若是16位乘法,结果默认在ax和dx中,dx高位,ax低位,共32位。

    格式:mul reg 或 mul 内存单元,支持内存单元的各种寻址方式。

    如mul word ptr [bx+si+8]代表:

    (ax)=(ax)*((ds)*16+(bx)+(si)+8)低16位
    (dx)=(ax)*((ds)*16+(bx)+(si)+8)高16位
    • 1
    • 2

    例:计算100*10

    mov al,100
    mov bl,10
    mul bl
    • 1
    • 2
    • 3
    参数的传递和模块化编程

    看下面一段程序:计算data中第一行的数的立方存在第二行

    assume cs:code
    data segment
    dw 1,2,3,4,5,6,7,8
    dd 0,0,0,0,0,0,0,0
    data ends
    
    code segment
    start:mov ax,data
    mov ds,ax
    mov si,0
    mov di,16
    
    mov cs,8
    s:mov bx,[si]
    call cube
    mov [di],ax
    mov [di].2,dx
    add si,2
    add di,4
    loop s
    
    mov ax,4c00h
    int 21h
    
    cube:mov ax,bx
    mul bx
    mul bx
    ret
    
    code ends
    end start
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    寄存器冲突

    观察下面将data中的数据全转化为大写的代码:

    assume cs:code
    data segment
    db 'word',0
    db 'unix',0
    db 'wind',0
    db 'good',0
    data ends
    
    code segment
    start:mov ax,data
    mov ds,ax
    mov bx,0
    
    mov cx,4
    s:mov si,bx
    call capital
    add bx,5
    loop s
    
    mov ax,4c00h
    int 21h
    
    capital:mov cl,[si]
    mov ch,0
    jcxz ok
    and byte ptr [si],11011111b
    inc si
    jmp short capital
    ok:ret
    code ends
    end start
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    这段代码有一个问题出在,主函数部分使用cx设置循环次数4次,在循环中调用了子函数,而子函数中有一个判断语句jcxz也是用了cx,并且在之前修改了cx的值,造成逻辑错误。虽然修改的方法有很多,但我们应遵循以下的标准:

    • 编写调用子程序的程序不必关心子程序使用了什么寄存器
    • 编写子程序不用关心调用子程序的程序使用了什么寄存器
    • 不会发生寄存器冲突

    针对这三点,我们可以如下修改代码:

    ···
    capital:push cx
    push si
    
    change:mov cl,[si]
    mov ch,0
    jcxz ok
    and byte ptr [si],11011111b
    inc si
    jmp short change
    
    ok:pop si
    pop cx
    ret
    ···
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    虽然和上面的程序中没有冲突的是si,但我们保险起见,在子程序开始时将子程序用到的所有的寄存器的内容存入栈中,在返回之前在出栈回到相应寄存器中。这样无论调用子程序的程序使用了什么寄存器,都不会产生寄存器冲突。

    标志寄存器

    标志寄存器

    CPU中有一种特殊的寄存器——标志寄存器(不同CPU中的个数和结构都可能不同),主要有以下三种作用:

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

    8086CPU中的标志寄存器有16位,其中存储的信息通常被称为程序状态字(PSW),标志寄存器以下简称为flag。标志位如图:

    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
    • 1
    • 2

    如上图所示,1,3,5,12,13,14,15位没有使用,没有任何意义,而其他几位都有不同的含义。

    ZF标志

    ZF位于flag第6位,零标志位,功能是记录相关指令执行后结果是否为0,如果结果为0,则ZF=1,否则ZF=0。如:

    mov ax,1
    sub ax,1
    • 1
    • 2

    执行后结果为0,ZF=1。一般情况下,运算指令(如add,sub,mul,div,inc,or,and)影响标志寄存器,而传送指令(如mov,push,pop)不影响标志寄存器。

    PF标志

    flag的第2位是PF标志位,奇偶标志位,功能是记录相关指令执行后,其结果的所有bit中1的个数是否为偶数,若1的个数是偶数,pf=1,如果是奇数,fp=0。如:

    mov al,1
    add al,10
    • 1
    • 2

    执行后结果为00001011b,有3个1,所以PF=0。

    SF标志

    flag的第7位是SF标志位,符号标志位,它记录相关指令执行后,结果是否为负,如果结果为负,则sf=1,结果为正,sf=0。计算机中通常用补码表示数据,一个数可以看成有符号数或无符号数,如:

    00000001B,可以看成无符号1或有符号+1
    10000001B,可以看成无符号129或有符号-127
    • 1
    • 2

    也就是说对于同一个数字,可以当做有符号数运算也可以当做无符号数运算。如:

    mov al,10000001b
    add al,1
    • 1
    • 2

    这段代码结果是(al)=10000010b,可以将add指令进行的运算当做无符号运算,那么相当于129+1=130,也可以当做有符号运算,相当于-127+1=-126。SF标志就是在进行有符号运算的时候记录结果的符号的,当进行无符号运算的时候SF无意义(但还会影响SF,只是对我们来说没有意义了)。

    CF标志

    flag的第0位是CF标志位,进位标志位,一般情况下载进行无符号运算时,他记录了运算结果的最高有效为向更高为的进位值,或从更高位的借位值。加入一个无符号数据是8位的,也就是0-7个位,那么在做加法的时候就可能造成进位到第8位,这时并不是丢弃这个进位,而是记录在falg的CF位上。如:

    mov al,98h
    add al,al
    • 1
    • 2

    执行后al=30h,CF=1。当两个数据做减法的时候有可能向更高位借位,如97h-98h借位后相当于197h-198h,CF也可以用来记录借位,如:

    mov al,97h
    sub al,98h
    • 1
    • 2

    执行后(al)=FFH,CF=1记录了向更高位借位的信息。

    OF标志

    在进行有符号运算的时候,如果结果超过了机器能表示的范围称为“溢出”。机器能表示的范围是指如8位寄存器存放或一个内存单元存放,表示范围就是-128~127,16位同理。如果超出了这个范围就叫做溢出,如:

    mov al,98
    add al,99
    
    mov al,0F0H
    add al,088H
    • 1
    • 2
    • 3
    • 4
    • 5

    第一段代码(al)=(al)+99=98+99=197超过了8位能表示的有符号数的范围,第二段代码结果(al)=(al)+(-120)=(-16)+(-12-)=-136也超过了8位有符号的范围,所以计算的结果是不可信的。如第一段代码计算之后(al)=0C5H,换成补码表示的是-59,98+99=-59很明显是不正确的结果。

    flag的第11位是OF标志位,溢出标志位,一般情况下,OF记录有符号数运算结果是否溢出,如果溢出则OF=1,如果没有溢出,OF=0。所以CF是对无符号数的标志,OF是对有符号的标志。但对于一个运算指令,他们是同时生效的,只不过这个指令究竟是有符号还是无符号,是看实际的操作的。有符号CF无意义,无符号OF无意义。

    adc指令

    adc是带进位加法指令,利用了CF标志位上记录的进位值。格式:adc 操作对象1,操作对象2。功能:操作对象1=操作对象1+操作对象2+CF。如abc ax,bx实现的是(ax)=(ax)+(bx)+CF,如:

    mov ax,2
    mov bx,1
    sub bx,ax
    adc ax,1
    • 1
    • 2
    • 3
    • 4

    注意这段代码,首先ax中的值是2,bx中的值是1,然后进行(bx)-(ax)的计算,结果是-1造成了无符号的借位,此时CF=1,在进行adc ax,1时,进行的是(ax)+1+CF=2+1+1=4。仔细分析一下就可以发现,如果把整个加法分开,低位先相加,然后高位相加再加上进位CF, 就是一个完整的加法运算,也就是说add ax,dx这个指令可以拆分为:

    add al,bl
    adc ah,bh
    • 1
    • 2

    所以有了adc这个指令我们就可以完成一些更庞大的数据量的加法运算。如计算1EF000H+000H的值:

    mov ax,001eh
    mov bx,0f000h
    add bx,1000h
    adc ax,0020h
    • 1
    • 2
    • 3
    • 4

    注:inc和loop指令不影响CF位。

    sbb指令

    sbb和adc类似,是带借位的减法,格式:sbb 操作对象1,操作对象2,执行的功能是操作对象1=操作对象1-操作对象2-CF,如:sbb ax,bx即(ax)=(ax)-(bx)-CF。sbb指令影响CF。

    cmp指令

    cmp是比较指令,cmp的功能相当于减法,只是不保存结果。cmp执行后影响标志寄存器,其他相关指令通过识别被影响的标志位来得知结果。格式:cmp 操作对象1,操作对象2,执行功能是计算对操作对象1-操作对象2但不保存结果,仅仅根据结果对标志位进行设置,如:cmp ax,ax结果为0,但并不保存在ax中,执行之后zf=1,pf=1,sf=0,cf=0,of=0。若执行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=0zf=0
    若(ax)<=(bx)则(ax)-(bx)既可能借位,结果可能为0,cf=1zf=1
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    但实际上往往会出现溢出,如34-(-96)=82H(82H是-126的补码),但应该等于130超出了补码表示的范围,所以sf=1。我们可以同时检验sf和of两个来验证cmp的结果:cmp ah,bh

    • 若sf=1,of=0说明没有溢出,那么sf的计算结果正确(ah)<(bh)
    • 若sf=1,of=1说明出现了溢出,那么sf结果相反(ah)>(bh)
    • 若sf=0,of=1说明有溢出,那么sf结果相反(ah)<(bh)
    • 若sf=0,of=0说明没有溢出,那么结果正确(ah)>=(bh)
    检测比较结果的条件转移指令

    下面几条指令和cmp一起使用,检测不同的标志位来达到不同的条件跳转效果:

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

    指令中的字母含义如下:

    • e:equa;
    • ne:not equal
    • b:below
    • nb:not below
    • a:above
    • na:not above

    上面的检测都是在cmp进行无符号比较时的检测位,有符号数检测原理一样,只是检测的标志位不同而已。下面看一个例子,如果(ah)=(bh)则(ah)=(ah)+(ah),否则(ah)=(ah)+(bh)

    cmp ah,bh
    je s
    add ab,bh
    jmp short ok
    s:add ah,ah
    ok:···
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里注意的是,je检测的是zf位,而不管之前执行的是什么指令,只要zf=1就会发生转移,所以cmp的位置需要仔细的把控,当然是否和cmp配合使用也是取决于编程者,下面例子实现了统计data中数值为8的字节个数,然后用ax保存:

    ···
    data segment
    db 8,11,8,1,8,5,63,38
    data ends
    ···
    mov ax,data
    mov ds,ax
    mov bx,0
    mov ax,0
    mov cx,8
    s:cmp byte ptr [bx],8
    jne next
    inc ax
    next:inc bx
    loop s
    ···
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    DF标志位和串传送指令

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

    • df=0每次操作后si,di递增
    • df=1每次操作后si,di递减

    串传送指令,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是将DS:SI指向的内存单元中的字节送入ES:DI中,然后根据DF的值对SI和DI增减1

    同理mobsw就是将DS:SI指向的内存单元中的字送入ES:DI中,然后根据DF的值对SI和DI增减2

    但一般来说,movsb和movsw都是和rep联合使用的,格式:rep movsb,这相当于:

    s:movsb
    loop s
    • 1
    • 2

    所以rep的作用是根据cx的值重复执行后面的串传送指令,由于每次执行movsb之后si和di都会自行增减,所以使用rep可以完成(cx)个字节的传送。movsw也一样。

    由于DF位决定着串传送的方向,所以这里有两条指令用来设置df的值:

    clddf=0
    stddf=1
    • 1
    • 2

    例子:使用串传送指令将data段中第一个字符串复制到他后面的空间中:

    ···
    data segment
    db 'Welcome to masm!'
    db 16 dup (0)
    data ends
    
    mov ax,data
    mov ds,ax
    mov si,0
    mov es,ax
    mov di,16
    mov cx,16
    cld
    rep movsb
    ···
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    pushf和popf

    pushf的功能是将标志寄存器的值入栈,popf是出栈标志寄存器。有了这两个命令,就可以直接访问标志寄存器了,如:

    mov ax,0
    push ax
    popf
    • 1
    • 2
    • 3
    标志寄存器在Debug中的表示

    Debug中-r查看寄存器信息,最后有一段表示,下面列出我们已知的寄存器在Debug里的表示:

    标志 值1的标记 值0的标记
    of OV NV
    sf NG PL
    zf ZR NZ
    pf PE PO
    cf CY NC
    df DN UP

    内中断

    内中断的产生

    任何一个通用CPU都拥有执行完当前正在执行的指令后,检测到从CPU发来的中断信息,然后立即去处理中断信息的能力。这里的中断信息是指几个具有先后顺序的硬件操作,当CPU出现下面请看时会产生中断信息,相应的中断信息类型码(供CPU区分来源,是字节型,共256种)如下:

    • 除法错误,如执行div指令出现除法溢出 0
    • 单步执行 1
    • 执行into指令 4
    • 执行int指令 指令执行的int n后面的n就是一个字节型立即数,即为中断类型码
    中断处理和中断向量表

    CPU接收到中断信息之后,往往要对中断信息进行处理,而如何处理使我们编程决定的。而CPU通过中断向量表来根据中断类型找到处理程序的入口地址(CS:IP)也称为中断向量。

    中断向量表中存放着不同的中断类型对应的中断向量(处理程序的入口地址),中断向量表存放在内存中,8086PC指定必须放在内存地址0处,从0000:0000到0000:03FF的1024个单元存放中断向量表,每个表项占两个字,四个字节。

    CPU会自动根据中断类型找到对应的中断向量并设置CS和IP的值,这个过程称为中断过程,步骤如下:

    1. (从中断信息中)取得中断类型码
    2. 标志寄存器的值入栈(暂存)pushf
    3. 设置标志寄存器第8位TF和第9位IF的值为0 TF=0,IF=0
    4. CS内容入栈 push cs
    5. IP内容入栈 push ip
    6. 在中断向量表中找到对应的CS和IP值并设置 (ip)=(N*4),(cs)=(N*4+2)

    这么做的目的是,在中断处理之后还要回复CPU的现场(各个寄存器的值),所以先把那些入栈。

    中断处理程序和iret指令

    运行中的CPU随时都可能接收到中断信息,所以CPU随时都可能执行中断程序,执行的步骤:

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

    iret的指令功能是:pop ip pop cs popf(前面说到了,这三个寄存器的入栈是硬件自动完成的,所以iret是和硬件自动完成的步骤配合使用的)。

    以处理0号除法溢出中断为例,我们想要编写除法溢出的中断处理程序需要解决如下几步问题:

    1. 编写程序
    2. 找到一段没有使用的内存空间
    3. 将程序写入到内存
    4. 将内存中的程序的入口写入0号中断的向量表位置

    我们可以采取下面框架来完成这个过程:

    ···
    start do0安装程序
    设置中断向量表
    mov ax,4c00h
    int 21h
    
    do0 程序部分
    mov ax,4c00h
    int 21h
    ···
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以看出我们分成了两部分,第一部分称之为“安装”,第二部分是代码实现。安装部分的函数实现思路如下:

    设置es:di至项目的地址
    设置ds:si指向源地址
    设置cx为传输长度
    设置传输方向为正
    rep movsb
    设置中断向量表
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    实现如下:

    start:mov ax,cs
    mov ds,ax
    mov si,offset do0
    mov ax,0
    es,ax
    mov di,200h
    mov cx,offset do0end-fooset do0
    cld
    rep movsb
    ···
    do0:代码
    do0end:nop
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这里offset do0end-fooset do0的意思是do0到do0end的代码长度,-是编译器可以识别并运算的符号,也就是说编译器可以再编译时处理表达式,如8-4等。还要注意的是,假如代码部分要输出“owerflow!”的话,需要将输出的内容写在代码部分并写入选择的内存中,否则如果单单在这个安装程序开始开辟一段data段的话,是会在程序返回时被释放。如:

    do0:jmp short do0start
    db "overflow!"
    
    do0start:
    ···
    do0end:nop
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    单步中断

    当标志寄存器的TF标志位为1的时候,CPU会在执行一条语句之后将资源入栈,然后去执行单步中断处理程序,如Debug就是运行在单步中断的条件下的,它能让CPU每执行一条指令都暂停,然后我们可以查看CPU的状态。但CPU可以防止在运行单步中断处理程序的时候再发生中断然后又去调用单步中断处理程序……CPU可以将TF置零,这样就不会再中断了。CPU提供这个功能就是为了单步跟踪程序的执行。

    但需要注意的是,CPU并不会每次接收中断信息之后立即执行,在某些特定情况下它不会立即响应中断,如设置ss寄存器的时候如果接收到了中断信息,就不会响应。因为我们需要连续设置ss和ip的值,在debug中单步执行的时候也是,mov ss,ax和mov sp,0是在一步之内执行的,所以我们需要灵活使用这个特性,sp紧跟着ss执行,而不要在设置ss和sp之间插入其他指令。

    int指令

    int指令

    int指令也会引发中断,使用格式是int n,n就是int引发的中断类型码,int中断的执行过程:

    1. 获取类型码n
    2. 标志寄存器入栈,if=0,tf=0
    3. cs,ip入栈
    4. (ip)=(n*4),(cs)=(n*4+2)
    5. 执行n号中断的程序

    所以我们可以使用int指令调用任何一个中断的中断程序,如int 0调用除法溢出中断。一般情况下,系统将一些具有一定功能的小程序以中断的方式提供给程序调用,当然也可以自己编写,可以简称为中断例程。

    编写中断例程

    如编写中断7ch的中断例程,实现word型数据的平方,返回dx和ax中。求2*3456^2,代码:

    assume cs:code
    code segment
    start mov ax,3456
    int 7ch
    add ax,ax
    adc dx,dx
    mov ax,4c00h
    int 21h
    code ends
    end start
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    接下来写7ch的功能和安装程序,并修改7ch中断向量表:

    assume cs:code
    code segment
    start:mov ax,cs
    mov ds,ax
    mov si,offset sqr
    mov ax,0
    mov es,ax
    mov di,200h
    mov cx,offset sqrend-offset sqr
    cld
    rep movsb
    
    mov ax,0
    mov es,ax
    mov word ptr es:[7ch*4],200h
    mov word ptr es:[7ch*4+2],0
    
    mov ax,4c00h
    int 21h
    
    sqr:mul ax
    iret
    
    sqrend:nop
    code ends
    end start
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    编写7ch中断实现loop指令,主程序输出80个“!”:

    ···
    start mov ax,0b800h
    mov es,ax
    mov di,160*12
    mov bx,offset s-offset se
    mov cx,80
    s:mov byte ptr es:[di],'!'
    add di,2
    int 7ch
    se:nop
    ···
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    7ch实现部分:

    lp:push bp
    mov bp,sp
    dec cx
    jcxz lpret
    add [bp+2],bx
    lpret:pop bp
    iret
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    因为bx里面是需要专一的偏移地址,而使用bp的时候默认段寄存器是ss,所以add [bp+2],bx就可以实现将栈中的sp的值修改回s处,自行推导一下就ok。

    BIOS和DOS提供的中断例程

    系统ROM中存放着一套程序,称为BIOS,除此之外还有DOS都提供了一套可以供我们调用的中断例程,不同历程有不同的中断类型码,并且还能根据传入的参数不同而实现不同的功能,也就是说同一个类型码的中断例程可以实现很多不同功能,如int 10h是BIOS提供的包含了多个和屏幕输出相关子程序的中断例程。传参数如下面例子:

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

    BIOS和DOS安装历程的过程是,开机后CPU一加电,自动初始化CS为0FFFFH,IP为0,而在这个地方有一个跳转指令,挑战到BIOS和系统检测初始化程序。在BIOS系统检测初始化程序中会设置中断向量表中的值。

    端口

    端口的概念

    各种存储器都要和CPU的地址线,数据线,控制线相连,在CPU看来,总线就是一个由若干个存储单元构成的逻辑存储器,称之为内存地址空间。除了各种存储器,通过总线和CPU相连的还有下面三种芯片:

    • 各种接口卡(如网卡显卡)上的接口芯片,他们控制接口卡工作
    • 主板上的接口芯片,CPU通过它们访问外部设备
    • 其他芯片,用来存储相关系统信息,或进行相应的输入输出

    上面的芯片中都有一种由CPU读写的寄存器,它们都和CPU的总线相连(通过各自的芯片),CPU对他们进行读写时候都通过控制线向他们所在的芯片发出端口读写指令。

    所以,对于CPU来说,将这些寄存器都当做端口,对他们进行统一编址,建立了一个端口地址空间,每一个端口拥有一个地址,所以CPU可以直接读取下面三个地方的数据:

    • CPU内部的寄存器
    • 内存单元
    • 端口
    端口的写

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

    对端口的读写不能使用mov,push,pop等内存读写指令,端口的读写指令只有两个:in和out分别用于从端口读取数据和往端口写入数据。

    访问端口的步骤:

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

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

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

    in al,20h
    out 20h,al
    • 1
    • 2

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

    mov dx,3f8h
    in al,dx
    out dx,al
    • 1
    • 2
    • 3
    CMOS RAM芯片

    PC中有一个叫做CMOS RAM的芯片,称为CMOS,有如下特征:

    • 包含一个实时钟和一个有128个存储单元的RAM存储器(早期的计算机64个字节)
    • 靠电池供电,关机后内部的实时钟仍可继续工作,RAM中的信息不丢失
    • 128个字节的RAM中,内部实时钟占用0~0dh单元保存时间信息,其余大部分单元用于保存系统配置信息,供系统启动时BIOS程序读取,BIOS也提供了相关的程序可以让我们在开机时配置CMOS中的系统信息。
    • 芯片内部有两个端口70h和71h,CPU通过这两个端口读写CMOS
    • 70h为地址端口,存放要访问CMOS单元的地址,71h为数据端口,存放从选定的单元中读取的数据,或写入的数据。

    所以可以看出,想要从CMOS中读取数据,应分两步,先将单元号送入70h,然后再从71h读出对应号的数据。

    shl和shr指令

    shl和shr是逻辑移位指令,shl是逻辑左移,功能为:

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

    如:mov al,01001000b shl al,1执行结束后(al)=10010000b,CF=0。

    注:如果移动位数大于1,那么必须将移动位数写在cl中。

    mov al,01010001b
    mov cl,3
    shl al,cl
    • 1
    • 2
    • 3

    执行后(al)=10001000b,最后移出的一位是0,所以CF=0。可以看出左移操作相当于x=x*2。

    右移shr同理,最高位用0补充,移出的写入CF,若移动位数大于1,也要写在cl中,相当于x=x/2

    在CMOS中存放着当前时间的年月日时分秒,分别存在下面的单元内:

    0 2 4 7 8 9

    每个信息使用一个字节存放,以BCD码的形式,BCD码是对0-9这几个数字使用二进制表示,如:

    0 1 2 3 4 5 6 7 8 9
    0000 0001 0010 0011 0100 0101 0110 0111 1000 1001

    如果要表示一个两位数如13,就是一个字节高四位是十位1的BCD码,低四位是个位3的BCD码,表示为00010011b。下面程序获取当前月份:

    ···
    mov al,8
    out 70h,al   ;要从8号单元读取数据,所以先将8号单元送入70h端口
    in al,71h    ;从71h端口拿数据
    
    mov ah,al    ;复制一下
    mov cl,4     
    shr ah,cl    ;ah右移四位,ah里面的就是月份的十位
    and al,00001111b  ;al里面剩下的就是月份的个位
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    外中断

    接口芯片和端口

    CPU除了需要拥有运算的能力,还要拥有I/O(输入输出)能力,我们键入一个字母,要能处理,所以我们需要面对的是:外部设备随时的输入和CPU何处得到外部设备的输入。

    外部设备拥有自己的芯片连接到主板上,这些芯片内部由若干寄存器,而CPU将这些寄存器当做端口访问,外设的输入或CPU向外设输出都是送给对应的端口然后再由芯片处理送给目标(CPU或外设)。

    外中断

    CPU提供外中断来处理这些如随时可能出现的来自外设的输入,在PC系统中,外中断源有以下两类:

    可屏蔽中断:CPU可以不响应的外部中断,CPU是否响应看标志寄存器IF的设置,如果IF=1,CPU执行完当前指令后响应中断,如果IF=0,则不响应。可屏蔽中断的执行步骤和内部中断类似:

    1. 获取中断类型码n(从外部通过总线输入)
    2. 标志寄存器入栈,IF=0,TF=0
    3. CS,IP入栈
    4. (IP)=(n*4),(CS)=(n*4+2)

    可见,将IF置零的原因是以免在处理中断程序的时候再发生中断。当然我们也可以选择处理,下面两个指令可以改变IF的值:sti,设置IF=1,cli,设置IF=0。

    不可屏蔽中断:CPU必须响应的外部中断,CPU检测到不可屏蔽中断后执行完当前指令立即响应中断。8086CPU中不可屏蔽中断的中断类型码固定位2,所以中断过程中不需要获取中断类型码,步骤:

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

    几乎所有由外设引发的外中断都是可屏蔽中断,如键盘输入,不可屏蔽中断通常是在系统中又必须处理的紧急情况发生时通知CPU的中断信息。

    PC键盘处理过程

    键盘上每个按键都相当于一个开关,按下就是开关接通,抬起就是开关断开。键盘上有一个芯片对键盘中每一个键盘的状态进行扫描,开关按下生成一个扫描码——通码,记录按下的按键位置,开关抬起也会产生一个扫描——断码,码记录松开的位置,都是送入60h端口。通码的第7位为0,断码第7位为1,也就是说断码=通码+80h。P247表。

    当键盘输入送达60h时,相关新品就会向CPU发送中断类型码为9的可屏蔽中断信息。CPU检测到该中断信息之后,如果IF=1,响应中断,引发中断过程并执行int9的中断例程。BIOS中int9的中断程序用来进行基本的键盘输入处理,步骤如下:

    1. 读出60h的扫描码
    2. 如果是字符的扫描码,将对应的字符的ASCII吗存入内存中的BIOS键盘缓冲区,如果是控制键(Ctrl)和切换键(CapsLock)扫描码,则将其转换为状态字(二进制位记录控制键和切换键状态的字节)写入内存中的存储状态字节的单元。
    3. 对键盘系统进行相关控制,如向新平发出应答

    BIOS中键盘缓冲区能存储15个键盘输入,每个键盘输入两字节,高位存放扫描码,低位存放字符。此外,0040:17单元存放键盘状态字节,记录了控制键和切换键的状态,记录信息如下:

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

    可以看书P276的一个改写int 9的中断例程。

    直接定址表

    描述单元长度的标号

    我们可以使用下面的标号来表示数据的开始:

    ···
    code segment
    a:db 1,2,3,4,5,6,7,8
    b:dw 0
    ···
    code ends
    ···
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    a,b都是代表对应数据的起始地址,但并不能判断数据的长度或类型。下面一段程序将a中的8个数累加存入b中:

    assume cs:code
    code segment
    a db 1,2,3,4,5,6,7,8
    b dw 0
    start mov si,0
    mov cx,8
    s:mov al,a[si]
    mov ah,0
    add b,ax
    inc si
    loop s
    mov ax,4c00h
    int 21h
    code ends
    end start
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    code段中a和b后并没有”:”号,这种写法同时描述内存地址和单元长度的标号。a描述了地址code:0和从这个地址开始后的内存单元都是字节单元,而b描述了地址code:8和从这个地址开始以后的内存单元都是字单元。所以b相当于CS:[8],a[si]相当于CS:0[si],使用这种标号,我们可以间接地访问内存数据。

    其它段中使用数据标号

    刚说的第一种标号即加”:”号的标号,只能使用在代码段中,不能在其他段中使用。如果想要在其它段中(如data段)使用标号可以使用第二种:

    assume cs:code,ds:data
    data segment
    a db 1,2,3,4,5,6,7,8
    b dw 0
    data ends
    ···
    start mov ax,data
    mov ds,ax
    mov si,0
    mov al,a[si]
    ···
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    如果想在代码段中直接使用数据标号访问数据,需要使用assume伪指令将标号所在段和一个寄存器联系起来,是让寄存器明白,我们要访问的数据在ds指向的段中,但编译器并不会真的将段地址存入ds中,我们做了如下假设之后,编译器在编译的时候就会默认ds中已经存放了data的地址,如下面的编译例子:

    mov al,a[si]
    编译为:mov al,[si+0]
    • 1
    • 2

    可以看出编译器默认了a[si]在ds所在的段中。所以我们需要手工指定ds指向data:

    mov ax,data
    mov ds,ax
    • 1
    • 2

    也可以这么使用:

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

    c处存放的是a和b的偏移地址,相当于c dw offset a,offset b。同理c dd a,b相当于c dw offset a,seg a,offset b,seg b即存的是a和b的段地址和偏移地址。

    直接定址表

    使用查表的方法编写相关程序,如输出一个字节型数据的16进制形式(子程序):

    showbyte jmp short show
    table db '0123456789ABCDEF'
    show:push bx
    push es
    mov ah,al
    she ah,1
    she ah,1
    she ah,1
    she ah,1 ;右移四位,位移子程序限制使用的寄存器数,只能这么移
    and al,00001111b
    mov bl,al
    mov bh,0
    mov ah,table[bx]  ;高四位作为相对于table的偏移,取得对应字符
    mov bx,0b800h
    mov es,bx
    mov es:[160*12+40*2],ah
    mov bl,al
    mov bh,0
    mov al,table[bx]
    mov es:[160*12+40*2+2],al
    pop es
    pop bx
    ret
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    可见我们直接使用需要的数值和地址的映射关系来寻找需要的数据。

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

    可以看书P296的例程,主要思想是,编写多个子程序实现不同功能,每个子程序有自己的标号,如sub1,sub2···等。将它们存在一个表中:

    table dw sub1,sub2,sub3,sub4
    • 1

    然后按照之前的方法使用如:

    setscreen:jmp short set
    table dw sub1,sub2,sub3,sub4
    set:push bx
    cmp ah,3
    ja sret
    mov bl,ah
    mov bh,0
    add bx,bx
    call word ptr table[bx]
    sret:pop bx
    ret
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

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

    键盘处理依次按下A,B,C,D,E,shift_A,A的过程:

    我们知道,键盘有16字的缓冲区,可以存放15个按键的扫描码和对应的ASCII码值,如下:

    |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
    • 1

    我们按下A时,引发键盘中断,CPU执行int 9中断例程,从60h端口读出A键通码,然后检测状态字,看是否有控制键或切换键按下,发现没有,将A的扫描码1eh和对应的ASCII码’a’61h写在缓冲区:

    |1e61|  |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
    • 1

    然后BCDE同理:

    |1e61|3062|2e63|2064|1265|  |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
    • 1

    在按下shift之后引发键盘中断,int 9程序接受了shift的通码之后设置0040:17处状态字第一位为1,表示左shift按下,接下来按A间,引发中断,int 9中断例程从60h端口督导通码之后检测状态字,发现左shift被按下,于是将A的键盘扫描码1eh和’A’的ASCII41h写入缓冲区:

    |1e61|3062|2e63|2064|1265|1e41| |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
    • 1

    松开shift,0040:17第一位变回0,之后又按下A和之前一样。

    int 16h读取键盘缓冲区

    int 16h可以供程序员调用,编号为0的功能是从键盘缓冲区读一个键盘输入,(ah)=扫描码,(al)=ascii码。如:

    mov ah,0
    int 16h
    |3062|2e63|2064|1265|1e41|  |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
    • 1
    • 2
    • 3

    执行后,缓冲区第一个没了,然后ah中是1eh,al中是61h。如果缓冲区为空的时候执行,那么会循环等待知道缓冲区有数据,所以int 16h的0号功能的步骤是:

    1. 检测键盘缓冲区是否有数据
    2. 没有则继续1
    3. 读取第一个单元的键盘输入
    4. 扫描码送ah,ascii码送al
    int 13h读写磁盘

    3.5寸软盘分为上下两面,每面80个磁道,每个磁道18个扇区,每个扇区512字节,共约1.44MB。磁盘的实际访问时磁盘控制器进行的,我们通过控制磁盘控制器来控制磁盘,只能以扇区为单位读写磁盘,每次需要给出面号,磁道号,和扇区号,面号和磁道号从0开始,扇区号从1开始。BIOS提供int 13h来实现访问磁盘,读取0面0道1扇区的内容到0:200的程序:

    mov ax,0
    mov es,ax
    mov bx,200h
    
    mov al,1   ;读取的扇区数
    mov ch,0   ;磁道号
    mov cl,1   ;扇区号
    mov dl,0   ;驱动器号,0开始,0软驱A,1软驱B,磁盘从80h开始,80h硬盘C,81h硬盘D
    mov dh,0   ;磁头号(软盘面号)
    mov ah,2   ;13h的功能号,2表示读扇区
    int 13h
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    es:bx指向接收数据的内存区。操作成功(ah)=0,(al)=读入的扇区数,操作失败(ah)=错误代码。将0:200的数据写入0面0道1扇区:

    mov ax,0
    miv es,ax
    mov bx,200h
    
    mov al,1   ;读取的扇区数
    mov ch,0   ;磁道号
    mov cl,1   ;扇区号
    mov dl,0   ;驱动器号
    mov dh,0   ;磁头号(软盘面号)
    mov ah,3   ;13h的功能号,3表示写扇区
    int 13h
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    es:bx指向写入磁盘的数据,操作成功(ah)=0,(al)=写入的扇区数,操作失败(ah)=错误代码

    展开全文
  • 汇编语言入门教程

    万次阅读 多人点赞 2018-08-30 23:07:49
    毕业一年了,之前的工作都是打杂(搞电子的都懂),现在换了工作后,正式开始自己的第一份从事嵌入式软件开发的工作,最近因为工作需要要学习汇编,在百度贴吧看到一篇文章写得不错,所以就转载了,好东西大家分享,...

           毕业一年了,之前的工作都是打杂(搞电子的都懂),现在换了工作后,正式开始自己的第一份从事嵌入式软件开发的工作,最近因为工作需要要学习汇编,在百度贴吧看到一篇文章写得不错,所以就转载了,好东西大家分享,这篇文章也是我博客的第一篇文章,以此来激励自己以后要多点更新博客,无论是原创还是转载,只为自己在痛苦的学习过程留下一点点脚印,也希望能帮助一些和我一样迷茫但会继续挖坑给自己跳的人。废话太多了,下面开始正题:

    学习编程其实就是学高级语言,即那些为人类设计的计算机语言。

    但是,计算机不理解高级语言,必须通过编译器转成二进制代码,才能运行。学会高级语言,并不等于理解计算机实际的运行步骤。

    计算机真正能够理解的是低级语言,它专门用来控制硬件。汇编语言就是低级语言,直接描述/控制 CPU 的运行。如果你想了解 CPU 到底干了些什么,以及代码的运行步骤,就一定要学习汇编语言。

    汇编语言不容易学习,就连简明扼要的介绍都很难找到。下面我尝试写一篇最好懂的汇编语言教程,解释 CPU 如何执行代码。

    一、汇编语言是什么?

    我们知道,CPU 只负责计算,本身不具备智能。你输入一条指令(instruction),它就运行一次,然后停下来,等待下一条指令。

    这些指令都是二进制的,称为操作码(opcode),比如加法指令就是00000011。编译器的作用,就是将高级语言写好的程序,翻译成一条条操作码。

    对于人类来说,二进制程序是不可读的,根本看不出来机器干了什么。为了解决可读性的问题,以及偶尔的编辑需求,就诞生了汇编语言。

    汇编语言是二进制指令的文本形式,与指令是一一对应的关系。比如,加法指令00000011写成汇编语言就是 ADD。只要还原成二进制,汇编语言就可以被 CPU 直接执行,所以它是最底层的低级语言。

    二、来历

    最早的时候,编写程序就是手写二进制指令,然后通过各种开关输入计算机,比如要做加法了,就按一下加法开关。后来,发明了纸带打孔机,通过在纸带上打孔,将二进制指令自动输入计算机。

    为了解决二进制指令的可读性问题,工程师将那些指令写成了八进制。二进制转八进制是轻而易举的,但是八进制的可读性也不行。很自然地,最后还是用文字表达,加法指令写成 ADD。内存地址也不再直接引用,而是用标签表示。

    这样的话,就多出一个步骤,要把这些文字指令翻译成二进制,这个步骤就称为 assembling,完成这个步骤的程序就叫做 assembler。它处理的文本,自然就叫做 aseembly code。标准化以后,称为 assembly language,缩写为 asm,中文译为汇编语言。

    每一种 CPU 的机器指令都是不一样的,因此对应的汇编语言也不一样。本文介绍的是目前最常见的 x86 汇编语言,即 Intel 公司的 CPU 使用的那一种。

    三、寄存器

    学习汇编语言,首先必须了解两个知识点:寄存器和内存模型。

    先来看寄存器。CPU 本身只负责运算,不负责储存数据。数据一般都储存在内存之中,CPU 要用的时候就去内存读写数据。但是,CPU 的运算速度远高于内存的读写速度,为了避免被拖慢,CPU 都自带一级缓存和二级缓存。基本上,CPU 缓存可以看作是读写速度较快的内存。

    但是,CPU 缓存还是不够快,另外数据在缓存里面的地址是不固定的,CPU 每次读写都要寻址也会拖慢速度。因此,除了缓存之外,CPU 还自带了寄存器(register),用来储存最常用的数据。也就是说,那些最频繁读写的数据(比如循环变量),都会放在寄存器里面,CPU 优先读写寄存器,再由寄存器跟内存交换数据。

    寄存器不依靠地址区分数据,而依靠名称。每一个寄存器都有自己的名称,我们告诉 CPU 去具体的哪一个寄存器拿数据,这样的速度是最快的。有人比喻寄存器是 CPU 的零级缓存。

    四、寄存器的种类

    早期的 x86 CPU 只有8个寄存器,而且每个都有不同的用途。现在的寄存器已经有100多个了,都变成通用寄存器,不特别指定用途了,但是早期寄存器的名字都被保存了下来。

    EAX

    EBX

    ECX

    EDX

    EDI

    ESI

    EBP

    ESP

    上面这8个寄存器之中,前面七个都是通用的。ESP 寄存器有特定用途,保存当前 Stack 的地址(详见下一节)。

    我们常常看到 32位 CPU、64位 CPU 这样的名称,其实指的就是寄存器的大小。32 位 CPU 的寄存器大小就是4个字节。

    五、内存模型:Heap

    寄存器只能存放很少量的数据,大多数时候,CPU 要指挥寄存器,直接跟内存交换数据。所以,除了寄存器,还必须了解内存怎么储存数据。

    程序运行的时候,操作系统会给它分配一段内存,用来储存程序和运行产生的数据。这段内存有起始地址和结束地址,比如从0x1000到0x8000,起始地址是较小的那个地址,结束地址是较大的那个地址。

    程序运行过程中,对于动态的内存占用请求(比如新建对象,或者使用malloc命令),系统就会从预先分配好的那段内存之中,划出一部分给用户,具体规则是从起始地址开始划分(实际上,起始地址会有一段静态数据,这里忽略)。举例来说,用户要求得到10个字节内存,那么从起始地址0x1000开始给他分配,一直分配到地址0x100A,如果再要求得到22个字节,那么就分配到0x1020。

    这种因为用户主动请求而划分出来的内存区域,叫做 Heap(堆)。它由起始地址开始,从低位(地址)向高位(地址)增长。Heap 的一个重要特点就是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。

    六、内存模型:Stack

    除了 Heap 以外,其他的内存占用叫做 Stack(栈)。简单说,Stack 是由于函数运行而临时占用的内存区域。

    请看下面的例子。

    intmain(){int a=2;int b=3;}

    上面代码中,系统开始执行main函数时,会为它在内存里面建立一个帧(frame),所有main的内部变量(比如a和b)都保存在这个帧里面。main函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间。

    如果函数内部调用了其他函数,会发生什么情况?

    intmain(){int a=2;int b=3;returnadd_a_and_b(a,b);}

    上面代码中,main函数内部调用了add_a_and_b函数。执行到这一行的时候,系统也会为add_a_and_b新建一个帧,用来储存它的内部变量。也就是说,此时同时存在两个帧:main和add_a_and_b。一般来说,调用栈有多少层,就有多少帧。

    等到add_a_and_b运行结束,它的帧就会被回收,系统会回到函数main刚才中断执行的地方,继续往下执行。通过这种机制,就实现了函数的层层调用,并且每一层都能使用自己的本地变量。

    所有的帧都存放在 Stack,由于帧是一层层叠加的,所以 Stack 叫做栈。生成新的帧,叫做"入栈",英文是 push;栈的回收叫做"出栈",英文是 pop。Stack 的特点就是,最晚入栈的帧最早出栈(因为最内层的函数调用,最先结束运行),这就叫做"后进先出"的数据结构。每一次函数执行结束,就自动释放一个帧,所有函数执行结束,整个 Stack 就都释放了。

    Stack 是由内存区域的结束地址开始,从高位(地址)向低位(地址)分配。比如,内存区域的结束地址是0x8000,第一帧假定是16字节,那么下一次分配的地址就会从0x7FF0开始;第二帧假定需要64字节,那么地址就会移动到0x7FB0。

    七、CPU 指令

    7.1 一个实例

    了解寄存器和内存模型以后,就可以来看汇编语言到底是什么了。下面是一个简单的程序example.c。

    intadd_a_and_b(int a,int b){returna+b;}intmain(){returnadd_a_and_b(2,3);}

    gcc 将这个程序转成汇编语言。

    $ gcc-S example.c

    上面的命令执行以后,会生成一个文本文件example.s,里面就是汇编语言,包含了几十行指令。这么说吧,一个高级语言的简单操作,底层可能由几个,甚至几十个 CPU 指令构成。CPU 依次执行这些指令,完成这一步操作。

    example.s经过简化以后,大概是下面的样子。

    _add_a_and_b:push%ebx mov%eax,[%esp+8]mov%ebx,[%esp+12]add%eax,%ebx pop%ebx ret _main:push3push2call _add_a_and_b add%esp,8ret

    可以看到,原程序的两个函数add_a_and_b和main,对应两个标签_add_a_and_b和_main。每个标签里面是该函数所转成的 CPU 运行流程。

    每一行就是 CPU 执行的一次操作。它又分成两部分,就以其中一行为例。

    push%ebx

    这一行里面,push是 CPU 指令,%ebx是该指令要用到的运算子。一个 CPU 指令可以有零个到多个运算子。

    下面我就一行一行讲解这个汇编程序,建议读者最好把这个程序,在另一个窗口拷贝一份,省得阅读的时候再把页面滚动上来。

    7.2 push 指令

    根据约定,程序从_main标签开始执行,这时会在 Stack 上为main建立一个帧,并将 Stack 所指向的地址,写入 ESP 寄存器。后面如果有数据要写入main这个帧,就会写在 ESP 寄存器所保存的地址。

    然后,开始执行第一行代码。

    push3

    push指令用于将运算子放入 Stack,这里就是将3写入main这个帧。

    虽然看上去很简单,push指令其实有一个前置操作。它会先取出 ESP 寄存器里面的地址,将其减去4个字节,然后将新地址写入 ESP 寄存器。使用减法是因为 Stack 从高位向低位发展,4个字节则是因为3的类型是int,占用4个字节。得到新地址以后, 3 就会写入这个地址开始的四个字节。

    push2

    第二行也是一样,push指令将2写入main这个帧,位置紧贴着前面写入的3。这时,ESP 寄存器会再减去 4个字节(累计减去8)。

    7.3 call 指令

    第三行的call指令用来调用函数。

    call _add_a_and_b

    上面的代码表示调用add_a_and_b函数。这时,程序就会去找_add_a_and_b标签,并为该函数建立一个新的帧。

    下面就开始执行_add_a_and_b的代码。

    push%ebx

    这一行表示将 EBX 寄存器里面的值,写入_add_a_and_b这个帧。这是因为后面要用到这个寄存器,就先把里面的值取出来,用完后再写回去。

    这时,push指令会再将 ESP 寄存器里面的地址减去4个字节(累计减去12)。

    7.4 mov 指令

    mov指令用于将一个值写入某个寄存器。

    mov%eax,[%esp+8]

    这一行代码表示,先将 ESP 寄存器里面的地址加上8个字节,得到一个新的地址,然后按照这个地址在 Stack 取出数据。根据前面的步骤,可以推算出这里取出的是2,再将2写入 EAX 寄存器。

    下一行代码也是干同样的事情。

    mov%ebx,[%esp+12]

    上面的代码将 ESP 寄存器的值加12个字节,再按照这个地址在 Stack 取出数据,这次取出的是3,将其写入 EBX 寄存器。

    7.5 add 指令

    add指令用于将两个运算子相加,并将结果写入第一个运算子。

    add%eax,%ebx

    上面的代码将 EAX 寄存器的值(即2)加上 EBX 寄存器的值(即3),得到结果5,再将这个结果写入第一个运算子 EAX 寄存器。

    7.6 pop 指令

    pop指令用于取出 Stack 最近一个写入的值(即最低位地址的值),并将这个值写入运算子指定的位置。

    pop%ebx

    上面的代码表示,取出 Stack 最近写入的值(即 EBX 寄存器的原始值),再将这个值写回 EBX 寄存器(因为加法已经做完了,EBX 寄存器用不到了)。

    注意,pop指令还会将 ESP 寄存器里面的地址加4,即回收4个字节。

    7.7 ret 指令

    ret指令用于终止当前函数的执行,将运行权交还给上层函数。也就是,当前函数的帧将被回收。

    ret

    可以看到,该指令没有运算子。

    随着add_a_and_b函数终止执行,系统就回到刚才main函数中断的地方,继续往下执行。

    add%esp,8

    上面的代码表示,将 ESP 寄存器里面的地址,手动加上8个字节,再写回 ESP 寄存器。这是因为 ESP 寄存器的是 Stack 的写入开始地址,前面的pop操作已经回收了4个字节,这里再回收8个字节,等于全部回收。

    ret

    最后,main函数运行结束,ret指令退出程序执行。

    八、参考链接

    Introduction to reverse engineering and Assembly, by Youness Alaoui

    x86 Assembly Guide, by University of Virginia Computer Science

    (完)

    此文章转自:https://baijiahao.baidu.com/s?id=1590302037132894549&wfr=spider&for=pc

    展开全文
  • 王爽《汇编语言》笔记(详细)

    万次阅读 多人点赞 2020-01-25 15:27:57
    一、基础知识 1、指令 机器指令:CPU能直接识别并执行的二进制编码...汇编语言发展至今,有以下3类指令组成。 汇编指令:机器码的助记符,有对应的机器码。 伪指令:没有对应的机器码,由编译器执行,计算机并不执...

    文章目录

    一、基础知识


    1、指令

    机器指令:CPU能直接识别并执行的二进制编码

    汇编指令:汇编指令是机器指令的助记符,同机器指令一一对应。

    指令:指令通常由操作码和地址码(操作数)两部分组成

    指令集:每种CPU都有自己的汇编指令集。

    汇编语言由3类指令组成。

    • 汇编指令
    • 伪指令:没有对应的机器码,由编译器执行,计算机并不执行
    • 其他符号:如+、-、*、/等,由编译器识别,没有对应的机器码。

    编译器:够将汇编指令转换成机器指令的翻译程序每一种CPU都有自己的汇编指令集。
    在这里插入图片描述
    在内存或磁盘上,指令和数据没有任何区别,都是二进制信息

    2、存储器

    随机存储器(RAM)在程序的执行过程中可读可写,必须带电存储

    只读存储器(ROM)在程序的执行过程中只读,关机数据不丢失
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    (以上3张图片来自王道考研 - 计算机组成原理课件)

    3、总线


    1、总线

    总线是连接各个部件的信息传输线,是各个部件共享的传输介质

    主板上有核心器件和一些主要器件,这些器件通过总线(地址总线、数据总线、控制总线)相连。这些器件有CPU、存储器、外围芯片组、扩展插槽等。扩展插槽上一般插有RAM内存条和各类接口卡。
    来自唐朔飞·计算机组成原理经典课件

    总线根据位置分类:

    • 片内总线(芯片内部总线)

    • 系统总线(计算机各部件之间的信息传输线)

      根据传送信息的不同,系统总线从逻辑上又分为3类,地址总线、控制总线和数据总线。

    CPU要想进行数据的读写,必须和外部器件(标准的说法是芯片)进行以下3类信息的交互。

    1. 地址总线:CPU通过地址总线来指定存储单元
      在这里插入图片描述
      1根导线可以传送的稳定状态只有两种,高电平或是低电平。用二进制表示就是1或0

    图示有10根地址线即一次可以传输10位,访问存储单元地址为1011,寻址范围为0 ~ (210 - 1)

    1. 数据总线:CPU与内存或其他器件之间的数据传送是通过数据总线来进行的
      在这里插入图片描述
      8根数据线一次可传送一个8位二进制数据(即一个字节),传送2个字节需要两次;16根数据线一次可传送2个字节(内存对齐核心原理)

    2. 控制总线:CPU对外部器件的控制是通过控制总线来进行的。

    有多少根控制总线,就意味着CPU提供了对外部器件的多少种控制。
    所以,控制总线的宽度决定了CPU对外部器件的控制能力。

    2、CPU对存储器的读写

    在这里插入图片描述
    1、 CPU通过地址线将地址信息3发出。
    2、 CPU通过控制线发出内存读命令,选中存储器芯片,并通知它,将要从中读取数据。
    3、 存储器将3号单元中的数据8通过数据线送入CPU。写操作与读操作的步骤相似。
    联想:在组成原理中用微操作表示:(PC) → MAR; 1 → R; M(MAR) → MDR; …

    3、CPU对外设的控制

    CPU对外设都不能直接控制,如显示器、音箱、打印机等。

    直接控制这些设备进行工作的是插在扩展插槽上的接口卡。

    扩展插槽通过总线和CPU相连,所以接口卡也通过总线同CPU相连。CPU可以直接控制这些接口卡,从而实现CPU对外设的间接控制。

    如:CPU无法直接控制显示器,但CPU可以直接控制显卡,从而实现对显示器的间接控制

    4、内存地址空间

    CPU将系统中各类存储器看作一个逻辑存储器,这个逻辑存储器就是我们所说的内存地址空间。
    对于CPU,所有存储器中的存储单元都处于一个统一的逻辑存储器中,它的容量受CPU寻址能力限制。(或许就是计组中学的统一编址吧)
    在这里插入图片描述
    每个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间。CPU在这段地址空间中读写数据,实际上就是在相对应的物理存储器中读写数据(对ROM写无效)。
    在这里插入图片描述

    二、寄存器


    1、寄存器

    CPU由运算器、控制器、寄存器等器件构成,这些器件靠片内总线相连。

    运算器进行信息处理;控制器控制各种器件进行工作;寄存器进行信息存储;

    8086CPU有14个寄存器:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW都是16位
    在这里插入图片描述

    16位结构CPU具有下面几方面的结构特性。

    • 运算器一次最多可以处理16位的数据;
    • 寄存器的最大宽度为16位;
    • 寄存器和运算器之间的通路为16位。

    8086CPU可以一次性处理以下两种尺寸的数据。

    • 字节:记为byte,一个字节由8个bit组成,可以存在8位寄存器中。
    • 字:记为word,一个字由两个字节组成,可以存在一个16位寄存器中(16位CPU)
      在这里插入图片描述
      8086采用小端模式:高地址存放高位字节,低地址存放低位字节。

    2、通用寄存器

    通用寄存器:通常用来存放一般性的数据,有AX、BX、CX、DX,它们可分为两个可独立使用的8位寄存器,

    16位 8高位 8低位
    AX AH AL
    BX BH BL
    CX CH CL
    DX DH DL

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

    一个8位寄存器所能存储的数据范围是0 ~ 28-1。

    3、8086CPU给出物理地址的方法

    8086CPU有20位地址总线,可以传送20位地址,达到1MB寻址能力。
    8086CPU又是16位结构,在内部一次性处理、传输、暂时存储的地址为16位。
    从8086CPU的内部结构来看,如果将地址从内部简单地发出,那么它只能送出16位的地址,表现出的寻址能力只有64KB。
    8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。
    在这里插入图片描述
    当8086CPU要读写内存时:

    1. CPU中的相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址;
    2. 地址加法器将两个16位地址合成为一个20位的物理地址

    地址加法器采用物理地址 = 段地址×16 + 偏移地址的方法用段地址和偏移地址合成物理地址。

    例如,8086CPU要访问地址为123C8H的内存单元,1230H左移一位(空出4位)加上00C8H合成123C8H

    4、段寄存器

    我们可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元,可以用分段的方式来管理内存。

    用一个段存放数据,将它定义为“数据段”;

    用一个段存放代码,将它定义为“代码段”;

    用一个段当作栈,将它定义为“栈段”。

    注意:

    • 一个段的起始地址一定是16的倍数;
    • 偏移地址为16位,变化范围为0-FFFFH,所以一个段的长度最大为64KB。
    • CPU可以用不同的段地址和偏移地址形成同一个物理地址。

    段寄存器:8086CPU有4个段寄存器:CS、DS、SS、ES,提供内存单元的段地址。

    1、CS和IP

    CS为代码段寄存器,IP为指令指针寄存器,

    CPU将CS、IP中的内容当作指令的段地址和偏移地址,用它们合成指令的物理地址,

    CPU将CS:IP指向的内容当作指令执行。(即PC)
    在这里插入图片描述
    8086CPU的工作过程简要描述

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

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

    8086CPU提供转移指令修改CS、IP的内容。

    • jmp 段地址:偏移地址:用指令中给出的段地址修改CS,偏移地址修改IP。如:jmp 2AE3:3

    • jmp 某一合法寄存器:仅修改IP的内容。如:jmp ax。在含义上好似:mov IP,ax

    8086CPU不支持将数据直接送入段寄存器的操作,这属于8086CPU硬件设计

    2、DS 和 [address]

    DS寄存器:通常用来存放要访问数据的段地址

    [address]表示一个偏移地址为address的内存单元,段地址默认放在ds中

    通过数据段段地址和偏移地址即可定位内存单元。

    mov bx, 1000H ;8086CPU不支持将数据直接送入段寄存器的操作
    
    mov ds, bx ;ds存放数据段地址
    
    mov [0], al ;将al数据(1字节)存到1000H段的0偏移地址处,即10000H
    
    mov ax, [2] ;将数据段偏移地址2处的一个字(8086为2字节)存放到ax寄存器
    
    add cx, [4] ;将偏移地址4处的一个字数据加上cx寄存器数据放到cx寄存器
    
    sub dx, [6] ;dx寄存器数据减去数据段偏移地址6处的字数据存到dx
    

    3、SS 和 SP

    在基于8086CPU编程的时候,可以将一段内存当作栈来使用。

    栈段寄存器SS,存放段地址,SP寄存器存放偏移地址,任意时刻,SS:SP指向栈顶元素

    8086CPU中,入栈时,栈顶从高地址向低地址方向增长。

    push ax表示将寄存器ax中的数据送入栈中,由两步完成。

    1. SP=SP-2,SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;
    2. 将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶。
      在这里插入图片描述

    pop ax表示从栈顶取出数据送入ax,由以下两步完成。

    1. 将SS:SP指向的内存单元处的数据送入ax中;
    2. SP=SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。

    实验

    1. 将10000H~1000FH这段空间当作栈,初始状态栈是空的;
    2. 设置AX=001AH,BX=001BH;
    3. 将AX、BX中的数据入栈;
    4. 然后将AX、BX清零;
    5. 从栈中恢复AX、BX原来的内容。
    mov ax, 1000H 
    mov ss, ax 
    mov sp, 0010H    ;初始化栈顶
    mov ax, 001AH
    mov bx, 001BH 
    
    push ax 
    push bx    ;ax、bx入栈
    
    sub ax, ax   ;将ax清零,也可以用mov ax,0,
                 ;sub ax,ax的机器码为2个字节,
                 ;mov ax,0的机器码为3个字节。
            
    sub bx, bx 
    
    pop bx  ;从栈中恢复ax、bx原来的数据
    pop ax  ;
    

    三、第一个程序

    1、汇编程序从写出到执行的过程

    在这里插入图片描述
    加载后,CPU的CS:IP指向程序的第一条指令(即程序的入口)

    ;1.asm
    assume cs:codesg ;将用作代码段的段codesg和段寄存器cs联系起来。
    
    codesg segment ;定义一个段,段的名称为“codesg”,这个段从此开始
    			   ;codesg是一个标号,作为一个段的名称,最终被编译连接成一个段的段地址
    
    	mov ax, 0123H
    	mov bx, 0456H 
    	add ax, bx
    	add ax, ax 
    	
    	mov ax, 4c00H 
    	int 21H ;这两条指令实现程序的返回
    	
    codesg ends ;名称为“codesg”的段到此结束
    
    end ;编译器在编译汇编程序的过程中,碰到了伪指令end,结束对源程序的编译
    

    在这里插入图片描述
    在这里插入图片描述

    2、程序执行过程跟踪

    DOS系统中.EXE文件中的程序的加载过程
    在这里插入图片描述

    在这里插入图片描述


    四、[bx] 和 loop指令


    1、[bx] 和 loop指令

    [bx] 的含义:[bx]同样表示一个内存单元,它的偏移地址在bx中,段地址默认在ds中

    loop指令的格式是:loop 标号,CPU执行loop指令的时候,要进行两步操作,

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

    2. 判断 cx 中的值,不为零则转至标号处执行程序,如果为零则向下执行。

    例如:计算212

    assume cs:code 
    
    code segment 
    	mov ax, 2
    	
    	mov cx, 11 ;循环次数
    s:  add ax, ax 
    	loop s     ;在汇编语言中,标号代表一个地址,标号s实际上标识了一个地址,
                   ;这个地址处有一条指令:add ax,ax。
                   ;执行loop s时,首先要将(cx)减1,然后若(cx)不为0,则向前
                   ;转至s处执行add ax,ax。所以,可以利用cx来控制add ax,ax的执行次数。
    	
    	mov ax,4c00h 
    	int 21h 
    code ends 
    end
    

    loop 和 [bx] 的联合应用

    计算ffff:0 ~ ffff:b单元中的数据的和,结果存储在dx中

    问题分析:

    1. 这些内存单元都是字节型数据范围0 ~ 255 ,12个字节数据和不会超过65535,dx可以存下
    2. 对于8位数据不能直接加到 dx

    解决方案:

    用一个16位寄存器来做中介。将内存单元中的8位数据赋值到一个16位寄存器a中,再将ax中的数据加到dx

    assume cs:code 
    
    code segment 
    	mov ax, 0ffffh ;在汇编源程序中,数据不能以字母开头,所以要在前面加0。
    	mov ds, ax 
    	mov bx, 0   ;初始化ds:bx指向ffff:0
    	mov dx, 0   ;初始化累加寄存器dx,(dx)= 0
    	
    	mov cx, 12  ;初始化循环计数寄存器cx,(cx)= 12
    s:  mov al, [bx]
    	mov ah, 0
    	add dx, ax  ;间接向dx中加上((ds)* 16 +(bx))单元的数值
    	inc bx      ;ds:bx指向下一个单元
    	loop s 
    	
    	mov ax, 4c00h 
    	int 21h 
    code ends 
    end
    

    2、段前缀

    mov ax, ds:[bx]
    mov ax, cs:[bx]
    mov ax, ss:[bx]
    mov ax, es:[bx]
    mov ax, ss:[0]
    mov ax, cs:[0]
    

    这些出现在访问内存单元的指令中,用于显式地指明内存单元的段地址
    的“ds:”,“cs:”,“ss:”,“es:”,在汇编语言中称为段前缀。

    段前缀的使用

    将内存ffff:0 ~ ffff:b单元中的数据复制到0:200 ~ 0:20b单元中。

    assume cs:code 
    
    code segment 
    	mov ax, 0ffffh 
    	mov ds, ax   ;(ds)= 0ffffh 
    	mov ax, 0020h
        mov es, ax   ;(es)= 0020h     0:200 等效于 0020:0
        mov bx, 0    ;(bx)= 0,此时ds:bx指向ffff:0,es:bx指向0020:0
        
    	mov cx,12   ;(cx)=12,循环12次
    s:  mov dl,[bx] ;(d1)=((ds)* 16+(bx)),将ffff:bx中的字节数据送入dl 
    	mov es:[bx],dl ;((es)*16+(bx))=(d1),将dl中的数据送入0020:bx 
    	inc bx  ;(bx)=(bx)+1
    	loop s 
    	
    	mov ax,4c00h 
    	int 21h 
    code ends 
    end
    

    五、包含多个段的程序

    程序中对段名的引用,将被编译器处理为一个表示段地址的数值。

    mov ax, data 
    
    mov ds, ax 
    
    mov bx, ds:[6]
    

    在代码段中使用数据

    ;计算 8 个数据的和存到 ax 寄存器
    assume cs:code 
    
    code segment 
    
    	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ;define word 定义8个字形数据
    
    	start:	mov bx, 0  ;标号start
    			mov ax, 0  
    			
    			mov cx, 8
    	s:		add ax, cs:[bx]
    			add bx, 2
    			loop s 
    			
    			mov ax, 4c00h 
    			int 21h 
    code ends
    end start    ;end除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方
    	     	 ;用end指令指明了程序的入口在标号start处,也就是说,“mov bx,0”是程序的第一条指令。
    

    在代码段中使用栈

    ;利用栈,将程序中定义的数据逆序存放。
    assume cs:codesg 
    
    codesg segment 
    	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ; 0-15单元
    	dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ; 16-47单元作为栈使用
    			
    	start:	mov ax, cs 
    			mov ss, ax 
    			mov sp, 30h ;将设置栈顶ss:sp指向栈底cs:30。   30h = 48d
    			mov bx, 0
    			
    			mov cx, 8
    	s:		push cs:[bx]
    			add bx, 2
    			loop s    ;以上将代码段0~15单元中的8个字型数据依次入栈
    			
    			mov bx, 0
    			
    			mov cx, 8
    	s0:		pop cs:[bx]		
    			add bx,2
    			loop s0   ;以上依次出栈8个字型数据到代码段0~15单元中
    			
    			mov ax,4c00h 
    			int 21h 
    codesg ends 
    end start	;指明程序的入口在start处
    

    将数据、代码、栈放入不同的段

    assume cs:code,ds:data,ss:stack 
    
    data segment 
    	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ;0-15单元
    data ends 
    
    stack segment 
    	dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ;0-31单元
    stack ends 
    
    code segment 
    	start:	mov ax, stack;将名称为“stack”的段的段地址送入ax
    			mov ss, ax
    			mov sp, 20h  ;设置栈顶ss:sp指向stack:20。 20h = 32d
    			
    			mov ax, data ;将名称为“data”的段的段地址送入ax
    			mov ds, ax   ;ds指向data段
    			
    			mov bx, 0    ;ds:bx指向data段中的第一个单元
    			
    			mov cx, 8
    	s:	    push [bx]
    			add bx, 2
    			loop s       ;以上将data段中的0~15单元中的8个字型数据依次入栈
    			
    			mov bx, 0
    			
    			mov cx, 8
    	s0:		pop [bx]
    			add bx, 2
    			loop s0      ;以上依次出栈8个字型数据到data段的0~15单元中
    			
    			mov ax, 4c00h 
    			int 21h 
    code ends
    end start
    ;“end start”说明了程序的入口,这个入口将被写入可执行文件的描述信息,
    ;可执行文件中的程序被加载入内存后,CPU的CS:IP被设置指向这个入口,从而开始执行程序中的第一条指令
    

    关于可执行文件结构与程序入口的详细描述参考:PE文件结构

    六、更灵活的定位内存地址的方法


    1、and 和 or

    and指令:逻辑与指令,按位进行与运算。

    mov al, 01100011B
    and al, 00111011B

    执行后:al=00100011B即都为1才为1

    or指令:逻辑或指令,按位进行或运算。

    mov al, 01100011B
    or al, 00111011B
    执行后:al=01111011B 即只要有一个为1就为1

    关于ASCII码
    世界上有很多编码方案,有一种方案叫做ASCII编码,是在计算机系统中通常被采用的。简单地说,所谓编码方案,就是一套规则,它约定了用什么样的信息来表示现实对象。比如说,在ASCII编码方案中,用61H表示“a”,62H表示“b”。一种规则需要人们遵守才有意义。

    在文本编辑过程中,我们按一下键盘的a键,就会在屏幕上看到“a”。我们按下键盘的a键,这个按键的信息被送入计算机,计算机用ASCII码的规则对其进行编码,将其转化为61H存储在内存的指定空间中;文本编辑软件从内存中取出61H,将其送到显卡上的显存中;工作在文本模式下的显卡,用ASCII码的规则解释显存中的内容
    61H被当作字符“a”,显卡驱动显示器,将字符“a”的图像画在屏幕上。我们可以看到,显卡在处理文本信息的时候,是按照ASCII码的规则进行的。这也就是说,如果我们要想在显示器上看到“a”,就要给显卡提供“a”的ASCIⅡ码,61H。如何提供?当然是写入显存中。

    以字符形式给出的数据

    assume cs:code,ds:data 
    
    data segment 
    	db 'unIx'   ;相当于“db 75H,6EH,49H,58H”
    	db 'foRK'
    data ends 
    
    code segment
    start:	mov al, 'a'  ;相当于“mov al, 61H”,“a”的ASCI码为61H;
    		mov b1, 'b'
    		
    		mov ax, 4c00h 
    		int 21h 
    code ends
    end start
    

    大小写转换的问题
    在这里插入图片描述
    小写字母的ASCII码值比大写字母的ASCII码值大20H

    大写字母ASCII码的第5位为0,小写字母的第5位为1(其他一致)

    assume cs:codesg,ds:datasg 
    
    datasg segment 
    	db 'BaSiC'
    	db 'iNfOrMaTion'
    datasg end
    
    codesg segment 
    	start:	mov ax, datasg 
    			mov ds, ax	;设置ds 指向 datasg段
    		
    			mov bx, 0	;设置(bx)=0,ds:bx指向’BaSic’的第一个字母
    			
    			mov cx, 5     	 ;设置循环次数5,因为’Basic'有5个字母
    	s:		mov al, [bx]     ;将ASCII码从ds:bx所指向的单元中取出
    			and al, 11011111B;将al中的ASCII码的第5位置为0,变为大写字母
    			mov [bx], al	 ;将转变后的ASCII码写回原单元
    			inc bx		     ;(bx)加1,ds:bx指向下一个字母
    			loop s 
    			
    			mov bx, 5	;设置(bx)=5,ds:bx指向,iNfOrMaTion'的第一个字母
    			
    			mov cx, 11	;设置循环次数11,因为‘iNfOrMaTion'有11个字母
    	s0:		mov al, [bx]
    			or al, 00100000B;将a1中的ASCII码的第5位置为1,变为小写字母
    			mov [bx], al 
    			inc bx
    			loop s0
    			
    			mov ax, 4c00h 
    			int 21h 
    codesg ends
    

    2、[bx+idata]

    [bx+idata]表示一个内存单元, 例如:mov ax, [bx+200]
    该指令也可以写成如下格式:

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

    用[bx+idata]的方式进行数组的处理

    assume cs:codesg,ds:datasg 
    
    datasg segment 
    	db 'BaSiC';转为大写
    	db 'MinIx';转为小写
    datasg ends
    
    codesg segment
    	start:
    		mov ax, datasg 
    		mov ds, ax 
    		mov bx, 0  ;初始ds:bx
    	
    		mov cx, 5
    	s:	mov al, 0[bx]  
    		and al, 11011111b ;转为大写字母
    		mov 0[bx], al ;写回
    		mov al, 5[bx]  ;[5 + bx]
    		or al, 00100000b ;转为小写字母
    		mov 5[bx], al 
    		inc bx
    		loop s
    		
    		mov ax, 4c00h 
    		int 21h
    codesg ends
    end start
    

    C语言描述

    int main()
    {
    	char a[] = "BaSic";
    	char b[] = "MinIX";
    	
    	int i = 0;
    	
    	do
    	{
    		a[i] = a[i] & 0xDF;
    		b[i] = b[i] | 0x20;
    		i++;
    	} while(i < 5);
    
    	return 0;
     } 
    

    3、SI 、DI 与 寻址方式的灵活应用

    1、si 、di

    si和di是8086CPU中和bx功能相近的寄存器,si和di不能够分成两个8位寄存器来使用。

    assume cs: codesg, ds: datasg 
    
    datasg segment 
    	db 'welcome to masm!';用si和di实现将字符串‘welcome to masm!"复制到它后面的数据区中。
    	db '................'
    datasg ends
    
    codesg segment 
    	start:	mov ax, datasg 
    			mov ds, ax 
    			mov si, 0
    			
    			mov cx, 8
    	s:		mov ax, 0[si] ;[0 + si]
    			mov 16[si], ax ;[16 + si] 使用[bx +idata]方式代替di,使程序更简洁
    			add si, 2 
    			loop s 
    			
    			mov ax, 4c00h 
    			int 21h 
    codesg ends 
    end start
    

    2、[bx + si] 和 [bx + di]

    [bx+si]和[bx+di]的含义相似

    [bx+si]表示一个内存单元,它的偏移地址为(bx)+(si)

    指令mov ax, [bx + si]的含义:将一个内存单元字数据的内容送入ax,段地址在ds中

    该指令也可以写成如下格式:mov ax, [bx][si]

    3、[bx+si+idata]和[bx+di+idata]
    [bx+si+idata]表示一个内存单元,它的偏移地址为(bx)+(si)+idata

    指令mov ax,[bx+si+idata]的含义:将一个内存单元字数据的内容送入ax,段地址在ds中

    4、不同的寻址方式的灵活应用
    [idata]用一个常量来表示地址,可用于直接定位一个内存单元;
    [bx]用一个变量来表示内存地址,可用于间接定位一个内存单元;
    [bx+idata]用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元;
    [bx+si]用两个变量表示地址;
    [bx+si+idata]用两个变量和一个常量表示地址。

    
    ;将datasg段中每个单词改为大写字母
    assume cs:codesg,ds:datasg,ss:stacksg 
    
    datasg segment
    	db 'ibm            ' ;16
    	db 'dec            ' 
    	db 'dos            '
    	db 'vax            '  ;看成二维数组
    datasg ends 
    
    stacksg segment ;定义一个段,用来做栈段,容量为16个字节
    	dw 0, 0, 0, 0, 0, 0, 0, 0
    stacksg ends 
    
    codesg segment 
    	start:	mov ax, stacksg 
    			mov ss, ax
    			mov sp, 16 
    			mov ax, datasg 
    			mov ds, ax 
    			mov bx, 0 ;初始ds:bx
    			
    			;cx为默认循环计数器,二重循环只有一个计数器,所以外层循环先保存cx值,再恢复,我们采用栈保存
    			mov cx, 4
    	s0:		push cx	;将外层循环的cx值入栈
    			mov si, 0
    			mov cx, 3	;cx设置为内层循环的次数
    	s:		mov al, [bx+si]
    			and al, 11011111b ;每个字符转为大写字母
    			mov [bx+si], al 
    			inc si
    			loop s 
    			
    			add bx, 16 ;下一行
    			pop cx	;恢复cx值
    			loop s0 ;外层循环的loop指令将cx中的计数值减1
    			
    			mov ax,4c00H 
    			int 21H 
    codesg ends
    end start
    

    七、数据处理的两个基本问题

    1、 bx、si、di和bp

    在8086CPU中,只有这4个寄存器可以用在“[…]”中来进行内存单元的寻址。

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

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

    2、机器指令处理的数据在什么地方

    数据处理大致可分为3类:读取、写入、运算。

    在机器指令这一层来讲,并不关心数据的值是多少,而关心指令执行前一刻,它将要处理的数据所在的位置。指令在执行前,所要处理的数据可以在3个地方:CPU内部、内存、端口
    在这里插入图片描述

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

    汇编语言中用3个概念来表达数据的位置

    • 立即数(idata)
    mov ax, 1                 ;对于直接包含在机器指令中的数据(执行前在CPU的指令缓冲器中)
    add bx, 2000h             ;在汇编语言中称为:立即数(idata)
    or bx, 00010000b
    mov al, 'a'
    
    • 寄存器
    mov ax, bx     ;指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名。
    mov ds, ax 
    push bx 
    mov ds:[0], bx 
    push ds 
    mov ss, ax
    mov sp, ax
    
    • 段地址(SA)和偏移地址(EA)
    ;指令要处理的数据在内存中,在汇编指令中可用[X]的格式给出EA,SA在某个段寄存器中。
    mov ax, [0]
    mov ax, [di]
    mov ax, [bx+8]
    mov ax, [bx+si]
    mov ax, [bx+si+8]   ;以上段地址默认在ds中
    
    mov ax, [bp]
    mov ax, [bp+8]
    mov ax, [bp+si]
    mov ax, [bp+si+8]   ;以上段地址默认在ss中
    
    mov ax, ds:[bp]
    mov ax, es:[bx]
    mov ax, ss:[bx+si]
    mov ax, cs:[bx+si+8] ;显式给出存放段地址的寄存器
    

    寻址方式
    在这里插入图片描述

    4、指令要处理的数据有多长

    8086CPU的指令,可以处理两种尺寸的数据,byte和word

    1. 通过寄存器名指明要处理的数据的尺寸。
      例如: mov al, ds:[0] 寄存器al指明了数据为1字节

    2. 在没有寄存器名存在的情况下,用操作符X ptr指明内存单元的长度,X在汇编指令中可以为wordbyte
      例如:mov byte ptr ds:[0], 1 byte ptr 指明了指令访问的内存单元是一个字节单元

    3. 有些指令默认了访问的是字单元还是字节单元
      例如,push [1000H],push 指令只进行字操作。

    5、寻址方式的综合应用

    在这里插入图片描述

    mov ax, seg 
    mov ds, ax 
    mov bx, 60h   ;确定记录地址,ds:bx 
    
    mov word ptr [bx+0ch], 38   ;排名字段改为38  [bx].0ch
    add word ptr [bx+0eh], 70   ;收入字段增加70  [bx].0eh
    mov si, 0   ;用si来定位产品字符串中的字符
    mov byte ptr [bx+10h+si], 'V'   ;[bx].10h[si]
    inc si 
    mov byte ptr [bx+10h+si], 'A'
    inc si 
    mov byte ptr [bx+10h+si], 'X'
    

    C语言描述

    /*定义一个公司记录的结构体*/
    struct company
    {
        char cn[3];/*公司名称*/
        char hn[9];/*总裁姓名*/
        int pm;/*排名*/
        int sr;/*收入*/
        char cp[3];/*著名产品*/
    };
    //sizeof (struct company) == 24
    
    int main()
    {
        /*定义一个公司记录的变量,内存中将存有一条公司的记录*/
        struct company dec = {"DEC", "Ken Olsen", 137, 40, "PDP"};
    
        int i;
    
        dec.pm = 38;
        dec.sr = dec.sr + 70;
    
        i = 0;
        dec.cp[i] = 'V'; //mov byte ptr [bx].10h[si], 'V'
        i++;
        dec.cp[i] = 'A';
        i++;
        dec.cp[i] = 'X';
    
        return 0;
    }
    
    

    6、div指令、dd、dup、mul指令

    div是除法指令

    1. 除数:有8位和16位两种,在一个寄存器内存单元中。

    2. 被除数:默认放在AXDX和AX中,
      如果除数为8位,被除数则为16位,默认在AX中存放;
      如果除数为16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位

    3. 结果:
      如果除数为8位,则AL存储除法操作的商AH存储除法操作的余数
      如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数。

    ;利用除法指令计算100001/100。
    ;100001D = 186A1H
    mov dx, 1
    mov ax, 86A1H ;(dx)*10000H+(ax)=100001
    mov bx, 100
    div bx
    
    ;利用除法指令计算1001/100
    mov ax, 1001
    mov bl, 100
    div b1
    

    伪指令dd

    db和dw定义字节型数据和字型数据。

    dd是用来定义dword(double word,双字)型数据的伪指令

    操作符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。
    db 3 dup ('abc', 'ABC') ;定义了18个字节,它们是abcABCabcABCabcABCC,相当于db 'abc', 'ABC' ,'abc' , 'ABC, 'abc', 'ABC'。
    

    mul 指令

    mul是乘法指令,使用 mul 做乘法的时候:相乘的两个数:要么都是8位,要么都是16位。

    • 8 位: AL中和 8位寄存器内存字节单元中;

    • 16 位: AX中和 16 位寄存器内存字单元中。

    结果

    • 8位:AX中;

    • 16位:DX(高位)和 AX(低位)中。

    格式:mul 寄存器mul 内存单元

    ;计算100*10
    ;100和10小于255,可以做8位乘法
    mov al,100
    mov bl,10
    mul bl
    
    ;结果: (ax)=1000(03E8H) 
    
    
    ;计算100*10000
    ;100小于255,可10000大于255,所以必须做16位乘法,程序如下:
    mov ax,100
    mov bx,10000
    mul bx
    
    ;结果: (ax)=4240H,(dx)=000FH     (F4240H=1000000)
    
    

    八、转移指令的原理


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

    8086CPU的转移行为有以下几类。

    • 只修改IP时,称为段内转移,比如:jmp ax
    • 同时修改CS和IP时,称为段间转移,比如:jmp 1000:0

    由于转移指令对IP的修改范围不同,段内转移又分为:短转移和近转移

    • 短转移IP的修改范围为-128 ~ 127
    • 近转移IP的修改范围为-32768 ~ 32767

    8086CPU的转移指令分为以下几类。

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

    1、操作符offset

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

    ;将s处的一条指令复制到s0处
    assume cs:codesg
    codesg segment
     s:   mov ax, bx           ;(mov ax,bx 的机器码占两个字节)
          mov si, offset s     ;获得标号s的偏移地址
          mov di, offset s0    ;获得标号s0的偏移地址
          
          mov ax, cs:[si]
          mov cs:[di], ax
     s0:  nop                     ;(nop的机器码占一个字节)
          nop
     codesg ends
     ends
    

    2、jmp指令

    jmp为无条件转移,转到标号处执行指令可以只修改IP,也可以同时修改CS和IP;

    jmp指令要给出两种信息:

    • 转移的目的地址
    • 转移的距离(段间转移、段内短转移,段内近转移)

    jmp short 标号 jmp near ptr 标号 jcxz 标号 loop 标号 等几种汇编指令,它们对 IP的修改

    是根据转移目的地址和转移起始地址之间的位移来进行的。在它们对应的机器码中不包含转移的目的地址,而包含的是到目的地址的位移距离。

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

    jmp short 标号(段内短转移)

    指令“jmp short 标号”的功能为(IP)=(IP)+8位位移,转到标号处执行指令

    (1)8位位移 = “标号”处的地址 - jmp指令后的第一个字节的地址;

    (2)short指明此处的位移为8位位移;

    (3)8位位移的范围为-128~127,用补码表示

    (4)8位位移由编译程序在编译时算出。

    assume cs:codesg
    codesg segment
      start:mov ax,0
            jmp short s ;s不是被翻译成目的地址
            add ax, 1
          s:inc ax ;程序执行后, ax中的值为 1 
    codesg ends
    end start
    

    CPU不需要这个目的地址就可以实现对IP的修改。这里是依据位移进行转移

    jmp short s指令的读取和执行过程:

    1. (CS)=0BBDH,(IP)=0006,上一条指令执行结束后CS:IP指向EB 03(jmp short s的机器码);
    2. 读取指令码EB 03进入指令缓冲器;
    3. (IP) = (IP) + 所读取指令的长度 = (IP) + 2 = 0008,CS:IP指向add ax,1;
    4. CPU指行指令缓冲器中的指令EB 03;
    5. 指令EB 03执行后,(IP)=000BH,CS:IP指向inc ax

    jmp near ptr 标号 (段内近转移)

    指令“jmp near ptr 标号”的功能为:(IP) = (IP) + 16位位移

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

    jmp far ptr 标号(段间转移或远转移)

    指令 “jmp far ptr 标号” 功能如下:

    • (CS) = 标号所在段的段地址;
    • (IP) = 标号所在段中的偏移地址。
    • far ptr指明了指令用标号的段地址和偏移地址修改CS和IP
    assume cs:codesg
    codesg segment
       start: mov ax, 0
    		  mov bx, 0
              jmp far ptr  s ;s被翻译成转移的目的地址0B01 BD0B
              db 256 dup (0) ;转移的段地址:0BBDH,偏移地址:010BH
        s:    add ax,1
              inc ax
    codesg ends
    end start
    

    在这里插入图片描述

    3、转移地址在寄存器或内存中的jmp指令

    jmp 16位寄存器 功能:IP =(16位寄存器)

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

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

    功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址。

    mov ax, 0123H
    mov ds:[0], ax
    jmp word ptr ds:[0]
    ;执行后,(IP)=0123H
    
    • jmp dword ptr 内存单元地址(段间转移)

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

    1. (CS)=(内存单元地址+2)
    2. (IP)=(内存单元地址)
    mov ax, 0123H
    mov ds:[0], ax;偏移地址
    mov word ptr ds:[2], 0;段地址
    jmp dword ptr ds:[0]
    ;执行后,
    ;(CS)=0
    ;(IP)=0123H
    ;CS:IP 指向 0000:0123。
    

    4、jcxz指令和loop指令

    jcxz指令

    jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,

    在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为-128~127。

    指令格式:jcxz 标号(如果(cx)=0,则转移到标号处执行。)

    当(cx) = 0时,(IP) = (IP) + 8位位移

    • 8位位移 = “标号”处的地址 - jcxz指令后的第一个字节的地址;
    • 8位位移的范围为-128~127,用补码表示;
    • 8位位移由编译程序在编译时算出。

    当(cx)!=0时,什么也不做(程序向下执行)

    loop指令

    loop指令为循环指令,所有的循环指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。

    对IP的修改范围都为-128~127。

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

    (cx) = (cx) - 1;如果 (cx) != 0,(IP) = (IP) + 8位位移。

    • 8位位移 = 标号处的地址 - loop指令后的第一个字节的地址;
    • 8位位移的范围为-128~127,用补码表示;
    • 8位位移由编译程序在编译时算出。

    如果(cx)= 0,什么也不做(程序向下执行)。

    九、call和ret指令


    call和ret指令都是转移指令,它们都修改IP,或同时修改CS和IP。

    1、ret 和 retf

    • ret指令用栈中的数据,修改IP的内容,从而实现近转移;

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

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

    (1)(IP) = ( (ss) * 16 + (sp) )

    (2)(sp) = (sp) + 2

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

    (1)(IP) = ( (ss) * 16 + (sp) )

    (2)(sp) = (sp) + 2

    (3)(CS) = ( (ss) * 16 + (sp) )

    (4)(sp) = (sp) + 2

    assume cs:code 
    stack seqment
    	db 16 dup (0)
    stack ends 
    
    code segment
    		mov ax, 4c00h
    		int 21h 
     start:	mov ax, stack 
     		mov ss, ax
     		mov sp, 16
    		mov ax, 0
    		push ax ;ax入栈
    		mov bx, 0
    		ret ;ret指令执行后,(IP)=0,CS:IP指向代码段的第一条指令。可以push cs  push ax  retf
    code ends
    end start
    

    2、call 指令

    call指令经常跟ret指令配合使用,因此CPU执行call指令,进行两步操作:

    (1)将当前的 IP 或 CS和IP 压入栈中;

    (2)转移(jmp)。

    call指令不能实现短转移,除此之外,call指令实现转移的方法和 jmp 指令的原理相同

    call 标号(近转移)

    CPU执行此种格式的call指令时,相当于进行 push IP jmp near ptr 标号

    call far ptr 标号(段间转移)

    CPU执行此种格式的call指令时,相当于进行:push CS,push IP jmp far ptr 标号

    call 16位寄存器

    CPU执行此种格式的call指令时,相当于进行: push IP jmp 16位寄存器

    call word ptr 内存单元地址

    CPU执行此种格式的call指令时,相当于进行:push IP jmp word ptr 内存单元地址

    mov sp, 10h
    mov ax, 0123h
    mov ds:[0], ax
    call word ptr ds:[0]
    ;执行后,(IP)=0123H,(sp)=0EH
    

    call dword ptr 内存单元地址

    CPU执行此种格式的call指令时,相当于进行:push CS push IP jmp dword ptr 内存单元地址

    mov sp, 10h
    mov ax, 0123h
    mov ds:[0], ax
    mov word ptr ds:[2], 0
    call dword ptr ds:[0]
    ;执行后,(CS)=0,(IP)=0123H,(sp)=0CH
    

    3、call 和 ret 的配合使用

    分析下面程序

    assume cs:code
    code segment
    start:	mov ax,1
    	    mov cx,3
         	call s ;(1)CPU指令缓冲器存放call指令,IP指向下一条指令(mov bx, ax),执行call指令,IP入栈,jmp
         	
    	    mov bx,ax	;(4)IP重新指向这里  bx = 8
         	mov ax,4c00h
         	int 21h
         s: add ax,ax
         	loop s;(2)循环3次ax = 8
    	    ret;(3)return : pop IP
    code ends
    end start
    

    call 与 ret 指令共同支持了汇编语言编程中的模块化设计

    编写子程序

    十、标志寄存器


    1、标志寄存器

    CPU内部的寄存器中,有一种特殊的寄存器(对于不同的处理机,个数和结构都可能不同)具有以下3种作用。

    (1)用来存储相关指令的某些执行结果;

    (2)用来为CPU执行相关指令提供行为依据;

    (3)用来控制CPU的相关工作方式。

    这种特殊的寄存器在8086CPU中,被称为标志寄存器(flag)。

    8086CPU的标志寄存器有16位,其中存储的信息通常被称为程序状态字(PSW-Program Status Word)

    flag寄存器是按位起作用的,它的每一位都有专门的含义,记录特定的信息。

    在这里插入图片描述
    在8086CPU的指令集中,有的指令的执行是影响标志寄存器的,比如,add、sub、mul、div、inc、or、and等,它们大都是运算指令(进行逻辑或算术运算);有的指令的执行对标志寄存器没有影响,比如,mov、push、pop等,它们大都是传送指令

    1、零标志位 (ZF)

    零标志位(Zero Flag)。它记录相关指令执行后,其结果是否为0。

    如果结果为0,那么zf = 1(表示结果是0);如果结果不为0,那么zf = 0。

    mov ax, 1
    sub ax, 1 ;执行后,结果为0,则zf = 1
    
    mov ax, 2
    sub ax, 1 ;执行后,结果不为0,则zf = 0
    

    2、奇偶标志位 (PF)

    奇偶标志位(Parity Flag)。它记录相关指令执行后,其结果的所有bit位中1的个数是否为偶数。

    如果1的个数为偶数,pf = 1,如果为奇数,那么pf = 0。

    mov al, 1
    add al, 10 ;执行后,结果为00001011B,其中有3(奇数)个1,则pf = 0;
    
    mov al, 1
    or al, 2  ;执行后,结果为00000011B,其中有2(偶数)个1,则pf = 1;
    

    3、符号标志位(SF)

    符号标志位(Symbol Flag)。它记录相关指令执行后,其结果是否为负。

    如果结果为负,sf = 1;如果非负,sf = 0。

    计算机中通常用补码来表示有符号数据。计算机中的一个数据可以看作是有符号数,也可以看成是无符号数。

    00000001B,可以看作为无符号数1,或有符号数+1;
    10000001B,可以看作为无符号数129,也可以看作有符号数-127。

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

    CPU在执行add等指令的时候,就包含了两种含义:可以将add指令进行的运算当作无符号数的运算,也可以将add指令进行的运算当作有符号数的运算

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

    mov al, 10000001B 
    add al, 1   ;执行后,结果为10000010B,sf = 1,表示:如果指令进行的是有符号数运算,那么结果为负;
    
    mov al, 10000001B
    add al, 01111111B   ;执行后,结果为0,sf = 0,表示:如果指令进行的是有符号数运算,那么结果为非负
    

    3、进位标志位(CF)

    进位标志位(Carry Flag)。一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值
    在这里插入图片描述
    97H - 98H 产生借位CF = 1 ==》 (al) = 197H - 98H = FFH

    4、溢出标志位(OF)

    溢出标志位(Overflow Flag)。一般情况下,OF记录了有符号数运算的结果是否发生了溢出。

    如果发生溢出,OF = 1;如果没有,OF = 0。

    CF和OF的区别:CF是对无符号数运算有意义的标志位,而OF是对有符号数运算有意义的标志位

    CPU在执行add等指令的时候,就包含了两种含义:无符号数运算和有符号数运算。

    • 对于无符号数运算,CPU用CF位来记录是否产生了进位;
    • 对于有符号数运算,CPU用OF位来记录是否产生了溢出,当然,还要用SF位来记录结果的符号。
    mov al, 98
    add al, 99   ;执行后将产生溢出。因为进行的"有符号数"运算是:(al)=(al)+ 99 = 98 + 99=197 = C5H 为-59的补码
                 ;而结果197超出了机器所能表示的8位有符号数的范围:-128-127。
                 ;add 指令执行后:无符号运算没有进位CF=0,有符号运算溢出OF=1
                 ;当取出的数据C5H按无符号解析C5H = 197, 当按有符号解析通过SP得知数据为负,即C5H为-59补码存储,
                 
    mov al,0F0H  ;F0H,为有符号数-16的补码   -Not(F0 - 1)
    add al,088H  ;88H,为有符号数-120的补码   -Not(88- 1)
                  ;执行后,将产生溢出。因为add al, 088H进行的有符号数运算结果是:(al)= -136 
                  ;而结果-136超出了机器所能表示的8位有符号数的范围:-128-127。
                  ;add 指令执行后:无符号运算有进位CF=1,有符号运算溢出OF=1
    

    2、adc指令和sbb指令

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

    指令格式:adc 操作对象1, 操作对象2

    功能:操作对象1 = 操作对象1 + 操作对象2 + CF

    mov ax, 2
    mov bx, 1
    sub bx, ax  ;无符号运算借位CF=1,有符号运算OF = 0
    adc ax, 1   ;执行后,(ax)= 4。adc执行时,相当于计算:(ax)+1+CF = 2+1+1 = 4。
    

    在这里插入图片描述

    ;计算1EF000H+201000H,结果放在ax(高16位)和bx(低16位)中。
    ;将计算分两步进行,先将低16位相加,然后将高16位和进位值相加。
    mov ax, 001EH 
    mov bx, 0F000H 
    add bx, 1000H
    adc ax, 0020H
    

    sbb指令

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

    指令格式:sbb 操作对象1, 操作对象2

    功能:操作对象1 = 操作对象1 - 操作对象2 - CF

    ;计算 003E1000H - 00202000H,结果放在ax,bx中,程序如下:
    mov bx, 1000H
    mov ax, 003EH
    sub bx, 2000H
    sbb ax, 0020H
    

    3、cmp指令

    cmp是比较指令,cmp的功能相当于减法指令,只是不保存结果。cmp指令执行后,将对标志寄存器产生影响。

    其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。

    cmp指令格式:cmp 操作对象1,操作对象2

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

    CPU在执行cmp指令的时候,也包含两种含义:进行无符号数运算和进行有符号数运算。

    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

    上面的表格可以正推也可以逆推

    如果用cmp来进行有符号数比较时
    SF只能记录实际结果的正负,发生溢出的时候,实际结果的正负不能说明逻辑上真正结果的正负
    但是逻辑上的结果的正负,才是cmp指令所求的真正结果,所以我们在考察SF的同时考察OF,就可以得知逻辑上真正结果的正负,同时就知道比较的结果。

    mov ah, 08AH  ; -Not(8A-1) = -118  即当成有符号数时为-118
    mov bh, 070H  ; 有符号数时最高位为0为正数, 70H = 112
    cmp ah, bh    ;(ah)-(bh)实际得到的结果是1AH 
    		      ; 在逻辑上,运算所应该得到的结果是:(-118)- 112 = -230
    		      ; sf记录实际结果的正负,所以sf=0
    

    cmp ah, bh
    (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)。

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

    可以根据某种条件,决定是否修改IP的指令

    jcxz它可以检测cx中的数值,如果(cx)=0,就修改IP,否则什么也不做。

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

    多数条件转移指令都检测标志寄存器的相关标志位,根据检测的结果来决定是否修改IP

    这些条件转移指令通常都和cmp相配合使用,它们所检测的标志位,都是cmp指令进行无符号数比较的时记录比较结果的标志位

    根据无符号数的比较结果进行转移的条件转移指令(它们检测zf、cf的值)

    指令 含义 检测的相关标志位
    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,b:below,a:above,n:not

    ;编程,统计data段中数值为8的字节的个数,用ax保存统计结果。
    mov ax, data 
    mov ds, ax 
    mov bx, 0   ;ds:bx指向第一个字节
    mov ax, 0   ;初始化累加器mov cx,8
    
    s:
    	cmp byte ptr [bx], 8   ;和8进行比较
    	jne next  ;如果不相等转到next,继续循环
    	inc ax  ;如果相等就将计数值加1
    next:
    	inc bx
    	loop s ;程序执行后:(ax)=3
    

    5、DF标志和串传送指令

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

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

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

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

    格式:rep movsb
    movsb和movsw进行的是串传送操作中的一个步骤,一般来说,movsb和movsw都和rep配合使用,
    功能:rep的作用是根据cx的值,重复执行后面的串传送指令

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

    • cld指令:将标志寄存器的df位置0
    • std指令:将标志寄存器的df位置1
    ;将data段中的第一个字符串复制到它后面的空间中。
    data segment 
    	db 'Welcome to masm!'
    	db 16 dup (0)
    data ends
    
    mov ax, data 
    mov ds, ax 
    mov si, 0   ;ds:si 指向data:0
    mov es, ax 
    mov di, 16  ;es:di指向data:0010
    
    mov cx, 16  ;(cx)=16,rep循环16次
    cld  ;设置df=0,正向传送
    rep movsb
    

    6、pushf和popf

    pushf的功能是将标志寄存器的值压栈,而popf是从栈中弹出数据,送入标志寄存器中

    pushf和popf,为直接访问标志寄存器提供了一种方法。

    十一、内中断

    1、内中断的产生

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

    中断信息可以来自CPU的内部和外部(内中断,外中断)

    内中断:当CPU的内部有需要处理的事情发生的时候,将产生中断信息,引发中断过程。这种中断信息来自CPU的内部

    8086CPU的内中断(下面四种情况将产生中断信息)

    • 除法错误,比如,执行div指令产生的除法溢出;
    • 单步执行;
    • 执行 into指令;
    • 执行 int指令。

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

    上述的4种中断源,在8086CPU中的中断类型码如下。

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

    2、中断处理程序、中断向量表、中断过程

    中断处理程序

    用来处理中断信息的程序被称为中断处理程序。

    根据CPU的设计,中断类型码的作用就是用来定位中断处理程序。比如CPU根据中断类型码4,就可以找到4号中断的处理程序

    中断向量表

    中断向量就是中断处理程序的入口地址。中断向量表就是中断处理程序入口地址的列表

    CPU用8位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址
    在这里插入图片描述

    中断过程

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

    简要描述如下

    1. 取得中断类型码N;
    2. pushf
    3. TF=0,IF=0 (为什么这样参考单步中断)
    4. push CS , push IP
    5. (IP)=(N * 4),(CS)=(N * 4 + 2)

    硬件在完成中断过程后,CS:IP将指向中断处理程序的入口,CPU开始执行中断处理程序。

    3、iret指令

    CPU随时都可能执行中断处理程序,中断处理程序必须一直存储在内存某段空间之中
    而中断处理程序的入口地址,即中断向量,必须存储在对应的中断向量表表项中。

    中断处理程序的常规编写步骤:

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

    iret 指令描述为:pop IP pop CS popf

    iret指令执行后,CPU回到执行中断处理程序前的执行点继续执行程序

    4、除法错误中断的处理

    mov ax, 1000h 
    mov bh, 1
    div bh ;除法溢出错误
    

    1、当CPU执行div bh时,发生了除法溢出错误,产生0号中断信息,从而引发中断过程,

    2、CPU执行0号中断处理程序

    3、系统中的0号中断处理程序的功能:显示提示信息“Divide overflow”后,返回到操作系统中。

    编程实验

    编程:编写0号中断处理程序do0,当发生除法溢出时,在屏幕中间显示“overflow!”,返回DOS。

    1、0000:0200至0000:02FF的256个字节的空间所对应的中断向量表项都是空的,可以将中断处理程序do0传送到内存0000:0200处。

    2、中断处理程序do0放到0000:0200,再将其地址登记在中断向量表对应表项

    • 0号表项的地址0:00:0字单元存放偏移地址,0:2字单元存放段地址
    • 将do0的段地址0存放在0000:0002字单元中,将偏移地址200H存放在0000:0000字单元
    assume cs:code
    
    code segment
    start:	
    		mov ax, cs
    		mov ds, ax
    		mov si, offset do0		;设置ds:si指向源地址
    		mov ax, 0
    		mov es, ax
    		mov di, 200h			;设置es:di指向目的地址0000:0200
    		mov cx, offset do0end - offset do0		;设置cx为传输长度 编译时给出do0部分代码长度
    		cld				        ;设置传输方向为正
    		rep movsb ;将do0的代码送入0:200处
    		
    		mov ax, 0               ;设置中断向量表
    		mov es, ax
    		mov word ptr es:[0*4], 200h
    		mov word ptr es:[0*4+2], 0
    
          	mov ax,4c00h
          	int 21h
    
    ;do0程序的主要任务是显示字符串
    do0:	jmp short do0 start 
          	db "overflow!"
    
    do0start:
          	mov ax, cs
          	mov ds, ax
          	mov si, 202h			;设置ds:si指向字符串
    
          	mov ax, 0b800h
          	mov es, ax
    		mov di, 12*160+36*2		;设置es:di指向显存空间的中间位置
    
            mov cx, 9				;设置cx为字符串长度
    	s:	mov al, [si]
          	mov es:[di], al
          	inc si
          	add di, 1
    		mov al, 02h             ;设置颜色
    		mov es:[di], al        
    		add di, 1
          	loop s
    
          	mov ax, 4c00h
          	int 21h
    do0end:	nop
    
    code ends
    end start
    
    

    5、单步中断

    CPU在执行完一条指令之后,如果检测到标志寄存器的TF位为1,则产生单步中断,引发中断过程。单步中断的中断类型码为1

    Debug是如何利用CPU所提供的单步中断的功能进行调试?如使用t命令查看寄存器状态

    Debug提供了单步中断的中断处理程序,功能为显示所有寄存器中的内容后等待输入命令

    在使用t命令执行指令时,Debug将TF设置为1,在CPU执行完这条指令后就引发单步中断,执行单步中断的中断处理程序,所有寄存器中的内容被显示在屏幕上,并且等待输入命令。

    在进入中断处理程序之前,设置TF=0。从而避免CPU在执行中断处理程序的时候发生单步中断

    6、int指令

    int指令的格式为:int n ,n为中断类型码,它的功能是引发中断过程。

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

    在程序中使用int指令调用任何一个中断的中断处理程序(中断例程)

    编写供应用程序调用的中断例程

    实验1

    ;求2 * 3456^2
    assume cs:code
    
    code segment
    
    start: 
         mov ax, 3456 ;(ax)=3456
    ​     int 7ch  ; 调用中断7ch的中断例程,计算ax中的数据的平方
    ​     add ax, ax  
    ​     adc dx, dx  ;存放结果,将结果乘以2
    
    ​     mov ax,4c00h
    ​     int 21h
    code ends
    end start 
    
    ;编程:安装中断7ch的中断例程
    ;功能:求一word型数据的平方。
    ;参数:(ax) = 要计算的数据。
    ;返回值:dx、ax中存放结果的高16位和低16位。
    
    assume cs:code
    
    code segment
    start:
    		mov ax,cs
    		mov ds,ax
    		mov si,offset sqr					;设置ds:si指向源地址
    		mov ax,0
    		mov es,ax
    		mov di,200h							;设置es:di指向目的地址
    		mov cx,offset sqrend - offset sqr	;设置cx为传输长度
    		cld									;设置传输方向为正
    		rep movsb
    
    		mov ax,0
    		mov es,ax
    		mov word ptr es:[7ch*4], 200h
    		mov word ptr es:[7ch*4+2], 0
    
    		mov ax,4c00h
    		int 21h
    
      sqr:  
    		mul ax
    		iret  ;CPU执行int 7ch指令进入中断例程之前,标志寄存器、当前的CS和IP被压入栈
    		      ;在执行完中断例程后,应该用iret 指令恢复int 7ch执行前的标志寄存器和CS、IP的
    sqrend:	nop
    
    code ends
    end start
    

    实验2

    ;功能:将一个全是字母,以0结尾的字符串,转化为大写。
    ;参数:ds:si指向字符串的首地址。
    ;应用举例:将data段中的字符串转化为大写。
    assume cs:code
    
    data segment
    	db 'conversation',0
    data ends
    
    code segment
    start:  mov ax, data
    		mov ds, ax
    		mov si, 0
    		int 7ch
    		
    		mov ax,4c00h
    		int 21h
    code ends
    end start   
    
    
    assume cs:code
    code segment
    
    start:
    		mov ax,cs
    		mov ds,ax
    		mov si,offset capital
    		mov ax,0
    		mov es,ax
    		mov di,200h
    		mov cx,offset capitalend - offset capital
    		cld
    		rep movsb
    
    		mov ax,0
    		mov es,ax
    		mov word ptr es:[7ch*4],200h
    		mov word ptr es:[7ch*4+2],0
    
    		mov ax,4c00h
    		int 21h
    
    capital:
    		push cx
    		push si
    		
    change: 
    		mov cl,[si]
    		mov ch,0
    		jcxz ok
    		and byte ptr [si],11011111b
    		inc si
    		jmp short change
    ok:	
    		pop si
    		pop cx
    		iret
    		
    capitalend:nop
    
    code ends
    
    end start
    
    

    7、BIOS和DOS所提供的中断例程

    在系统板的ROM中存放着一套程序,称为BIOS(基本输入输出系统)

    BIOS中主要包含以下几部分内容

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

    程序员在编程的时候,可以用int 指令直接调用BIOS和DOS系统提供的中断例程,来完成某些工作。
    和硬件设备相关的DOS中断例程中,一般都调用了BIOS的中断例程。

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

    BIOS和DOS提供的中断例程是如何安装到内存中的呢?

    1、开机后,CPU一加电,初始化(CS)= 0FFFFH,(IP)= 0,自动从FFFF:0单元开始执行程序。FFFF:0处有一条转跳指令,CPU执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序。

    2、初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。
    注意,对于BIOS所提供的中断例程,只需将入口地址登记在中断向量表中即可,因为它们是固化到ROM中的程序,一直在内存中存在。

    3、硬件系统检测和初始化完成后,调用int 19h进行操作系统的引导。从此将计算机交由操作系统控制。

    4、DOS启动后,除完成其他工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量。

    BIOS中断例程应用

    一般来说,一个供程序员调用的中断例程中往往包括多个子程序,中断例程内部用传递进来的参数来决定执行哪一个子程序。

    BIOS和DOS提供的中断例程,都用 ah 来传递内部子程序的编号。

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

    assume cs:code 
    
    code segment
    ;int 10h中断例程的"设置光标位置"功能
    mov ah, 2;设置光标调用第10h号中断例程的2号子程序,功能为设置光标位置(可以提供光标所在的行号、列号和页号作为参数)
    
    ;设置光标到第0页,第5行,第12列
    mov bh, 0;第0页
    mov dh, 5;dh中放行号
    mov dl, 12;dl中放列号
    int 10h
    
    ;int10h中断例程的"在光标位置显示字符"功能。
    mov ah,9 ;调用第10h号中断例程的9号子程序,功能为在光标位置显示字符
    ;提供要显示的字符、颜色属性、页号、字符重复个数作为参数
    mov al,'a'  ;字符
    mov b1,11001010b  ;颜色属性
    mov bh,0  ;第0页
    mov cx,3  ;字符重复个数
    int 10h
    
    code ends 
    end
    

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

    DOS中断例程应用
    int 21h中断例程是DOS提供的中断例程,4ch号功能,即程序返回功能

    mov ah, 4ch ;调用第21h号中断例程的4ch号子程序,功能为程序返回,可以提供返回值作为参数
    mov al, 0 ;返回值
    int 21h
    

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

    assume cs:code 
     
    data segment 
    	db	'Welcome to masm',  '$'     ;“$”本身并不显示,只起到边界的作用
    data ends 
    
    code segment
    start:	mov ah, 2 ;10号中断设置光标位置功能
    		mov bh, 0 ;第0页
    		mov dh, 5;dh中放行号
    		mov dl, 12 ;dl中放列号
    		int 10h 
    		
    		mov ax, data 
    		mov ds, ax 
    		mov dx, 0 ;ds:dx指向字符串的首地址data:0  (参数)
    		mov ah, 9 ;调用第21h号中断例程的9号子程序,功能为在光标位置显示字符串,可以提供要显示字符串的地址作为参数
    		int 21h 
    		
    		mov ax, 4c00h ;21号中断程序返回功能
    		int 21h 
    code ends
    end start
    

    十二、端口

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

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

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

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

    从CPU的角度,将这些寄存器都当作端口,对它们进行统一编址,从而建立了一个统一的端口地址空间。
    每一个端口在地址空间中都有一个地址。在访问端口的时候,CPU通过端口地址来定位端口。因为端口所在的芯片和CPU通过总线相连,

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

    • CPU内部的寄存器;
    • 内存单元;
    • 端口。

    1、端口的读写

    端口地址和内存地址一样,通过地址总线来传送。在PC系统中,CPU最多可以定位64KB个不同的端口。则端口地址的范围为0-65535

    端口的读写指令只有两条:inout,分别用于从端口读取数据和往端口写入数据。

    在in和out指令中,只能使用ax或al来存放从端口中读入的数据或要发送到端口中的数据。

    ;对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端口写入一个字节
    

    2、CMOS RAM芯片

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

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

    CMOS RAM中存储的时间信息

    在CMOS RAM中,存放着当前的时间:年、月、日、时、分、秒。长度都为1个字节,
    存放单元为:

    9 8 7 6 5 4 3 2 1 0

    BCD码是以4位二进制数表示十进制数码的编码方法 4 == 0100B

    一个字节可表示两个BCD码。则CMOS RAM存储时间信息的单元中,存储了用两个BCD码表示的两位十进制数,高4位的BCD码表示十位,低4位的BCD码表示个位。比如,00010100b表示14。

    ;编程,在屏幕中间显示当前的月份。
    assume cs:code
    code segment 
    start:	mov al,8 ;从CMOS RAM的8号单元读出当前月份的BCD码。
    		out 70h,al 
    		in al, 71h ;从数据端口71h中取得指定单元中的数据:
    		
    		mov ah, al ;al中为从CMOS RAM的8号单元中读出的数据
    		mov cl, 4
    		shr ah, cl ;ah中为月份的十位数码值,左移四位空出四位
    		and al, 00001111b ;al中为月份的个位数码值
    		
    		add ah, 30h ;BCD码值+30h=十进制数对应的ASCII
    		add al, 30h 
    		
    		mov bx, 0b800h 
    		mov es, bx 
    		mov byte ptr es:[160*12+40*2], ah ;显示月份的十位数码
    		mov byte ptr es:[160*12+40*2+2], al ;接着显示月份的个位数码
    		
    		mov ax,4c00h
    		int 21h
    code ends
    end start
    

    3、shl和shr指令

    shl和shr是逻辑移位指令

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

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

    shr是逻辑右移指令,同理

    mov al, 01001000b 
    shl al, 1 ;将a1中的数据左移一位执行后(al)=10010000b,CF=0。
    
    mov al, 01010001b 
    mov cl, 3 ;如果移动位数大于1时,必须将移动位数放在cl中
    shl al, c1
    
    mov al, 10000001b 
    shr al, 1  ;将al中的数据右移一位执行后(al)=01000000b,CF=1。
    

    将X逻辑左移一位,相当于执行X=X*2。
    将X逻辑右移一位,相当于执行X=X/2

    十三、外中断

    1、外中断

    CPU在计算机系统中,除了能够执行指令,进行运算以外,还应该能够对外部设备进行控制,接收它们的输入,向它们进行输出(I/O能力)

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

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

    即:CPU通过端口和外部设备进行联系

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

    PC系统中,外中断源有两类

    1、可屏蔽中断

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

    可屏蔽中断信息来自于CPU外部,中断类型码是通过数据总线送入CPU的;而内中断的中断类型码是在CPU内部产生的。

    中断过程中将IF置0的原因就是,在进入中断处理程序后,禁止其他的可屏蔽中断。
    如果在中断处理程序中需要处理可屏蔽中断,可以用指令将IF置1。

    8086CPU提供的设置IF的指令:sti,设置IF=1;cli,设置IF=0。

    2、不可屏蔽中断

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

    对于8086CPU,不可屏蔽中断的中断类型码固定为2,所以中断过程中,不需要取中断类型码。则不可屏蔽中断的中断过程为:①标志寄存器入栈,IF=0,TF=0;②CS、IP入栈;③(IP)=(8),(CS)=(0AH)。

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

    2、PC机键盘的处理过程

    键盘中有一个芯片对键盘上的每一个键的开关状态进行扫描。按下一个键时,开关接通,该芯片就产生一个扫描码,扫描码说明了按下的键在键盘上的位置。扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为60h。松开按下的键时,也产生一个扫描码,扫描码说明了松开的键在键盘上的位置。松开按键时产生的扫描码也被送入60h端口中。

    一般将按下一个键时产生的扫描码称为通码,松开一个键产生的扫描码称为断码。

    扫描码长度为一个字节,通码的第7位为0,断码的第7位为1
    即:断码 = 通码 + 80h。比如,g键的通码为22h,断码为a2h

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

    在这里插入图片描述

    BIOS提供了int 9中断例程,用来进行基本的键盘输入处理,主要的工作如下:
    (1)读出60h端口中的扫描码;
    (2)如果是字符键的扫描码,将该扫描码和它所对应的字符码(即ASCII码)送入内存中的BIOS键盘缓冲区; 如果是控制键(比如Ctrl)和切换键(比如CapsLock)的扫描码,则将其转变为状态字节写入内存中存储状态字节的单元;
    (3)对键盘系统进行相关的控制,比如说,向相关芯片发出应答信息。

    BIOS键盘缓冲区可以存储15个键盘输入,一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。

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

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

    编写int 9中断例程

    ;编程:在屏幕中间依次显示“a”~“z”,并可以让人看清。在显示的过程中,按下'Esc'键后,改变显示的颜色。
    
    ;完整功能代码:
    
    assume cs:code
    
    stack segment
    	db 128 dup (0)
    stack ends
    
    data segment
    	dw 0,0
    data ends
    
    code segment
    start:	
    	mov ax,stack
    	mov ss,ax
    	mov sp,128
    	mov ax,data
    	mov ds,ax
    	mov ax,0
    	mov es,ax
    
    	push es:[9*4]
    	pop ds:[0]
    	push es:[9*4+2]
    	pop ds:[2]		;将原来的int 9中断例程的入口地址保存在ds:0、ds:2单元中
    
    	mov word ptr es:[9*4], offset int9
    	mov es:[9*4+2], cs	;在中断向量表中设置新的int 9中断例程的入口地址
    
    ;显示字符串
    	mov ax, 0b800h
    	mov es, ax
    	mov ah, 'a'
    s:	
    	mov  es:[160*12+40*2], ah
    	call delay
    	inc ah
    	cmp ah, 'z'
    	jna s
    	mov ax,0
    	mov es,ax
    
    	push ds:[0]
    	pop es:[9*4]
    	push ds;[2]
    	pop es;[9*4+2]   	;将中断向量表中int 9中断例程的入口恢复为原来的地址
    
    	mov ax,4c00h
    	int 21h
    
    ;将循环延时的程序段写为一个子程序
    delay:	
    	push ax 
    	push dx
    	mov dx, 2000h  ;用两个16位寄存器来存放32位的循环次数
    	mov ax, 0
    s1: 	
    	sub ax, 1
    	sbb dx, 0
    	cmp ax, 0
    	jne s1
    	cmp dx, 0
    	jne s1
    	pop dx
    	pop ax
    	ret
    
    ;------以下为新的int 9中断例程--------------------
    
    int9:	
    	push ax
    	push bx
    	push es
    
    	in al, 60h;从端口60h读出键盘的输入
    
    	pushf ;标志寄存器入栈
    
    	pushf   
    	pop bx
    	and bh,11111100b
    	push bx
    	popf	;TF=0,IF=0
    	
    	call dword ptr ds:[0] 	;对int指令进行模拟,调用原来的int 9中断例程
    
    	cmp al,1
    	jne int9ret
    
    	mov ax,0b800h
    	mov es,ax
    	inc byte ptr es:[160*12+40*2+1]  ;属性增加1,改变颜色
    
    int9ret:
    	pop es
    	pop bx
    	pop ax
    	iret
    
    code ends
    
    end start
    
    

    CPU对外设输入的通常处理方法
    (1)外设的输入送入端口;
    (2)向CPU发出外中断(可屏蔽中断)信息;
    (3)CPU检测到可屏蔽中断信息,如果IF=1,CPU在执行完当前指令后响应中断,执行相应的中断例程;
    (4)可在中断例程中实现对外设输入的处理。

    端口和中断机制,是CPU进行I/O的基础。

    十四、直接定址表

    assume cs:code
    code segment
             a : db 1,2,3,4,5,6,7,8  ;在后面加有“:”的地址标号,只能在代码段中使用,不能在其他段中使用。
             b : dw 0
    start :mov si,offset a
             mov bx,offset b
             mov cx,8
        s : mov al,cs:[si]
             mov ah,0
             add cs:[bx],ax
             inc si
             loop s
             mov ax,4c00h
             int 21h
    code ends
    end start
    
    

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

    描述了单位长度的标号

    assume cs:code
    code segment
              a db 1,2,3,4,5,6,7,8 ;标号a、b后面没有":",因此它们是可以同时描述内存地址和单元长度的标号。
                                   ;标号a,描述了地址code:0,和从这个地址开始,以后的内存单元都是字节单元
              b dw 0               ;标号b描述了地址code:8,和从这个地址开始,以后的内存单元都是字单元。
    start :  mov si,0
              mov cx,8
        s :   mov al,a[si]
              mov ah,0
              add b,ax
              inc si
              loop s
              mov ax,4c00h
              int 21h
    code ends
    end start
    
    

    使用数据标号来描述存储数据的单元的地址和长度。

    assume cs:code,ds:data ;用伪指令assume将标号所在的段和一个段寄存器联系起来(编译器需要)
    data segment          
              a db 1,2,3,4,5,6,7,8
              b dw 0
    data ends
    code segment
    start:  mov ax,data
              mov ds,ax ;真正确定ds寄存器
              mov si,0
              mov cx,8
    s:       mov al,a[si] ;编译为:mov al,[si+0] 默认所访问单元的段地址在ds
              mov ah,0
              add b,ax ;编译为:add [8],ax
              inc si
              loop s
              mov ax,4c00h
              int 21h
    code ends
    end start
    
    data segment
    	a db 1,2,3,4,5,6,7,8
    	b dw 0
    	c dw a, b ;等价于c dw offset a, offset b
    	;数据标号c处存储的两个字型数据为标号a、b 的偏移地址
    data ends
    
    data segment
    	a db 1,2,3,4,5,6,7,8
    	b dw 0
    	c dd a,b ;等价于c dw offset a, seg a, offset b, seg b
    	;数据标号c处存储的两个双字型数据为标号a的偏移地址和段地址、标号b 的偏移地址和段地址
    data ends
    
    

    seg操作符,功能为取得某一标号的段地址

    建立一张表,表中依次存储字符“0”~“F”,我们可以通过数值0 ~ 15直接查找到对应的字符

    assume cs:code
    
    code segment
    start:  
    		mov al,0eh
    
            call showbyte
    
            mov ax,4c00h
            int 21h
    
    ;子程序:
    ;用al传送要显示的数据
    
    showbyte:
            jmp short show
    
            table db '0123456789ABCDEF'	;字符表
    
    show:   push bx
            push es
    
            mov ah,al
            shr ah,1           
            shr ah,1
            shr ah,1
            shr ah,1			    ;右移4位,ah中得到高4位的值
            and al,00001111b		;al中为低4位的值
    
            mov bl,ah
            mov bh,0
            mov ah,table[bx]		;用高4位的值作为相对于table的偏移,取得对应的字符
    
            mov bx,0b800h
            mov es,bx
            mov es:[160*12+40*2],ah
    
            mov bl,al
            mov bh,0
            mov al,table[bx]		;用低4位的值作为相对于table的偏移,取得对应的字符
            
            mov es:[160*12+40*2+2],al
    
            pop es
            pop bx
            ret
    
    code ends
    end start
    
    

    十五、 指令系统总结

    我们对8086CPU的指令系统进行一下总结。读者若要详细了解8086指令系统中的各个指令的用,可以查看有关的指令手册。

    8086CPU提供以下几大类指令。

    1. 数据传送指令
      mov、push、pop、pushf、popf、xchg 等都是数据传送指令,这些指令实现寄存器和内存、寄器和寄存器之间的单个数据传送。
    2. 算术运算指令
      add、sub、adc、sbb、inc、dec、cmp、imul、idiv、aaa等都是算术运算指令,这些指令实现存器和内存中的数据的算数运算。它们的执行结果影响标志寄存器的sf、zf、of、cf、pf、af位。
    3. 逻辑指令
      and、or、not、xor、test、shl、shr、sal、sar、rol、ror、rcl、rcr等都是逻辑指令。除了not指外,它们的执行结果都影响标志寄存器的相关标志位。
    4. 转移指令
      可以修改IP,或同时修改CS和IP的指令统称为转移指令。转移指令分为以下几类。
      (1)无条件转移指令,比如,jmp
      (2)条件转移指令,比如,jcxz、je、jb、ja、jnb、jna等;
      (3)循环指令,比如,loop
      (4)过程,比如,call、ret、retf
      (5)中断,比如,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 等前缀指令配合使用。

    文中大部分的图片来自王爽《汇编语言》有些图片来自刘宏伟·计算机组成原理课件和王道考研计算机组成原理
    博主靠这本书入门汇编,只是匆匆看了一遍,很多地方理解片面甚至错误,将来发现一定修正


    展开全文
  • 8086汇编语言讲座

    千人学习 2019-12-17 11:40:31
    关于8086汇编语言,是与计算机原理相结合的,通过汇编语言可以加深对计算机原理的掌握和理解。 另外汇编语言还是可以干成事的,比如破解,外挂等都会用到它。
  • 汇编语言程序设计

    千人学习 2018-10-22 21:38:07
    汇编语言是一门低级程序设计语言,在数以千计的计算机语言中,有着不可替代的重要地位,广泛地用于开发操作系统内核、设备驱动程序等。随着近年来物联网、嵌入式系统的发展,汇编语言在行业中的地位也再次攀升,在...
  • 汇编语言》第三版答案课后练习答案集

    万次阅读 多人点赞 2019-01-26 22:01:54
    相逢即是缘,记得关注我噢! 检测点1.1 (1)1个CPU的寻址能力为8KB,那么它的地址总线的宽度为 13位。 (2)1KB的存储器有 1024 个存储单元,存储单元的编号从 0 到 1023 。 (3)1KB的存储器可以存储 8192(2^13)...

    相逢即是缘,记得关注我噢!

    检测点1.1

    (1)1个CPU的寻址能力为8KB,那么它的地址总线的宽度为 13位。

    (2)1KB的存储器有 1024 个存储单元,存储单元的编号从 0 到 1023 。

    (3)1KB的存储器可以存储 8192(2^13) 个bit, 1024个Byte。

    (4)1GB是 1073741824 (2^30) 个Byte、1MB是 1048576(2^20) 个Byte、1KB是 1024(2^10)个Byte。

    (5)8080、8088、80296、80386的地址总线宽度分别为16根、20根、24根、32根,则它们的寻址能力分别为: 64 (KB)、 1 (MB)、 16 (MB)、 4 (GB)。

    (6)8080、8088、8086、80286、80386的数据总线宽度分别为8根、8根、16根、16根、32根。则它们一次可以传送的数据为: 1 (B)、 1 (B)、 2 (B)、 2 (B)、 4 (B)。

    (7)从内存中读取1024字节的数据,8086至少要读 512 次,80386至少要读 256 次。

    (8)在存储器中,数据和程序以 二进制 形式存放。

    解题过程:

    (1)1KB=1024B,8KB=1024B*8=2^N,N=13。

    (2)存储器的容量是以字节为最小单位来计算的,1KB=1024B。

    (3)8Bit=1Byte,1024Byte=1KB(1KB=1024B=1024B*8Bit)。

    (4)1GB=1073741824B(即230)1MB=1048576B(即220)1KB=1024B(即2^10)。

    (5)一个CPU有N根地址线,则可以说这个CPU的地址总线的宽度为N。这样的CPU最多可以寻找2的N次方个内存单元。(一个内存单元=1Byte)。

    (6)8根数据总线一次可以传送8位二进制数据(即一个字节)。

    (7)8086的数据总线宽度为16根(即一次传送的数据为2B)1024B/2B=512,同理1024B/4B=256。

    (8)在存储器中指令和数据没有任何区别,都是二进制信息。
    检测点 2.1

    (1) 写出每条汇编指令执行后相关寄存器中的值。

    mov ax,62627 AX=F4A3H

    mov ah,31H AX=31A3H

    mov al,23H AX=3123H

    add ax,ax AX=6246H

    mov bx,826CH BX=826CH

    mov cx,ax CX=6246H

    mov ax,bx AX=826CH

    add ax,bx AX=04D8H

    mov al,bh AX=0482H

    mov ah,bl AX=6C82H

    add ah,ah AX=D882H

    add al,6 AX=D888H

    add al,al AX=D810H

    mov ax,cx AX=6246H

    检测点2.1

    (2) 只能使用目前学过的汇编指令,最多使用4条指令,编程计算2的4次方。

    mov ax,2 AX=2

    add ax,ax AX=4

    add ax,ax AX=8

    add ax,ax AX=16

    检测点2.2

    (1) 给定段地址为0001H,仅通过变化偏移地址寻址,CPU的寻址范围为 0010H 到 1000FH 。

    解题过程:

    物理地址=SA*16+EA

    EA的变化范围为0h~ffffh

    物理地址范围为(SA16+0h)~(SA16+ffffh)

    现在SA=0001h,那么寻址范围为

    (0001h16+0h)~(0001h16+ffffh)

    =0010h~1000fh
    检测点2.2

    (2) 有一数据存放在内存20000H单元中,现给定段地址为SA,若想用偏移地址寻到此单元。则SA应满足的条件是:最小为 1001H ,最大为 2000H 。

    当段地址给定为 1001H 以下和 2000H 以上,CPU无论怎么变化偏移地址都无法寻到20000H单元。

    解题过程:

    物理地址=SA*16+EA

    20000h=SA*16+EA

    SA=(20000h-EA)/16=2000h-EA/16

    EA取最大值时,SA=2000h-ffffh/16=1001h,SA为最小值

    EA取最小值时,SA=2000h-0h/16=2000h,SA为最大值

    检测点2.3

    下面的3条指令执行后,cpu几次修改IP?都是在什么时候?最后IP中的值是多少?

    mov ax,bx

    sub ax,ax

    jmp ax

    答:一共修改四次

    第一次:读取mov ax,bx之后

    第二次:读取sub ax,ax之后

    第三次:读取jmp ax之后

    第四次:执行jmp ax修改IP

    最后IP的值为0000H,因为最后ax中的值为0000H,所以IP中的值也为0000H
    检测点3.1

    (1) 在DEBUG中,用 “D 0:0 lf” 查看内存,结果如下:

    0000:0000 70 80 F0 30 EF 60 30 E2-00 80 80 12 66 20 22 60

    0000:0010 62 26 E6 D6 CC 2E 3C 3B-AB BA 00 00 26 06 66 88

    下面的程序执行前,AX=0,BX=0,写出每条汇编指令执行完后相关寄存器中的值

    mov ax,1

    mov ds,ax

    mov ax,[0000] ax= 2662H

    mov bx,[0001] bx= E626H

    mov ax,bx ax= E626H

    mov ax,[0000] ax= 2662H

    mov bx,[0002] bx= D6E6H

    add ax,bx ax= FD48H

    add ax,[0004] ax= 2C14H

    mov ax,0 ax= 0

    mov al,[0002] ax= 00e6H

    mov bx,0 bx= 0

    mov bl,[000c] bx= 0026H

    add al,bl ax= 000CH

    检测点3.1

    (2) 内存中的情况如图3.6所示

    各寄存器的初始值:cs=2000h,ip=0,ds=1000h,ax=0,bx=0;

    检测点3.2

    (1)补全下面的程序,使其可以将10000H-1000FH中的8个字,逆序拷贝到20000H-2000FH中。

    mov ax,1000H

    mov ds,ax

    mov ax,2000H

    mov ss,ax

    mov sp,10h

    push [0]

    push [2]

    push [4]

    push [6]

    push [8]

    push [A]

    push [C]

    push [E]

    检测点3.2

    (2)补全下面的程序,使其可以将10000H-1000FH中的8个字,逆序拷贝到20000H-2000FH中。

    mov ax,2000H

    mov ds,ax

    mov ax,1000H

    mov ss,ax

    mov sp,0

    pop [e]

    pop [c]

    pop [a]

    pop [8]

    pop [6]

    pop [4]

    pop [2]

    pop [0]
    检测点6.1

    (1)下面的程序实现依次用内存0:0~0:15单元中的内容改写程序中的数据,完成程序:

    assume cs:codesg

    codesg segment

        dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
    

    start: mov ax,0

        mov ds,ax
    
        mov bx,0
    
        mov cx,8
    
    s:  mov ax,[bx]
    
         mov cs:[bx],ax
    
        add bx,2
    
        loop s
    
        mov ax,4c00h
    
        int 21h
    

    codesg ends

    end start

    检测点6.1

    (2)下面的程序实现依次用内存0:0~0:15单元中的内容改写程序中的数据,数据的传送用栈来进行。栈空间设置在程序内。完成程序:

    assume cs:codesg

    codesg segment

        dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
    
        dw 0,0,0,0,0,0,0,0,0,0
    

    start: mov ax, codesg ;或mov ax, cs

        mov ss,ax
    
        mov sp, 24h    ;或mov sp, 36     ;(第一版填1ah或26)
    
        mov ax,0
    
        mov ds,ax
    
        mov bx,0
    
        mov cx,8
    
    s:  push [bx]
    
         pop cs:[bx]   ;或 pop ss:[bx]
    
        add bx,2 
    
        loop s
    
        mov ax,4c00h
    
        int 21h
    

    codesg ends

    end start

    (1)程序如下。

    assume cs:code

    data segment

    dw 2 dup (0)

    data ends

    code segment

    start: mov ax,dtat

         mov ds,ax
    
         mov bx,0
    
         jmp word ptr [bx+1]
    

    code ends

    end start

    若要使jmp指令执行后,CS:IP指向程序的第一条指令,在data段中应该定义哪些数据?

    答案①db 3 dup (0)

    答案②dw 2 dup (0)

    答案③dd 0

    jmp word ptr [bx+1]为段内转移,要CS:IP指向程序的第一条指令,应设置ds:[bx+1]的字单元(2个字节)存放数据应为0,则(ip)=ds:[bx+1]=0

    简单来说就是,只要ds:[bx+1]起始地址的两个字节为0就可以了

    检测点9.1

    (1)程序如下。

    assume cs:code

    data segment

    dd 12345678h

    data ends

    code segment

    start: mov ax,data

         mov ds,ax
    
         mov bx,0
    
         mov [bx],  bx      ;或mov [bx], word ptr 0     ;或mov [bx], offset start
    
         mov [bx+2],  cs    ;或mov [bx+2],  cs          ;或mov [bx+2], seg code  
    
         jmp dword ptr ds:[0]
    

    code ends

    end start

    补全程序,使用jmp指令执行后,CS:IP指向程序的第一条指令。

    第一格可填①mov [bx],bx ②mov [bx],word ptr 0 ③mov [bx],offset start等。

    第二格可填①mov [bx+2],cs ②mov [bx+2],cs ③mov [bx+2],seg code等。

    解析:

    jmp dword ptr ds:[0]为段间转移,(cs)=(内存单元地址+2),(ip)=(内存单元地址),要CS:IP指向程序的第一条指令,第一条程序地址cs:0,应设置CS:IP指向cs:0

    程序中的mov [bx],bx这条指令,是将ip设置为0

    mov [bx+2],cs,将cs这个段地址放入内存单元

    执行后,cs应该不变,只调整ip为0,(ip)=ds:[0]=0

    检测点9.1

    (3)用Debug查看内存,结果如下:

    2000:1000 BE 00 06 00 00 00 …

    则此时,CPU执行指令:

    mov ax,2000h

    mov es,ax

    jmp dword ptr es:[1000h]

    后,(cs)= 0006H ,(ip)= 00BEH

    解析:

    jmp dword ptr为段间转移,高位存放段地址,低位存放偏移地址

    (cs)=(内存单元地址+2),(ip)=(内存单元地址)

    根据书P16,对于寄存器AX,AH为高位(前1字节为高位),AL为低位(后1字节为低位)

    推算出(内存单元地址)=00beh,(内存单元地址+2)=0006h

    根据书P182,高位存放段地址(后2个字节为高位),低位存放偏移地址(前2个字节为低位)

    (cs)=(内存单元地址+2),(ip)=(内存单元地址)

    推算出(cs)=0006h,(ip)=00beh

    检测点9.2

    补全编程,利用jcxz指令,实现在内存2000H段中查找第一个值为0的字节,找到后,将它的偏移地址存储在dx中。

    assume cs:code

    code segment

    start: mov ax,2000h

        mov ds,ax
    
        mov bx,0
    
     s: mov ch,0   
    
        mov cl,[bx]
    
        jcxz ok        ;当cx=0时,CS:IP指向OK
    
        inc bx     
    
        jmp short s
    
    ok: mov dx,bx
    
        mov ax ,4c00h
    
        int 21h
    

    code ends

    end start
    检测点9.3

    补全编程,利用loop指令,实现在内存2000H段中查找第一个值为0的字节,找到后,将它的偏移地址存储在dx中。

    assume cs:code

    code segment

    start: mov ax,2000h

        mov ds,ax
    
        mov bx,0
    
      s:mov cl,[bx]
    
        mov ch,0
    
        inc cx     
    
        inc bx
    
        loop s
    
     ok:dec bx
    
        mov dx,bx
    
        mov ax,4c00h
    
        int 21h
    

    code ends

    end start

    书P101,执行loop s时,首先要将(cx)减1。

    “loop 标号”相当于

    dec cx

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

    检测点10.1

    补全程序,实现从内存1000:0000处开始执行指令。

    assume cs:code

    stack segment

     db 16 dup (0)
    

    stack ends

    code segment

    start: mov ax,stack

     mov ss,ax
    
     mov sp,16
    
     mov ax, 1000h
    
     push ax
    
     mov ax,   0  
    
     push ax
    
     retf
    

    code ends

    end start

    执行reft指令时,相当于进行:

    pop ip

    pop cs

    根据栈先进后出原则,应先将段地址cs入栈,再将偏移地址ip入栈。

    检测点10.2

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

    内存地址 机器码 汇编指令 执行后情况

    1000:0 b8 00 00 mov ax,0 ax=0 ip指向1000:3

    1000:3 e8 01 00 call s pop ip ip指向1000:7

    1000:6 40 inc ax

    1000:7 58 s:pop ax ax=6

    用debug进行跟踪确认,“call 标号”是将该指令后的第一个字节偏移地址入栈,再转到标号处执行指令。

    assume cs:code

    code segment

    start: mov ax,0

     call s
    
     inc ax
    

    s: pop ax

     mov ax,4c00h
    
     int 21h
    

    code ends

    end start

    检测点10.3

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

    内存地址 机器码 汇编指令 执行后情况

    1000:0 b8 00 00 mov ax,0 ax=0,ip指向1000:3

    1000:3 9a 09 00 00 10 call far ptr s pop cs,pop ip,ip指向1000:9

    1000:8 40 inc ax

    1000:9 58 s:pop ax ax=8h

                            add ax,ax          ax=10h
    
                            pop bx             bx=1000h
    
                            add ax,bx          ax=1010h
    

    用debug进行跟踪确认,“call far ptr s”是先将该指令后的第一个字节段地址cs=1000h入栈,再将偏移地址ip=8h入栈,最后转到标号处执行指令。

    出栈时,根据栈先进后出的原则,先出的为ip=8h,后出的为cs=1000h
    检测点10.4

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

    内存地址 机器码 汇编指令 执行后情况

    1000:0 b8 06 00 mov ax,6 ax=6,ip指向1000:3

    1000:3 ff d0 call ax pop ip,ip指向1000:6

    1000:5 40 inc ax

    1000:6 58 mov bp,sp bp=sp=fffeh

                         add ax,[bp]    ax=[6+ds:(fffeh)]=6+5=0bh
    

    用debug进行跟踪确认,“call ax(16位reg)”是先将该指令后的第一个字节偏移地址ip入栈,再转到偏移地址为ax(16位reg)处执行指令。

    检测点10.5

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

    assume cs:code

    stack segment

     dw 8 dup (0)
    

    stack ends

    code segment

    start: mov ax,stack

     mov ss,ax
    
     mov sp,16
    
     mov ds,ax
    
     mov ax,0
    
     call word ptr ds:[0eh]
    
     inc ax
    
     inc ax
    
     inc ax
    
     mov ax,4c00h
    
     int 21h
    

    code ends

    end start

    推算:

    执行call word ptr ds:[0eh]指令时,先cs入栈,再ip=11入栈,最后ip转移到(ds:[0eh])。(ds:[0eh])=11h,执行inc ax……最终ax=3

    题中特别关照别用debug跟踪,跟踪结果不一定正确,但还是忍不住去试试,看是什么结果。

    根据单步跟踪发现,执行call word ptr ds:[0eh]指令时,显示ds:[0eh]=065D。

    ds:0000~ds:0010不是已设置成stack数据段了嘛,不是应该全都是0的嘛。

    于是进行了更详细的单步跟踪,发现初始数据段中数据确实为0,但执行完mov ss,ax;mov sp,16这两条指令后,数据段中数据发生改变。这是为什么呢?中断呗~~~~

    检测点10.5

    (2)下面的程序执行后,ax和bx中的数值为多少?

    assume cs:codesg

    stack segment

    dw 8 dup(0)
    

    stack ends

    codesg segment

    start:

    mov ax,stack
    
    mov ss,ax
    
    mov sp,10h
    
    mov word ptr ss:[0],offset s ;(ss:[0])=1ah
    
    mov ss:[2],cs                ;(ss:[2])=cs
    
    call dword ptr ss:[0]        ;cs入栈,ip=19h入栈,转到cs:1ah处执行指令
    
                                 ;(ss:[4])=cs,(ss:[6])=ip
    
    nop
    

    s: mov ax,offset s ;ax=1ah

    sub ax,ss:[0ch]              ;ax=1ah-(ss:[0ch])=1ah-19h=1
    
    mov bx,cs                    ;bx=cs=0c5bh
    
    sub bx,ss:[0eh]              ;bx=cs-cs=0
    
    mov ax,4c00h
    
    int 21h
    

    codesg ends

    end start

    检测点11.1

    写出下面每条指令执行后,ZF、PF、SF、等标志位的值。

    sub al,al al=0h ZF=1 PF=1 SF=0

    mov al,1 al=1h ZF=1 PF=1 SF=0

    push ax ax=1h ZF=1 PF=1 SF=0

    pop bx bx=1h ZF=1 PF=1 SF=0

    add al,bl al=2h ZF=0 PF=0 SF=0

    add al,10 al=12h ZF=0 PF=1 SF=0

    mul al ax=144h ZF=0 PF=1 SF=0

    检测点涉及的相关内容:

    ZF是flag的第6位,零标志位,记录指令执行后结果是否为0,结果为0时,ZF=1

    PF是flag的第2位,奇偶标志位,记录指令执行后结果二进制中1的个数是否为偶数,结果为偶数时,PF=1

    SF是flag的第7位,符号标志位,记录有符号运算结果是否为负数,结果为负数时,SF=1

    add、sub、mul、div 、inc、or、and等运算指令影响标志寄存器

    mov、push、pop等传送指令对标志寄存器没影响。

    检测点11.2

    写出下面每条指令执行后,ZF、PF、SF、CF、OF等标志位的值。

              al                 CF    OF    SF    ZF    PF
    

    sub al,al 0h/0000 0000b 0 0 0 1 1

    mov al,10h 10h/0010 0000b 0 0 0 1 1

    add al,90h a0h/1010 0000b 0 0 1 0 1

    mov al,80h 80h/1000 0000b 0 0 1 0 1

    add al,80h 0h/0000 0000b 1 1 0 1 1

    mov al,0fch 0fch/1111 1100b 1 1 0 1 1

    add al,05h 1h/0000 0001b 1 0 0 0 0

    mov al,7dh 7dh/1111 1101b 1 0 0 0 0

    add al,0bh 88h/1000 1000b 0 1 1 0 1

    检测点涉及的相关内容:

    ZF是flag的第6位,零标志位,记录指令执行后结果是否为0,结果为0时,ZF=1

    PF是flag的第2位,奇偶标志位,记录指令执行后结果二进制数中1的个数是否为偶数,结果为偶数时,PF=1

    SF是flag的第7位,符号标志位,记录有符号运算结果是否为负数,结果为负数时,SF=1

    CF是flag的第0位,进位标志位,记录无符号运算结果是否有进/借位,结果有进/借位时,SF=1

    OF是flag的第11位,溢出标志位,记录有符号运算结果是否溢出,结果溢出时,OF=1

    add、sub、mul、div 、inc、or、and等运算指令影响flag

    mov、push、pop等传送指令对flag没影响

    检测点11.3

    (1)补全下面的程序,统计F000:0处32个字节中,大小在[32,128]的数据个数。

     mov ax,0f000h
    
     mov ds,ax
    
     mov bx,0      ;ds:bx指向第一个字节
    
     mov dx,0      ;初始化累加器
    
     mov cx,32
    

    s: mov al,[bx]

     cmp al,32     ;和32进行比较
    
     jb s0         ;如果低于al转到s0,继续循环
    
     cmp al,128    ;和128进行比较
    
     ja s0         ;如果高于al转到s0,继续循环
    
     inc dx
    

    s0: inc bx

     loop s
    

    [32,128]是闭区间,包括两端点的值

    (32,128)是开区间,不包括两端点的值
    检测点11.3

    (2)补全下面的程序,统计F000:0处32个字节中,大小在(32,128)的数据个数。

     mov ax,0f000h
    
     mov ds,ax
    
     mov bx,0      ;ds:bx指向第一个字节
    
     mov dx,0      ;初始化累加器
    
     mov cx,32
    

    s: mov al,[bx]

     cmp al,32      ;和32进行比较
    
     jna s0        ;如果不高于al转到s0,继续循环
    
     cmp al,128    ;和128进行比较
    
     jnb s0        ;如果不低于al转到s0,继续循环
    
     inc dx
    

    s0: inc bx

     loop s
    

    [32,128]是闭区间,包括两端点的值

    (32,128)是开区间,不包括两端点的值

    检测点11.4

    下面指令执行后,(ax)= 45h

    mov ax,0

    push ax

    popf

    mov ax,0fff0h

    add ax,0010h

    pushf

    pop ax

    and al,11000101B

    and ah,00001000B

    推算过程:

    popf后,标志寄存器中,本章节介绍的那些标志位都为0(但是此时标志寄存器并不是所有位置都为0,这个不用关心,没学过的位置用先代替),向下进行,那么pushf将计算后的当时状态的标志寄存器入栈,然后pop给ax,这是ax是寄存器的值(这个值中包含了我们的号),接下来就是对那些没有学过的标志位的屏蔽操作,这就是最后两条指令的意义所在,将不确定的位置都归0,那么只剩下我们能够确定的位置了,所以,结果就可以推理出来了。

    mov ax,0

    push ax

    popf

    mov ax,0fff0h

    add ax,0010h

    pushf

    pop ax 0 0 0 0 of df if tf sf zf 0 af 0 pf 0 cf

                     0  0  0  0  0  0  *  *  0  1  0  *  0  1  0  1
    
                     ax=flag=000000** 010*0101b
    

    and al,11000101B al=01000101b=45h

    and ah,00001000B ah=00000000b=0h

    检测点12.1

    (1)用debug查看内存,情况如下:

    0000:0000 68 10 A7 00 8B 01 70 00-16 00 9D 03 8B 01 70 00

    则3号中断源对应的中断处理程序入口的偏移地址的内存单位的地址为: 0070:018b

    检测点涉及相关内容:

    一个表项存放一个中断向量,也就是一个中断处理程序的入口地址,这个入口地址包括段地址和偏移地址,一个表项占两个字,高地址存放段地址,低地址存放偏移地址

    检测点12.1

    (2)

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

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

    检测点涉及相关内容:

    一个表项存放一个中断向量,也就是一个中断处理程序的入口地址,这个入口地址包括段地址和偏移地址,一个表项占两个字,高地址存放段地址,低地址存放偏移地址
    检测点13.1

    7ch中断例程如下:

    lp: push bp

     mov bp,sp
    
     dec cx
    
     jcxz lpret
    
     add [bp+2],bx
    

    lpret: pop bp

     iret
    

    (1)在上面的内容中,我们用7ch中断例程实现loop的功能,则上面的7ch中断例程所能进行的最大转移位移是多少?

    最大位移是FFFFH
    检测点13.1

    (2)用7ch中断例程完成jmp near ptr s指令功能,用bx向中断例程传送转移位移。

    应用举例:在屏幕的第12行,显示data段中以0结尾的字符串。

    assume cs:code

    data segment

     db 'conversation',0
    

    data ends

    code segment

    start:

     mov ax,data
    
     mov ds,ax
    
     mov si,0
    
     mov ax,0b800h
    
     mov es,ax
    
     mov di,12*160
    

    s: cmp byte ptr [si],0

     je ok
    
     mov al,[si]
    
     mov es:[di],al
    
     inc si
    
     add di,2
    
     mov bx,offset s-offset ok
    
     int 7ch
    

    ok: mov ax,4c00h

     int 21h
    

    code ends

    end start

    jmp near ptr s指令的功能为:(ip)=(ip)+16位移,实现段内近转移

    assume cs:code

    code segment

    start:

    mov ax,cs

    mov ds,ax

    mov si,offset do0 ;设置ds:si指向源地址

    mov ax,0

    mov es,ax

    mov di,200h ;设置es:di指向目标地址

    mov cx,offset do0end-offset do0 ;设置cx为传输长度

    cld ;设置传输方向为正

    rep movsb

    mov ax,0

    mov es,ax

    mov word ptr es:[7ch*4],200h

    mov word ptr es:[7ch*4+2],0 ;设置中断向量表

    mov ax,4c00h

    int 21h

    do0:

     push bp
    

    mov bp,sp

     add [bp+2],bx                    ;ok的偏移地址+bx得到s的偏移地址
    

    pop bp

    iret

    mov ax,4c00h

    int 21h

    do0end:

     nop
    

    code ends

    end start
    检测点13.2

    判断下面说法的正误:

    (1)我们可以编程改变FFFF:0处的指令,使得CPU不去执行BIOS中的硬件系统检测和初始化程序。

    答:错误,FFFF:0处的内容无法改变。

    检测点13.2

    判断下面说法的正误:

    (2)int 19h中断例程,可以由DOS提供。

    答:错误,先调用int 19h,后启动DOS。
    检测点14.1 读取写入CMOS RAM单元内容

    (1)编程,读取CMOS RAM的2号单元内容。

    assume cs:code

    code segment

    start: mov al,2 ;赋值al

        out 70h,al      ;将al送入端口70h
    
        in al,71h       ;从端口71h处读出单元内容
    
        mov ax,4c00h
    
        int 21h
    

    code ends

    end start
    检测点14.1

    (2)编程,向CMOS RAM的2号单元写入0。

    assume cs:code

    code segment

    start: mov al,2 ;赋值al

        out 70h,al      ;将al送入端口70h
    
        mov al,0        ;赋值al
    
        out 71h,al      ;向端口71h写入数据al
    
        mov ax,4c00h
    
        int 21h
    

    code ends

    end start
    编程,用加法和移位指令计算(ax)=(ax)*10

    提示:(ax)*10=(ax)*2+(ax)*8

    assume cs:code

    code segment

    start: mov bx,ax

        shl ax,1   ;左移1位(ax)=(ax)*2
    
        mov cl,3
    
        shl bx,cl       ;左移3位(bx)=(ax)*8
    
        add ax,bx       ;(ax)=(ax)*2+(ax)*8
    
        mov ax,4c00h
    
        int 21h
    

    code ends

    end start

    ;应用举例:计算ffh*10

    assume cs:code
    
    code segment
    
    start:  mov ax,0ffh
    
    mov bx,ax
    
    shl ax,1   ;左移1位(ax)=(ax)*2
    
    mov cl,3
    
    shl bx,cl       ;左移3位(bx)=(ax)*8
    
    add ax,bx       ;(ax)=(ax)*2+(ax)*8
    
    mov ax,4c00h
    
    int 21h
    code ends
    
    end start
    

    小技巧:
    左移1位,N=(N)*2

    左移2位,N=(N)*4

    左移3位,N=(N)*8

    左移4位,N=(N)*16

    左移5位,N=(N)*32

    展开全文
  • 什么是汇编语言

    万次阅读 多人点赞 2018-11-19 21:47:32
    汇编语言(assembly language)是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号...
  • 汇编语言软件

    2020-07-30 23:31:58
    汇编语言开发工具link.exe lib.exe debug.exe edit.com
  • 汇编语言中的'#'

    千次阅读 2018-12-04 16:39:46
    汇编语言中的“#”代表的是寻址方式为立即寻址。 比如指令: MOV A,#21H MOV意思是单片机片内RAM之间传送,该指令的意思是:将值21H送给寄存器A中暂存; 如果把**“#”去掉**,MOV A,21H该指令寻址方式变了,为直接...
  • 汇编语言的所有指令

    万次阅读 多人点赞 2019-09-26 21:41:36
    1、数据传送指令集MOV 功能: 把源操作数送给目的操作数 语法: MOV 目的操作数,源操作数 格式: MOV r1,r2 MOV r,m MOV m,r MOV r,data XCHG 功能: 交换两个操作数的数据 语法: XCHG ...语法
  • 汇编语言入门:源程序(一)

    万次阅读 多人点赞 2019-03-11 22:51:38
    汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编汇编...
  • 王爽前辈汇编语言第三版PDF下载

    千次阅读 多人点赞 2016-03-01 20:44:55
    汇编语言第三版PDF格式下载:点击打开链接 汇编语言第三版检测点答案下载:点击打开链接
  • 汇编语言中OUT和IN的用法

    万次阅读 多人点赞 2020-08-20 19:08:51
    汇编语言中,CPU对外设的操作通过专门的端口读写指令来完成;  读端口用IN指令,写端口用OUT指令。  例子如下:  IN AL,21H;表示从21H端口读取一字节数据到AL  IN AX,21H;表示从端口地址21H读取1字节数据到AL...
  • 看了一篇讲解高级语言、汇编语言和机器语言的博客,感觉很受益,在这里简单总结一下。 1. 机器语言 计算机执行的二进制命令,都是0和1表示的。 2. 汇编语言 具有一定意义的文字命令,与机器语言一一对应。汇编语言...
  • C学习笔记(一) C语言和汇编语言的区别   C语言和汇编语言的区别: 汇编:效率高,对硬件的可操控性更强,体积小,不易维护,可移植性很差  C:效率比较低,硬件可操控性比较差,目标代码体积大,容易维护,可移植...
  • 写一个简单的汇编语言并运行

    万次阅读 2017-02-14 01:54:47
    最近在学汇编,刚刚开始学习发的博文都是一些比较简单的入门的东西,希望以后会越来越好。接下来讲一下怎样写一个简单的汇编脚本并成功运行。(大牛勿喷)首先用notepad++编写一段汇编程序: 这里我是在32位的...
  • 汇编语言——输出字符串

    万次阅读 2018-12-01 12:16:38
    DATA SEGMENT BUF DB 'HOW DO YOU DO?$' DATA ENDS CODE SEGMENT mov ax,DATA mov ds,ax mov dx,OFFSET BUF mov ah,09H int 21H mov ah,4CH int 21H CODE ENDS  
  • 如何把c语言转成汇编语言[整理]

    万次阅读 2013-10-16 12:02:13
     使用gcc -S 1.c可以把1.c转成特殊的1.s,感觉其实是类似于汇编,然后可以修改其代码,要想继续编译可以用gcc -s 1.s然后就可以实现 2.使用VC++ 编译器 自带的 dumpbin 就可以 做反汇编。   如vc++中在C:\...
  • c语言和汇编语言的区别

    万次阅读 多人点赞 2016-11-04 18:18:30
    C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言;尽管C语言提供了许多低级处理的功能,但仍然保持着良好跨平台的特性,以一个标准...
  • 汇编语言--adc指令

    万次阅读 2020-09-11 23:44:28
    adc是带进位加法指令,它利用了CF位上记录的进位值。 指令格式:adc 操作对象1, 操作对象2 功能:操作对象1 = 操作对象1 + 操作对象2 + CF
1 2 3 4 5 ... 20
收藏数 194,146
精华内容 77,658
关键字:

汇编语言