汇编语言笔记(全)

汇编语言最近系统的学了下汇编语言,下面是学习笔记,用的书是清华大学出版社出版的汇编语言第三版,作者王爽(最经典的那版)。汇编语言基础知识寄存器寄存器(内存访问)简单编程[BX]和loop指令程序的段灵活定位内存地址数据处理的两个基本问题转移指令原理call和ret指令标志寄存器内中断int指令端口外中断直接定址表使用BIOS进行键盘输入和磁盘读写汇编语言指令组成CPU与外部器件交互需要总线小结接口卡存储器内存地址空间内部总线通用寄存器简单的汇编指令CPU位结构CPU表示物理地址段寄存器与指令指针寄存器修改CS,IP小结实验内存到寄存器的储存DS寄存器mov,add,sub栈寄存器SS,SP与push,pop实验一个汇编语言程序一些伪指令功能内存单元的描述loop指令Debug和masm编译器对指令的不同处理安全的编程空间段前缀的使用数据段栈段数据,代码,栈的程序段and和or指令ASCII码和字符形式的数据[bx+idata]的内存表示方法与数组处理SI和DI寄存器两个基本问题bx,si,di和bp数据的位置数据的长度div指令伪指令dddup转移指令offset操作符jmp指令jmp+地址远转移jmp+寄存器|内存转移jcxz指令loop指令根据位移进行转移的指令总结ret和retfcall指令联合使用ret和callmul指令参数的传递和模块化编程寄存器冲突标志寄存器ZF标志PF标志SF标志CF标志OF标志adc指令sbb指令cmp指令检测比较结果的条件转移指令DF标志位和串传送指令pushf和popf标志寄存器在Debug中的表示内中断的产生中断处理和中断向量表中断处理程序和iret指令单步中断int指令编写中断例程BIOS和DOS提供的中断例程端口的概念端口的写CMOS RAM芯片shl和shr指令接口芯片和端口外中断PC键盘处理过程描述单元长度的标号其它段中使用数据标号直接定址表程序入口地址的直接定址表int 9中断例程对键盘输入的处理int 16h读取键盘缓冲区int 13h读写磁盘基础知识汇编语言指令组成汇编指令:机器码的助记符,有对应的机器码。伪指令:没有对应的机器码,编译器执行,机器不执行。其他符号:如+-*/有编译器识别,无对应机器码。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=18add ax,8将AX寄存器中的数加8AX=AX+8mov ax,bx将BX中的数据存入AXAX=BXadd ax,bx将AX中的数据和BX中的数据相加存入AXAX=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:1F602000H段中的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,IPCS和IP寄存器不可以使用传送指令mov来改变,而能改变CS,IP内容的指令是转移指令。jmp指令用法:jmp 段地址:偏移地址 同时修改CS和IP的值 如jmp 2AE3:3 结果CS=2AE3H IP=0003Hjmp 某一合法寄存器 只修改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,1000mov ds,bxmov al,[0]  123123当然,从AL寄存器中将数据送入内存只要反过来使用mov就可以了,mov [0],al如果需要传输字型数,只要使用对应的16位寄存器就可以了,传输的是以相应地址开始的一个字型数据(连续两个字节)。如mov [0],cx。mov,add,submov常见语法:mov 寄存器,数据       mov ax,8mov 寄存器,寄存器     mov ax,bxmov 寄存器,内存单元    mov ax,[0]mov 内存单元,寄存器    mov [0],axmov 段寄存器,寄存器    mov ds,axmov 寄存器,段寄存器    mov ax,ds  123456123456add,sub常见语法:add 寄存器,数据        add ax,8add 寄存器,寄存器      add ax,bxadd 寄存器,内存单元    add ax,[0]add 内存单元,寄存器    add [0],axsub和add一样  1234512345注意,add,sub不可以操作段寄存器。栈栈是一种后进先出的存储空间,从栈顶出栈入栈。LIFO(last in first out)入栈指令:push ax ax中的数据送入栈顶出栈指令:pop ax 栈顶送入ax入栈和出栈指令都是以字为单位的。P58图栈寄存器SS,SP与push,popCPU通过SS寄存器和SP寄存器来知道栈的范围,段寄存器SS存放的是栈顶的段地址,SP寄存器存放的是栈顶的偏移地址。所以,任意时刻SS:SP指向栈顶元素。指令push ax执行过程:SP=SP-2,SP指针向前移动两格代表新栈顶AX中的数据送入SS:SP目前指向的内存字单元,P59图所以栈顶在低地址,栈底在高地址。初始状态下,SP指向栈底的下一个单元。反之pop ax执行过程相反。8086CPU并不会自己检测push是否会超栈顶,pop是否会超栈底。push和pop可以加寄存器,段寄存器,内存单元(直接偏移地址[address])指定栈空间通常通过指定SS来进行,如:指定10000H~1000FH为栈空间mov ax,1000mov ss,axmov sp 0010  12341234注:将一个寄存器清零 sub ax,ax 两个字节,mov ax,0 三个字节注:若设定一个栈段为10000H~1FFFFH,栈空的时候SP=0(要知道入栈操作先SP-2,然后再送入栈)实验Debug中的t命令一次执行一条指令,但如果执行的指令修改了ss段寄存器,下一条命令也会紧跟着执行(中断机制)。简单编程一个汇编语言程序编写编译(masm5.0)连接一些伪指令功能assume cs:codesgcodesg segmentmov ax,0123mov bx,0456add ax,bxadd ax,axmov ax,4c00int 21codesg endsend  123456789101112131415123456789101112131415涉及到的一些知识:XXX segment···XXXendssegment和ends成对出现,代表一个段的开始和结束。一个汇编程序可以有多个段,代码,数据和栈等,至少要有一个段。endend代表一个汇编程序结束,遇到end编译器停止编译。assumeassume 假设,假设某一个段寄存器和程序中的一个段关联。可以理解为将这个段寄存器指向程序段的段地址标号(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进行两步操作:(cx)=(cx)-1(cx)不为零则跳至标号处执行程序。所以CX中存放的是循环次数,一个简单的例子如下(计算2^12):assume cs:codecode segmentmov ax,2mov cx,11s:add ax,axloop smov ax,4c00hint 21hcode endsend  12345678910111213141234567891011121314所以使用loop注意三点:先设置cx的值 mov cx,循环次数设置标号与执行循环的程序段 s:执行程序段在程序段最后写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:codecode segmentmov ax,0ffffhmov ds,axmov ax,0020hmov es,axmov bx,0mov cx,12s:mov dl,[bx]mov es:[bx],dlinc bxloop smov ax,4c00hint 21hcode endsend  1234567891011121314151617181920212212345678910111213141516171819202122[bx]直接使用的时候默认段前缀是ds,但要使用其他的段前缀,如es就要在前面加上。程序的段数据段一般一个程序想要使用内存空间,有两种方法,在程序加载的时候系统分配或在需要使用的时候向系统申请,我们先考虑第一种情况。所以我们应事先将所需的数据存入内存中的某一段中,但我们又不可以随意的指定内存地址,以下面的求8个数据累加和的代码为例:assume cs:codecode segmentdw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987hmov bx,0mov ax,0mov cx,8s:add ax,cs:[bx]add bx,2loop smov ax,4c00hint 21hcode endsend  123456789101112131415161718123456789101112131415161718代码第一行的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,0987hstart:mov bx,0···code endsend start  12345671234567在第一条指令前加start,后面的end变成end start,end除了通知编译器程序在哪里结束之外,也可以通知程序的入口在哪,也就是第一条语句,在这里编译器就知道了mov bx,0是程序的第一条指令。也就是说,我们想要CPU从何处开始执行程序,只要在源程序中使用end 标号指定就好了。所以有如下框架:assume cs:codecode segment···数据···start:···代码···code endsend start  12345671234567栈段看下面一段使8个数逆序存放的代码:assume cs:codesgcodesg segmentdw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987hdw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0start:mov ax,csmov ss,axmov sp,30hmov bx,0mov cx,8s:push cs:[bx]add bx,2loop smov bx,0mov cx,8s0:pop cs:[bx]add bx,2loop s0mov ax,4c00hint 21hcodesg endsend start  123456789101112131415161718192021222324252627123456789101112131415161718192021222324252627在定义了8个字型数据之后,又定义了16个取值为0的字型数据,用作栈空间。所以dw这个定义不仅仅用来定义数据,也可以用来开辟内存空间留给之后的程序使用。数据,代码,栈的程序段在8086CPU中,一个段的长度最大为64KB,所以如果我们将数据或栈空间定义的比较大,就不能像前面一样编程了。我们需要将代码,数据,栈放入不同的段中:assume cs:code,ds:data,ss:stackdata segmentdw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987hdata endsstack segmentdw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0srack endscode segmentstart:mov ax,stackmov ss,axmov sp,20hmov ax,datamov ds,axmov bx,0mov cx,8s:push [bx]add bx,2loop smov bx,0mov cx,8s0:pop [bx]add bx,2loop s0mov ax,4c00hint 21hcode endsend start  123456789101112131415161718192021222324252627282930313233343536123456789101112131415161718192021222324252627282930313233343536我们可以这样在写代码时就将程序分为几个段,这段代码中,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,stackmov ss,axmov sp,20h  123123这三条语句后才会将stack段当做栈空间开使用。也就是说,CPU如何区分哪个段的功能,全靠我们使用汇编指令对ds,ss,cs寄存器的内容设置来指定。灵活定位内存地址and和or指令and:逻辑与指令,按位与运算,如:mov al,01100011Band al,00111011B  1212执行结果是al=00100011B,所以我们想要把某一位置零的时候可以使用and指令。or:逻辑或指令,按位或运算,如:mov al,01100011Bor al,00111011B  1212执行结果是al=01111011B,or指令可以将相应位置1。ASCII码和字符形式的数据在汇编语言中我们可以使用’···’的方式指明数据是以字符形式给出的,编译器会自动将它们转化为ASCII码。例如:assume cs:code,ds:datadata segmentdb 'unIX'db 'foRK'data endscode segmentstart:mov al,'a'mov bl,'b'mov ax,4c00hint 21hcode endsend start  123456789101112123456789101112db和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  12341234可见,只有第5位(从右往左数,从0开始计数)在大写和小写的二进制中是不一样的,所以我们只要把所有字母的二进制第五位置零,那就是大写,置1就是小写。代码如下:assume cs:codesg,ds:datasgdatasg segmentdb 'BaSiC'db 'iNfOrMaTiOn'datasg endscodesg segmentstart:mov ax,datasgmov ds,axmov bx,0mov cx,5s:mov al,[bx]and al,11011111Bmov [bx],alinc bxloop smov bx,5mov cx,11s0:mov al,[bx]or al,00100000Bmov [bx],alinc bxloop s0mov ax,4c00hint 21hcodesg endsend start  123456789101112131415161718192021222324252627282930313233123456789101112131415161718192021222324252627282930313233[bx+idata]的内存表示方法与数组处理除了使用[bx]来表示一个内存单元外,我们还可以使用[bx+idata]来表示一个内存单元,他表示的意思是偏移地址为(bx)+idata(bx中的数值加idata)的内存单元。当然也可写为[idata+bx],除此之外还可写为,200[bx],[bx].200。既然有了这种表示方法,我们就可以使用这种方法来操作数组,刚才将两个字符串改变大小写的代码的循环部分可以如下优化:···s:mov al,[bx]and al,11011111Bmov [bx],almov al,[5+bx]or al,00100000Bmov [5+bx],alinc bxloop s···  1234567891012345678910当然也可写为0[bx]和5[bx],注意这种写法和C语言中数组的相似之处:C语言中数组表示为a[i],汇编语言中表示为5[bx]。SI和DI寄存器SI和DI功能和BX相似,但不可以拆分为两个8位寄存器。也就是说下面代码等价:mov bx|si|di,0mov ax,[bx|si|di]mov ax,[bx|si|di+123]  123123所以在这里可以使用更方便的方式:[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  1234512345那我们总结一下这些内存寻址方法:[idata]用一个常量表示偏移地址,直接定位一个内存单元[bx]用一个变量表示偏移地址,定位一个内存单元[bx+idata]用一个常量和一个变量表示偏移地址,可在一个起始地址的基础上间接定位一个内存单元[bx+si]用两个变量表示偏移地址[bx+si+idata]用两个变量和一个常量表示偏移地址使用双循环,使用一个寄存器暂存cs的值,如:···mov cx,4s0:mov dx,cxmov si,0mov cx,3s:mov al,[bx+si]and al,11011111bmov [bx+si],alinc siloop sadd bx,16mov cx,dxloop s0···  1234567891011121314151612345678910111213141516假如循环比较复杂,没有多余的寄存器可用,我们可以使用内存暂存cx或其他数据:···dw 0···mov cx,4s0:mov ds:[40H],cxmov si,0mov cx,3s:mov al,[bx+si]and al,11011111bmov [bx+si],alinc siloop sadd bx,16mov cx,ds:[40H]loop s0···  123456789101112131415161718123456789101112131415161718这么使用的话注意需要在数据段声明用来暂存的内存,好在程序加载时分配出来。当然,在需要暂存的地方,还是建议使用栈:···dw 0,0,0,0,0,0,0,0···mov ax,stacksgmov ss,axmov sp,16···mov cx,4s0:push cxmov si,0mov cx,3s:mov al,[bx+si]and al,11011111bmov [bx+si],alinc siloop sadd bx,16pop cxloop s0···  1234567891011121314151617181920212212345678910111213141516171819202122数据处理的两个基本问题两个基本问题处理的数据在什么地方要处理的数据有多长接下来的讨论中,使用reg来表示一个寄存器,使用sreg来表示一个段寄存器。所以:reg:ax,bx,cx,dx,ah,al,bh,bl,ch,cl,dh,dl,sp,bp,si,disreg:ds,ss,cs,esbx,si,di和bp在8086CPU中,只有这四个寄存器可以使用[···]来进行内存寻址,可以单个出现,或以下面组合出现(常数可以随意出现在这些表示方法中):bx+si/dibp+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)  123123数据的位置绝大部分机器指令都是用来处理数据的,基本可分为读取,写入,运算。在机器指令这个层面上,并不关心数据是什么,而关心指令执行前数据的位置。一般数据会在三个地方,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’0C1370E4010'PDP’···mov ax,segmov ds,axmov bx,60hmov word ptr [bx].0ch,38    ;第三字段改为38add word ptr [bx].0eh,70    ;第四字段改为70mov si,0mov byte ptr [bx].10h[si],'v'   ;修改最后一个字段的三个字符inc simov byte ptr [bx].10h[si],'A'inc simov byte ptr [bx].10h[si],'X'···  1234567891011121314151612345678910111213141516这段代码中地址的使用类似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)的余数;  1212div word ptr es:[0]表示:(al)=[(dx)*10000H+(ax)]/((es)*16+0)的商(ah)=[(dx)*10000H+(ax)]/((es)*16+0)的余数  1212例:计算100001/100,因为100001(186A1H)大于65535,则需要存放在ax和dx两个寄存器,那么除数100只能存放在一个16位的寄存器中,实现代码:mov dx,1mov ax,86A1Hmov bx,100div bx  12341234执行之后(ax)=03E8H(1000),(dx)=1。伪指令dddd是一个伪指令,类似dw,但dd是用来定义dword(double word,双字),如:dd 1  ;2字,4字节dw 1  ;1字,2字节db 1  ;1字节  123123将data段中第一个数据除以第二个数据,商存入第三个数据:···data segmentdd 100001dw 100dw 0data ends···mov ax,datamov ds,axmov ax,ds:[0]mov dx,ds:[2]div word ptr ds:[4]mov ds:[6],ax···  12345678910111213141234567891011121314总结一下div相关:div后面跟的是除数被除数位数是除数两倍被除数存在ax中或ax+dx(ax低,dx高)商在ax或al中,余数在ah或dx中(高余数,低商)dupdup是一个操作符,由编译器识别,和db,dw,dd配合使用,如:db 3 dup (0)表示定义了三个值是0的字节,等价于db 0,0,0db 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 starts:mov ax,offset s  1212这里就是将start和s的偏移地址分别送给ax,也就是0和3jmp指令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)  12341234执行之后ax值为1,因为跳过了add指令。还应注意的是,jmp short短转移指令并不会在机器码中直接写明需要转移的地址(0BBD:0008),jmp的机器码是EB03并没有包含转移的地址,这里的转移距离是相对计算而出的地址,来看下面的执行过程:(CS)=0BBDH,(IP)=0006H,CS:IP指向EB03(jmp short s)读取指令EB03进入指令缓冲器(IP)=(IP)+指令长度,即(IP)=(IP)+2=0008H,之后CS:IP指向add ax,1CPU指向指令缓冲器中的指令EB03执行之后(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  123456123456可以看出,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:123Hjcxz指令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和retfret和call都是转移指令,都是修改IP的值,或同时修改CS和IP。ret指令用栈中的数据修改IP,实现的是近转移;retf指令用栈中的数据修改CS和IP的值,实现远转移。格式:直接用 ret。ret执行步骤:(IP)=((SS)*16+(SP))(SP)=(SP)+2retf执行步骤:(IP)=((SS)*16+(SP))(SP)=(SP)+2(CS)=((SS)*16+(SP))(SP)=(SP)+2所以ret指令相当于 pop ip,执行retf指令相当于执行pop ip,pop cs。call指令call指令也是一个转移指令,执行格式:call 目标(具体使用接下来说明),call的执行步骤:将当前的IP或CS和IP入栈转移call不能实现短转移,但它实现转移的原理和jmp相同。根据位移转移:call 标号,近转移,16位转移范围,也是使用相对的转移地址。执行步骤:(SP)=(SP)-2((SS)*16+(SP))=(IP)(IP)=(IP)+16所以执行这条命令相当于执行push ip,jmp near ptr 标号。直接使用地址进行(远)转移:call far ptr 标号,执行步骤:(SP)=(SP)-2((SS)*16+(SP))=(CS)(SP)=(SP)-2((SS)*16+(SP))=(IP)(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 word ptr 内存单元地址,当然还有call dword ptr 内存单元地址,这样进行的就是远转移。联合使用ret和call联合使用ret和call实现子程序的框架:assume cs:codecode segmentmain:···call sub1···mov ax,4c00hint 21hsub1:···call sub2···retsub2:···retcode endsend main  12345678910111213141516171819201234567891011121314151617181920mul指令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位  1212例:计算100*10mov al,100mov bl,10mul bl  123123参数的传递和模块化编程看下面一段程序:计算data中第一行的数的立方存在第二行assume cs:codedata segmentdw 1,2,3,4,5,6,7,8dd 0,0,0,0,0,0,0,0data endscode segmentstart:mov ax,datamov ds,axmov si,0mov di,16mov cs,8s:mov bx,[si]call cubemov [di],axmov [di].2,dxadd si,2add di,4loop smov ax,4c00hint 21hcube:mov ax,bxmul bxmul bxretcode endsend start  1234567891011121314151617181920212223242526272829303112345678910111213141516171819202122232425262728293031寄存器冲突观察下面将data中的数据全转化为大写的代码:assume cs:codedata segmentdb 'word',0db 'unix',0db 'wind',0db 'good',0data endscode segmentstart:mov ax,datamov ds,axmov bx,0mov cx,4s:mov si,bxcall capitaladd bx,5loop smov ax,4c00hint 21hcapital:mov cl,[si]mov ch,0jcxz okand byte ptr [si],11011111binc sijmp short capitalok:retcode endsend start  1234567891011121314151617181920212223242526272829303112345678910111213141516171819202122232425262728293031这段代码有一个问题出在,主函数部分使用cx设置循环次数4次,在循环中调用了子函数,而子函数中有一个判断语句jcxz也是用了cx,并且在之前修改了cx的值,造成逻辑错误。虽然修改的方法有很多,但我们应遵循以下的标准:编写调用子程序的程序不必关心子程序使用了什么寄存器编写子程序不用关心调用子程序的程序使用了什么寄存器不会发生寄存器冲突针对这三点,我们可以如下修改代码:···capital:push cxpush sichange:mov cl,[si]mov ch,0jcxz okand byte ptr [si],11011111binc sijmp short changeok:pop sipop cxret···  123456789101112131415123456789101112131415虽然和上面的程序中没有冲突的是si,但我们保险起见,在子程序开始时将子程序用到的所有的寄存器的内容存入栈中,在返回之前在出栈回到相应寄存器中。这样无论调用子程序的程序使用了什么寄存器,都不会产生寄存器冲突。标志寄存器标志寄存器CPU中有一种特殊的寄存器——标志寄存器(不同CPU中的个数和结构都可能不同),主要有以下三种作用:存储相关指令的某些执行结果为CPU执行相关质量提供行为依据控制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  1212如上图所示,1,3,5,12,13,14,15位没有使用,没有任何意义,而其他几位都有不同的含义。ZF标志ZF位于flag第6位,零标志位,功能是记录相关指令执行后结果是否为0,如果结果为0,则ZF=1,否则ZF=0。如:mov ax,1sub ax,1  1212执行后结果为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,1add al,10  1212执行后结果为00001011b,有3个1,所以PF=0。SF标志flag的第7位是SF标志位,符号标志位,它记录相关指令执行后,结果是否为负,如果结果为负,则sf=1,结果为正,sf=0。计算机中通常用补码表示数据,一个数可以看成有符号数或无符号数,如:00000001B,可以看成无符号1或有符号+110000001B,可以看成无符号129或有符号-127  1212也就是说对于同一个数字,可以当做有符号数运算也可以当做无符号数运算。如:mov al,10000001badd al,1  1212这段代码结果是(al)=10000010b,可以将add指令进行的运算当做无符号运算,那么相当于129+1=130,也可以当做有符号运算,相当于-127+1=-126。SF标志就是在进行有符号运算的时候记录结果的符号的,当进行无符号运算的时候SF无意义(但还会影响SF,只是对我们来说没有意义了)。CF标志flag的第0位是CF标志位,进位标志位,一般情况下载进行无符号运算时,他记录了运算结果的最高有效为向更高为的进位值,或从更高位的借位值。加入一个无符号数据是8位的,也就是0-7个位,那么在做加法的时候就可能造成进位到第8位,这时并不是丢弃这个进位,而是记录在falg的CF位上。如:mov al,98hadd al,al  1212执行后al=30h,CF=1。当两个数据做减法的时候有可能向更高位借位,如97h-98h借位后相当于197h-198h,CF也可以用来记录借位,如:mov al,97hsub al,98h  1212执行后(al)=FFH,CF=1记录了向更高位借位的信息。OF标志在进行有符号运算的时候,如果结果超过了机器能表示的范围称为“溢出”。机器能表示的范围是指如8位寄存器存放或一个内存单元存放,表示范围就是-128~127,16位同理。如果超出了这个范围就叫做溢出,如:mov al,98add al,99mov al,0F0Hadd al,088H  1234512345第一段代码(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,2mov bx,1sub bx,axadc ax,1  12341234注意这段代码,首先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,bladc ah,bh  1212所以有了adc这个指令我们就可以完成一些更庞大的数据量的加法运算。如计算1EF000H+000H的值:mov ax,001ehmov bx,0f000hadd bx,1000hadc ax,0020h  12341234注: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=0且zf=0若(ax)<=(bx)则(ax)-(bx)既可能借位,结果可能为0,cf=1或zf=1  123456123456但实际上往往会出现溢出,如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=1jne不等于转移zf=0jb小于转移cf=1jnb不小于转移cf=0ja大于转移cf=0且zf=0jna不大于转移cf=1或zf=1指令中的字母含义如下:e:equa;ne:not equalb:belownb:not belowa:abovena:not above上面的检测都是在cmp进行无符号比较时的检测位,有符号数检测原理一样,只是检测的标志位不同而已。下面看一个例子,如果(ah)=(bh)则(ah)=(ah)+(ah),否则(ah)=(ah)+(bh)cmp ah,bhje sadd ab,bhjmp short oks:add ah,ahok:···  123456123456这里注意的是,je检测的是zf位,而不管之前执行的是什么指令,只要zf=1就会发生转移,所以cmp的位置需要仔细的把控,当然是否和cmp配合使用也是取决于编程者,下面例子实现了统计data中数值为8的字节个数,然后用ax保存:···data segmentdb 8,11,8,1,8,5,63,38data ends···mov ax,datamov ds,axmov bx,0mov ax,0mov cx,8s:cmp byte ptr [bx],8jne nextinc axnext:inc bxloop s···  1234567891011121314151612345678910111213141516DF标志位和串传送指令flag的第10位是DF标志位,方向标志位,在串处理中,每次操作si,di的增减。df=0每次操作后si,di递增df=1每次操作后si,di递减串传送指令,movsb,这个指令相当于执行:((es)*16+(di))=((ds)*16+(si))如果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:movsbloop s  1212所以rep的作用是根据cx的值重复执行后面的串传送指令,由于每次执行movsb之后si和di都会自行增减,所以使用rep可以完成(cx)个字节的传送。movsw也一样。由于DF位决定着串传送的方向,所以这里有两条指令用来设置df的值:cld:df=0std:df=1  1212例子:使用串传送指令将data段中第一个字符串复制到他后面的空间中:···data segmentdb 'Welcome to masm!'db 16 dup (0)data endsmov ax,datamov ds,axmov si,0mov es,axmov di,16mov cx,16cldrep movsb···  123456789101112131415123456789101112131415pushf和popfpushf的功能是将标志寄存器的值入栈,popf是出栈标志寄存器。有了这两个命令,就可以直接访问标志寄存器了,如:mov ax,0push axpopf  123123标志寄存器在Debug中的表示Debug中-r查看寄存器信息,最后有一段表示,下面列出我们已知的寄存器在Debug里的表示:标志值1的标记值0的标记ofOVNVsfNGPLzfZRNZpfPEPOcfCYNCdfDNUP内中断内中断的产生任何一个通用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的值,这个过程称为中断过程,步骤如下:(从中断信息中)取得中断类型码标志寄存器的值入栈(暂存)pushf设置标志寄存器第8位TF和第9位IF的值为0 TF=0,IF=0CS内容入栈 push csIP内容入栈 push ip在中断向量表中找到对应的CS和IP值并设置 (ip)=(N*4),(cs)=(N*4+2)这么做的目的是,在中断处理之后还要回复CPU的现场(各个寄存器的值),所以先把那些入栈。中断处理程序和iret指令运行中的CPU随时都可能接收到中断信息,所以CPU随时都可能执行中断程序,执行的步骤:保存用到的寄存器处理中断回复用到的寄存器用iret返回iret的指令功能是:pop ip pop cs popf(前面说到了,这三个寄存器的入栈是硬件自动完成的,所以iret是和硬件自动完成的步骤配合使用的)。以处理0号除法溢出中断为例,我们想要编写除法溢出的中断处理程序需要解决如下几步问题:编写程序找到一段没有使用的内存空间将程序写入到内存将内存中的程序的入口写入0号中断的向量表位置我们可以采取下面框架来完成这个过程:···start do0安装程序设置中断向量表mov ax,4c00hint 21hdo0 程序部分mov ax,4c00hint 21h···  1234567891012345678910可以看出我们分成了两部分,第一部分称之为“安装”,第二部分是代码实现。安装部分的函数实现思路如下:设置es:di至项目的地址设置ds:si指向源地址设置cx为传输长度设置传输方向为正rep movsb设置中断向量表  123456123456实现如下:start:mov ax,csmov ds,axmov si,offset do0mov ax,0es,axmov di,200hmov cx,offset do0end-fooset do0cldrep movsb···do0:代码do0end:nop  123456789101112123456789101112这里offset do0end-fooset do0的意思是do0到do0end的代码长度,-是编译器可以识别并运算的符号,也就是说编译器可以再编译时处理表达式,如8-4等。还要注意的是,假如代码部分要输出“owerflow!”的话,需要将输出的内容写在代码部分并写入选择的内存中,否则如果单单在这个安装程序开始开辟一段data段的话,是会在程序返回时被释放。如:do0:jmp short do0startdb "overflow!"do0start:···do0end:nop  123456123456单步中断当标志寄存器的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中断的执行过程:获取类型码n标志寄存器入栈,if=0,tf=0cs,ip入栈(ip)=(n*4),(cs)=(n*4+2)执行n号中断的程序所以我们可以使用int指令调用任何一个中断的中断程序,如int 0调用除法溢出中断。一般情况下,系统将一些具有一定功能的小程序以中断的方式提供给程序调用,当然也可以自己编写,可以简称为中断例程。编写中断例程如编写中断7ch的中断例程,实现word型数据的平方,返回dx和ax中。求2*3456^2,代码:assume cs:codecode segmentstart mov ax,3456int 7chadd ax,axadc dx,dxmov ax,4c00hint 21hcode endsend start  1234567891012345678910接下来写7ch的功能和安装程序,并修改7ch中断向量表:assume cs:codecode segmentstart:mov ax,csmov ds,axmov si,offset sqrmov ax,0mov es,axmov di,200hmov cx,offset sqrend-offset sqrcldrep movsbmov ax,0mov es,axmov word ptr es:[7ch*4],200hmov word ptr es:[7ch*4+2],0mov ax,4c00hint 21hsqr:mul axiretsqrend:nopcode endsend start  12345678910111213141516171819202122232425261234567891011121314151617181920212223242526编写7ch中断实现loop指令,主程序输出80个“!”:···start mov ax,0b800hmov es,axmov di,160*12mov bx,offset s-offset semov cx,80s:mov byte ptr es:[di],'!'add di,2int 7chse:nop···  123456789101112345678910117ch实现部分:lp:push bpmov bp,spdec cxjcxz lpretadd [bp+2],bxlpret:pop bpiret  12345671234567因为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  123456123456BIOS和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分别用于从端口读取数据和往端口写入数据。访问端口的步骤:CPU通过地址总线降低至信息60h发出CPU通过控制线发出端口读命令,选中端口所在芯片,并通知它要从中读取数据端口所在芯片将目标端口中的数据通过数据线送入CPU注:在in和out指令中,只能通过ax或al来存放从端口中读入的数据或要发送到端口中的数据,且访问8位端口时,用al,访问16位端口用ax。对0~255以内的端口进行读写时:in al,20hout 20h,al  1212对256~65535的端口进行读写时,需要将端口号写在dx中:mov dx,3f8hin al,dxout dx,al  123123CMOS RAM芯片PC中有一个叫做CMOS RAM的芯片,称为CMOS,有如下特征:包含一个实时钟和一个有128个存储单元的RAM存储器(早期的计算机64个字节)靠电池供电,关机后内部的实时钟仍可继续工作,RAM中的信息不丢失128个字节的RAM中,内部实时钟占用0~0dh单元保存时间信息,其余大部分单元用于保存系统配置信息,供系统启动时BIOS程序读取,BIOS也提供了相关的程序可以让我们在开机时配置CMOS中的系统信息。芯片内部有两个端口70h和71h,CPU通过这两个端口读写CMOS70h为地址端口,存放要访问CMOS单元的地址,71h为数据端口,存放从选定的单元中读取的数据,或写入的数据。所以可以看出,想要从CMOS中读取数据,应分两步,先将单元号送入70h,然后再从71h读出对应号的数据。shl和shr指令shl和shr是逻辑移位指令,shl是逻辑左移,功能为:将一个寄存器或内存单元中的数向左移位将最后移出的一位写入CF最低位补0如:mov al,01001000b shl al,1执行结束后(al)=10010000b,CF=0。注:如果移动位数大于1,那么必须将移动位数写在cl中。mov al,01010001bmov cl,3shl al,cl  123123执行后(al)=10001000b,最后移出的一位是0,所以CF=0。可以看出左移操作相当于x=x*2。右移shr同理,最高位用0补充,移出的写入CF,若移动位数大于1,也要写在cl中,相当于x=x/2在CMOS中存放着当前时间的年月日时分秒,分别存在下面的单元内:秒分时日月年024789每个信息使用一个字节存放,以BCD码的形式,BCD码是对0-9这几个数字使用二进制表示,如:01234567890000000100100011010001010110011110001001如果要表示一个两位数如13,就是一个字节高四位是十位1的BCD码,低四位是个位3的BCD码,表示为00010011b。下面程序获取当前月份:···mov al,8out 70h,al   ;要从8号单元读取数据,所以先将8号单元送入70h端口in al,71h    ;从71h端口拿数据mov ah,al    ;复制一下mov cl,4     shr ah,cl    ;ah右移四位,ah里面的就是月份的十位and al,00001111b  ;al里面剩下的就是月份的个位  123456789123456789外中断接口芯片和端口CPU除了需要拥有运算的能力,还要拥有I/O(输入输出)能力,我们键入一个字母,要能处理,所以我们需要面对的是:外部设备随时的输入和CPU何处得到外部设备的输入。外部设备拥有自己的芯片连接到主板上,这些芯片内部由若干寄存器,而CPU将这些寄存器当做端口访问,外设的输入或CPU向外设输出都是送给对应的端口然后再由芯片处理送给目标(CPU或外设)。外中断CPU提供外中断来处理这些如随时可能出现的来自外设的输入,在PC系统中,外中断源有以下两类:可屏蔽中断:CPU可以不响应的外部中断,CPU是否响应看标志寄存器IF的设置,如果IF=1,CPU执行完当前指令后响应中断,如果IF=0,则不响应。可屏蔽中断的执行步骤和内部中断类似:获取中断类型码n(从外部通过总线输入)标志寄存器入栈,IF=0,TF=0CS,IP入栈(IP)=(n*4),(CS)=(n*4+2)可见,将IF置零的原因是以免在处理中断程序的时候再发生中断。当然我们也可以选择处理,下面两个指令可以改变IF的值:sti,设置IF=1,cli,设置IF=0。不可屏蔽中断:CPU必须响应的外部中断,CPU检测到不可屏蔽中断后执行完当前指令立即响应中断。8086CPU中不可屏蔽中断的中断类型码固定位2,所以中断过程中不需要获取中断类型码,步骤:标志寄存器入栈,IF=0,TF=0CS,IP入栈(IP)=(8),(CS)=(0AH)几乎所有由外设引发的外中断都是可屏蔽中断,如键盘输入,不可屏蔽中断通常是在系统中又必须处理的紧急情况发生时通知CPU的中断信息。PC键盘处理过程键盘上每个按键都相当于一个开关,按下就是开关接通,抬起就是开关断开。键盘上有一个芯片对键盘中每一个键盘的状态进行扫描,开关按下生成一个扫描码——通码,记录按下的按键位置,开关抬起也会产生一个扫描——断码,码记录松开的位置,都是送入60h端口。通码的第7位为0,断码第7位为1,也就是说断码=通码+80h。P247表。当键盘输入送达60h时,相关新品就会向CPU发送中断类型码为9的可屏蔽中断信息。CPU检测到该中断信息之后,如果IF=1,响应中断,引发中断过程并执行int9的中断例程。BIOS中int9的中断程序用来进行基本的键盘输入处理,步骤如下:读出60h的扫描码如果是字符的扫描码,将对应的字符的ASCII吗存入内存中的BIOS键盘缓冲区,如果是控制键(Ctrl)和切换键(CapsLock)扫描码,则将其转换为状态字(二进制位记录控制键和切换键状态的字节)写入内存中的存储状态字节的单元。对键盘系统进行相关控制,如向新平发出应答BIOS中键盘缓冲区能存储15个键盘输入,每个键盘输入两字节,高位存放扫描码,低位存放字符。此外,0040:17单元存放键盘状态字节,记录了控制键和切换键的状态,记录信息如下:位含义0右shift,1表示按下1左shift,1按下2Ctrl,1按下3Alt,1按下4ScrollLock状态,1表示指示灯亮5NumLock状态,1表示小键盘输入的是数字6CapsLock状态,1表示大写字母7Insert状态,1表示处于删除状态可以看书P276的一个改写int 9的中断例程。直接定址表描述单元长度的标号我们可以使用下面的标号来表示数据的开始:···code segmenta:db 1,2,3,4,5,6,7,8b:dw 0···code ends···  12345671234567a,b都是代表对应数据的起始地址,但并不能判断数据的长度或类型。下面一段程序将a中的8个数累加存入b中:assume cs:codecode segmenta db 1,2,3,4,5,6,7,8b dw 0start mov si,0mov cx,8s:mov al,a[si]mov ah,0add b,axinc siloop smov ax,4c00hint 21hcode endsend start  123456789101112131415123456789101112131415code段中a和b后并没有”:”号,这种写法同时描述内存地址和单元长度的标号。a描述了地址code:0和从这个地址开始后的内存单元都是字节单元,而b描述了地址code:8和从这个地址开始以后的内存单元都是字单元。所以b相当于CS:[8],a[si]相当于CS:0[si],使用这种标号,我们可以间接地访问内存数据。其它段中使用数据标号刚说的第一种标号即加”:”号的标号,只能使用在代码段中,不能在其他段中使用。如果想要在其它段中(如data段)使用标号可以使用第二种:assume cs:code,ds:datadata segmenta db 1,2,3,4,5,6,7,8b dw 0data ends···start mov ax,datamov ds,axmov si,0mov al,a[si]···  12345678910111234567891011如果想在代码段中直接使用数据标号访问数据,需要使用assume伪指令将标号所在段和一个寄存器联系起来,是让寄存器明白,我们要访问的数据在ds指向的段中,但编译器并不会真的将段地址存入ds中,我们做了如下假设之后,编译器在编译的时候就会默认ds中已经存放了data的地址,如下面的编译例子:mov al,a[si]编译为:mov al,[si+0]  1212可以看出编译器默认了a[si]在ds所在的段中。所以我们需要手工指定ds指向data:mov ax,datamov ds,ax  1212也可以这么使用:data segmenta db 1,2,3,4,5,6,7,8b dw 0c a,bdata ends  1234512345c处存放的是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 showtable db '0123456789ABCDEF'show:push bxpush esmov ah,alshe ah,1she ah,1she ah,1she ah,1 ;右移四位,位移子程序限制使用的寄存器数,只能这么移and al,00001111bmov bl,almov bh,0mov ah,table[bx]  ;高四位作为相对于table的偏移,取得对应字符mov bx,0b800hmov es,bxmov es:[160*12+40*2],ahmov bl,almov bh,0mov al,table[bx]mov es:[160*12+40*2+2],alpop espop bxret  12345678910111213141516171819202122231234567891011121314151617181920212223可见我们直接使用需要的数值和地址的映射关系来寻找需要的数据。程序入口地址的直接定址表可以看书P296的例程,主要思想是,编写多个子程序实现不同功能,每个子程序有自己的标号,如sub1,sub2···等。将它们存在一个表中:table dw sub1,sub2,sub3,sub4  11然后按照之前的方法使用如:setscreen:jmp short settable dw sub1,sub2,sub3,sub4set:push bxcmp ah,3ja sretmov bl,ahmov bh,0add bx,bxcall word ptr table[bx]sret:pop bxret  12345678910111234567891011使用BIOS进行键盘输入和磁盘读写int 9中断例程对键盘输入的处理键盘处理依次按下A,B,C,D,E,shift_A,A的过程:我们知道,键盘有16字的缓冲区,可以存放15个按键的扫描码和对应的ASCII码值,如下:|   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |  11我们按下A时,引发键盘中断,CPU执行int 9中断例程,从60h端口读出A键通码,然后检测状态字,看是否有控制键或切换键按下,发现没有,将A的扫描码1eh和对应的ASCII码’a’61h写在缓冲区:|1e61|  |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |  11然后BCDE同理:|1e61|3062|2e63|2064|1265|  |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |  11在按下shift之后引发键盘中断,int 9程序接受了shift的通码之后设置0040:17处状态字第一位为1,表示左shift按下,接下来按A间,引发中断,int 9中断例程从60h端口督导通码之后检测状态字,发现左shift被按下,于是将A的键盘扫描码1eh和’A’的ASCII41h写入缓冲区:|1e61|3062|2e63|2064|1265|1e41| |   |   |   |   |   |   |   |   |   |   |   |   |   |   |  11松开shift,0040:17第一位变回0,之后又按下A和之前一样。int 16h读取键盘缓冲区int 16h可以供程序员调用,编号为0的功能是从键盘缓冲区读一个键盘输入,(ah)=扫描码,(al)=ascii码。如:mov ah,0int 16h|3062|2e63|2064|1265|1e41|  |   |   |   |   |   |   |   |   |   |   |   |   |   |   |  123123执行后,缓冲区第一个没了,然后ah中是1eh,al中是61h。如果缓冲区为空的时候执行,那么会循环等待知道缓冲区有数据,所以int 16h的0号功能的步骤是:检测键盘缓冲区是否有数据没有则继续1读取第一个单元的键盘输入扫描码送ah,ascii码送alint 13h读写磁盘3.5寸软盘分为上下两面,每面80个磁道,每个磁道18个扇区,每个扇区512字节,共约1.44MB。磁盘的实际访问时磁盘控制器进行的,我们通过控制磁盘控制器来控制磁盘,只能以扇区为单位读写磁盘,每次需要给出面号,磁道号,和扇区号,面号和磁道号从0开始,扇区号从1开始。BIOS提供int 13h来实现访问磁盘,读取0面0道1扇区的内容到0:200的程序:mov ax,0mov es,axmov bx,200hmov al,1   ;读取的扇区数mov ch,0   ;磁道号mov cl,1   ;扇区号mov dl,0   ;驱动器号,0开始,0软驱A,1软驱B,磁盘从80h开始,80h硬盘C,81h硬盘Dmov dh,0   ;磁头号(软盘面号)mov ah,2   ;13h的功能号,2表示读扇区int 13h  12345678910111234567891011es:bx指向接收数据的内存区。操作成功(ah)=0,(al)=读入的扇区数,操作失败(ah)=错误代码。将0:200的数据写入0面0道1扇区:mov ax,0miv es,axmov bx,200hmov al,1   ;读取的扇区数mov ch,0   ;磁道号mov cl,1   ;扇区号mov dl,0   ;驱动器号mov dh,0   ;磁头号(软盘面号)mov ah,3   ;13h的功能号,3表示写扇区int 13h  12345678910111234567891011es:bx指向写入磁盘的数据,操作成功(ah)=0,(al)=写入的扇区数,操作失败(ah)=错误代码

(0)

相关推荐