8086处理器基础知识
汇编语言是大小写不敏感的低级语言,意思就是mov和MOV是汇编语言来说是一样的,当然大小写混用也是可以的,如Mov
在8086汇编语言里,数字默认是十进制的,如果是其他进制数字则需要标识,我们用十进制和十六进制较多,记住这两个即可,特别注意如果十六进制数首个字符是字母则需要在前面多添一个0(因为不加0编译器会将它看作一个变量名,而不是一个十六进制数字),如
mov ax,50;50是十进制的
mov ax,50h;50h是十六进制的
mov ax,abc2h;报错
mov ax,0abc2h;需要在a前添0
80x86微处理器有8086、8088、80286等型号,我们要学习的就是8086微处理器的汇编指令系统,主要记住下表内容
型号 | 字长(位) | 数据总线宽度(位) | 地址总线宽度(位) | 寻址空间 |
---|---|---|---|---|
8086 | 16 | 16 | 20 | 1M |
- 字长:计算机一次性能处理二进制数的位数叫字长
- 寻址空间:可理解成地址范围1M=220,计算机内部存储空间都会编上号,就像每个户人家都有一个门牌号一样,总共有1M户人家,依次被编号0~ 220-1
由于地址总线的宽度是20位,而数据总线宽度只有16位,为了能够表示20位的地址,我们采用如下方式
两个十六位数字的经过一定的计算变成一个20位的地址
2000 h ∗ 16 + 1000 h = 21000 h 2000h*16+1000h=21000h 2000h∗16+1000h=21000h
(这里的h代表十六进制,每一位代表4个二进制数)
2000h乘以16,相当于十六进制数左移一位
2000h和1000h这里被称为虚地址(也叫逻辑地址)
而计算出的21000h称为实地址(也叫物理地址)
此时这里的2000h就称为段地址,后面的1000h就称为偏移地址。那么存放段地址的寄存器就是段寄存器
8086寄存器
段寄存器和其他寄存器也不能随意搭配使用,下表是8086的段寄存器和存放偏移地址的寄存器之间的默认组合
段 | 偏移 |
---|---|
CS(code segment) | IP (index pointer) |
SS (stack segment) | SP(stack pointer)、IP |
DS(data segment) | BX(base)、DI(destination index)、SI(source index)或一个16位数字 |
ES (extra segment) | DI(用于串指令) |
通用寄存器和专用寄存器以及它们的常用用途
通用寄存器 | 用途 |
---|---|
AX(accumulate) | 主要用于算术运算,在乘除指令存放操作数,I/O指令与外部设备传送信息 |
BX (base) | 在计算实地址时,用作基址寄存器 |
CX(count) | 在移位指令、循环指令(loop)和串处理指令用作隐含的计数器 |
DX(data) | 在双字长(即32位)运算时会把DX、AX组合用来存放双字长数,DX存放高位字,AX存放低位字,对某些I/O操作,DX可以存放I/O的端口地址 |
专用寄存器 | 用途 |
---|---|
SP(stack pointer) | 它与堆栈段寄存器(SS)一起来确定堆栈段中栈顶的位置 |
IP (instruction pointer) | 指令指针寄存器,用来存放代码段中指令的偏移地址 |
FLAGS(标志寄存器)又称程序状态寄存器(PSW,program status word) | 顾名思义,用来存放各种状态的寄存器 |
所有的通用寄存器都可以自由使用
但每个寄存器又有其单独的用途
AX,BX,CX,DX 可以拆分为高8位和低8位使用, 分别是AH&AL, BH&BL…
而SP, BP, SI, DI四个只能以16位的方式使用
SP, BP, SI, DI四个寄存器更经常的用途是在储存器寻址时, 提供偏移地址
FLAGS, 此寄存器是按位使用的, 各个位的功能如下:
状态标志位 | 功能 |
---|---|
OF(overflow flag) | 作为有符号数溢出标志,如果运算结果的次高位产生一个进位或借位,则OF置1,否则OF为0。 |
CF(carry) | 作为无符号数溢出标志,如果运算结果的最高位产生一个进位或借位,则CF置1,否则CF为0。 |
SF(sign) | 运算结果为负数SF置1,否则SF为0 |
ZF(zero) | 运算结果为0数ZF置1,否则ZF为0 |
AF(auxiliary carry flag) | 辅助进位位,反映半个字节运算结果产生进位或借位的情况,如1000h+1234h,即记录00h+34h进位或借位的情况,有进位置1,否则为0 |
PF(parity flag) | 奇偶位,如果运算结果“1”的个数为偶数时,则PF置1, 否则PF为0 |
TF(trap flag) | 单步标志位,用于程序跟踪调试。当TF=1,CPU进入单步方式,TF=0,CPU正常工作。 |
IF(interrupt flag) | 中断允许位,当IF=1时,CPU为开中断。 当IF=0时,CPU为关中断。 |
DF(direction flag) | 方向位,决定串操作指令执行时的指针寄存器的调整方向,DF=1,SI和DI减小,使得串处理从高地址向低地址方向处理,DF=0,则相反,常用于字符串正序和倒序输出 |
tips:
CF是最高位产生了进位或借位时的标志,是无符号数溢出的标志位
OF是次高位产生了进位或借位时的标志位,是有符号数溢出的标志位
同符号数相加,运算结果符号变了,说明发生溢出
不同符号数相加,一定不会发生溢出
数据储存方式
80X86的CPU绝大多数是用小端模式进行存储,而ARM绝大多数都是大端存储
模式 | 特点 |
---|---|
大端模式 | 数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中 |
小端模式 | 是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中 |
总结:大端就是高放低,小端就是高放高,8086是小端。
寻址方式
一条指令一般都是由一个操作码和多个操作数构成的
mov ax,5
;mov为操作码
;ax和5为操作数
;二地址指令
寻址方式是用来确定操作数地址, 从而找到操作数的
通常操作数字段为1个或2个, 也有少数是3个的,有几个操作数就是几地址指令,如一地址指令, 二地址指令, 三地址指令
下表是一些寻址方式及其举例(名字可以不用死记,记住它们的例子和特征就好)
寻址方式 | 特点 | 举例 |
---|---|---|
立即寻址方式 | 操作数是一个数字 | mov al,5 |
寄存器寻址方式 | 操作数放在一个寄存器里 | mov ax,bx |
直接寻址方式 | 只含位移量 | mov ax,[2000h] |
寄存器间接寻址方式 | 只含基址或只含变址 | mov ax,[bx] |
寄存器相对寻址方式 | 基址或变址+位移量 | mov ax,count[si] |
基址变址寻址方式 | 基址+变址 | mov ax,[bx][di] |
相对基址变址寻址方式 | 基址+变址+位移量 | mov count[bx][si] |
每个指令通常都有其对应的寻址方式, 操作数要么就是在指令中显式给出,要么就是在寄存器中,要么就是在内存中。
直接显示给出就是立即寻址方式,那个数字就被称为立即数
在寄存器中,就是寄存器寻址方式
在内存中
寄存器间接寻址
eg:mov ax,[si]
如果在内存中,那就要根据以下公式来辨别其寻址方式了
有效地址的计算公式:
EA(effective address) = 基址 + (变址 * 比例因子) + 位移量
操作数的偏移地址称为有效地址EA, 有效地址可以用以下四种成分组成:
成分 | 功能 |
---|---|
位移量(displacement) | 位移量是存放在指令中的一个8位, 16位或32位的数,但他不是一个立即数,而是一个地址 |
基址(base) | 基址是存放在基址寄存器BX或BP中的内容,其指有效地址的基址部分。通常要来指向数据段中的数组和字符串的首地址 |
变址(index, 即下标) | 变址是存放在变址寄存器SI或DI中的内容,它通常用来访问数组中的某个元素和字符串中的某个字符。 |
比例因子(scale factor, 80386机型后才有) | 比例因子的值通常就是1,2,4,8, 比例因子用于在寻址过程中将变址寄存器的内容乘以比例因子来取得变址值, 所以在访问元素长度为2,4,8字节的数组特别有用 |
至于为何要上4种,成分 而不仅仅基址+位移量两种, 是因为引入变址和比例因子使得有效地址的计算更加灵活
其中除比例因子以外, 其他3个都可以是负数, 以保证指针移动的灵活性
每种成分可有可无,但比例因子和变址必须同时存在,故根据这个公式可以有23种寻址方式,但含比例因子的寻址方式只80386及其以后的机型有,我们主要掌握8086的寻址方式即可,整理如下表
寻址方式 | 特点 | 举例 |
---|---|---|
直接寻址方式 | 只含位移量 | mov ax,[2000h] |
寄存器间接寻址方式 | 只含基址或只含变址 | mov ax,[bx] |
寄存器相对寻址方式 | 基址或变址+位移量 | mov ax,count[si] |
基址变址寻址方式 | 基址+变址 | mov ax,[bx][di] |
相对基址变址寻址方式 | 基址+变址+位移量 | mov count[bx][si] |
寻址方式只确定了操作数的偏移地址,前面也说过
操作数的实地址=段地址*16+偏移地址
段寄存器和其他寄存器也不能随意搭配使用,下表是8086的段寄存器和存放偏移地址的寄存器之间的默认组合
段 | 偏移 |
---|---|
CS(code segment) | IP (index pointer) |
SS (stack segment) | SP(stack pointer)、IP |
DS(data segment) | BX(base)、DI(destination index)、SI(source index)或一个16位数字 |
ES (extra segment) | DI(用于串指令) |
我们也可以强制指定其他段寄存器使用, 称为段跨越前缀, 如:
mov ax,[0000]
;程序默认使用的是ds:0000,那它实地址为ds*16+0000
而使用了跨越段前缀就使用你指定的段寄存器
mov ax,es:[0000]
;程序就会使用es:0000,那它实地址为es*16+0000
凡事都可能有例外,有三种情况不允许使用段跨越前缀:
- 串处理指令的目的串必须使用ES段
- PUSH指令的目的和POP指令的源必须使用SS段
- 指令必须存放在CS段中
80x86指令系统
主要指令可以分为以下6种:
数据传送指令
串处理指令
算数指令
控制转移指令
逻辑指令
处理机控制指令
数据传送指令
数据传送指令用于把数据, 地址或立即数传送到寄存器或储存单元中, 即相当于实现赋值功能
通用数据传送指令
指令 | 功能 | 格式举例 |
---|---|---|
MOV (move) | 传送 | mov ax,5 |
PUSH(push onto the stack) | 进栈 | push src |
POP(pop from the stack) | 出栈 | pop dst |
XCHG(exchange) | 交换 | xchg oper1,oper2 |
MOV:
正确和错误格式举例
;--正确--
mov ax,5;寄存器赋值
mov num,5;符号地址赋值(符号地址相当于高级语言中的变量名)
mov [bx],1000h;[]内bx的值相当于地址,将1000h赋值给存储单元的[bx]中
mov ax,2000h
mov ss,ax;需要借助一个通用寄存器来给段寄存器赋初值
;--错误--
mov num1,num2;此时num1和num2都是符号地址(相当于高级语言中的变量名),都是存储器,mov指令不允许在存储单元之间传送数据
mov ss,2000h;不可直接给段寄存器赋值
PUSH & POP
PUSH支持所有的寻址方式
(寄存器reg, 储存器mem, 立即数data, 段寄存器segreg)
POP支持除立即数以外的任何寻址方式
二者的执行效果:
相当于对堆栈指针SP移位, 16位操作±2字节, 32位操作±4字节, 而后将SRC的数据进栈弹出SRC或将栈顶元素存入DST
注意:执行PUSH AX后, 栈顶指针SP-2, 而POP后, 栈顶指针SP+2
;---错误---
pop 123h;pop不支持立即数寻址方式
;---正确---
push 123h;PUSH支持所有的寻址方式
push ax
XCHG
此指令可以在寄存器之间或寄存器与储存器之间交换信息
(高级语言中需要使用3条语句)
格式:
XCHG OPR1 OPR2
使用要点:
两个操作数必须有一个在寄存器中
不允许使用段寄存器
IN输入指令 & OUT输出指令
IN & OUT用于所有的I/O端口与CPU之间的通信
XLAT换码指令
通常用于编码的快速转换
首先在使用XLAT指令前, 通常已经在Data Segment数据段建立的一个转换表(如数码管的段显示表, 其实就是一个数组), 将此表的首地址加载给BX, 并在AX中装入偏移量, 用XLAT后, CPU会将DS+BX+AX指向的值(注意是指向的值)装入AX中
XLAT OPR ;OPR为操作数, 通常是符号化的首地址, 这种写法提高程序的可读性
XLAT ;直接就这样, 简化版
;执行效果:
;16位: 将BX+AL的值赋给AL, 此时必须零扩展到16位
;32位: 将EBX+AL的值赋给AL, 此时必须零扩展到32位
如:
;执行前: (BX)=0040H, (AL)=0FH, (DS)=F000H,
XLAT
;执行后, AX的值为F000H+0040H+0FH地址处储存的值
地址传送指令
注意这里和之前的MOV指令不同的是, MOV传送的是源操作数中的值, 而地址传送指令传送的是源操作数的地址
LEA(load effective address)有效地址送寄存器指令
格式:
LEA REG, SRC
;将SRC的有效地址存入REG寄存器中
REG可以是16位或32位寄存器, 但是不可以使用段寄存器
SRC可以是除立即寻址方式和寄存器寻址方式以外的任何一种储存器寻址方式(即前头的8 种)
如:
;执行前: (BX)=0400H, (SI)=003CH
LEA BX, [BX+SI+0F62H]
;执行后: (BX)=0400+003C+0F62=139EH, 这里得到的是有效地址, 而不是地址指向的值
类型转换指令
此类型指令均不影响标志位
CBW ;convert byte to word 字节转换为字, 将AL符号扩展到AX
;如果AL的最高有效位为0, 则AH=0, 否则AH=0FFH
CWD/CWDE ;convert word th double word 字转换为字节, 将AX符号扩展到DX,
;形成DX:AX双字, 如果AL的最高有效位为0, 则DX=0, 否则DX=0FFFFH
CDQ ;convert double to quad 双字转换为4字, 将EAX符号扩展到EDX,
;形成EDX:EAX中的4字
BSWAP ;byte swap 字节交换
;使指令指定的32位寄存器的字节次序变反, 即1,4互换, 2,3互换
算数指令
此类指令与FLAGS标志位关联紧密
加法指令
指令 | 功能 | 格式举例 |
---|---|---|
ADD DST, SRC | 普通加法指令 | add ax,bx |
ADC DST, SRC (add with carry) | 带进位加法,计算双精度数 | ADC DX, BX |
INC | 自增加1, 相当于++ | inc cx |
ADD:
普通加法指令, 对CF进位标志有影响
指令对标志位的影响:
CF=1 最高有效位向高位有进位
CF=0 最高有效位向高位无进位
OF=1 两个同符号数相加(正数+正数 或 负数+负数),结果符号与其相反。
OF=0 两个不同符号数相加,或同符号数相加,结果符号与其相同。
ADC:
进位加法指令, 通常与ADD联用, 用于计算双精度数. 其中先将低位用ADD, 再将高位用ADC
计算时将CF也加入计算, 如果相加产生进位, 会再使用一个寄存器存放高位字, DX:AX, BX:CX
如:
;初始: DX=0002H, AX=0F365H, BX=0005H, CX=0E024H
ADD AX, CX
;执行后: AX=0D389H, SF=1, ZF=0, CF=1, OF=0
ADC DX, BX
;执行后: DX=0008H, SF=0, ZF=0, CF=0, OF=0
指令对标志位的影响:
CF=1 最高有效位向高位有进位
CF=0 最低有效位相高位无进位
OF=1 两个同符号数相加,结果符号与其相反,
OF=0 两个同符号数相加,或同符号相加,结果符号与其相同
INC:
++指令
指令对标志位的影响:
对CF无影响
OF=1 两个同符号数相加,结果符号与其相反,
OF=0 两个同符号数相加,或同符号相加,结果符号与其相同。
减法指令
指令 | 功能 | 格式举例 |
---|---|---|
SUB DST, SRC (subtract) | 减法 | sub ax,bx |
SBB DST, SRC (subtract with borrow) | 带借位减法 | sbb dx,bx |
DEC OPR (decrement) | 自减一 | dec ax |
NEG OPR (negate) | 求补 | neg ax |
CMP OPR1, OPR2 (compare) | 比较 | cmp ax,bx |
CPU中的减法都是将SRC取反+1后加上DST, 与加法就差一个求补的过程
SUB:
普通减法指令, 与ADD相似, 当DST<SRC时会产生借位, 时CF=1
指令对标志位的影响:
CF=1 二进制减法运算中最高有效位向高位有借位(被减数小于减数,不够减的情况)
CF=0 二进制减法运算中最高有效为向高位无借位(被减数〉=减数,够减的情况)
OF=1 两数符号相反(正数-负数,或负数-正数),而结果符号与减数相同。
OF=0 同符号数相减时,或不同符号数相减,其结果符号与减数不同。
SBB:
带借位的减法指令, 与ADC类似, 与SUB联用, 用于计算双精度数, 低位用SUB, 高位用SBB
指令对标志位的影响:
CF=1 二进制减法运算中最高有效位向高位有借位(被减数小于减数,不够减的情况)
CF=0 二进制减法运算中最高有效为向高位无借位(被减数〉=减数,够减的情况)
OF=1 两数符号相反(正数-负数,或负数-正数),而结果符号与减数相同。
OF=0 同符号数相减时,或不同符号数相减,其结果符号与减数不同。
DEC:
自减一指令, 即–
指令对标志位的影响:
对CF无影响
OF=1 两数符号相反(正数-负数,或负数-正数),而结果符号与减数相同。
OF=0 同符号数相减时,或不同符号数相减,其结果符号与减数不同。
NEG:
求补指令, 将OPR取反后+1
指令对标志位的影响:
CF=1 不为0的操作数求补时
CF=0 为0的操作数求补时
OF=1 操作数为-128(字节运算)或操作数为-32768(字运算)
OF=0 当求补运算的操作数不为-128(字节)或-32768(字)时
CMP:
比较指令, 与SUB指令类似, 将OPR1-OPR2, 但是不保存结果, 只是根据结果操作CF位
指令对标志位的影响:
CF=1 二进制减法运算中最高有效位向高位有借位(被减数小于减数,不够减的情况)
CF=0 二进制减法运算中最高有效为向高位无借位(被减数〉=减数,够减的情况)
OF=1 两数符号相反(正数-负数,或负数-正数),而结果符号与减数相同。
OF=0 同符号数相减时,或不同符号数相减,其结果符号与减数不同。
CMPXCHG & CMPXCHG8B
比较并交换指令, SRC只能用8位, 16位, 32为寄存器, DST可用寄存器或任意寻址的方式
<这个比较作用比较迷惑, 先放着>
乘法指令
指令 | 功能 |
---|---|
MUL SRC (unsigned multiple) | 无符号乘法 |
IMUL SRC (signed multiple) | 有符号乘法 |
SRC可以使用处立即数外的任何一种寻址方式 |
MUL:
无符号乘法, 将SRC与ac相乘, 使用AL, AX或EAX, 并将结果扩容一倍分别保存在AX, DX:AX, EDX:EAX中, 具体使用的ac有SRC的长度决定
IMUL:
有符号乘法, 具体操作与MUL相同
其中使用MUL会将SRC解释为无符号数, 而IMUL会将SRC解释为有符号数, 所以混用这两个会出大问题
乘法指令对于符号位的影响:
乘法指令对于处CF&OF外的flag无定义(即使用乘法指令后其他位不确定)
对于MUL: 当乘积的高位部分全为0(即乘积没有超出原大小), CF=OF=0, 否则CF=OF=1
对于IMUL: 如果乘积的高位是低位的符号拓展, 则CF=OF=0, 否则CF=OF=1
注意: CF和OF一起变
IMUL的双, 三操作数:
IMUL允许使用双, 三操作数, 因为这种指令不会像无操作数的版本对结果进行拓展, 所以必须确保结果不会溢出, 而单操作数保证不会溢出, 具体使用:
IMUL REG, SRC ;执行效果: REG=REG*SRC , 其中REG不扩展
IMUL REG, SRC, IMM ;执行效果: REG=SRC*IMM , 其中REG不扩展
除法指令
指令 | 功能 |
---|---|
DIV SRC (unsigned divide) | 无符号数除法 |
IDIV SRC (signed divide) | 有符号数除法 |
;SRC可以采用除立即数以外的任何一种寻址方式 |
注意: 除法指令的SRC不可以使用立即数
除法指令与乘法指令相似, 同样也是将ac除以SRC, 但是会窄化ac(乘法指令是扩展ac), 其中SRC最少为16位, 所以经常需要对SRC进行扩展
对于AC的大小选择根据SRC而定, AC的大小为SRC的两倍, 即当SRC为8bit时, 使用AX, 并将结果保存在AL中
除法指令需要注意的是余数的保存:
当SRC为8bit时, 商保存在AL, 余数保存在AH
当SRC为16bit时, 商保存在AX, 余数保存在DX
当SRC为32bit时, 商保存在EAX, 余数保存在EDX
除法指令对所有的FLAGS都无定义(即使用后所有位都不确定)
逻辑指令
这部分的功能与高级语言相同
逻辑运算指令 | 功能 |
---|---|
AND DST, SRC | 逻辑与 |
OR DST, SRC | 逻辑或 |
NOT OPR | 逻辑非 |
XOR DST, SRC | 异或 |
这几个指令都是将结果保存在第一个操作数中
并且, 指令的操作数必须有一个在寄存器中, 而其他的可以使用任意一种寻址方式
AND:
AND <寄存器/存储单元>,<立即数/寄存器/存储单元>
指令对标志位的影响:
指令执行后 CF 和 OF 置零,AF无定义。
PF=1 结果操作数中1的个数为偶数时置1
PF=0 结果操作数中1的个数为奇数时置0
OR:
OR <寄存器/存储单元>,<立即数/寄存器/存储单元>
指令对标志位的影响:
令执行后 CF 和 OF 置零,AF无定义。
PF=1 结果操作数中1的个数为偶数时置1
PF=0 结果操作数中1的个数为奇数时置0
NOT:
NOT <寄存器/储存单元>
对OPR取非, 并将结果存在OPR中
不允许使用立即数(就一个操作数TM怎么用立即数)
对FLAGS标志寄存器无影响
XOR:
XOR <寄存器/存储单元>,<立即数/寄存器/存储单元>
指令对标志位的影响:
令执行后CF 和 OF 置零,AF无定义。
PF=1 结果操作数中1的个数为偶数时置1
PF=0 结果操作数中1的个数为奇数时置0
移位指令 | 功能 |
---|---|
SHL OPR, CNT (shift logical left) | 逻辑左移 |
SHR OPR, CNT (shift logical right) | 逻辑右移 |
SAL OPR, CNT (Shift arithmetic left) | 算术左移 |
SAR OPR, CNT (shift arithmetic right) | 算术右移 |
ROL OPR, CNT (rotate left) | 循环左移 |
ROR OPR, CNT (rotate right) | 循环右移 |
RCL OPR, CNT (rotate left through carry | 带进位循环左移 |
RCR OPR, CNT (rotate right through carry | 带进位循环右移 |
串处理指令
串指令用于处理存储在存储器里的数据串, 具体可理解为字符串, 字符数组这一类的东西, 类似于C++中的字符串处理函数等等
指令 | 功能 |
---|---|
MOVS | 串传送 |
CMPS | 串比较 |
SCAL | 串扫描 |
LODS | 从串取 |
STOS | 存入串 |
INS | 串输入 |
OUTS | 串输出 |
配合使用的指令前缀: | |
指令 | 功能 |
-------- | ----- |
REP string_primitive | 重复 |
REPE/REPZ | 相等/为零则重复 |
REPNE/REPNZ | 不相等/不为零则重复 |
串处理指令使用的默认寄存器:
SRC string: DS:SI
DST string: ES:DI
AC: AL 或 AX
所以使用串处理指令之前需要保证DS & ES都储存了对应字符串的段地址
所以串处理指令的操作数仅仅向编译器提供了类型, 以便编译器选用合适的指令和每次操作后指针的偏移量
方向标志在串处理指令中的作用:
主要是方向标志位DF的作用:
DF=1:每次操作后使SI和DI减小
DF=0:每次操作后使SI和DI增大
变更方向标志的两条指令:
CLD:该指令使DF=0 clear DF
STD:该指令使DF=1 set DF
通常需要在串指令之前做的操作:
把存放在数据段中的源串首地址(如反向传送则应是末地址)放入源变址寄存器中;
把将要存放数据串的附加段中的目的串首地址(或反向传送时的末地址)放入目的变址寄存器中;
把数据串长度放入计数寄存器;
建立方向标志。
而后再开始串指令操作
串处理指令基本都不改变FLAGS标志寄存器
REP:
REP是串处理的核心, 建立并简化了循环, 如果没有REP等循环操作, 串处理指令仅仅执行一次
串处理操作中的循环操作, 与手写循环相比更为简单, 重复执行相应的串操作直到计数寄存器为0为止
可以使用的指令为:
上头除CMPS和SCAS, 因为这两个需要利用其判比条件
(这俩是REPE/REPZ, REPNE/REPNZ专用)
使用CX(16bit)或ECX(32bit)作为计数器
具体的选用根据地址长度(16bit/32bit)而定(自动判断)
具体执行的操作:
while(true){
if countReg==0
break;
else
countReg-=1;
执行后面的串处理指令;
}
MOVS:串传送指令
用于将已存在的串传送到指定位置, 类似于MOV, 不过可以联用REP, 更加方便
MOVS DST, SRC
MOVSB ;byte
MOVSW ;word
MOVSD ;double
后三种格式明确指明了操作的字节长度, 但第一种必须在操作数中向编译器指明
不同的字节长度对应了SRC与DST在每次操作后的偏移量: 字节±1, 字±2, 以此类推
其中, 加减操作的决定有FLAGS的DF方向标志位决定(具体操作看上头)
MOVS的寻址方式是编译器默认的(使用两个隐含的寻址方式, 具体看上头), 两个操作数只是提供给编译器做类型检查用
不改变FLAGS标志寄存器
STOS:存入串指令
将一个值连续的存储到一段存储器中, 即相当于在一个数组中存入相同的值
常用STOS指令在初始化某一缓冲区
;根据DST自动判定类型以选用合适的长度
STOS DST
;指定字节长度
STOSB
STOSW
STOSD
该指令把AL、AX或EAX的内容存入由目的变址寄存器指向的附加段的某单元中,并根据DF的值及数据类型修改目的变址寄存器的内容(自动偏移)
不改变FLAGS标志寄存器
LODS从串取指令
从指定数据串中连续读取内容, 通常不与REP联用, 即很少循环这个指令
LODS SRC
LODSB
LODSW
LODSD
该指令把由SRC指向的内容送到AL、AX或EAX中,并根据方向标志和数据类型修改源变址寄存器的内容(自动偏移)
指令允许使用段跨越前缀来指定非数据
不改变FLAGS标志寄存器
REPE/REPZ & REPNE/REPNZ:
当相等/为零时重复串操作
当不相等/不为零时重复串操作
配合CMPS和SCAS指令使用, 不同的指令对应不同的跳出条件
具体执行的操作:
while(true){
if CX==0或ZF==1
break;
else
CX--;
执行后头的串指令;
}
即: 当比较结束时, 只有两种情况:
CX=0, ZF=1
CX!=0, ZF=0 表明字符串
CMPS:串比较指令
CMPS SRC, DST
CMPSB
CMPSW
CMPSD
指令把由SRC指向的数据段中的一个字节、字或双字与由DST所指向的附加段中的一个字节、字或双字相减,但不保存结果,只根据结果设置FLAGS:
指令对条件码的影响:
CF=1 二进制减法运算中最高有效位向高位有借位(被减数小于减数,不够减的情况)
CF=0 二进制减法运算中最高有效为向高位无借位(被减数〉=减数,够减的情况)
OF=1 两数符号相反(正数-负数,或负数-正数),而结果符号与减数相同。
OF=0 同符号数相减时,或不同符号数相减,其结果符号与减数不同
与转移地址有关的寻址方式
本部分主要是使用JMP等转移指令指令修改IP寄存器, 指向指定语句的地址, 使程序跳转到指定的语句执行
前排提醒: 几种寻址方式提供的都只有一个OPR, 真正区别的是前头的操作符
段内直接寻址
这个用的多,程序简单, 无需使用其他的寻址方式
特征: 跳转的指针只有一个位移量, EA=IP+位移量
操作符NEAR PTR, SHORT
执行后: IP=IP+位移量
;格式:
JMP 位移量
;例如:
JMP NEAR PTR PTOGIA
JMP SHORT QUEST
;其中PROGIA和QUEST都是转向的符号地址
段内间接寻址
特征: EA=REG或MEM (某一寄存器或储存单元的内容)
操作符WORD PTR
JMP BX
JMP WORD PTR[BP+TABLE]
段间直接寻址
特征: 提供了一个转向段地址, 和一个偏移地址
操作符FAR PTR
执行后: CS= 偏移地址的段地址(转向段地址), IP=偏移地址
JMP FAR PTR NEXTROUTINT
段间间接寻址
特征: 用储存器中的两个相继字(组成Double Word), 来取代IP和CS的内容
操作符DWORD PTR
其中: IP=前两个字节, CS=后两个字节
其中, 储存单元的寻址可以使用处立即数和直接寄存器之外的所有方式
JMP DWORD PTR[INTERS+BX]
JMP DWORD PTR[EDI]
控制转移指令
各种转移指令, 结合指令寻址方式, 在各个语句之间进行跳转
无条件跳转指令
相当于C++中的goto, 100%跳转
通用格式:
JMP (跳转指令)跳转地址OPR
其中OPR通常直接使用符号地址(即相当于标识符)
而转移方式的名称都与对应的指令寻址方式相关, 直接可以想到
段内直接短转移
采用段内直接寻址方式
JMP SHORT OPR
执行的操作:(IP)<–(IP)+8位位移量
段内直接近转移
采用段内直接寻址方式
这里与短转移的不同主要是: 操作数的地址为16位以上
JMP NEAR PTR OPR
执行的操作:(IP)<–(IP)+16位位移量
段内间接近转移
采用段内间接寻址方式
之前使用的都是相当于立即数的符号地址, 而这里使用的是正经的寻址方式
其将OPR的地址传给IP
JMP FAR PTR OPR
执行的操作:(IP)<–(EA)
段间直接远转移
采用段间直接寻址方式
这里使用直接寻址的方式, OPR通常直接使用符号地址
JMP DWORD PTR OPR
(IP)<–OPR的段内偏移地址
(CS)<–OPR所在段的段地址
段间间接远转移
这里与上头的类似, 使用处立即数和寄存器之外的任何寻址方式
JMP DWORD PTR OPR
执行的操作:
(IP)<–(EA)
(CS)<–(EA+2)
条件转移指令
注意与if相反, 条件转移指令满足条件跳转
下头东西多, 所有冗余的记其中一个即可(达到应用的地步), 其他的保证看得懂就好
1、根据单个条件标志的设置情况转移
这组包括10种指令。它们一般适用于测试某一次运算的结果并根据其不同特征产生程序分支作不同处理的情况
具体判定条件:
JZ(或JE)(Gump if zcro,or cqual)结果为零(或相等)则转移。
格式:JZ(或JE)OPR
测试条件:ZF=1
JNZ(或 JNE)(Gump if not zero.or not equal)结果不为零(或不相等)则转移。
格式:JNZ(或JNE)OPR
测试条件:ZF=0
JS(jump if sign)结果为负则转移。
格式:JS OPR
测试条件:SF=1
JNS(jump if not sign)结果为正则转移。
格式:JNS OPR
测试条件:SF=0
JO(jump if overflow)溢出则转移。
格式:JO OPR
测试条件:OF=1
JNO(Gump if not overflow)不溢出则转移。
格式:JNO OPR
测试条件:OF=0
JP(或JPE)(jump if parity,or parity even)奇偶位为1则转移。
格式:JP(或JPE)OPR
测试条件:PF=1
JNP(或JPO)(jump if not parity,or parity odd)奇偶位为0则转移。
格式:JNP(或JPO)OPR
测试条件:PF=0
JB(或JNAE,或JC)(Gump if below,or not above or equal,or carry)低于,或者不高于或等于,或进位为1则转移。
格式:JB(或JNAE,或JC)OPR
测试条件:CF=1
JNB(或JAE,或INC)Xiump if not blow,or above or equal,or not cary)不低于,或者高于或等于,或进位为零则转移。
格式:JNB(或JAE,或JNC)OPR
测试条件: CF=0
最后两种指令在这一组指令中可以只看作JC和JNC,它们只用CF的值来判别是否
转移。
- 比较两个无符号数,并根据比较的结果转移
本部分的几个核心字母(与无符号数有区分):
B: below
A: above
E: equal
快速浏览大表:
指令 | 作用 |
---|---|
JB(或JNAE,或JC) | 低于,或者不高于或等于,或进位位为1则转移 |
JNB(或JAE,或JNC) | 不低于,或者高于或等于,或进位位为0则转移 |
JBE(或JNA)(jump if below or equal,or not above) | 低于或等于,或不高于则转移。测试条件:CFVZF=1 |
JNBE(或JA)(jump if not below or equal,or above) | 不低于或等于,或高于则转移 测试条件:CFVZF=0 |
以上JB、JNB两种指令与(1)组指令中的9和10两种完全相同。 |
- 比较两个带符号数,并根据比较结果转移
本部分的几个核心字母(与无符号数有区分):
L: less
G: greater
E: equal
指令 | 作用 | 格式 | 测试条件 |
---|---|---|---|
JL(或JNGE)(jump if less,or not greater or equal) | 小于,或者不大于或等于则转移 | JL(或JNGE)OPR | SFVOF=1 |
JNL(或JGE)(jump if not less,or greater or equal) | 不小于,或者大于或等于则转移 | JNL(或JGE)OPR | SFVOF=0 |
JLE(或JNG)(jump if less or equal,or not greater) | 小于或等于,或者不大于则转移 | JLE(或JNG)OPR | (SF∀OF)∨ZF=1 |
JNLE(或 JG)(Gump if not less or cqual,or greater) | 不小于等于, 或者大于则转移。 | JNLE(或JG)OPR | (SFVOF)VZF=0 |
- 测试CX或ECX的值为0则转移指令
这一块就这两个指令
JCXZ(jump if CX register is zero)CX寄存器的内容为零则转移。
格式: JCXZ OPR
测试条件:(CX)=0
JECXZ (jump if ECX register is zero) ECX寄存器的内容为零则转移
格式:JECXZ OPR
测试条件: (ECX)=0
循环指令:
此为更为简单的实现循环的指令:
LOOP(loop)循环
LOOPZ/LOOPE(loop while zero,or equal)当为零或相等时循环
LOOPNZ/LOOPNE(loop while nonzero,or not equal)当不为零或不相等时循环
格式;
LOOP OPR
LOOPZ(或LOOPE)OPR
LOOPNZ(或LOOPNE)OPR
LOOP循环指令。
格式:LOOP OPR
测试条件:(Count Reg)=0
LOOPZ / LOOPE 当为零或相等时循环指令。
格式:LOOPZ(或LOOPE)OPR
测试条件:ZF=1且(Count Reg)=0
LOOPNZ / LOOPNE 当不为零或不相等时循环指令。
格式:LOOPNZ(或LOOPNE)OPR
测试条件:ZF=0且(Count Reg)=0
LOOP: 在实地址模式下使用CX寄存器, 保护模式下使用ECX寄存器
LOOPW:在任何模式下都会使用CX寄存器作为计数器
LOOPD:在任何模式下都会使用ECX寄存器作为计数器
子程序
子程序相当于函数, 集成分割一定的功能
(子程序的定义在下头的PROC伪操作中)
主要使用两个指令:
CALL DST
RET
RET EXP
CALL & REP 指令的功能与JMP无条件跳转相似, 但其会操作堆栈来修改IP寄存器以实现不同的地址转移与返回, 可以理解为:
call指令,相当于
CALL ADDRESS
;相当于:
push IP ;具体应该说是call下面一行的ip
jmp ADDRESS
;*********************
ret ;指令,相当于
pop IP
ret num ;带参数的ret, n为立即数
;相当于:
POP IP
ADD SP, 2
ADD SP, num ;最后参数的作用!
CALL指令具体执行的操作对程序员不可见
几种不同的CALL具体过程如下:
<1> ret
用栈中的数据改动IP的地址,从而实现近转移
( ip ) = ( (ss)*16+ sp )
( sp ) =( sp ) + 2
相当于pop ip
<2>retf
用栈中的数据来改动CS以及IP的值,实现段间转移
( ip ) = ( (ss)*16+ sp )
( sp ) =( sp ) + 2
( cs ) = ( (ss)*16+ sp )
( sp ) =( sp ) + 2
相当于
Pop ip
Pop cs
<3> call xxx(行号)
先把当前IP压栈,然后跳转,相当于实现近转移
( sp ) = ( sp ) – 2
( (ss)*16+ sp ) = ( ip )
( ip ) = ( ip ) + 16位位移
相当于:
Push ip
Jmp near ptr xxx(行号)
<4>call far ptr
把CS。IP压栈处理,然后实现跳转,相当于段间转移。远转移
( sp ) = ( sp ) – 2
( (ss)*16+ sp ) = ( cs )
( sp ) = ( sp ) – 2
( (ss)*16+ sp ) = ( ip )
(cs) = 当前行号段地址
(ip) = 当前行号偏移地址
相当于:
Push cs
Push ip
Jmp far ptr xxx
<5> call reg(16bit)
跳转到16位寄存器上中存储的地址
( sp ) = (sp) – 2
( (SS)*16 + (sp) ) = (IP)
(IP) = ( 16bit Reg )
相当于:
Push IP
Jmp 16bit Reg
<6> call word ptr 内存单元地址
相当于
Push IP
Jmp word ptr 内存单元地址
如:call word ptr ds:[0]
<7> call dword ptr 内存单元地址
相当于
Push cs
Push ip
Jmp dword ptr 内存单元地址
比如:jmp dword ptr DS:[0];
CALL指令结合之前的几种转移地址的寻址方式使用
外部设备操作:
外部设备与主机(CPU和存储器, 键盘, 显示器等)的通信是通过外设接口进行的,每个接口由一组寄存器组成:
– 数据寄存器:存放要在外设和主机间传送的数据。
– 状态寄存器:保存外部设备或接口的状态信息。
– 命令寄存器: CPU给外设或接口的控制命令通过此寄存器送给外部设备。
实际上外部设备的操作就是操作这些个寄存器
DOS功能调用:
DOS功能调用是DOS操作系统为用户提供的许多功能子程序,可以实现输入输出(相当于标准输入输出)
基本的调用方式是:
设置调用所需的参数(其实就是寄存器);
功能号送AH寄存器;
用INT 21H来调用(INT为interrupt, CPU中断)
键盘输入:
功能号01
功能:从键盘输入一个字符
入口参数:无
出口参数:键入字符的ASCII码存AL寄存器
实例:
MOV AH , 01
INT 21H
显示输出:
功能号02
功能:将一个字符送显示器显示
入口参数:显示字符的ASCII码存DL寄存器
出口参数:无
实例:
MOV DL , ’A’
MOV AH , 02
INT 21H
字符串输入:
功能号0A
功能:从键盘输入一个字符串
入口参数:存放输入字符串的数据区地址存DX寄存器
出口参数:字符串的长度和每个字符的ASCII码存入数据区的相应位置
例如:
;数据段定义字符串缓冲区如下(都是伪操作):
maxlen db 16 ;最大字符数(包括\n)
actlen db ?
string db 16 dup(?)
;输入字符串的指令如下:
lea dx, maxlen ;注意送DX的是maxlen, 不是string (而读取时是string)
mov ah, 0ah
int 21h
注意:
最后存入的字符串中还有其他东西:
第1个字节: 最大字符数,由程序员给出(如上, 通常和字符串内存申请定义在一起)
第2个字节: 实际输入字符数,自动填入
第3个字节开始: 字符串按字节存入缓冲区
最后结束字符串的回车0DH也要占用1个字节
在这里插入图片描述
所以在这之后如果需要读取字符串, 则需要跳过前两个字节
字符串输出:
功能号09
功能:将一个字符串送显示器显示
入口参数:显示字符串的首地址存入DX寄存器
出口参数:无
例如:
;数据段定义字符串如下:
string db ‘hello,world!$’ ;最后的$相当于C原生字符串最后的\0, 起到终止符的作用
;输出字符串的指令如下:
lea dx, string ;注意这里就直接是string了
mov ah, 09h
int 21h
伪指令
汇编中の几种指令:
指令(之前Ch.3中的都是这玩意):
在程序运行期间由计算机执行
伪操作(相当于预处理指令):
又称伪指令,在汇编程序对源程序汇编期间由汇编程序处理的操作
宏指令(就是C++中的宏):
在汇编程序对源程序汇编期间由汇编程序展开宏操作
指令集的选择:
默认为只适用8086指令集
段定义伪操作:
段在汇编中的作用相当于C++中的函数, 即各个段互相组合拼凑成了.asm程序
格式:
segment_name SEGMENT
. . .
segment_name ENDS
段定义中的内容:
对于数据段、附加段和堆栈段来说,一般是存储单元的定义、分配伪操作;
对于代码段则是指令及伪操
ASSUME伪操作:
明确段和段寄存器的关系
ASSUME伪操作格式:
ASSUME segreg: seg_name, …
段寄存器名(segreg)必须是CS、 DS、 ES或SS
段名(seg_name)必须是由SEGMENT定义的段中的段名
例如:
data segment
. . .
data ends
code segment
assume cs: code , ds: data
start:
mov ax , data
mov ds , ax
. . .
mov ax, 4c00h ;这两条指令是DOS的返回指令, 使用完DOS后通常(基本)需要返回程序
int 21h
code ends
end start
data_byte db 10,3*20,10h,?
data word dw 100,100h,-5,?
message db 'HELLO'
;使用dup(即duplicate缩写)重复申请一定数量的内存(根据Mnemonics决定)
;dup具体的语法就是下头的:
array1 db 2 dup(0,1,2,2)
array2 db 100 dup(?)
;注意dup操作可以嵌套
array3db 100 dup(0,2 dup(1,2),0,3)
注意: variable符号地址的使用:
只能将符号地址传递到对应类型的寄存器中:
oper1 db 1 , 2
oper2 dw 1234h , 5678h
;如这两个都是错误的
mov ax , oper1+1 ;为8bit
mov al , oper2 ;为16bit
如果需要强制使用这种方法, 需要使用ptr类型限定符
PTR强制转换:
汇编里面 ptr 用来临时指定类型
可以理解为,ptr是临时的类型转换,相当于C语言中的强制类型转换
格式: type PTR variable
其中, type可以是BYTE 、 WORD 、 DWORD 、FWORD 、 QWORD、 TBYTE
其作用相当于C++中的*解引用
具体是在从给定的地址指向的储存空间中取出数据时,指明按何种类型取出
详细解答:
mov cl,byte ptr [eax]
;[eax]是将eax中的值解释为一个地址
;而后byte ptr是将这个地址指向的储存空间中储存的值解释为byte
;而后MOV这个byte类型的值到cl中
mov cx, word ptr [eax]
;word ptr是将这个地址指向的储存空间中储存的值解释为word
mov ecx, dword ptr [eax]
;DWORD ptr是将这个地址指向的储存空间中储存的值解释为Double Word
例如:
oper1 db 1 , 2
oper2 dw 1234h , 5678h
. . .
mov ax , word ptr oper1+1
;(ax)=3402h 取的是oper1的第二个元素, 并将其解释为word
mov al , byte ptr oper2
;(al)=34h 取得是oper2的第一个元素, 并将其解释为byte
label伪操作
用于定义变量variable的类型属性,
前头的ptr限定符只是一次性的临时解释, label相当于是永久改变了标签的属性
格式: name label type
例如:
word_array dw 50 dup(?)
word_array label byte ;;此后word_array相当于db 100dup(?)
$伪操作:
$表示的是地址计数器的值
在汇编程序MASM对源程序汇编的过程中,使用地址计数器( location counter )保存当前正在汇编的指令的偏移地址(相当于自动控制地址计数器)
具体使用:
array dw 1 , 2 , $+4 ,3 ,4 , $+4
效果如下:
0078H的值为0078(当前地址计数器的值)+4
ORG伪操作:
用来设置当前地址计数器的值
如不希望连续分配内存时可以使用这个玩意:
格式: ORG constant expression
例如:
VECTORS SEGMENT
ORG 10 ;调整地址计数器的值为10D, 即0AH
VECT1 DW 47A5H ;偏移地址为0AH
ORG 20 ;调整地址计数器的值为20D, 即14H
VECT2 DW 0C596H ;偏移地址为14H
. . .
VECTORS ENDS
OFFSET:
用于汇编程序回送变量或标号的偏移地址值
格式: OFFSET variable或label
例如:
MOV BX , OFFSET oper1
;该指令与下面指令是等价的:
LEA BX , oper1
上机实验
Debug
使用它,可以查看 CPU 各种寄存器中的内容、内存的情况和在机器码级跟踪程序的运行。
Debug 的常用命令 | 功能 |
---|---|
R 命令 | 查看、改变 CPU 寄存器的内容; |
D 命令 | 显示寄存器中的内容; |
E 命令 | 改写内存中的内容; |
U 命令 | 将内存中的机器指令翻译成汇编指令; |
T 命令 | 执行一条机器指令;(这个用得多) |
A 命令 | 以汇编指令的格式在内存中写入一条机器指令 |
汇编程序基础模板
;******************
DATAS SEGMENT
;数据段, 用于变量的定义
DATAS ENDS
;******************
STACKS SEGMENT
;堆栈段, 当前程序暂时没有用到
STACKS ENDS
;******************
CODES SEGMENT
ASSUME CS:CODES,DS:DATAS,SS:STACKS ;assume只起声明的作用, 并不真正操作
START:
MOV AX,DATAS ;将DATAS送ds寄存器, 才有用
MOV DS,AX
;******************
;主程序编写部分
;******************
MOV AX, 4C00H ;相当于main函数最后的return
INT 21H ;只有在程序需要结束时才使用, 平时使用int 21时并不需要
CODES ENDS
END START ;标准格式
循环结构
循环程序由三部分组成:
设置循环的初始条件
循环体
循环控制部分:
计数控制、特征值控制、地址边界控制
单循环:
循环输出BX内储存的值:
MOV CH, 4 ;循环控制, 相当于int i
rotate: MOV CL, 4 ;每次移位次数为4
ROL BX, CL ;移位指令
MOV AL, BL ;将移位后BX的第8位赋给AL
AND AL, 0FH ;将AH置零, 由于计算机最低只能进行字节操作, 这里需要4bit操作, 所以这么整
ADD AL, 30H ;转换为ASCII码
CMP AL, 3AH ;将小写a~f转化为大写A~F
JL printit ;跳转指令
ADD AL, 7 ;如果为a~f, 则转化为大写
printit:
MOV DL, AL ;显示输出的入口参数
MOV AH, 2 ;显示输出的功能号
INT 21H ;CPU中断
DEC CH ;循环控制
JNZ rotate ;跳回程序, 完成循环
把Y中1的个数存入COUNT单元中
;***************
DATAS SEGMENT
number dw 78a4h ;这玩意就是Y
address dw number
count dw ?
DATAS ENDS
;***************
CODES SEGMENT
ASSUME CS:CODES, DS:DATAS
START:
MOV AX, DATAS
MOV DS, AX
;***************
MOV CX, 0 ;重置CX,用于后头的计数
MOV BX, address ;将address中的值传给BX
MOV BX, [BX] ;将BX中的值指向的内存空间中的值传递给BX
;执行后(BX)=78a4H
REPEAT1:
TEST BX, 0FFFFH ;AND一下, 主要是刷新FLAGS, 用于后头的判定
JZ EXIT1 ;检测BX中的1是否全都移出了, 满足则代表循环结束
JNS SHIFT ;通过符号位判定最高位是否为1
INC CX ;如果为1, 则CX++
SHIFT:
SHL BX, 1 ;逻辑左移1位
JMP REPEAT1 ;建立循环
EXIT1:
MOV CX, count ;最后将CX中的结果装入count中
;***************
MOV AX, 4C00H
INT 21H
CODES ENDS
END START
经典例题
插入排序
;***************
DATAS SEGMENT
X DW ? ;-1
ARRAY_HEAD DW 3,5,15,23,37,49,52,65,78,99
ARRAY_END DW 105
N DW 32
DATAS ENDS
;***************
CODES SEGMENT
ASSUME CS:CODES, DS:DATAS
START:
MOV AX, DATAS
MOV DS, AX ;基本操作
;***************
MOV AX,N ;
MOV X,-1 ;执行后(X)=0FFFFH
MOV SI,0 ;初始化下标
COMPARE:
CMP ARRAY_END[SI],AX ;将array中的数从后往前与N比较
JLE INSERT ;如果array[si]<N, 则插入N
MOV BX,ARRAY_END[SI] ;否则将array[si]后移动一位
MOV ARRAY_END[SI+2],BX
SUB SI,2 ;改变下标, array是DW,所以SI-=2
JMP COMPARE ;跳转达成循环
INSERT:
MOV ARRAY_END[SI+2],AX ;在已经空出的位置插入N
;****************
MOV AX, 4C00H
INT 21H
CODES ENDS
END START
多重循环:
冒泡排序:
;***************
DATAS SEGMENT
A DW 5, 8, 16, 32, 84
N DW 5
DATAS ENDS
;***************
CODES SEGMENT
ASSUME CS:CODES, DS:DATAS
;***************
START:
MOV AX, DATAS
MOV DS, AX ;基本操作
;***************
MOV CX,N ;将数组元素个数存放到CX
DEC CX ;CX--
LOOPOUT: ;外层循环
MOV DI,CX ;在DI中暂存外层循环数
MOV BX,0 ;BX作为数组下标, 初始为0
LOOPIN: ;内层循环
MOV AX,A[BX] ;将数组的A[I]装入AX
CMP AX,A[BX+2] ;A[I]与A[I+1]比较
JGE CONTINUE ;如果A[I]>=A[I+1]则跳过交换的步骤
XCHG AX,A[BX+2] ;交换数组两个元素的值
MOV A[BX],AX ;交换数组两个元素的值
CONTINUE:
ADD BX,2 ;循环控制: 下标++
LOOP LOOPIN ;每次LOOP都使CX--
MOV CX,DI ;重置CX, 即外层循环数
LOOP LOOPOUT ;每次LOOP都使CX--
;***************
MOV AX, 4C00H
INT 21H
CODES ENDS
END START
汇编附加段
附加段用于数据的定义
但是实际上在8086中, 数据定义在附加段和数据段的作用是相同的, 只需要在使用时整对段地址就好, 即
;将对应的段地址存到段地址寄存器
MOV AX, DATA
MOV DS, AX
MOV AX, EXTRA
MOV ES, AX
但通常在附加段中保存大数据, 如串和数组
子程序
NEAR:
提供段内调用
当子程序与调用点定义在同一个段中时使用
FAR:
提供段间调用
当子程序与调用点定义在不同的段中时使用
但是当定义在同一个段中时也可以使用
所以这是一个通用的选择
注意这两个控制的都是RET指令的返回方式
关于汇编中的start:
start为程序的入口, 程序加载到内存之后CS:IP会指向这个标号, 从START指向的指令开始运行
同时, start只是一个标号, 并没有强制的语法特定, 使用其他也可,如改成hello也是可以的
子程序的定义位置:
由于上头的start程序入口的问题, 所以子程序不能定义在start & END start中
可以定义的位置有两个:
与start & END start定义在同一代码段中, 但是需要定义在其之前或其之后
(类似于main与其他函数的关系)
可以定义在其他代码段中
子程序的传参&返回值:
寄存器传参:
类似于形参传递.寄存器法
寄存器法就是将入口参数和出口参数存放在约定的寄存器中。
优点:数据传递书读快、编程较方便、节省内存单元。
缺点:当传递参数过多时候,由于寄存器个数有限,及寄存器的频繁使用,将导致寄存器不足。
适用:参数较少的子程序
储存单元传参:
类似于函数使用全局变量
把入口参数和出口参数都放在既定的储存单元中
优点:不占用寄存器、参数个数任意、每个子程序要处理的数据和送出的结构都有独立的存储单元
缺点:但用一定数量的存储单元,增加编程中对变量定义的难度
堆栈传参:
类似于传递变量指针
堆栈法是利用堆栈来传递参数
通常将变量的地址保存到堆栈中
优点:参数不占用寄存器,和存储单元。参数存放在公共堆栈区,处理完后客恢复。参数个数一般不限
缺点:由于参数和子程序混杂在一起,存取参数时候必须小心计算它在堆栈中的位置。要注意断点的保存和恢复
地址表传参:
类似于传递变量指针
这种方法是把参数组成的一张参数表放在某个存储区中,然后只要主程序和子程序约定好这个存储区的首地址和存放的内容,在主程序中将参数传递给地址表,在子程序中根据地址表给定的参数就可以完成操作。
10进制到16进制的转换程序:
DECIHEX SEGMENT
ASSUME CS: DECIHEX
;******************
DECIBIN PROC NEAR ;负责从键盘读取10进制数值
MOV BX, 00H
NEWCHAR:
MOV AH, 01H ;DOS调用,键盘输入值
INT 21H
SUB AL, 30H ;将读取的ASCII转化为数值
JL EXIT ;如果小于30H,则代表不是0~9,直接退出
CMP AL, 09H ;与09H比较
JG EXIT ;如果大于09H,则代表输入的不是0~9,直接退出
CBW ;将AL拓展到AX
XCHG AX, BX ;将AX中新输入的数与BX中原有的结果交换
MOV CX, 10 ;将乘数存入CX预备
MUL CX ;将AX中原有的数*10
XCHG AX, BX ;将*10后的结果与BX交换,
ADD BX, AX ;将新输入的值加到原有的值上
JMP NEWCHAR ;输入新字符, 达成循环
EXIT:
RET
DECIBIN ENDP
;******************
BINIHEX PROC NEAR ;负责将10进制转化为16进制并输出
MOV CH, 4
ROTATE:
MOV CL, 4 ;设定ROL的移位次数为4
ROL BX, CL ;将BX中的数循环左移4位, MSB的4位出现在LSB的4位中
MOV AL, BL ;将BL转移到AL中用于后头计算
AND AL, 0FH ;掩码覆盖MSB4位
ADD AL, 30H ;将数值转化为相应的ASCII码
CMP AL, 3AH ;检测是否大于9
JL PRINTIT ;如果大于9,则为A~F
ADD AL, 7 ;转化为A~F
PRINTIT:
MOV DL, AL ;将AL输出
MOV AH, 02H
INT 21H
DEC CH ;循环控制
JNZ ROTATE ;跳回达成循环, 如果CH==0,则退出循环
RET
BINIHEX ENDP
;******************
CRLF PROC NEAR ;格式控制,输出回车与换行</br>
MOV DL, 0DH
MOV AH, 02H
INT 21H
MOV DL, 0AH
MOV AH, 02H
INT 21H
RET
CRLF ENDP
;******************
MAIN PROC FAR ;MAIN主程序
PUSH DS ;由于是DOS调用Main,所以需要保存寄存器
SUB AX, AX
PUSH AX
CALL DECIBIN ;执行几个子程序
CALL CRLF
CALL BINIHEX
CALL CRLF
RET
MAIN ENDP
;******************
DECIHEX ENDS
END MAIN ;程序结尾必有的东西
输入16进制输出10进制:
程序框架:
本程序在main中调用各个子程序, 而main与几个子程序定义在同一个代码段中, 所以其他子程序为near
DISPLAY EQU 2H
KEY_IN EQU 1H
DOSCALL EQU 21H
;******************
HEXIDEC SEGMENT
;******************
MAIN PROC FAR ;MAIN主程序
ASSUME CS: HEXIDEC
START:
PUSH DS ;由于是DOS调用Main,所以需要保存寄存器
SUB AX, AX
PUSH AX
CALL HEXIBIN ;执行几个子程序
CALL CRLF
CALL BINIDEC
CALL CRLF
JMP MAIN ;跳回MAIN达成死循环
RET
MAIN ENDP
;*******************
HEXIBIN PROC NEAR;输入10进制ASCII字符并转化为16进制数值储存
MOV BX, 0 ;初始化BX
NEWCHAR:
MOV AH, KEY_IN ;键盘输入
INT DOSCALL
;0~9 ?
SUB AL, 30H ;判定输入的值是否在数字之前
JL EXIT ;ASCII在数字之前, 直接结束循环
CMP AL, 10D ;判定输入的值是否在数字之后
JL ADD_TO ;如果小于10,则是0~9
;a~f ?
SUB AL, 27H ;如果输入大于9,则跳转到字符a开始
CMP AL, 0AH ;边界值a
JL EXIT ;ASCII在a~f之前, 直接结束循环
CMP AL, 10H ;边界值f
JGE EXIT ;ASCII在a~f之后, 直接结束循环
;(BX)<-(BX)*16+(AX)
ADD_TO:
MOV CL, 4 ;设置SHL移位次数
SHL BX, CL ;逻辑左移
MOV AH, 0 ;AX高位置零
ADD BX, AX ;将BX左移后空出的4位填入新输入的数值
;完成循环
JMP NEWCHAR ;输入新字符,直到最后一个不满足条件才跳出
EXIT:
RET
HEXIBIN ENDP
;*********************
;将BX中的数转换为10进制并输出
;按位取并转化为ACSII输出
BINIDEC PROC NEAR
MOV CX, 10000D
CALL DEC_DIV
MOV CX, 1000D
CALL DEC_DIV
MOV CX, 100D
CALL DEC_DIV
MOV CX, 10D
CALL DEC_DIV
MOV CX, 1D
CALL DEC_DIV
RET
BINIDEC ENDP
;****************
;****************
;被除数 BX
;除数 CX
;商 AX
;余数 DX
DEC_DIV PROC NEAR
MOV AX, BX ;将被除数拷贝到AX中执行运算
MOV DX, 0 ;DX初始化
DIV CX ;除以CX,按位取出BX中的10进制数
MOV BX, DX ;将余数丢到BX中重新作为除数
MOV DL, AL ;将商丢到DL中
ADD DL, 30H ;转化为ASCII数字
MOV AH, DISPLAY ;召唤DOS显示
INT DOSCALL
RET
DEC_DIV ENDP
;*****************
CRLF PROC NEAR ;格式控制,输出回车与换行</br>
MOV DL, 0DH
MOV AH, 02H
INT 21H
MOV DL, 0AH
MOV AH, 02H
INT 21H
RET
CRLF ENDP
;******************
HEXIDEC ENDS
;******************
END START