汇编学习笔记

当年上大学,学这个好痛苦,现在反过来看其实也不难

Posted by yasin on August 3, 2017

前段时间搞微信余额修改遇到瓶颈,想着还是学学汇编吧,工作之余看了几个月的书,收获还是不少,把重点记录在此,方便查阅。看的书是王爽老师的《汇编语言》,写的真是非常的清晰易懂,推荐想学汇编的先看这本书。

基础知识

汇编指令

首先汇编是不区分大小写的。 汇编有3类指令

1.汇编指令:和机器码一一对应

2.伪指令:没有对应的机器码,由编译器执行,计算机不执行,有点像define

3.其他符号:如+、-、*、/等,由编译器识别,没有对应的机器码

存储器

存储器被划分成若干个存储单元,每个存储单元从0开始编号,例如一个存储器有128个存储单元,编号从0~127。 每个存储单元能放一个Byte(字节)1Byte=8bit。也就是说每个存储单元是8位的。

CPU数据读写

cpu对存储器的读写需要3个信息交互

1.存储单元的地址(地址信息)

2.器件的选择、读或写的命令(控制信息)

3.读或写的数据(数据信息)

用生活中的例子来表示就像快递,首先要知道快递网点的地址,然后要知道你是取还是寄包裹,最后要把包裹带上。 信息信号物理上是通过总线来传输的,逻辑上又分为了地址总线 控制总线 数据总线

要让CPU进行数据读写,只要输入驱动它工作的电平信息(机器码)就行了。

CPU通过地址总线来指定存储单元,CPU有N根地址线就能寻找2的N次方个内存单元。(用2进制传输一个数字,表示取第几个单元)。

CPU通过数据总线来进行数据传输,数据总线的宽度决定了CPU和外接的数据传输速度。一根线一次传输一个bit。8086CPU总线宽度为16,一次能传输2个字节的数据。

CPU通过控制总线来对外部器件进行控制,控制总线是一些不同控制线的集合。控制总线的数量决定了CPU对外部器件有多少种控制。对于内存读取来说,有一根“读信号输出”的控制线复制CPU向外传送读信号。当CPU向控制线输出低电平表示读取数据;还有一根“写信号输出”的控制线负责传送写信号。

总的来说

1.地址总线的宽度决定了CPU的寻找能力。

2.数据总线的宽度决定了CPU与其他器件进行数据传送时的单次数据量,进而决定了传送次数。

3.控制总线的宽度决定了CPU对系统中其他器件的控制能力。

寄存器

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

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

汇编其实就是操作寄存器,程序员通过指令读写寄存器来实现对CPU的控制。不同的CPU寄存器的个数、结构是不同的。

8086的14个寄存器分别为AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW。

8086的所有寄存器都是16位的。AX、BX、CX、DX、这4个通常用来存放一般性的数据,被称为通用寄存器。为了兼容老CPU的8位寄存器,这4个寄存器都可以分为两个8位的独立寄存器使用。

  • AX分为AH和AL
  • BX分为BH和BL
  • CX分为CH和CL
  • DX分为DH和DL

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

  • 字节:记为byte,一个字节由8个bit组成,可以存放在8位寄存器中。
  • 字:记为Word,一个字有两个字节组成,这两个字节分别称为这个字的高位字节和低位字节。

一个内存单元可存放8位数据,CPU寄存器可存放n个8位的数据,也就是说,计算机中的数据大多是有1~N个8位数据构成。为了直观显示,一般用16进制来显示。比如20000写成4E20就能直观的看出,这个数据是由4E和20这两个8位数据构成。

内存地址

CPU访问内存单元时,需要给出内存单元的地址,所有的内存单元构成的存储空间是一个一维的线性空间,每个内存单元在这个空间中都有唯一的地址,我们将这个唯一的地址称为物理地址。

CPU通过地址总线送入存储器的是一个内存单元的物理地址,这个物理地址是在CPU内部形成的,不同的CPU可以有不同的形成方式。

8086形成物理地址的方法 8086CPU用一个20位的地址总线,寻址能力1MB。但是又是16位的结构,内部存储、传输、暂时存储的地址为16位。这样如果只是直接发出,那么只能送出16位的地址,寻址能力就只有64KB。比较浪费资源。 所以8086采用了一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。流程如下

1.CPU中的相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址。

2.段地址和偏移地址通过内部总线送入地址加法器。

3.地址加法器将两个16位的地址合成位一个20位的物理地址。

4.地址加法器通过内部总线将20位物理地址送入输入输出控制电路。

5.输入输出控制电路将20位物理地址送上地址总线。

6.20位物理地址被地址总线传送到存储器。

地址加法器采用物理地址=段地址x16+偏移地址的方法用段地址和偏移地址合成物理地址。 传入段地址1230和00C8两个地址,把段地址x16即左移一位(二进制的左移4位),变成12300。然后和偏移地址相加得到123CB。

段地址在8086的段寄存器中存放,8086有4个段寄存器CS、DS、SS、ES

指令寄存器CS,IP

CS和IP是8086中最关键的寄存器,它们只是了CPU当前要读取指令的地址。CS为代码段寄存器,IP为指令指针寄存器,从名称上我们可以看出它们和指令的关系。 在8086中,任意时刻,设CS中的内容为M,IP中的内容为N,8086将从Mx16+N单元开始,读取一条指令并执行。 也就是说任意时刻CPU将CS:IP指向的内容当做命令执行。读取一条指令后,IP中的值自动增加,让CPU读取下一条指令,当前指令长度决定了IP的增加值。 具体流程如下

1.从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器;

2.IP=IP+所读取指令的长度,从而指向下一条指令;

3.执行指令。转到步骤1,重复。

在8086中,加电启动或复位后,CS=FFFFH,IP=0000。即FFFF0H是开机后执行的第一条指令。

CS和IP不能通过MOV来赋值,能够修改CS、IP内容的指令被统称为转移指令。最简单的就是jmp。 当我们想修改CS、IP的内容可以通过jmp 段地址:偏移地址这样的指令来完成。如

jmp 2AE3:3  ->  CS=2AE3H IP=0003H  -> 2AE33H
jmp 3:0B16  ->  CS=0003H IP=0B16H  -> 00B46H

若我们只想修改IP的内容,需要用一个寄存器做中转。如

ax = 1000H;
jmp ax,
IP就变为了1000H,CS不变。含义上和MOV IP,AX 类似。

内存中字的存储

CPU中,用16位寄存器来存储一个字,在内存中一个字要用两个地址连续的字节单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。

DS段地址寄存器与[address]偏移地址

8086中内存地址由段地址和偏移地址组成,DS寄存器通常用来存放将要访问数据的段地址。 使用mov ax,[偏移地址]就能把DSx16+偏移地址的值取出来放到ax中。 给DS寄存器赋值需要通过一个寄存器来中转。

mov bx,1000H
mov ds,bx

常用指令

指令形式 例子
mov 寄存器,数据 mov ax,8
mov 寄存器,寄存器 mov ax,bx
mov 寄存器,内存单元 mov ax,[0]
mov 内存单元,寄存器 mov [0],ax
mov 段寄存器,寄存器 mov ds,ax
指令形式 例子
add 寄存器,数据 add ax,8
add 寄存器,寄存器 add ax,bx
add 寄存器,内存单元 add ax,[0]
add 内存单元,寄存器 add [0],ax
指令形式 例子
sub 寄存器,数据 sub ax,8
sub 寄存器,寄存器 sub ax,bx
sub 寄存器,内存单元 sub ax,[0]
sub 内存单元,寄存器 sub [0],ax

栈和SS,SP寄存器

栈和收纳箱有点像,最先放进去的东西,最后才能拿出来,先进后出,后进先出。 8086入栈和出栈指令为PUSHPOP。8086的入栈和出栈都是以字节为单位。

push ax表示将寄存器ax中的数据送入栈中。
pop ax表示从栈顶取出数据送入ax。

8086中有两个寄存器,段寄存器SS和寄存器SP,栈顶的段地址存放在SS中,偏移地址存放在SP中。任意时刻,SS:SP指向栈顶元素。push指令和pop指令执行时,CPU通过SS和SP找到栈顶的地址。 push指令的执行过程如下

1.SP向上移动2位。SP=SP-2。

2.将数据放入SS:SP指向的内存单元。

可以看出入栈时,栈顶从高地址向低地址方向移动。

pop和push的执行过程相反 1.将SS:SP指向内存单元处的数据送入ax中。 2.SP向下移动2位。SP=SP+2。

Debug使用

Debug常用功能

column column
R 查看、改变CPU寄存器的内容
D 查看内存中的内容
E 改写内存中的内容
U 将内存中的机器指令翻译成汇编指令
T 执行一条机器指令
A 以汇编指令的格式在内存中写入一条机器指令

R命令

R命令直接输入是查看寄存器内容,R 寄存器表示要修改寄存器的值,按Enter键后,出现;输入你想改的值。

D命令

为设置地址的D命令会列出Debug预设的地址的内容。D传入一个地址,D 段地址:偏移地址,会显示从指定内存单元开始的128个内存单元的内容。当传入过一次地址后,直接输入D会列出后续的内容。

E命令

E命令通过E 段地址:偏移地址选定要修改的起始单元,会让你一次输入要为的值。

直接输入16精致数据

-e 1000:0 b8 01 00 b9 02 00 01 c8

可以直接用E命令写入字符。

-e 1000:0 'a' 'b' 'c' 1 2 3 4

e命令也可以直接输入字符串

-e 1000:0 "helloworld"

可以通过e命令输入16精制的命令的机器码,然后用U命令查看啊,机器指令和对应的汇编指令。

U命令

U命令可以查看内存里的机器指令和对应的汇编指令。

U 1000:0

A命令

T命令可以执行内存里的指令。 先用R命令修改CS和IP为1000:0,然后直接输入T即可执行一条指令。执行后IP会对应自增,要执行下一条指令直接再次输入T即可。

A命令可以通过汇编指令的形式写入指令。

-a 1000:0
1000:0000 mov ax,1
.
.
.
1000:000D add ax,ax
依次输入指令。两次回车结束

伪指令

segment和ends

segmentends是一对成对使用的伪指令,作用是定义一个段,segment表示段开始,ends表示段结束。 用法:

段名 segment

	代码在此

段名 ends

end

end是一个汇编程序的结束标记,编译器遇到end就会结束后面的编译。

assume

assume表示“假设”,它假设某一寄存器和程序中的某一个用segment...ends定义的段相关联。

assume cs:codesg 	//因为段的内容是代码段,所以把codesg代码段和cs寄存器联系起来

codesg segment 	  //codesg代码段开始
    mov ax,0H
    
    mov ax,4c00H     //这两句代码表示程序返回
    int 21H
codesg ends 		 //codesg代码段结束

end //程序结束

[BX]和loop指令

[BX]

[BX]和[0]类似,需要配合ds段寄存器使用

mov ax,[bx] //[bx]是偏移地址,ds是段地址,即

inc

inc bx 表示bx中内容+1

mov bx,1
inc bx	//bx=2

[loop]循环

loop循环的次数是cx寄存器控制的。

loop执行步骤:

1.cx-1

2.判断cx的值,不为0就跳转至标号处执行,为0就向下执行。

    mov ax,2
    mov cx,10 //设置执行的次数
s:  add ax,ax //设置要重复执行的标号
    loop s

包含多个段的程序

在代码段中使用数据

代码里面有数据的存在,需要致命程序的入口,不然不知道哪里开始是代码。标号会转化为一个入口地址,存储在可执行文件的描述信息中。当程序被加载入内存之后,加载者从程序的可执行文件的描述信息中读取到程序的入口地址,设置CS:IP。CPU就能从我们希望的地址处开始执行。

    assume cs:code
    code segment
    	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h//声明一些数据
        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//这里指明了程序的入口标号。

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

    assyme cs:code,ds:data,ss:stack  //把code和cs寄存器,data和ds寄存器,stack和ss寄存器关联起来
    data segment  //数据段开始,data已经和ds寄存器相关联了
    	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,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    stack ends    //栈段结束
    
    code segment  //代码段开始
    	start: mov ax,stack//栈顶地址初始化,段地址为stack的起始段地址
               mov ss,ax
               mov sp,20h  //ss:sp指向stack:20
               
               mov ax,data //数据地址初始化到data的起始段地址
               mov ds,ax
               mov bx,0    //指向数据的第一个单元
               
               mov cx,8    //设置循环次数
            s: push [bx]   //把ds:bx数据入栈
               add bx,2    //指向下一个数据
               loop s      //循环
               
               mov bx,0
               mov cx,8
           s0: pop [bx]    //出栈到ds:bx,上一个循环最后入栈的现在在最上面,第一个出栈
               add bx,2    //偏移地址指向下一个位置
               loop s0
               
               mov ax,4c00h//中断程序
               int 21h
    code ends     //代码段结束
    end start     //指明程序执行入口

上面代码中的code,data,stack没有实际意义,只是方便我们辨识,段中的内容不会被自动识别为数据还是栈还是指令,这些都是由CS:IP,SS:SP,DS的设置来决定的, CS:IP指向就当指令来执行。 SS:SP指向就当栈来执行。 DS指向就当数据来执行

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

and和or指令

and

and是逻辑与指令,按位比较,相同就为1,不同就为0

    mov al,01100011B
    and al,00111011B
    得al = 00100011B

利用and指令能快速的把指定位设为0,其他位不变

    and al,11111110B //将第0位设为0,其他位不变
    and al,01111111B //将第7位设为0,其他位不变

or

or是逻辑或指令,按位比较,都为0才是0。

    mov al,01100011B
    or  al,00111011B
    得  al=01111011B

利用or能快速的将指定为设为1,其他位不变。

    or al,10000000B //将第7位设为1,其他位不变
    or al,00000001B //将第0位设为1,其他位不变

字符形式给出数据

在汇编中,可以用'......'的方式给出数据,编译器会转化为对应的ASCII码。

    assume cs:code,ds:data
    
    data segment
    db 'unIX' 		//相当于db 75H,6EH,49H,58H
    db 'foRK'		//相当于db 66H,6FH,52H,4BH
    data ends
    
    code segment
    start: mov al,'a' //相当于mov al,61H
    	   mov bl,'b' //相当于mov al,62H
           
           mov ax,4c00h
           int 21h
    code ends
    end start

大小写转换

ASCII中,同一个字母的大小写ASCII码相差20H,即十进制的32

字母 16进制 二进制
A 41 01000001
a 61 01100001

从2进制可以看出,第五位正好控制大小写,大写为0,小写为1。应为二进制的第五位刚好等于10进制的32,16进制的20H。

这样,通过前面的andor就能轻松控制大小写了。

and al,11011111B //将al中的ASCII码第五位置为0,变成大写字母
or  al,00100000B //将al中的ASCII码第五位置为1,变成小写字母

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

bx可以加一个起始偏移地址,不改变bx,可以在一个循环里,很方便的对多个偏移地址进行操作。

[bx+0]//起始偏移地址不变
[bx+5]//从第5个开始加偏移地址

//上面的代码可以简写成下面的样式,和C语言的数组很像。
0[bx]
5[bx]

SI和DI

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

    //这3段代码功能相同
    mov bx,0
    mov ax,[bx]
    
    mov si,0
    mov ax,[si]
    
    mov di,0
    mov ax,[di]
    
    //si和di一样能使用[si+idata]的形式
    mov ax,[si+100]

[bx+si]和[bx+di]

bx,si,di不仅能加一个数,还能用[bx+si]和[bx+di]的形式来指明一个内存单元

也可以写成

    mov ax,[bx][si]//常用写法

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

除了bx和si,di的相加,还能再多加一个数。

有下面及种写法

    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

bp

在[…]的用法中,只有4个寄存器能这样写,bx,si,di和bp

前面讲到bx可以和si,di采用组合的形式来表示一个地址mov ax,[bx+200+si]。 bp一样也可以mov ax,[bp+200+si],但是有一点bp和bx不能组合像mov ax,[bx+bp]是错误的。

在[…]中使用寄存器bp,如果没有显性的给出段地址,段地址默认是在ss中。

    mov ax,[bp] //mov ax,ss:bp

数据处理的两个基本问题

指令执行前,所要处理的数据可以在3个地方:CPU内部、内存、端口。

汇编中数据位置的表达

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

1.立即数(idata) mov ax,1

2.寄存器 mov ax,bx

3.段地址+偏移地址 mov ax,[bx+si+8]

寻址方式

寻址方式 名称 用法
[idata] 直接寻址 [idata]
[bx] 寄存器间接寻址 [bx]
[bx+idata] 寄存器相对寻址 用于结构体[bx].idata、用于数组idata[si]、用于二维数组[bx][idata]
[bx+si] 基址变址寻址 用于二维数组[bx][si]
[bx+si+idata] 相对基址变址寻址 用于表格(结构)中的数组项[bx].idata[si]、用于二维数组idata[bx][si]

指令要处理的数据长度

通过寄存器名指定

8086的指令可以处理2中尺寸的数据,byteword,汇编语言通过指明寄存器名(ax还是al)指明要处理的数据的尺寸。

    mov ax,1 //指明了指令是字操作
    mov bx,ds[0]
    mov ds,ax
    mov al,1 //指明了指令是字节操作
    mov al,bl

通过操作符指定

在没有寄存器名的情况下,用操作符X ptr指明内存单元的长度,X可以为wordbyte

1.指明为字操作

    mov word ptr ds:[0],1
    inc word otr [bx]

2.指明为字节操作

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

其他方法

有一些指令默认了访问的是字单元还是字节单元,如push [1000H],就不用指明访问的是字单元还是字节单元,因为push指令只进行字操作。

div指令

    //被除数默认是在AX或(DX和AX)中
    div 除数的内存单元
    
    //al=ax/ds:0的商   ah=ax/ds:0的余 因为是除数是8位AX存放商和余就够了
    div byte ptr ds:[0]

div是除法指令,使用时应注意以下问题。 1.除数:有8位和16位两种 2.被除数:默认放在AX或DX和AX中,如果除数为8位,被除数则为16位,默认在AX中存放。如果除数为16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位。 3.结果:如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数。如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数。

做成表格看起来比较直观

除数 除数8位 除数16位
被除数 被除数为16位,默认在AX中存放 被除数为32位,在DX和AX中存放,DX存放高16位,AX存放低16位
结果 AL存储除法操作的商,AH存储除法操作的余数 AX存储除法操作的商,DX存储除法操作的余数

计算100001/100


    //因为100001大于65535是一个32位的,被除数应为16位
    //先把100001拆成高16位和低16位,高16位放到dx,低16位放到ax中
    mov dx,1
    mov ax,86A1H //1*10000H+86A1H=100001
    mov bx,100
    div bx
    //结果ax=03E8(即商为1000),dx=1(余数为1)。

伪指令dd

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

     data segment
     db 1 //01H 在data:0处,占1个字节
     dw 1 //0001H 在data:1处,占1个字
     dd 1 //00000001H 在data:3处,占2个字

dup

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

    //定义了3个字节,值都是0,相当于db 0,0,0
    db 3 dup (0)
    
    //定义了9个字节,分别是0,1,2,0,1,2,0,1,2相当于db 0,1,2,0,1,2,0,1,2
    db 3 dup (0,1,2)
    
    定义了18个字节,值是'abcABCabcABCabcABC'相当于db 'abcABCabcABCabcABC'
    db 3 dup ('abc',"ABC")

可见dup的使用格式如下

    数据类型  重复的次数  dup (重复的数据)

转移指令的原理

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

转移指令的分类

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

转移类型 示例
只修改IP,称为段内转移 jmp ax
同时修改CS和IP,称为段间转移 jmp 1000:0

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

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

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

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

操作符offset

offset是由编译器处理的符号,它的功能是取得标号的偏移地址。

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

offset取得了标号的starts的偏移地址。

jmp指令

jmp是无条件转移指令,可以只修改IP,也可以同时修改CSIP

jmp指令要给出两种信息:

1.转移的目的地址

2.转移的距离

jmp short 标号 表示转移到标号处执行指令 这种格式的jmp指令实现的是段内短转移,它对IP的修改范围为-128~127。指令中的short说明指令进行的是短转移。指令中的标号指明了要转移的目的地,转移指令结束后,CS:IP应该指向标号处的指令。

    assume cs:codesg
    
    codesg segment
     start:mov ax,0
           jmp short s  //跳转到s标号处继续执行
           add ax,1     //这句被跳过了
         s:inc ax
    codesg ends
    end start
jmp short 标号

jmp short指令对应的机器码中不包含转移的目的地址,而是转移的位移。 jmp short的实际功能为IP=IP+(8位的位移)。

1.8位位移=标号处的地址-jmp指令后的第一个字节的地址。

2.short指明了此处的位移为8位。

3.8位位移范围为-128~127,用补码表示。

4.8位位移由编译器在编译时算出。

jmp near ptr 标号

还有一种和jmp short 标号功能相近的指令格式,jmp near ptr 标号,它实现的是段内近转移。 jmp near ptr 标号的功能为IP=IP+(16位位移)。

1.16位位移=标号处的地址-jmp指令后的第一个字节的地址。

2.near ptr指明此处的位移为16位位移,进行的是段内近转移。

3.16位位移的范围为-32768~32767,用补码表示。

4.16位位移由编译程序在编译时算出。

jmp far ptr 标号

前面的jmp指令,对应的机器指令中没有目的地址,而是相对于当前IP的偏移地址。

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  //机器码中包含了目的地址
           db 256 dup (0) //被跳过
         s:add ax,1
           inc ax
    codesg ends
    end start
转移地址在寄存器中的jmp指令

指令格式:jmp 16位寄存器 功能:IP = 16位寄存器中的转移地址

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

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

1.jmp word ptr 内存单元地址(段内转移) 功能:从内存单元地址处取出转移目的的偏移地址(一个字)。 内存单元地址可用任意寻址方式给出

    mov ax,0123H
    mov ds:[0],ax
    jmp word ptr ds:[0]
    //IP=0123H

2.jmp dword ptr 内存单元地址(段间转移) 功能:从内存单元地址处取出转移目的地址(两个字),高地址的字是转移目的的段地址,低地址的字是转移目的的偏移地址。 CS = 内存单元地址+2 IP = 内存单元地址

    mov ax,0123H
    mov ds:[0],ax
    mov word ptr ds:[2],0 //指明存的是字,占2个位置
    jmp dword ptr ds:[0]
    //CS=0,IP=0123,CS:IP指向0000:0123

jcxz指令

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

指令格式:jcxz 标号(如果cx = 0 ,转移到标号处执行)。 当cx = 0时,IP = IP+8位的位移。 8位位移 = 标号处的地址-jcxz指令后的第一个字节的地址。 8位位移的范围为-128~127,用补码表示。 8位位移由编译程序在编译时算出。

cx≠0时什么也不做

loop指令

loop指令为循环指令,所有循环指令都是短转移。

指令格式:loop 标号(cx = cx-1,如果cx ≠ 0,转移到标号处执行。等于0就继续向下执行) cx = cx-1 如果cx ≠ 0,IP = IP+8位位移。 8位位移 = 标号处的地址 - loop指令后的第一个字节的地址。 8位位移的范围为 - 128~127,用补码表示。 8位位移由编译程序在编译时算出。

CALL和RET指令

ret和retf

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

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

1.IP=ss*16+SP

2.sp=sp+2

相当于进行了 pop IP 操作

CPU指令retf指令时,进行下面4步操作:

1.IP = ss*16+sp

2.sp = sp+2

3.CS = ss*16+sp

4.sp = sp+2

相当于进行了: pop IP pop CS 操作

例:ret指令执行后,IP = 0,CS:IP指向代码段的第一条指令。

    assume cs:code
    stack segment
	  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
            mov bx,0
            ret
    code ends
    
    end start

例:retf指令执行后,CS:IP指向代码段的第一条指令

    assume cs:code
    stack segment
	  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 cs
            push ax
            mov bx,0
            retf
    code ends
    
    end start

call指令

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

1.将当前的IP或CS和IP压入栈中。

2.转移

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

相对地址的call 指令格式:call 标号(将当前的IP压栈后,转移到标号处执行指令)

CPU执行这种格式的call指令时,进行如下的操作:

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

16位位移 = 标号处的地址 - call指令后的第一个字节的地址。 16位位移的范围为-32768~32767,用补码表示。 16位位移由编译程序在编译时算出。

“call 标号” 相当于

    push IP
    jmp near ptr 标号

目的地址的call 指令格式:call far ptr 标号 这种格式的call指令实现的是段间转移。

CPU执行此种格式的call指令是,进行如下的操作。

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

“call far ptr 标号” 相当于

    push CS
    push IP
    jmp far ptr 标号

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

指令格式:call 16位 reg

    sp = sp-2
    ss*16+sp = IP
    IP = 16位reg

相当于

    push IP
    jmp 16位reg

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

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

  • call word ptr 内存单元地址 (仅修改IP) 相当于
      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 内存单元地址 (同时修改CS和IP) 相当于
      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
    

call和ret的配合使用

    assume cs:code
    code segment
    start: mov ax,1
     	   mov cx,3
           call s       //将当前IP的值压栈,然后跳转到s标号执行
           mov bx,ax
           mov ax,4c00h
           int 21h
        s: add ax,ax
	       loop s
           ret          //从栈中取一个值送入IP中,转移到IP处执行,
                        //即call s的下一句代码
	code ends
    end start

由此可以看出call和ret可以实现子程序的效果,s标号的代码段执行完后又回到了前面的代码继续执行。

子程序框架

    assume cs:code
    code segment
      main:                //主程序
            ...
            call sub1      //调用子程序1
            ...
            mov ax,4c00h
            int 21h
      sub1:                //子程序1
            ...
            call sub2      //调用子程序2
            ...
            ret            //子程序1的返回到主程序
      sub2:                //子程序2
            ...
            ret            //子程序2返回到子程序1
    code ends
    end main

mul指令

mul是乘法指令。 两个相乘的数:必须同时为8位或者16位。如果是8位,一个默认放在AL中,另一个放在8位寄存器或内存字节单元中。如果是16位乘法,一个默认在AX中,另一个放在16位reg或内存字单元中。 结果:如果是8位乘法,结果默认放在AX中,如果是16位乘法,结果高位默认放在DX中,低位放在AX中。

指令格式:mul reg 或者 mul 内存单元

内存单元可以用不同的寻址方式给出,如

    mul byte ptr ds:[0]
    //含义为ax=al*(ds*16+0);
    
    mul word ptr [bx+si+8]
    //含义为ax=ax*(ds*16+bx+si+8)结果的低16位
    //     dx=ax*(ds*16+bx+si+8)结果为高16位

例如:

    //计算100*10
    mov al,100
    mov bl,10
    mul bl
    //结果ax=1000

标识寄存器

CPU内部的寄存器中,有一种特殊的寄存器,有一下作用

1.用来存储相关指令的某些执行结果。

2.用来为CPU执行相关指令提供行为依据。

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

这种特殊的寄存器在8086中,被称为标识寄存器。8086的寄存器有16位,其中存储的信息通常被称为程序状态字(PSW)。

有的指令影响标志寄存器,如add、sub、mul、div、inc、or、and等,大多是运算指令。 有的指令不影响标识寄存器,如mov、push、pop等,大多是传送指令。

flag寄存器每一位都有专门的含义,记录特定的信息。

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

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

ZF标志

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

如:

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

PF标志

flag的第2位是PF,奇偶标志位。它记录相关指令执行后,其结果的所有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

SF标志

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

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

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

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

    mov al,10000001B
    add al,1
    //结果 al=10000010B
    //可以将add指令进行的运算当做无符号数的运算,也可以当做有符号数计算(-127+1),结果为-126(10000010B)。

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

CF标志

flag的第0位是CF,进位标志位。一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高为的借位值。

对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第N-1位,就是它的最高有效位。而假象存在的第N位,就是相对于最高有效位的更高位

  7 6 5 4 3 2 1 0
假想的更高位 0(最高有效位) 0 0 1 1 1 0 0

当两个数据相加的时候,有可能产生从最高有效位向更高位的进位。这个进位值就用CF来保存 如:

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

而当两个数据做减法的时候,有可能向更高位借位。比如,两个8位数据:97H-98H,将产生借位,借位后相当于197H-98H,而flag的CF位也可以用来记录这个借位值。 如:

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

OF标志

在进行有符号运算的时候,如果结果超过了机器所能表示的范围称为溢出。

8位的寄存器有效范围是-128~127,16位寄存器的有效范围是-32768~32767。如果运算结果超出了机器所能表达的范围,将产生溢出。 如:

    mov al,98
    add al,99
    //执行后将产生溢出,al=al+99=98+99=197
    //197超过了8位有符号数的范围:-128~127
    //得到的结果是al=0C5H,因为是有符号数的运算,所以al存的是补码,C5H是有符号数的-59,98-99=-59显然是不对的。

面对这种情况,flag的第11位OF(溢出标志位)就起作用了。 如果发生了溢出OF=1;如果没有OF=0。

  • CF是对无符号数运算有意义的标志位
  • OF是对有符号数运算有意义的标志位。

adc指令

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

指令格式:adc 操作对象1,操作对象2 功能:操作对象1 = 操作对象1+操作对象2+CF adc ax,bx实现功能ax=ax+bx+CF

    mov ax,2
    mov bx,1
    sub bx,ax
    adx ax,1
    //执行后ax=4,adc执行时相当于ax+1+CF=2+1+1=4
    
    mov ax,1
    add ax,ax
    adc ax,3
    //执行后ax=5,adc执行时相当于ax+3+CF=2+3+0=5
    
    mov al,98H
    add al,al
    adc al,3
    //执行后al=34,adc执行时相当于al+3+CF=30H+3+1=34H

可以看出adc指令比add指令多加了一个CF位的值。CF的值有adc指令前面的指令决定的。

adc指令是用来进行加法的第二步运算的。add和adc指令的配合能对更大的数据进行加法运算。

例0198H和0183H相加,可以分为两步来进行

1.低位相加         				  add al,bl
2.高位相加再加上低位相加产生的进位值。adc ah,bh

adc就是用来进行第二步的,会自动帮我们加上进位值CF。(add指令执行后,CF表示低位相加的进位置)

sbb指令

和adc指令类似,通过CF位来记录借位值。

指令格式:sbb 操作对象1,操作对象2 功能:操作对象1 = 操作对象1-操作对象2-CF sbb ax,bx实现的功能是ax=ax-bx-CF

利用sbb指令可以对任意大数据进行减法运算 如:

    //计算003E1000H-00202000H,结果放在ax,bx中
    mov bx,1000H
    mov ax,003EH
    sub bx,2000H//如果这里借位了,CF为1
    sbb ax,0020H//减去低位的借位值

cmp指令

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

指令格式:cmp 操作对象1,操作对象2 功能:计算操作对象1-操作对象2 当不保存结果,仅设置标识寄存器。

cmp ax,bx

结果 减法运算 标志状态
ax=bx ax-bx=0 zf=1
ax≠bx ax-bx≠0 zf=0
ax<bx ax-bx将借位 cf=1
ax≥bx ax-bx不借位 cf=0
ax>bx ax-bx不借位,结果也不为0 cf=0;zf=0
ax≤bx ax-bx即可能借位,结果可能为0 cf=1;zf=1

这个比较的设计思路是通过减法运算,影响标识寄存器,来得到大小。

标志状态 结果
zf=1 ax=bx
zf=0 ax≠bx
cf=1 ax<bx
cf=0 ax≥bx
cf=0;zf=0 ax>bx
cf=1;zf=1 ax≤bx

前面的是无符号数的规则,下面看看有符号数

sf=1;of=0 没有溢出,逻辑正负=实际正负,ab<bh。

sf=1;of=1 有溢出,逻辑正负≠实际正负,因sf=1,实际结果为负,又有溢出,表示是溢出导致的结果为负。逻辑上必然为正,ah>bh。

sf=0;of=1 有溢出,逻辑正负≠实际正负,实际结果为正,又有溢出,表示是溢出导致的结果为正,逻辑上必然为负,ah<bh。

sf=0,of=0 没有溢出,逻辑正负=实际正负,sf=0实际非负,逻辑也非负,ah≥bh

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

jcxz是一个条件转移指令,它可以检测cx中的数值,如果cx=0,就修改IP,否则什么也不做。所有条件转移指令的转移位移都是-128~127。

除了jcxz之外,CPU还提供了其他条件转移指令,大多数条件转移指令都检测标志寄存器的相关标志位来决定是否修改IP。 通常检测被cmp指令影响的那些标志位,所以这些条件指令通常和cmp配合使用。

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

e:表示equal ne:表示not equal b:表示below nb:表示not below a:表示above na:表示not above

例:

    //实现ah=bh则ah=ah+ah,否则ah=ah+bh
    cmp ah,bh    //比较ah和bh改变对应的标志位
    je s		 //标志位符合等于跳转到s标号执行
    add ah,bh	 //标志位不符合等于执行这里
    jmp short ok //跳过s标号的语句
  s:add ah,ah
  ok:....

DF标志和串传送指令

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

串传送指令 格式:movsb 功能:执行movsb相当于执行以下步骤

    1.es*16+di=ds*16+si
    2.如果df=0则 si=si+1,di=di+1
    3.如果df=1则 si=si-1,di=di-1

也可以按字来传送 格式:movsw 将ds:si指向的内存单元中的字送入es:di中,然后根据标识寄存器df位的值,将si和di加2或减2。

相当于

    mov es:[di],word ptr ds:[si]
	如果df=0
    add si,2
    add di,2
    
    如果df=1
    sub si,2
    sub di,2

一般情况下movsb和movsw进行的是串传送操作中的一个步骤,需要和rep配合使用。

    rep movsb//循环调用movsb,循环cx次

    //和下面代码功能一样
    s:movsb
      loop s

由于flag的df位决定这串传送指令执行后,si和di的改变方向,我们需要提前设置好df位的值。 8086提供了2条指令来对df位进行设置。

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

例:

    //将data段的第一个字符串复制到它后面的空间
    data segment
      db 'Welcome to masm!'
      db 16 dup (0)
    data ends
    
    code segment
      mov ax,data  //设置数据的起始位置
      mov ds,ax
      mov si,0
      mov es,ax    //设置数据的目标位置
      mov di,16    
      mov cx,16    //设置循环次数
      cld          //设置方向,si和di递增
      rep movsb    //开始循环传送
    code ends

pushf和popf

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

标志寄存器在Debug中的表示

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

内中断

当CPU内部发生以下事情的时候,将产生相应的中断信息。

  • 除法错误,如执行div指令产生的除法移除。中断码:0
  • 单步执行。中断码:1
  • 执行into指令。中断码:4
  • 执行int指令。中断码为指令携带,int n,终端吗即为n

中断向量表中存储了中断码对应的处理程序的入口地址。 中断向量表在内存中保存,其中存放了256个中断源所对应的中断处理程序的入口。CPU通过中断码就能找到对应的处理程序。 在8086中,中断向量表指定放在内存地址0处,从0000:0000到0000:03FF的1024个单元中。不能放在别处。

8086收到中断信息后,中断过程

  • 从中断信息中取得中断类型码。
  • 标志寄存器的值入栈(因为中断过程中要改变标志寄存器的值,所有先将其保存在栈中)。
  • 设置标识寄存器的第8位TF和第9位IF的值为0
  • CS的内容入栈
  • IP的内容入栈
  • 从内存地址为(中断类型码4)和(中断类型码4+2)的两个字单元中读取中断处理程序的入口,设置IP和CS。

中断处理程序和iret指令

中断处理程序的编写方法和子程序比较相似

  • 保存用的的寄存器
  • 处理中断
  • 恢复用到的寄存器
  • 用iret指令返回

iret实现了下面的功能

    pop IP
    pop CS
    popf

单步中断

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

  • 取得中断类型码1
  • 标志寄存器入栈,TF、IF设置为0
  • CS、IP入栈
  • IP=14,CS=14+2

Debug程序的T命令就是通过这个实现的。使用t命令执行指令时,Debug将TF设置为1。这条指令执行完后,引发单步中断,执行单步中断处理程序。

CPU提供单步中断功能的原因就是,为单步跟踪程序的执行过程,提供了实现机制。

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

如:

    //将栈顶设置为1000:0
    mov ax,1000h
    mov ss,ax
    mov sp,0
    而不应该
    mov ax,1000h
    mov ss,ax
    mov ax,0
    mov sp,0

int指令

格式:int n。n为中断类型码

可以在程序中使用int 指令调用任何一个中断的中断处理程序。

int配合iret配合使用与call指令和ret指令的配合使用有相似的思路。

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

  • 开机后,CPU加电,初始化CS=0FFFFH,IP=0,自动从FFFF:0单元开始执行程序。FFFF:0处有一条跳转指令,CPU执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序。
  • 初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。
  • 硬件系统检测和初始化完成后,调用int 19h进行操作系统的引导,从此将计算机交由操作系统控制。
  • DOS启动后,除完成其他工作外,还将它所提供的中断例程转入内存,并建立相应的中断向量。

BIOS中断例程的应用

int 10h 中断例程是BIOS提供的中断例程,其中包含了多个和屏幕输出相关的程序。

设置光标位置

    mov ah,2    //设置用int 10h中断例程里面的2号子程序
    mov bh,0    //第0页
    mov dh,5    //行号
    mov dl,12   //列号
    int 10h

在光标位置显示字符

    mov ah,9    //9号子程序,在光标位置显示字符
    mov al,'a'  //要显示的字符
    mov bl,7    //颜色
    mov bh,0    //第0页
    mov cx,3    //字符重复个数
    int 10h     //

bl中颜色的格式如下

7 6 5 4 3 2 1 0
BL R G B I R G B
闪烁 背景 背景 背景 高亮 前景 前景 前景

若想显示红的高亮将bl设置为11001010b就可以了。

DOS中断例程应用

int 21h就是DOS提供的中断例程。

    mov ah,4ch   //表示调用4ch号子程序
    mov al,0     //返回值
    int 21h

int 21h在光标位置显示字符串的功能

    ds:dx 指向字符串   //要显示的字符串用$作为结束符
    mov ah,9         //使用第9号子程序
    int 21h

在屏幕5行12列显示字符串

    assume cs:code
    
    data segment
      db 'Welcome to masm','$'
    data ends
    
    code segment
      start:mov ah,2    //选择设置光标功能
            mov bh,0    //设置页数
            mov dh,5    //行数
            mov dl,12   //列数
            int 10h     //执行,移动光标
            
            mov ax,data //设置数据
            mov ds,ax   
            mov dx,0    //ds:dx指向字符串的首地址data:0
            mov ah,9    //选择9号功能,显示字符串
            int 21h     //执行
            
            mov ax,4c00h//相当于设置mov ah,4ch
            int 21h
    code ends
    end start

端口

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

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

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

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

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

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

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

端口的读写

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

对端口的读写不能用mov、push、pop等内存读写指令。

端口的读写指令只有两条inout,分别用于从端口读取数据和往端口写入数据。

访问端口:

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

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

inout指令中,只能使用axal来存放从端口中读取或要发送到端口中的数据。

访问8位端口时用al,访问16位端口用ax。

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的读写分两步进行。1.将2送入端口70h; 2.从端口71h读出2号单元的内容

shl和shr指令

shl和shr是逻辑位移指令。

shl:

  • 将一个寄存器或内存单元中的数据向左移位
  • 将最后移出的移位写入CF中
  • 最低位补0
    mov al,01001000b
    shl al,1   //将al中的数据左移一位
    //执行后al=10010000b,CF=0

CMOS RAM中存储的时间信息

在CMOS RAM中,存放着当前的时间:年、月、日、时、分、秒。这6个信息长度都为1字节,存放单元为: 秒0 分2 时4 日7 月8 年9

这些数据以BCD码方式存放。一个字节可以表示两个BCD码,高4位BCD码表示十位,低4位BCD码表示各位。 例:00010100b表示14

    //读取月份
    mov al,8    //设置要读取的单元
    out 70h,al  //要读取的单元地址传给CMOS
    in al,71h   //从数据端口71h中取得指定单元的数据

外中断

CPU除了能执行指令,进行运算外,还应该能够对外部设备进行控制,接受它们的输入,向它们输出。I/O能力。

外设的输入不直接送入内存和CPU,而是送入相关的接口芯片的端口中。CPU向外设的输出也不是直接送入外设。 CPU还可以向外设输出控制命令,也都是先送到相关芯片的端口中,在由相关芯片根据命令对外设实施控制。

CPU通过端口和外部设备进行联系

当外设的输入到达,相关芯片将向CPU发出相应的中断信息。CPU执行完当前指令后,检测到发送过来的中断信息,引发中断过程,处理外设的输入。

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

1.可屏蔽中断 可屏蔽中断是CPU可以不响应的外中断。CPU是否响应可屏蔽中断,要看标志寄存器的IF位的设置,IF=1,CPU执行完当前指令后响应中断,IF=0,不响应。 前面讲到的内中断中有一步是IF=0,TF=0,这里就能开出作用了,内中断时不响应可屏蔽中断。

8086提供了设置IF的指令 sti,设置IF=1。cli,设置IF=0。

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

  • 标志寄存器入栈,设置IF=0,TF=0
  • CS、IP入栈
  • IP=0,CS=0AH

几乎所有由外设引发的外中断,都是可屏蔽中断。 不可屏蔽中断是系统中有必须处理的紧急情况发生时用来通知CPU的中断信息。

PC机键盘的处理过程

键盘输入

键盘上的每一个键相当于一个开关,键盘中有一个芯片对键盘上的每一个键的开关状态进行扫描。按下一个键时,开关接通,该芯片就产生一个扫描码,扫描码说明了键在键盘的位置,扫描码被送入主板上的相关接口芯片的寄存器中,端口为60h。 松开按键时,也产生一个扫描码。说明了松开按键的位置,页送入60h端口中。

一般讲按下产生的扫描码称为通码,松开时产生的称为断码。扫描码长度为一个字节,通码的第7位为0,断码第7位为1。 断码=通码+80h

引发9号中断

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

执行9号中断

BIOS提供了int 9中断例程,用来进行基本的键盘输入处理。

1.读出60h端口中的扫描码

2.如果是字符键的扫描码,将该扫描码和它所对应的字符码(ASCII码)送入内存中的BIOS键盘缓存区;如果是控制键(如Ctrl)和切换键的扫描码,则将其转变为状态字节,写入内存中存储状态的字节单元。

3.对键盘系统进行相关的控制,比如说,向相关芯片发出应答信息。

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

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

第几字节 置1表示
0 右shift
1 左shift
2 Ctrl
3 Alt
4 Scroll灯亮
5 小键盘输入的是数字
6 输入大写字母
7 删除态

直接定址表

前面有用标号来标记指令、数据、段的起始地址的用法。这些标号仅仅表示了内存单元的地址。

还要一种标号,不但表示内存单元的地址,还表示内存单元的长度,即表示在此标号处的单元,是一个字节单元,还是字单元,还是双字单元。

如:

    assume cs:code
    code segment
        //这里的标号后面没有":",它们是同时描述内存地址和单元长度的标号
        a db 1,2,3,4,5,6,7,8  //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

这样指定之后,下面的指令中,标号b代表了一个内存单元,地址为code:8,长度为两个字节。

指令:  mov ax,b
相当于:mov ax,cs:[8]

指令:  mov b,2
相当于:mov word ptr cs:[8],2

指令:  inc b
相当于:inc word ptr cs:[8]

在其他段中使用数据标号

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

    assume cs:code,ds:data //把data和ds寄存器联系起来
    data segment
       a db 1,2,3,4,5,6,7,8
       b dw 0
       c dw a,b   //可以将标号当作数据来定义,标号所表示的地址当作数据的值。
                  //相当于c dw offset a,seg a,offset b,seg b。seg操作符能取得标号的段地址
    data ends
    
    code segment
    start: mov ax,data   //先要把ds设置为data段的段地址
           mov ds,ax
           
           mov si,0
           mov cx,8
        s: mov al,a[si]  //相当于mov al,[si+0]
           mov ah,0
           add b,ax      //相当于add [8],ax
           inc si
           loop s
           
           mov ax,4c00h
           int 21h
    code ends
    end start

程序的入口地址也能用直接定址表存放

    setscreen: jmp short set
         table dw sub1,sub2,sub3,sub4
          set: push bx
               cmp ah,3                //判断功能号是否大于3
               ja sret
               mov bl,ah
               mov bh,0
               add bx,bx               //计算子程序的便宜地址
               
               call word ptr table[bx] //调用对应的子程序
               
         sret: pop bx
               ret
    

运用前面的知识还能改成下面这样

	setscreen: cmp ah,0
               je do1
               cmp ah,1
               je do2
               cmp ah,2
               je do3
               cmp ah,3
               je do4
               jmp short sret
          do1: call sub1
               jmp short sret
          do2: call sub2
               jmp short sret
          do3: call sub3
               jmp short sret
          do4: call sub4
          
         sret: ret