王爽汇编全书知识点大纲
第一章 基础知识
-
机器语言
机器语言是机器指令的集合,电子计算机的机器指令是一系列二进制数字.计算机将之转换为一系列高低电平脉冲信号来驱动硬件工作的.
-
汇编语言的产生
由于机器语言指令都是由01组成,难以编写,记忆和维护程序.所以汇编语言为了解决这一问题产生.汇编语言又称为机器语言的助记符
-
汇编语言的组成
汇编指令:有对应的机器码.机器码的助记符
伪指令:没有对应的机器码,由编译器执行,计算机并不执行.
其他符号:如+,-,*,/等,由编译器识别,没有对应的机器码
-
存储器
相当于大脑,程序指令和数据在存储器中都是以二进制数据形式存放,没有什么区别.且内存是动态存储数据的,程序或文件从磁盘加载到内存才能由cpu直接读取.断电后,内存中的数据就消失了.磁盘才是永久存储数据的地方.
存储单元从0开始编号.能存储一个8位二进制数的单元,称为存储单元.为一个byte.大小,单位B. 位的单位为bit.向上为kb,mb,GB,TB,PB
-
cpu对存储器的读写
必要的信息交互
(1). 存储单元的地址(地址信息),地址总线(cpu要从存储器中读取数据,首先要要确认,某个存储单元的地址.)
(2).器件的选择,读或写的命令(控制信息)控制总线(另外计算器中不只有存储器一种器件,其它器件,也有存储器.要确定从什么器件读取.)
(3).读或写的数据(数据信息),数据总线.
总线与信息传送 : 电子计算机能处理,传输的信息都是电信号,电信号当然通过导线传送,也就是总线(数根导线的集合).
读取例子:Mov Ax,[3]
CPU通过地址线将地址信息3发出cpu通过控制总线发出读命令,并选中存储器芯片,并通知他,将要从中取数据,存储器将3号单元中的数据8通过数据线送入cpu
-
三大总线
地址总线
地址总线:8根,表示能寻址0-1023的内存单元.能寻址2^8个内存单元,代表了cpu的寻址范围
- 数据总线
假如数据总线有8根,那么每次只能传送1B数据.每次只能传送一个8位数.
- 控制总线
假如控制总线位有8根,那么有2^8种控制方式,控制总线的宽度决定了CPU对外部器件的控制能力.
-
内存地址空间
每种器件cpu都通过总线将其连接,cpu在操控它们的时候,都把它们当成是内存来对待.(正如linux把每个硬件都当成文件来对待),操作显示器就是操作显存,显存就可以当文件一样进行读写操作.cpu操作硬件实际上就是操作硬件的内存或端口
0-7fffh的32kb空间为主随机存储器的地址空间.地址8000h-9fffh的8kb空间为显存地址空间A000h~ffffh的24kb空间为各个rom的地址空间.(写入显存地址空间,就会显示在显示器上,写入只读rom地址空间,那么没有任何效果,我们基于一个计算机硬件系统编程的时候,必须知道这个系统中的内存地址空间分配情况).
BIOS有一个只读room,是硬件和操作系统的接口.
第二章 寄存器
-
cpu的基本结构
运算器:运算器进行信息处理
寄存器:寄存器进行信息存储
控制器:控制器控制各种器件进行工作
内部总线:用来连接cpu内部各种元器件的导线集合
寄存器:AX,BX,CX,DX, SI,DI,SP,BP,IP, CS,SS,DS,ES, PSW
-
通用寄存器
为了和上一代cpu兼容可以拆分16位通用寄存器,拆分为两个8位寄存器.从右向左,以0开始编号.
AX可拆分为AH和AL
BX可拆分为BH和BL
CX可拆分为CH和CL
DX可拆分为DH和DL
-
字的存储和数制表示
字在寄存器中的存储:高8位放在寄存器的高8位中,低8位放在寄存器的低8位中
字在内存中的存储:低位存储在低地址中,高位存储在高地址中.称为小尾存储.
数制的表示:源代码中默认数制是十进制,在数字后面添加h表示16进制,如果16进制最高位为字符,那么必须在前面添0, 如果数字后面加b后缀表示二进制数字. 特别注意,在debug中数制默认是16进制.
-
几条汇编指令
汇编指令默认不区分大小写
假设ax和bx中的值为8226h,add ax,bx执行后,相加的结果为1044ch,但是ax存储不下,最后ax中的值为044ch
假设ax中的值为00c5h,add al,85h执行结果为158h,al存储不下会设置标志位.千万不要认为高位进位会存储在ah中,这是错误的想法,最后ax中的结果为0058h.
下面的指令是错误指令:
mov ax,bl //两个寄存器尺寸不一致
mov bh,ax //两个寄存器尺寸大小不一致
mov al 20000 //20000在al中不能存储
add al 100H //100h超过al中存储的最大范围
- 计算2^4:
Mov ax,2
ADD AX,AX
ADD AX,AX
ADD AX,AX
-
8086物理地址
内存空间是一个线性空间.每一个内存单元在这个空间中都有唯一的地址,我们将这个唯一的地址称为物理地址.
16位机的含义:
运算器一次最多处理 16 位的数据
寄存器最大宽度为 16 位
寄存器和运算器之间通路为 16 位,即数据总线 16 条
物理地址的计算:为了用16位地址寻址更多的内存,8086采取用连个16位寄存器合成一个20位地址,即
段寄存器中的地址*16+16位偏移地址
一个段的起始地址必然是16的倍数
因为偏移寄存器只有16位大小,所以给定一个段,它最大能在当前段寻址64kb
寻址过程:段地址和偏移地址通过内部总线送入一个称为地址加法器的部件,地址加法器通过内部总线将20位物理地址送入输入输出控制电路.输入输出控制电路将20位物理地址送上地址总线.20位物理地址总线传送到存储器.
一个地址可以用不同的段形式表示.
-
CS和IP
段寄存器:CS,DS,ES,SS
CS位代码段寄存器,IP位代码偏移地址寄存器,cpu把任何时刻cs和ip指向的地址处的数据当做指令执行.
指令的读取和执行:
假设地址2000:0~2000:2中存储着指令mov ax,123h
初始状态cs=2000h,ip=0,将从内存2000h:0中读取指令执行
将CS和IP的值送上地址加法器合成一个20位的物理地址
地址加法器将物理地址送入输入输出电路
输入输出电路将物理地址20000h送上地址总线
从地址20000h中读取数据通过数据总线送入cpu
输入输出电路将指令mov ax,123h送入指令缓冲器
读取一条指令后ip的值自动增加,指向可以读取的下一条指令
执行控制器执行指令mov ax,123h
然后重复上面的步骤
8086cpu加电启动或复位后,cs和ip被设置为cs=ffffh,ip=0000h,cpu从内存FFFF0h单元中读取指令并执行.
修改cs和ip的指令:
通过修改cs和ip的指向,起到控制cpu执行流程和跳转等功能.在debug中可以通过mov指令修改cs的值,但是不能用mov修改ip的值
在cpu中程序员能够用指令读写的部件只有寄存器
mov指令不能用于设置cs:ip的值.原因很简单,因为没有这个功能.但是debug中可以用mov cs,xxx
想同时修改 CS,IP
jmp 1000:0 则 CS=1000H,IP=0000H //这条指令只在debug中有效
仅修改 IP 的内容
jmp 某一合法寄存器 //在源代码中也有效
-
debug调试指令
用 R(register) 命令查看或改变 cpu 寄存器内容
r ax/其它寄存器 功能:修改 ax 寄存器内容
直接r, 查看各个寄存器的值.
用 D(dump)命令查看内存中内容
d 1000:0 会显示10000:0地址开始的128个内存单元的内容
D 段地址:开始偏移地址 结束偏移地址 可以查看指定内存单元的内容例子如下:
D 1000:0 f (1000:0 ~ 1000:f内存单元的数据) 再次输入D会把后续的128个内存单元内容显示出来.
用 E (enter)命令改写内存中的内容
采用“e 起始段地址:偏移地址 数据 数据 数据..." e 1000:0 0 1 2 3 4 5 6 7 写机器码.这是一次性输入
用一个一个的输入方式,改写内存的内容 (按空格键陆续输入,敲回车结束输入.) 例如 e 1000:0,以向内存中写入字符 e 1000:0 1 'a' 2 'b' 3 'c' 写机器码和字符.
注:e命令写进去的机器码数值16进制形式,所以数字不能超过两位十六位数字,eg:111这是错误的(因为debug中默认是16进制数制).
用 U(unassemble) 查看内存中机器码含义,用 T 执行cs:ip指向的指令.
U 段地址:偏移地址
查看指定地址间的汇编指令:U 段地址:偏移地址 结束地址
用 A(assemble) 以汇编指令形式在内存中写入机器指令. A 段地址:偏移地址
用g(go) 命令跳到某指令,g 地址
eg:g 0012(表示从当前cs:ip指向的指令执行,一直到(ip) = 0012为止.) 用 t(trace) 命令单步执行 用 p (proceed)命令跳跃循环
q退出debug
dosbox中可以按alt+enter键实现全屏.
debug界面认识
- 习题讲解:下面的代码几次修改IP的值
mov ax,bx
sub ax,ax
jmp ax
答:一共4次,第一次:读取完mov ax,bx后.第二次:读取完sub ax,bx后.第三次:读取完jmp ax后.第四次:执行完jmp ax后.
第三章 寄存器内存访问
-
字在内存中的存储
低位字节存放在低地址单元中,高位字节存放在高地址单元中.
字单元:两个字节联合起来的16位.存储单元.起始地址为N的字单元简称为N地址字单元
-
DS和[ADRESS]访问内存单元
ds寄存器用来存放要访问的内存单元的段地址
8086硬件设计的问题,不能将立即数直接用mov指令传送到段寄存器中
在源代码中使用[idata]时,[idata]会被当成idata.
调试的时候,[idata]才会被翻译成内存单元.源代码中需要使用[bx/si/di..]等
表示内存单元.也可以使用段前缀, 段寄存器 : [idata]
-
字的传送
只要在mov指令中给出的寄存器是16位,就会对字进行操作.
也可以用尺寸指针指明要操作的内存单元大小,byte ptr [],word ptr [],dword ptr[]
-
mov/add/sub指令详解
mov指令有以下几种格式:
mov 寄存器,数据
mov 寄存器,寄存器
mov 寄存器,内存单元
mov 内存单元,寄存器
mov 段寄存器,寄存器
mov 寄存器,段寄存器 //这个是自己实验添加的
mov 段寄存器,内存单元//这个是自己实验添加的
mov 内存单元,段寄存器//这个是自己实验添加的
注意事项:
- 从这几种格式可以看出,操作数不能同时为段寄存器
- 也不能同时为内存操作数.
- 不能将立即数直接送入段寄存器.
- 立即数只能放在右边,如果此时左边操作数为内存单元,需要指明内存单元尺寸
- 操作数不能出现ip寄存器
- add指令有以下几种格式:
add 寄存器,数据
add 寄存器,寄存器
add 寄存器,内存单元
add 内存单元,寄存器
add byte/word/... ptr 内存单元,数据
注意事项:
- add指令中不能出现段寄存器操作数
- add中操作数不能同时为内存单元
- 第一个操作数必须是寄存器或者内存单元
- add指令中也不能出现ip寄存器
sub指令,格式和使用方式同add指令
-
数据段
我们可以将一组已知起始地址的内存单元定义为数据段,该段<= 64kb
然后将ds设置为起始地址,然后该地址开始的段就可以当做数据段使用
同理可以设置ss:sp的指向,来构造栈段
代码段是通过,
end 标号
指明起始地址(标号就是代码段开始处)-
栈
后进先出数据结构,最初栈指针指向高地址,越压栈,sp寄存器指针越往低地址移动.
通过ss:sp指定和初始化栈.
push和pop指令出栈和入栈
push 寄存器/内存单元/段寄存器 : sp=sp-2 mov (sp),(寄存器/内存单元/段寄存器)
pop 寄存器/内存单元/段寄存器: mov (寄存器/内存单元/段寄存器),(sp) sp=sp+2
注意事项:
操作数不能是ip寄存器,栈每次操作一个字
栈可以超界,且没有任何保护机制
栈环绕问题
如果将10000H~1FFFFH这段空间当做栈段,初始状态是空的,此时ss=1000h,sp=?
答:sp=0,因为此时会发生栈环绕问题,FFFF的底端无法寻址,所以就会循环跑到另一头0上面去.,此时压栈会发生,sp=0-2=0FFFEH
- 编程例子:
编程:将10000h~1000Fh这段空间当做栈,初始状态是空的,设置ax=001ah,bx=001bh,将ax,bx入栈,然后将ax,bx清零,从栈中恢复ax,bx的值.
解答:
mov ax,1000h
mov ss,ax
mov sp,10h
mov ax,001ah
mov bx,001bh
push ax
push bx
sub ax,ax
sub bx,bx
pop bx
pop ax
利用栈交换ax,bx内容
push ax
push bx
pop ax
pop bx
....
-
实验2
可以在调试命令中需要段地址的地方,使用段寄存器.例子如下:
d 1000:0 会将段地址1000送入ds中.
e ds:0 0 1 2 3 4
u cs:0 ;以汇编指令形式,显示当前代码
a ds:0
ss:sp的设置指令应该放在一起紧挨着,单步中断并不会中断,这两条 指令会连续执行,因为中断也设计压栈操作,那么假设如果在sp还未设置时,发生了中断,那么中断的压栈操作将会压入不正确的栈顶,紧接着又设置了新的sp值,那么程序就不会正确运行.
第4章 第一个程序
-
源程序到可执行程序
源程序文本文件-->编译链接--->生成可执行程序
可执行程序包含从源程序中翻译过来的机器码和数据,还有相关的描述信息(比如,程序有多大,要占用多少内存空间等)
操作系统根据可执行文件的描述信息,将可执行文件中的机器码和数据加载入内存,并进行相关的初始化,并进行相关的初始化(比如设置CS:IP指向第一条指令),然后cpu执行程序.
如果不用end指明程序入口,那么编译成程序时,cpu会自动将cs,ip指向改程序数据开头.如果指定了标号,那么cpu会将cs和ip指向标号地址处.
-
源程序
assume cs:code
code segment
mov ax,0123h
mov bx,0456h
add ax,bx
add ax,ax
mov ax, 4c00h
int 21h
code ends
end
xxx segment ....xxx ends伪指令是定义一个段.标号就是偏移地址,不需要用offset获取
end伪指令 : 表示结束一个汇编源程序的编译,end 后面可以接一个标号来指明可执行代码入口.
assume伪指令 : 它假设程序的某个段寄存器与某个段标号相关联.通过这种关联,在需要的情况下,编译程序可以将段寄存器和某一个具体的段想联系.
程序返回 : mov ax,4c00h int 21h程序返回.告诉运行本程序的程序,运行结束后的结果状态.ah=4c,传递中断子程序功能号,表示调用21中断的4c号子程序. Al=0表示返回值为0.如果不正确返回
程序就会运行不正常.
-
链接
当源程序很大的时候,我们可以将源程序分成多个程序,然后编译成多个obj文件.然后连接在一起,
程序中调用了某个库文件中子程序,需要将这个库文件和程序生成的目标文件连接到一起,生成一个可执行文件.
一个源程序编译后,得到了存有机器码的目标文件,目标文件中的有些内容还不能直接生成可执行文件.连接程序将这些内容处理为最终可执行信息.所在,在只有一个源程序文件,而又不需要调用某个库中的子程序的情况下,也必须用连接程序对目标文件进行处理,生成可执行文件.
-
dos程序的加载流程
dos中直接运行exe文件时,首先加载入内存,设置cs:ip的入口(end指定的标号处).
找到一段起始地址为SA:0000(即起始地址的偏移地址为0)的容量足够的空闲内存区
前256个字节中创建一个段前缀psp的数据区,dos利用psp来和被加载程序进行通信.ds首先初始化时,是指向psp数据区的.
程序放在SA+10H:0地址处,CS:IP指向这里
-
实验3
杂项知识:将程序加载入内存执行,cx存储的是程序的长度
第五章 [bx]和loop
-
[bx]
要完整描述一个内存单元需要两种信息:@内存单元地址@内存单元长度
表示段地址在ds中,bx中存放的是该段的偏移地址.
-
Loop指令
(cx)=(cx) - 1
判断cx中的值是否为零,非零继续循环,零则退出循环.
循环例子程序:计算2^12次方
assume cs:code
code segment
mov ax,2
mov cx,11
s:
add ax,ax
loop s
mov ax, 4c00h
int 21h
code ends
end
例子2:计算123*236
assume cs:code
code segment
mov ax,0
mov cx,123
s:
add ax,236
loop s
mov ax, 4c00h
int 21h
code ends
end
inc/dec 寄存器或
inc/dec (byte /word/..) ptr [内存单元]
自增或自减指令
-
杂项
在汇编源程序中,数据不能以字母开头需要在前面添加一个0.
在 dos 下,一般情况
0:200-0:2ff
空间中没有系统或其他程序的数据或代码遇到loop指令时,用p,可以跳过循环.遇到int 21是也可以用p命令结束程序
g 地址偏移 ,直接跳转到该地址处执行
-
loop和bx 的联合应用
计算ffff:0~ffff:b每个内存单元中的数据相加的和,结果存储在dx中
assume cs:code
code segment
mov ax,0ffffh
mov ds,ax
mov bx,0
mov dx,0
mov cx,12
mov ah,0
s:
mov al,[bx]
add dx,ax
inc bx
loop s
mov ax, 4c00h
int 21h
code ends
-
段前缀
可以在[idata]或[bx]前面加上cs,ds,es,ss等段前缀来指明段地址
cs/ds/es/ss : []
-
段前缀的使用
编程:将内存ffff:0ffff:b单元的数据复制到0:200020b中
assume cs:code
code segment
mov ax,0ffffh
mov ds,ax
mov ax,0020h
mov es,ax
mov bx,0
mov cx,6
s:
mov ax,[bx]
mov es:[bx],ax
add bx,2
loop s
mov ax, 4c00h
int 21h
code ends
end
-
实验4 bx和loop的使用
编程,向内存0:200~0:23f依次传送数据0-63(3fh)
assume cs:code
code segment
mov ax,0020h
mov ds,ax
mov bx,0
mov cx,64
s:
mov [bx],bl
inc bx
loop s
mov ax, 4c00h
int 21h
code ends
end
第六章 包含多个段的程序
-
在代码中使用数据
程序获得所需空间的方法有两种
是在程序加载的时候为程序分配
是在程序运行过程中向系统申请.我们只讨论前者.因为后者属于动态分配内存.这两种方式都是由操作系统分配.
还有就是在dos时代,可以直接将数据送入内存.当数据有很多时,这种方法存储明显不现实
db定义字节, dw定义字数据,dd定义双字,dq8字节,dt十字节
-
程序框架
assume cs:code
code segment
start:
程序代码
mov ax, 4c00h
int 21h
code ends
end start
end代表程序源代码结束,并指明可执行代码入口地址.
如果指明入口地址,那么从程序的首地址开始执行,不管起始地址处是否是可执行代码.
CPU根据什么设置CS:IP指向程序的第一条要执行的指令?是由可执行文件中的描述信息指明的.描述信息则主要是编译,连接程序对源程序中相关伪指令进行处理所得到的信息.
即使我们用了类似Assume cs:code,ds:data这样的伪指令,在执行的时候,Cpu也不会把他们的段寄存器指向相应的内容,cpu在加载程序运行把CS:IP指向我们Start入口处,我们在这里调整ss,ds等段寄存器的指向.
-
在代码中使用栈
利用栈将程序中的数据逆序存放
assume cs:code
code segment
dw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h
dw 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0,0,0,0,0
start:
mov ax,cs
mov ss,ax
mov sp,30h
mov bx,0
mov cx,8
s:
push cs:[bx]
add bx,2
loop s
mov bx,0
mov cx,8
s1:
pop cs:[bx]
add bx,2
loop s1
mov ax, 4c00h
int 21h
code ends
end start
- 栈使用技巧:在内存之间传递数据,连个变量之间的传递,可以先用栈做中转,即先压栈变量1
然后弹出来给变量2
-
将数据,代码,栈放入不同的段
-
实验5
非常重要:如果段中的数据占N个字节,则程序加载后,该段实际占有的空间为 ((N+15)/16)_16 。
解析:
N分为被16整除和不被16整除。
当N被16整除时: 占有的空间为(N/16)_16
当N不被16整除时: 占有的空间为(N/16+1)_16,N/16得出的是可以整除的部分,还有一个余数,余数肯定小于16,加上一个16。
程序加载后分配空间是以16个字节为单位的,也就是说如果不足16个字节的也分配16个字节。
两种情况总结成一个通用的公式:((N+15)/16)_16
每个段(例如 数据段,代码段)必须是 16字节的整数倍(系统规定)所以小于16个字节的会自动的变成16个字节
程序开始时ds指向psp比cs大10h,cx中存放着程序的长度
编写代码,将a段和b段的数据相加,保存在c段中.
assume cs:code
a segment
db 1, 2, 3, 4, 5, 6, 7, 8
a ends
b segment
db 1, 2, 3, 4, 5, 6, 7, 8
b ends
c segment
db 0, 0, 0, 0, 0, 0, 0, 0
c ends
code segment
start: mov ax, a
mov ds, ax
mov ax, b
mov es,ax
mov ax, c
mov ss,ax
mov ax,0
mov bx,0
mov cx,8
s: mov al,ds:[bx]
add al,es:[bx]
mov ss:[bx],al
inc bx
loop s
mov ax, 4c00h
int 21h
code ends
end start
第7章 更灵活的定位内存的方法
-
and和or 指令
and指令,按位进行与运算,or按位进行或运算.
第一个操作数必须寄存器或内存单元(内存单元必须指明尺寸)
操作数不能是段寄存器,或ip寄存器
两个操作数的尺寸必须一致
操作数不能同时是两个内存单元.
-
关于ascii码
大写字母在6590(41h5Ah)(0100000101011010)开始,小写字母97122(61h-7Ah)(01100001~01111010)开始.
大写字母第6位为0,小写字母第6位为1(索引从1开始算)
重点是了解了字符的ascii码的规律后,可以利用and和or指令进行大小写转换
数字的ascii码4857(30h-49h)(00110000b00111001b).
数字字符,跟二进制数字的区别是数字字符第5位和第6位都为1而,数字值第7位和第8位都为0
有助于理解ascii码压缩和非压缩指令
编码方案:在显示器上呈现的文字等都是文本字符串,在计算机内部是以二进制形式存储,通过解码,显示器在屏幕上画出文本,其中一种编码方案是ascii码.
文本处理过程例子:我们按下键盘a键,这个按键的信息被送入计算机,计算机用ascii吗的规则对其进行编码,将其转化为61h存储在内存的指定空间中,文本编辑软件从内存中取出61h,将其送到显卡上的显存中;工作在文本模式下的显卡,用ascii码的规则解释显存中的内容.61h被当作字符"a".显卡驱动显示器,将字符"a"的图像画在屏幕上.
db 'unIX' <=> db 75h,6eh, 49h,58h.
mov al, 'a' <=> mov al,97
-
大小写转换问题例子代码
写一段程序将 'BaSic'字符串转化为大写,将'iNfOrMaTiOn'转换为小写
assume cs:code, ds:data
data segment
db 'BaSic'
db 'iNfOrMaTiOn'
data ends
code segment
start:
mov ax, data
mov ds, ax
mov bx,0
mov cx,5
s0:
mov al,[bx]
and al,11011111b
mov [bx],al
inc bx
loop s0
mov cx,11
mov bx,5
s1:
mov al,[bx]
or al,00100000b
mov [bx], al
inc bx
loop s1
mov ax, 4c00h
int 21h
code ends
end start
-
更灵活的寻址
[bx+idata] <=> idata[bx],[bx].idata
可以用于数组寻址,例子如下,将数据段中第一个字符串转换为大写,第二个转换为小写
assume cs:code, ds:data
data segment
db 'BaSic'
db 'MinIX'
data ends
code segment
start:
mov ax, data
mov ds, ax
mov bx,0
mov cx,5
s0:
mov al,[bx]
and al,11011111b
mov [bx],al
mov al,[bx+5]
or al,00100000b
mov [bx+5],al
inc bx
loop s0
mov ax, 4c00h
int 21h
code ends
end start
技巧:因为第一个字符串和第二个字符串一样长,我们就可以用bx+idata寻址,且可以用一个循环就可以达到转换目的,因为长度一样长.
C语言:a[i]<=>idata[i]
[bx+si],[bx+di]
[bx+si+idata],[bx+di+idata]默认段地址在ds中.
[bp+si+idata],[bp+di+idata] 如果bp没有给出段前缀,默认段地址在ss中.
注意事项 : bp和bx不能同时出现,di和si也不能同时出现.且di和si不能拆分成8位寄存器.
-
寻址综合应用
将data段中的每个单词的首字母大写.每个字符串占16个字节.
assume cs:code, ds:data
data segment
db '1\. file '
db '2\. edit '
db '3\. search '
db '4\. view '
db '5\. options '
db '6\. help '
data ends
code segment
start:
mov ax, data
mov ds, ax
mov bx,0
mov cx, 6
s0:
mov al,[bx+3]
and al,11011111b
mov [bx+3],al
add bx,16
loop s0
mov ax, 4c00h
int 21h
code ends
end start
- 将data段中的每个单词改为大写字母,由于每个字符串一样长,我们可以用嵌套循环
assume cs:codesg, ds:datasg
datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
datasg segment
codesg segment
start:mov ax,datasg
mov ds,ax
mov bx,0
mov cx,4
mov ax,0
s0: mov dx,cx
mov si,0
mov cx,3
s1:mov al,[bx+si]
and al,11011111b
mov [bx+si], al
inc si
loop s1
add bx,16
mov cx,dx
loop s0
codesg ends
end start
这里注意保存外层循环的cx值.可以用其它寄存器保存,也可以保存在内存单元中,也可以保存在栈中
- 将data段中的前四个字母改为大写.
assume cs:codesg, ds:datasg
datasg segment
db '1\. display '
db '2\. brows '
db '3\. replace '
db '4\. modify '
datasg segment
codesg segment
start:
mov ax, datasg
mov ds,ax
mov bx,0
mov cx,4
s1:
push cx
mov cx,4
mov si,0
s2:
mov al,[bx+si+3]
and al,11011111b
mov [bx+si+3],al
inc si
loop s2
add bx, 16
pop cx
loop s1
mov ax, 4c00h
int 21h
codesg ends
end start
注意:si需要在循环中每次重置为0
-
实验6
实践本单元所有程序
第八章 数据处理的两个基本问题
-
两个基本问题
处理的数据在什么地方
cpu内部(寄存器,缓存)
端口
内存
数据的尺寸有多长
寄存器的大小指明尺寸
用byte/word/dword ptr [寻址寄存器或段前缀]来指明.
-
数据的位置表达
立即数:直接包含在指令中的立即数在执行前是在cpu内部的指令缓冲器中.
寄存器
内存单元:bx系列寻址组合默认段地址在ds中,bp系列组合默认地址在ss中
-
寻址的综合应用
关于DEC公司的一条记录(1982年)如下:
公司名称:DEC
总裁姓名:Ken Olsen
排名:137
收入: 40(40亿美元)
著名产品:PDP(小型机)
mov ax,seg
mov ds,ax
mov bx,60h
mov word ptr [bx+0ch],38
mov word ptr [bx+0eh],70
mov byte ptr [bx+10h],'V'
inc bx
mov byte ptr [bx+10h],'A'
inc bx
mov byte ptr [bx+10h],'X'
-
几条指令
div指令
格式:
div reg
div 内存单元 //必须指明内存单元尺寸
指令说明
除数:有8位和16位两种在reg或内存单元中.
被除数:放ax或ax和dx中,如果除数为8位,则被除数为16位,默认在ax中存放,如果除数为16位,则被除数则为32位.在dx和ax中存放,dx存放高16位,ax存放低16位
结果:如果除数为8位,al存放商,AH存放余数,如果除数为16位,则AX存放除法的商,dx存放除法操作的余数..
例子:利用除法计算1000001/100
因为100001大于65536,化成16进制为186A1
mov dx,1
mov ax,86A1h
mov bx,100
div bx
- 例子:编程,利用除法指令计算1001/100
mov ax,1001
mov bl,100
div bl
- 用div计算data段中的第一个数据除以第二个数据后的结果,商存在第三个数据的存储单元中
assume cs:code, ds:data
data segment
dd 100001
dw 100
dw 0
data ends
code segment
start:
mov ax, data
mov ds,ax
mov bx,0
mov ax,[bx]
mov dx,[bx+2]
div word ptr [bx+4]
mov [bx+6],ax
mov ax, 4c00h
int 21h
code ends
end start
dd伪指令:定义双字型数据. 还有db/dw/dq(八字节)/dt(十字节)
dup伪指令:db/dw/dd 重复次数 dup (需要重复的数据列表)
eg:db 3 dup (0,1,2) 一共占用3*3=9个字节
-
实验7
代码如下:
assume cs:code,ds:data,es:table
data segment
db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
db '1993','1994','1995'
dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
dd 345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000
dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226
dw 11452,14430,15257,17800
data ends
table segment
db 21 dup ('year summ ne ?? ')
table ends
code segment
start:
mov ax,data
mov ds,ax
mov ax,table
mov es,ax
mov bx,0
mov si,0
mov di,0
mov cx,21
s1:
mov ax,[bx]
mov es:[di],ax
mov ax,[bx+2]
mov es:[di+2],ax
mov byte ptr [di+4],' '
mov ax,84[bx]
mov es:[di+5],ax
mov ax,84[bx+2]
mov es:[di+7],ax
mov byte ptr [di+9],' '
mov ax,168[si]
mov es:[di+10],ax
mov byte ptr [di+12],' '
mov dx,86[bx]
mov ax,84[bx]
div word ptr 168[si]
mov es:[di+13],ax
mov es:[di+15],' '
add bx,4
add si,2
add di,16
loop s1
mov ax, 4c00h
int 21h
code ends
end start
第九章 转移指令的原理
-
offset操作符
操作符offset在汇编语言中是由编译器处理的符号.功能是获取标号的偏移地址
段标号名称就代表地址,所以不需要offset 操作符.
Nop指令占用一个字节
-
转移指令分类
可以修改ip或同时修改CS或IP的指令统称为转移指令:
(1).只修改ip:段内转移
8位数值范围的修改IP,称为短转移
修改范围为16位的称为近转移
(2).同时修改Cs和IP时,称为段间转移.
- 8086CPU的转移指令分为以下几类:
(1)无条件转移指令(如:jmp)
(2)条件转移指令
(3)循环指令
(4)过程
(5)中断
-
无条件跳转jmp指令
跳转到标号:
短转移:jmp sort 标号 (机器码中包含的是8位位移)
近转移:jmp near ptr 标号(机器码中包含16位位移)
位移计算:要跳转的标号的偏移地址 减掉 跳转指令的下一条指令的偏移地址,位移在编译时由汇编器算出.
远转移:jmp far ptr 标号,cs=标号段地址,ip=标号偏移地址,该指令机器码中不是用的位移表示.而是包含了转移地址4个字节.机器码的高位字单元是段地址,低位时偏移地址
转移地址在寄存器:jmp 16位寄存器
转移地址在内存中的jmp指令
段内转移:jmp word ptr 内存单元地址
段间转移:jmp dword ptr 内存单元地址(高字单元是段地址,低地址字单元是偏移地址)
-
条件转移指令jcxz
所有条件转移都是短转移.,在对应的机器码中包含转移的位移.
格式:jcxz 标号,当cx=0时就跳转到标号处.
例子程序:补全程序,利用jcxz指令,实现在内存2000h段中,查找第一个值为0的字节,找到后,将它的偏移地址存储在dx中
assume cs:code
code segment
start:
mov ax,2000h
mov ds,ax
mov bx,0
s:
mov cx, 0
mov cl,[bx]
jcxz ok
inc bx
jmp short s
ok:
mov dx, bx
mov ax, 4c00h
int 21h
code ends
end start
-
loop指令
所有的循环指令都是短转移.对应的机器码包含的是位移.
例子程序:例子程序:补全程序,利用loop指令,实现在内存2000h段中,查找第一个值为0的字节,找到后,将它的偏移地址存储在dx中
assume cs:code
code segment
start: mov ax,2000h
mov ds,ax
mov bx,0
s:mov cl,[bx]
mov ch,0
inc cx
inc bx
loop s
ok:dec bx
mov dx,bx
mov ax,4c00h
int 21h
code ends
end start
inc cx是防止当cx为0时,loop指令会首先cx=cx-1会变成0ffffh,就会循环ffff次.
-
实验
实验8:
assume cs:codesg
codesg segment
mov ax,4c00h
int 21h
start: mov ax,0 ax=0
s: nop 占一字节,机器码90
nop 占一字节,机器码90
mov di,offset s (di)=s偏移地址
mov si,offset s2 (si)=s2偏移地址
mov ax,cs:[si] (ax)=jmp short s1指令对应的机器码EBF6
mov cs:[di],ax jmp short s1覆盖s处指令2条nop指令
s0: jmp short s 执行到这里,不会再继续向下执行,直接跳回mov ax,4c00h了
s1: mov ax,0
int 21h
mov ax,0
s2: jmp short s1
nop
codesg ends
end start
因为jmp short s1<=>EB 位移.位移等于S1标号偏移地址减去nop指令的偏移地址大约为-10.当跳转到s处执行jmp short s1,实际上是执行EB -10.会向前跳转10个字节,也就是说会执行程序返回的代码.
实验9:根据材料编程:在屏幕中间分别显示绿色,绿底红色,白色蓝底的字符串’welcome to masm!’
显示字符相关知识: B8000h-bffffh共32kb的空间,为80*25彩色字符模式的显示缓存区.一个字符占两个字节,后面的字节是该字符的一些颜色属性信息.所以一屏幕内容占缓冲区4000b约等于4kb
assume cs:code, ds:data
data segment
db 'welcome to masm!'
data ends
code segment
start:
mov ax, data
mov ds, ax
mov bx,0
mov ax,0b800h
mov es, ax
mov di,11*160+72
mov cx,16
s:
mov al,[bx]
mov es:[di], al
mov byte ptr es:[di+1],00000010b
mov es:160[di],al
mov byte ptr es:161[di],00100100b
mov es:320[di],al
mov byte ptr es:321[di],01110001b
add di,2
inc bx
loop s
mov ax, 4c00h
int 21h
code ends
end start
第十章 call和ret指令
-
ret和retf指令
ret<=>pop ip<=>(ip)=((ss)*16+(sp)),(sp)=(sp)+2
retf<=>pop ip,pop cs <=>(ip)=((ss)_16+(sp)),(sp)=(sp)+2
(cs)=((ss)_16+(sp)),(sp)=(sp)+2
-
call指令
call指令执行的步骤
将当前call指令后的下一条指令的ip,或cs和ip压入栈中
跳转到目标地址执行
(sp)=(sp) - 2
((ss)*16+(sp)) = (ip)
(ip)=(ip)+16位位移
call指令不能实现短转移,此外转移的方法同jmp指令一样.
call 标号:16位位移=标号处的地址-call指令后的第一个字节的地址, 位移范围为-32768~32767
有等同于 push ip,jmp near ptr 标号
- call far ptr 标号:实现的是段间转移.
push cs,push ip, jmp far ptr 标号
- 转移地址在寄存器中的call指令:call 16位reg
push ip, jmp reg
转移地址在内存中的call指令:
call word ptr 内存单元地址
push ip, jmp word ptr 内存单元地址
- call dword ptr 内存单元地址
push cs, push ip,jmp dword ptr 内存单元地址
-
call和ret的配合使用
子程序框架:
assume cs:code
code segment
main:
call sub1
:
:
call sub2
:
:
mov ax, 4c00h
int 21h
sub1:
:
:
sub2:
:
code ends
所以c语言中的函数名其实就是一个标识子程序的地址
-
mul指令
要求:
两个相乘的数,要么都是8位,要么都是16位,如果是8位,那么一个乘数默认放在AL中,另一个放在8位reg或内存单元中,如果是16位,一个默认在ax中,另一个放在16位reg或内存单元中
结果:如果是8位乘法,结果默认放在AX中,如果是16位乘法,结果高位默认放在dx中,低位放在ax中.
格式:mul reg/内存单元(必须指明内存单元尺寸)
例子:计算100*10,因为两个乘数都是8位,所以用8位乘法
mov al,100
mov bl,10
mul bl
计算 100 * 10000,必须用16位乘法
mov ax,100
mov bx,10000
mul bx
-
模块化程序设计
参数和结果传递问题:
用寄存器传递参数和用寄存器存储函数结果值
例子:计算n的立方,用bx传递参数dx和ax返回结果值
cube:
mov ax,bx
mul bx
mul bx
ret
参数的批量传递:可以将参数放在一组内存单元中,然后用si等间接寄存器指向它.返回值也可以这样做.
例子:将一个si指向的全是字母的字符串转换为大写
capital:
and byte ptr [si], 11011111b
inc si
loop capital
ret
用栈来传递参数
寄存器冲突问题:
例子程序:将一个全是字母,以0结尾的字符串,转化为大写si指向该字符串
capital:mov cl, [si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short capital
ok : ret
如果调用该子程序中的代码中使用了cx寄存器,将产生错误.解决办法是在该子程序开头压栈保存需要使用的寄存器,在返回前弹出该寄存器的值.改正如下
capital:push cx
push si
change:
mov cl, [si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short change
ok :pop si
pop cx
ret
- 所以子程序框架如下:
子程序标号:
子程序中使用的寄存器入栈保存
子程序指令标号:
....
子程序返回标号:
子程序中保存的寄存器出栈恢复
子程序返回(ret,retf)
-
实验10 编写子程序
显示字符串子程序:show_str
功能:在指定的位置,用指定的颜色,显示一个用0结束的字符串
参数:(dh)=行号(取值范围024),(dl)=列号(取值范围是079).(cl)=颜色,ds:si指向字符串的首地址
返回值:无
show_str:
push ax
push bx
push cx
push dx
push es
push si
push di
begn:
mov ax,0b800h
mov es,ax
mov al,160
mul dh
mov bx,ax
mov al,2
mul dl
add bx,ax
mov di,bx
mov al,cl
mov ch,0
s:
mov cl,[si]
jcxz retn
mov es:[di],cl
mov es:[di+1],al
inc si
add di,2
jmp short s
retn:
pop di
pop si
pop es
pop dx
pop cx
pop bx
pop ax
ret
解决触发溢出问题子程序:
名称:divdw
功能:进行不会产生溢出的除法运算,被除数位dword型,除数位word型.结果为dword型
参数:(ax)=dword型数据的低16位,(dx)=dword型数据的高16位,(cx)=除数
返回:(dx)=结果的高16位,(ax)=结果的低16位,(cx)=余数
divdw:
push ax
push bx
push cx
push dx
begn:
mov bx,ax
mov ax,dx
mov dx,0
div cx
push ax
mov ax,bx
div cx
mov cx,dx
pop dx
retn:
pop dx
pop cx
pop bx
pop ax
ret
将数值以十进制形式显示:
名称:dtoc
功能:将word型数据转变为表示十进制数的字符串,字符串以0结尾
参数:(ax)=word型数据,ds:si指向字符串首地址
返回值:无
dtoc:
push ax
push bx
push cx
push dx
push ds
push si
push 0
begn:
mov dx,0
mov bx,10
div bx
mov cx,ax
jcxz sw1
add dx,30h
push dx
jmp short begn
sw1:add dx,30h
push dx
sw2:
pop cx
jcxz retn
mov [si],cl
inc si
jmp short sw2
retn:
mov [si+1],0
pop si
pop ds
pop dx
pop cx
pop bx
pop ax
ret