汇编语言最近系统的学了下汇编语言,下面是学习笔记,用的书是清华大学出版社出版的汇编语言第三版,作者王爽(最经典的那版)。 基础知识汇编语言指令组成CPU与外部器件交互需要存储单元地址(地址信息) 器件选择,读写命令(控制信息) 数据(数据信息)
总线总线就是一根根导线的集合,分为 小结汇编指令和机器指令一一对应 每一种cpu都有自己的汇编指令集 在存储器中指令和数据都是二进制,没有任何区别 CPU可以直接使用的信息存放在存储器中(内存) 接口卡CPU无法直接控制显示器,键盘等的外围设备,但CPU通过直接控制这些外围设备在主板上的接口卡来控制这些设备。 存储器随机存储器(RAM):带电存储,关机丢失,可读可写 只读存储器(ROM):关机不丢,只能读取 (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可以处理以下两种数据 简单的汇编指令指令 | 操作 | 高级语言 |
---|
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这个地址通过下面方法: 段寄存器与指令指针寄存器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指令用法: 小结8086CPU有四个段寄存器,CS是用来存放指令的段地址的段寄存器 IP用来存放指令的偏移地址 CS:IP指向的内容在任意时刻会被当做指令执行 使用转移指令修改CS和IP的内容 实验Debug命令: R:查看,改变CPU寄存器内容 直接-r查看寄存器内容 -r 寄存器名,改变寄存器内容
D:查看内存中内容 E:改写内存中内容 U:将内存中的机器指令翻译成汇编指令 T:执行一条机器指令 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
123456123456 add,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:codesg
codesg segment
mov ax,0123mov bx,0456add ax,bxadd ax,ax
mov ax,4c00
int 21codesg endsend
123456789101112131415123456789101112131415 涉及到的一些知识: XXX segment···XXXends end 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进行两步操作: (cx)=(cx)-1 (cx)不为零则跳至标号处执行程序。
所以CX中存放的是循环次数,一个简单的例子如下(计算2^12): assume cs:code
code segmentmov ax,2mov cx,11s:add ax,ax
loop smov ax,4c00h
int 21h
code ends
end
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:code
code segmentmov ax,0ffffhmov ds,axmov ax,0020hmov es,axmov bx,0mov cx,12s:mov dl,[bx]mov es:[bx],dlinc bx
loop smov ax,4c00h
int 21h
code ends
end
1234567891011121314151617181920212212345678910111213141516171819202122 [bx]直接使用的时候默认段前缀是ds,但要使用其他的段前缀,如es就要在前面加上。 程序的段数据段一般一个程序想要使用内存空间,有两种方法,在程序加载的时候系统分配或在需要使用的时候向系统申请,我们先考虑第一种情况。所以我们应事先将所需的数据存入内存中的某一段中,但我们又不可以随意的指定内存地址,以下面的求8个数据累加和的代码为例: assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987hmov bx,0mov ax,0mov cx,8s:add ax,cs:[bx]add bx,2loop smov ax,4c00h
int 21h
code ends
end
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,0987h
start:mov bx,0···
code endsend start
12345671234567 在第一条指令前加start,后面的end变成end start,end除了通知编译器程序在哪里结束之外,也可以通知程序的入口在哪,也就是第一条语句,在这里编译器就知道了mov bx,0是程序的第一条指令。也就是说,我们想要CPU从何处开始执行程序,只要在源程序中使用end 标号指定就好了。 所以有如下框架: assume cs:code
code segment
···数据···
start:
···代码···
code endsend start
12345671234567 栈段看下面一段使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,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,4c00h
int 21h
codesg ends
end start
123456789101112131415161718192021222324252627123456789101112131415161718192021222324252627 在定义了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,0srack ends
code 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,4c00h
int 21h
code ends
end 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:data
data segment
db 'unIX'db 'foRK'data endscode segment
start:mov al,'a'mov bl,'b'mov ax,4c00h
int 21h
code endsend start
123456789101112123456789101112 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
12341234 可见,只有第5位(从右往左数,从0开始计数)在大写和小写的二进制中是不一样的,所以我们只要把所有字母的二进制第五位置零,那就是大写,置1就是小写。代码如下: assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'db 'iNfOrMaTiOn'datasg endscodesg segment
start:mov ax,datasg
mov ds,ax
mov bx,0mov cx,5s:mov al,[bx]and al,11011111B
mov [bx],al
inc bx
loop s
mov bx,5mov cx,11s0:mov al,[bx]or al,00100000B
mov [bx],al
inc bx
loop s0
mov ax,4c00h
int 21h
codesg 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 bx
loop 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 si
loop sadd bx,16mov cx,dx
loop 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 si
loop 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 si
loop sadd bx,16pop cx
loop s0
···
1234567891011121314151617181920212212345678910111213141516171819202122 数据处理的两个基本问题两个基本问题处理的数据在什么地方 要处理的数据有多长
接下来的讨论中,使用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中,只有这四个寄存器可以使用[···]来进行内存寻址,可以单个出现,或以下面组合出现(常数可以随意出现在这些表示方法中): 注:如果使用了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) 寄存器 段地址(SA)和偏移地址(EA)
总结一下寻址方式: 寻址方式 | 含义 | 名称 |
---|
[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,所以在使用数据的时候要指明数据尺寸。 灵活使用寻址方式的例子,修改下面内存空间中的数据: 段seg:60 起始地址 | 内容 |
---|
00 | 'DEC’ | 03 | 'Ken Oslen’ | 0C | 137 | 0E | 40 | 10 | '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是除法指令,需要注意以下三点: 格式:div reg或div 内存单元,所以div byte ptr ds:[0]表示: (al)=(ax)/((ds)*16+0)的商;
(ah)=(ax)/((ds)*16+0)的余数;
1212 div 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相关: dupdup是一个操作符,由编译器识别,和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范围-128~127 近转移:修改IP范围-32768~32767
转移指令分类:
offset操作符offset是由编译器处理的符号,它能去的标号的偏移地址,如: start:mov ax,offset starts:mov ax,offset s
1212 这里就是将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)
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,1 CPU指向指令缓冲器中的指令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加内存使用的时候有两种用法: 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和retfret和call都是转移指令,都是修改IP的值,或同时修改CS和IP。 ret指令用栈中的数据修改IP,实现的是近转移;retf指令用栈中的数据修改CS和IP的值,实现远转移。格式:直接用 ret。 ret执行步骤: (IP)=((SS)*16+(SP)) (SP)=(SP)+2
retf执行步骤: (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:code
code segmentmain:···call sub1
···mov ax,4c00h
int 21hsub1:···call sub2
···retsub2:···retcode ends
end main
12345678910111213141516171819201234567891011121314151617181920 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位
1212 例:计算100*10 mov al,100mov bl,10mul bl
123123 参数的传递和模块化编程看下面一段程序:计算data中第一行的数的立方存在第二行 assume cs:code
data segment
dw 1,2,3,4,5,6,7,8dd 0,0,0,0,0,0,0,0data ends
code 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,4c00h
int 21hcube:mov ax,bxmul bxmul bxretcode ends
end start
1234567891011121314151617181920212223242526272829303112345678910111213141516171819202122232425262728293031 寄存器冲突观察下面将data中的数据全转化为大写的代码: assume cs:code
data segment
db 'word',0db 'unix',0db 'wind',0db 'good',0data endscode segment
start:mov ax,data
mov ds,ax
mov bx,0mov cx,4s:mov si,bx
call capitaladd bx,5loop s
mov ax,4c00h
int 21h
capital:mov cl,[si]
mov ch,0jcxz okand byte ptr [si],11011111b
inc si
jmp short capital
ok:ret
code 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或有符号+1
10000001B,可以看成无符号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=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 sadd ab,bhjmp short oks:add ah,ahok:···
123456123456 这里注意的是,je检测的是zf位,而不管之前执行的是什么指令,只要zf=1就会发生转移,所以cmp的位置需要仔细的把控,当然是否和cmp配合使用也是取决于编程者,下面例子实现了统计data中数值为8的字节个数,然后用ax保存: ···
data segment
db 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 bx
loop s
···
1234567891011121314151612345678910111213141516 DF标志位和串传送指令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
···
123456789101112131415123456789101112131415 pushf和popfpushf的功能是将标志寄存器的值入栈,popf是出栈标志寄存器。有了这两个命令,就可以直接访问标志寄存器了,如: mov ax,0push ax
popf
123123 标志寄存器在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种)如下: 中断处理和中断向量表CPU接收到中断信息之后,往往要对中断信息进行处理,而如何处理使我们编程决定的。而CPU通过中断向量表来根据中断类型找到处理程序的入口地址(CS:IP)也称为中断向量。 中断向量表中存放着不同的中断类型对应的中断向量(处理程序的入口地址),中断向量表存放在内存中,8086PC指定必须放在内存地址0处,从0000:0000到0000:03FF的1024个单元存放中断向量表,每个表项占两个字,四个字节。 CPU会自动根据中断类型找到对应的中断向量并设置CS和IP的值,这个过程称为中断过程,步骤如下: (从中断信息中)取得中断类型码 标志寄存器的值入栈(暂存)pushf 设置标志寄存器第8位TF和第9位IF的值为0 TF=0,IF=0 CS内容入栈 push cs IP内容入栈 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 do0
cld
rep movsb
···do0:代码do0end:nop
123456789101112123456789101112 这里offset do0end-fooset do0的意思是do0到do0end的代码长度,-是编译器可以识别并运算的符号,也就是说编译器可以再编译时处理表达式,如8-4等。还要注意的是,假如代码部分要输出“owerflow!”的话,需要将输出的内容写在代码部分并写入选择的内存中,否则如果单单在这个安装程序开始开辟一段data段的话,是会在程序返回时被释放。如: do0:jmp short do0start
db "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=0 cs,ip入栈 (ip)=(n*4),(cs)=(n*4+2)
执行n号中断的程序
所以我们可以使用int指令调用任何一个中断的中断程序,如int 0调用除法溢出中断。一般情况下,系统将一些具有一定功能的小程序以中断的方式提供给程序调用,当然也可以自己编写,可以简称为中断例程。 编写中断例程如编写中断7ch的中断例程,实现word型数据的平方,返回dx和ax中。求2*3456^2,代码: assume cs:code
code segmentstart mov ax,3456int 7chadd ax,ax
adc dx,dx
mov ax,4c00hint 21h
code endsend start
1234567891012345678910 接下来写7ch的功能和安装程序,并修改7ch中断向量表: assume cs:code
code segmentstart:mov ax,csmov ds,axmov si,offset sqrmov ax,0mov es,axmov di,200hmov cx,offset sqrend-offset sqr
cld
rep movsbmov ax,0mov es,axmov word ptr es:[7ch*4],200hmov word ptr es:[7ch*4+2],0mov ax,4c00h
int 21hsqr:mul ax
iretsqrend:nopcode ends
end 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···
12345678910111234567891011 7ch实现部分: lp:push bpmov bp,spdec cx
jcxz lpretadd [bp+2],bxlpret:pop bp
iret
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
123456123456 BIOS和DOS安装历程的过程是,开机后CPU一加电,自动初始化CS为0FFFFH,IP为0,而在这个地方有一个跳转指令,挑战到BIOS和系统检测初始化程序。在BIOS系统检测初始化程序中会设置中断向量表中的值。 端口端口的概念各种存储器都要和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
123123 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是逻辑左移,功能为: 将一个寄存器或内存单元中的数向左移位 将最后移出的一位写入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中存放着当前时间的年月日时分秒,分别存在下面的单元内: 每个信息使用一个字节存放,以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,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=0 CS,IP入栈 (IP)=(n*4),(CS)=(n*4+2)
可见,将IF置零的原因是以免在处理中断程序的时候再发生中断。当然我们也可以选择处理,下面两个指令可以改变IF的值:sti,设置IF=1,cli,设置IF=0。 不可屏蔽中断:CPU必须响应的外部中断,CPU检测到不可屏蔽中断后执行完当前指令立即响应中断。8086CPU中不可屏蔽中断的中断类型码固定位2,所以中断过程中不需要获取中断类型码,步骤: 标志寄存器入栈,IF=0,TF=0 CS,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按下 | 2 | Ctrl,1按下 | 3 | Alt,1按下 | 4 | ScrollLock状态,1表示指示灯亮 | 5 | NumLock状态,1表示小键盘输入的是数字 | 6 | CapsLock状态,1表示大写字母 | 7 | Insert状态,1表示处于删除状态 |
可以看书P276的一个改写int 9的中断例程。 直接定址表描述单元长度的标号我们可以使用下面的标号来表示数据的开始: ···code segmenta:db 1,2,3,4,5,6,7,8b:dw 0
···code ends···
12345671234567 a,b都是代表对应数据的起始地址,但并不能判断数据的长度或类型。下面一段程序将a中的8个数累加存入b中: assume cs:code
code segment
a 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 si
loop smov ax,4c00h
int 21h
code ends
end start
123456789101112131415123456789101112131415 code段中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
1234512345 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 bxpush es
mov ah,al
she ah,1she ah,1she ah,1she ah,1 ;右移四位,位移子程序限制使用的寄存器数,只能这么移and al,00001111b
mov bl,al
mov bh,0mov ah,table[bx] ;高四位作为相对于table的偏移,取得对应字符
mov bx,0b800h
mov es,bx
mov es:[160*12+40*2],ah
mov bl,al
mov bh,0mov al,table[bx]
mov es:[160*12+40*2+2],alpop espop bx
ret
12345678910111213141516171819202122231234567891011121314151617181920212223 可见我们直接使用需要的数值和地址的映射关系来寻找需要的数据。 程序入口地址的直接定址表可以看书P296的例程,主要思想是,编写多个子程序实现不同功能,每个子程序有自己的标号,如sub1,sub2···等。将它们存在一个表中: table dw sub1,sub2,sub3,sub4
11 然后按照之前的方法使用如: setscreen:jmp short settable dw sub1,sub2,sub3,sub4set:push bx
cmp 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码送al
int 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
12345678910111234567891011 es: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
12345678910111234567891011 es:bx指向写入磁盘的数据,操作成功(ah)=0,(al)=写入的扇区数,操作失败(ah)=错误代码
|