汇编语言
第一章 基础知识
汇编语言的组成
- 汇编指令:机器码的助记符,有对应的机器码。
- 伪指令:没有对应的机器码,由编译器执行,计算机并不执行。
- 其他符号:如+、-、*、/等,由编译器识别,没有对应的机器码
在内存或磁盘上,指令和数据没有任何区别,都是二进制信息。
CPU对存储器的读写
存储器,即我们平时所说的内存,存储了指令和数据。存储器被划分为若干个存储单元,每个存储单元从0开始顺序编号,这些编号可以看作存储单元在存储器中的地址。
CPU想要进行数据的读写,必须和外部器件(芯片)进行下面3类信息交互:
- 存储单元的地址(地址信息)
- 器件的选择,读或写的命令(控制信息)
- 读或写的数据(数据信息)
CPU通过地址总线、控制总线和数据总线将这些信息传到存储器芯片中。
总线中有
n
n
n根数据线就可以传输
2
n
2^n
2n的数据。控制总线是一些不同控制线的集合。控制总线的宽度决定了CPU对外部器件的控制能力。
第二章 寄存器
一个典型的CPU由运算器、控制器、寄存器等器件构成。在CPU中:
- 运算器进行信息处理;
- 存储器进行信息存储;
- 控制器控制各种器件进行工作;
- 内部总线连接各种器件,在他们之间进行数据的传送。
8086CPU有14个寄存器,每个寄存器有一个名称。这些寄存器是:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW。
通用寄存器
8086CPU的所有寄存器都是16位的,可以存放两个字节。AX、BX、CX、DX这4个寄存器通常用来存放一般性的数据,被称为通用寄存器。而上一代的寄存器都是8位的,因此这四个寄存器又可以分为两个可独立使用的8位寄存器来用。AX=AH+AL;BX=BH+BL;CX=CH+CL;DX=DH+DL。L代表的是X的低8位寄存器,H代表的是高八位寄存器。
8086CPU可以一次性处理以下两种尺寸的数据:
- 字节:byte,8bit
- 字:word,2byte,16bit
几条汇编指令
指令 | 意义 |
---|---|
mov a,b | 把b移入a中 |
add a,b | a+=b |
物理地址
8086CPU有20位地址总线,但它是16位结构,在内部一次性处理、传输、暂时存储的地址为16位。所以8086CPU采用两个16位地址合成的方法来形成一个20位的物理地址。
当8086CPU要读写内存时:
- CPU中的相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址;
- 段地址和偏移地址通过内部总线送入一个称为地址加法器的部件;
- 地址加法器将两个16位地址合成为一个20位的物理地址;
- 地址加法器通过内部总线将20位物理地址送入输入输出控制电路;
- 输入输出控制电路将20位物理地址送上地址总线
- 20位物理地址被地址总线传送到存储器
地址加法器采用物理地址=段地址*16+偏移地址的方法用段地址和偏移地址合成物理地址。
段寄存器
8086CPU有4个段寄存器:CS、DS、SS、ES。当8086CPU要访问内存时,由这4个段寄存器提供内存单元的段地址。
CS和IP
CS为代码段寄存器,IP为指令指针寄存器。若CS内容为M,IP内容为N,则8086CPU将从内存为M*16+N单元开始读取一条指令并执行,其工作过程可以概括为:
- 从CS:IP指向的内存单元中读取指令,读取的指令进入指令缓冲器;
- IP=IP+所读取指令的长度,从而指向下一条指令;
- 执行指令。跳转到步骤(1),重复这个过程。
mov指令无法修改CS:IP的值,jmp指令可以。
第三章 寄存器(内存访问)
内存中字的存储
CPU中用16位寄存器来存储一个字。高8位存放高位字节,低8位存放低位字节。而在内存单元中低位字节放在低地址单元中,高位字节放在高地址单元中(小端法)。
字单元:存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。
DS和[address]
DS寄存器给出CPU要读写的内存单元的段地址。偏移地址用[偏移地址]
表示
8086CPU不支持将数据直接送入段寄存器的操作,因此需要利用另一个一般的寄存器进行中转:
mov bx,1000H
mov ds,bx
mov [0],al
用mov指令访问内存单元可以只给出单元的偏移地址,此时段地址默认在DS寄存器中
字的传送
一些可以执行的指令:
指令 | 目标 | 来源 |
---|---|---|
mov | 寄存器 | 数据 |
mov | 寄存器 | 寄存器 |
mov | 寄存器 | 内存单元 |
mov | 内存单元 | 寄存器 |
mov | 段寄存器 | 寄存器 |
mov | 寄存器 | 段寄存器 |
mov | 内存单元 | 段寄存器 |
mov | 段寄存器 | 内存单元 |
栈
特点:后进先出
CPU提供的栈机制
8086CPU提供入栈和出栈的指令分别是PUSH和POP
栈顶元素由SS:IP指向。
push ax
:
- SP=SP-2,SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新栈顶
- 将ax的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶
pop ax
:
- 将SS:SP指向的内存单元处的数据送入ax中
- SP=SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新栈顶
内存分配:当你将地址A~地址B当作栈空间时,SP的初始位置应该是地址B的偏移地址+2.(栈空间的第一个元素的偏移地址是地址B的偏移地址-2,SP是基于偏移地址-2再加的4)
我们可以将一组地址连续、起始地址是16的倍数的内存单元当作栈空间来使用。使用的时候要注意栈可能越界(push和pop都可能越界),这个时候都会把其他不相干的地址空间的数据覆盖掉或者取出。
第四章 第一个程序
一个源程序从写出到执行的过程
- 编写汇编源程序
- 对源程序进行编译连接生成可执行文件
- 执行可执行文件中的程序
可执行文件中包含两部分内容:
- 程序(从源程序中的汇编指令翻译过来的机器码)和数据(源程序中定义的数据)
- 相关的描述信息(比如,程序有多大、要占用多少内存空间等)
源程序
下面是一段简单的汇编语言源程序
assume cs:codesg
codesg segment
mov ax,0123H
mov bx,0456H
add ax,bx
add ax,ax
mov ax,4c00H
int 21H
codesg ends
end
汇编语言源程序中包含汇编指令和伪指令。
伪指令
XXX segment…… XXX ends
segment和ends的功能是定义一个段。segment说明一个段开始,ends说明一个段结束。一个段必须有一个名称来标识,使用格式为:
段名 segment
……
段名 ends
end
end是一个汇编程序的结束标记,编译器在编译汇编程序的过程中,如果碰到了伪指令end,就结束对源程序的编译。
end!=ends,后者是和segment成对使用的,可以看成是“end segment”。
assume
假设某一段寄存器和程序中的某一个用segment…end定义的段相关联。
源程序中的“程序”
源程序包括伪指令和汇编指令。汇编指令组成了最终由计算机执行的程序,伪指令是由编译器处理的。这里说的程序就是指源程序中最终由计算机执行、处理的指令或数据
标号
一个标号作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址
程序的结构
- 定义一个段的名称
- 在这个段中写入汇编指令
- 指出程序在哪里结束(end)
- 将段和寄存器联系起来(assume)
程序返回
mov ax,4c00H
int 21H
程序是由command.com从可执行文件中加载入内存并转交CPU的控制权的,因此结束运行后要把控制权还给它。
一个程序前会有256位的PSP内容,因此程序内容在DS所指的后256位处存储。
编辑、编译、连接、执行
第五章 [bx]和loop指令
概述
[bx]和内存单元的描述
[bx]表示一个内存单元,它的偏移地址在bx中(段地址在ds中)
()表示一个寄存器或一个内存单元中的内容。里面的元素可以有3种类型:①寄存器名。②段寄存器名。③内存单元的物理地址(一个20位的数据)。
idata表示常量。
[BX]
mov ax,[bx]
bx中存放的数据作为一个偏移地址EA,段地址SA默认存放在ds中,将SA:EA处的数据送入ax中。即:(ax)=((ds)*16+(bx))。
mov [bx],ax
bx中存放的数据作为一个偏移地址EA,段地址SA默认存放在ds中,将ax处的数据送入SA:EA中。即:((ds)*16+(bx))=(ax)。
Loop指令
loop指令的格式是:loop 标号。
CPU执行loop指令时,要进行两步操作:①(cx)=(cx)-1。②判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。
用cx和loop指令相配合实现循环功能的3个要点:
- 在cx中存放循环次数。
- loop指令中的标号所标识地址要在前面。
- 要循环执行的程序段,要写在标号和loop指令的中间。
用cx和loop指令相配合实现循环功能的程序框架如下:
mov cx, 循环次数
s:
循环执行的程序段
loop s
在汇编源程序中,数据不能以字母开头,以字母开头的数据要在前面补0。
段前缀
mov ax,ds:[bx]
这种出现在访问内存单元的指令中,用于显示地指明内存单元的段地址的“ds:”,在汇编语言中称为段前缀。
一段安全的空间
我们需要直接向一段内存中写入内容,这段内存空间不应存放系统或其他程序的数据或代码,否则写入操作很可能引发错误。在DOS方式下,一般情况,0:200-0:2ff空间中没有系统或其他程序的数据或代码,以后我们需要直接向一段内存中写入内容时,就使用0:200-0:2ff这段空间
第六章 包含多个段的程序
在代码段中使用数据
dw
定义字型数据,“define word”。dw定义的数据处于代码段的最开始,从CS中可以得到定义的数据的段地址,起始的偏移地址为0。
但因为前面的内容是数据而不是指令,如果直接运行会将这些数据错误地当成指令执行,因此需要表明程序的入口,从入口处开始执行指令。
start(可以替换成任何东西)
用于表明程序的入口
end start
用于通知编译器程序的入口在什么地方。
有了这两个伪指令我们可以这样安排程序的框架:
assume cs : code
code segment
…数据…
start:
…代码…
code ends
end start
在代码段中使用栈
dw
不仅可以用来定义数据,还可以用来开辟空间以供之后使用。此时不管所开辟的空间里存储的是什么都没有关系,它们最终的效果是一样的。
将数据、代码、栈放入不同的段
定义多个段的方法
对于不同的段有不同的段名即可。
对段地址的引用
段名相当于一个标号,它代表了段地址。因此在汇编指令里段名是一个代表地址的数值,不可以直接放到段寄存器里。mov ds, data
这段代码是错的,应该写成
mov ax,data
mov ds,ax
“代码段”、“数据段”、“栈段”完全是我们的安排
你可以更改任何命名,只需要最后实现的时候一一对应上就可以。
第七章 更灵活的定位内存地址的方式
and和or指令
and指令
“逻辑与”指令,按位进行与运算。
or指令
“逻辑或”指令,按位进行或运算
以字符形式给出的数据
以' '
的方式指明数据是以字符的形式给出的。编译器将把它们转化为相应的ASCII码
大小写转换问题
通常的思路是大写字母+20H
小写字母-20H
。但这样会涉及很多判断。另一种方法是,大小写字母之间差32(10进制),因此他们的第五位(从第0位开始算起)是不一样的,大写字母第五位是0,小写字母第五位是1。通过and 11011111B
可将小写字母变成大写字母。通过or 00100000B
可将大写字母变为小写字母。
第八章 数据处理的两个基本问题
处理的数据在什么地方?
bx、si、di、bp
- 在8086CPU中,只有这4个寄存器可以用在
[ ]
中来进行内存单元的寻址 - 在
[ ]
中,这四个寄存器可以单个出现,或只能以4种组合出现:bx和si、bx和di、bp和si、bp和di - 只要在
[ ]
中使用寄存器bp,而指令中没有显性地给出段地址,段地址就默认在ss中。否则默认在ds中
寻址方式
SA:段地址。EA:偏移地址
寻址方式 | 含义 | 名称 |
---|---|---|
[idata] | EA=idata;SA=(ds) | 直接寻址 |
[bx] | EA=(bx);SA=(ds) | 寄存器间接寻址 |
[bx+idata] | EA=(bx)+idata;SA=(ds) | 寄存器相对寻址 |
[bx+si] | EA=(bx)+(si);SA=(ds) | 基址变址寻址 |
[bx+si+idata] | EA=(bx)+(si)+idata;SA=(ds) | 相对基址变址寻址 |
汇编语言为结构体提供了更贴切的寻址书写方式。[bx].idata
、[bx].idata[si]
。
指令要处理的数据有多长?
- 通过寄存器名指明要处理的数据的尺寸。
- 在没有寄存器名存在的情况下,用操作符
X ptr
来指明内存单元的长度,X在汇编指令中可以为word或byte。 push
指令只进行字操作。
div指令
div指令是除法指令。使用div做除法时应注意以下问题。
- 除数:有8位和16位两种,在一个reg或内存单元中
- 被除数:默认放在AX或DX和AX中,如果除数为8位,被除数则为16位,默认在AX中存放;如果除数为16为,被除数则为32位,在DX和AX中存放,DX中存放高16位,AX中存放低16位。
- 结果:如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数;如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数。
伪指令dd
dd
是用来定义dword(double word,双字)型数据的,占4个字节。
dup
dup
用来进行数据的重复,使用格式为
d(字长) 重复的次数 dup (重复的数据)
例如db 3 dup('a','B')
的定义了6个字节,是’aBaBaB’。
第九章 转移指令的原理
可以修改IP,或同时修改CS和IP的指令统称为转移指令。统括地讲,转移指令就是可以控制CPU执行内存中某处代码的指令。
8086CPU的转移行为有以下几类:
- 只修改IP时称为段内转移,比如
jmp ax
,短转移IP的修改范围为 − 128 — 127 -128—127 −128—127。近转移IP的修改范围为 − 32768 — 32767 -32768—32767 −32768—32767。 - 同时修改CS和IP时,称为段间转移,比如
jmp 1000:0
8086CPU的转移指令分为以下几类:
- 无条件转移指令(如:
jmp
) - 条件转移指令
- 循环指令(如:
loop
) - 过程
- 中断
操作符offset
offset
的功能是取得标号的偏移地址。比如下面的程序:
assume cs:codesg
codesg segment
start : mov ax,offset start ;相当于mov ax,0
s : mov ax,offset s ;相当于mov ax,3
start是代码段中的标号,它所标记的指令是代码段中的第一条指令,偏移地址为0。第一条指令长度为3个字节,所以第二条指令(s)的偏移地址是3。
jmp指令
jmp
为无条件转移指令,可以只修改IP,也可以同时修改CS和IP。
jmp
指令要给出两种信息:
- 转移的目的地址
- 转移的距离(段间转移、段内短转移、段内近转移)
依据位移进行的jmp指令
CPU在执行jmp指令的时候并不需要转移的目的地址。需要的是两条指令之间的位移,这个位移是由编译器通过标号算出(标号处的地址-jmp指令后的第一个字节的地址),用补码表示的(有正负号)
jmp short 标号(转到标号处执行指令)
(IP)=(IP)+8位位移
这种格式的jmp指令实现的是段内短转移,它对IP的修改范围为
−
128
—
127
-128—127
−128—127
jmp near 标号(转到标号处执行指令)
(IP)=(IP)+16位位移
这种格式的jmp指令实现的是段内近转移,它对IP的修改范围为
−
32768
—
32767
-32768—32767
−32768—32767
转移的目的地址在指令中的jmp指令
jmp far ptr 标号
实现的是段间转移,又称为远转移。far ptr指明了指令用标号的段地址和偏移地址修改CS和IP。这个时候机器码里会直接显示目的指令的地址(高地址处存放转移的段地址,低地址处存放指令的偏移地址)而不是两个指令之间的位移。
转移地址在寄存器中的jmp指令
指令格式:jmp 16位reg
功能:(IP)=(16位reg)
转移地址在内存中的jmp指令
转移地址在内存中的jmp指令有两种格式
jmp word ptr 内存单元地址(段内转移)
从内存单元地址处开始存放着一个字,是转移的目的偏移地址。jmp dword ptr 内存单元地址(段间转移)
从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。(CS)=(内存单元地址+2)(IP)=(内存单元地址)
jcxz指令
jcxz
指令为有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移而不是目的地址。对IP的修改范围都是
−
128
—
127
-128—127
−128—127。
指令格式:jcxs 标号
(如果(CX)=0,转移到标号处执行。)
操作:当(CX)=0时,(IP)=(IP)+8位位移。
loop指令
loop指令为循环指令,所有的循环指令都是短转移,在对应的机器码中包含转移的位移而不是目的地址。对IP的修改范围都是
−
128
—
127
-128—127
−128—127。
指令格式:loop 标号
((CX)=(CX)-1,如果(CX)≠0,转移到标号处执行。)
操作:
- (CX)=(CX)-1
- 当(CX)≠0时,(IP)=(IP)+8位位移。
根据位移进行转移的意义
方便了程序段在内存中的浮动装配。
编译器对转移位移超界的检测
根据位移进行转移的指令,它们的转移范围受到转移位移的限制,如果在源程序中出现了转移范围超界的问题,在编译的时候,编译器将报错。
形如jmp 2000:0100
的转移指令,是在Debug中使用的汇编指令,汇编编译器并不认识。如果在源程序中使用,编译时也会报错。
第十章 CALL和RET指令
call和ret指令都是转移指令,它们都修改IP,或同时修改CS和IP。它们经常被共同用来实现子程序的设计。
ret和retf
CPU执行ret指令时,进行下面两步操作:
- (IP)=((ss)*16+(sp))
- (sp)=(sp)+2
可以看出CPU相当于在执行pop IP
。
CPU执行retf指令时,进行下面4步操作:
- (IP)=((ss)*16+(sp))
- (sp)=(sp)+2
- (CS)=((ss)*16+(sp))
- (sp)=(sp)+2
可以看出CPU相当于在执行pop IP
pop CS
。
call 指令
CPU执行call指令时,进行两步操作:
- 将当前的IP或CS和IP压入栈中
- 转移
call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp指令的原理相同。
依据位移进行转移的call指令
call 标号(将当前的IP压栈后,转到标号处执行指令)
CPU执行此种格式的call指令时,进行如下的操作:
- (sp)=(sp)-2 ((ss)*16+(sp)=(IP)
- (IP)=(IP)+16位位移
16位位移=标号处的地址-call指令后的第一个字节的地址,范围为-32768~32767,用补码表示。16位位移由编译程序在编译时算出。
CPU在执行“call 标号”时,相当于进行:
push IP
jmp near ptr 标号
转移的目的地址在指令中的call指令
call far ptr 标号实现的是段间转移。
CPU执行此种格式的call指令时,进行如下的操作。
- (sp)=(sp)-2 ((ss)*16+(sp))=(CS) (sp)=(sp)-2 ((ss)*16+(sp))=(IP)
- (CS)=标号所在段的段地址 (IP)=标号在段中的偏移地址
CPU在执行“call far ptr 标号”时相当于在执行:
push CS
push IP
jmp far ptr 标号
转移地址在寄存器中的call指令
指令格式:call 16位reg
功能:
(sp)=(sp)-2
((ss)*16+(sp))=(IP)
(IP)=(16位reg)
CPU在执行“call far ptr 标号”时相当于在执行:
push IP
jmp 16位reg
转移地址在内存中的call指令
转移地址在内存中的call指令有两种格式
(1)call word ptr 内存单元地址
CPU在执行“call word ptr 内存单元地址”时相当于在执行:
push IP
jmp word ptr 内存单元地址
(2)call dword ptr 内存单元地址
CPU在执行“call dword ptr 内存单元地址”时相当于在执行:
push CS
push IP
jmp dword ptr 内存单元地址
call和ret的配合使用
assume cs:code
code segment
main: :
:
call sub1 ;调用子程序sub1
:
:
mov ax,4c00h
int 21h
sub1: : ;子程序sub1开始
:
call sub2 ;调用子程序sub2
:
:
ret ;子程序返回
sub2: : ;子程序sub2开始
:
:
ret ;子程序返回
code ends
end main
mul指令
mul reg
mul 内存单元
内存单元可以用不同的寻址方式给出,前面要声明是byte ptr还是word ptr。
使用mul做乘法的时候,注意以下两点:
- 两个相乘的数:两个相乘的数,要么都是8位,要么都是16位。如果是8位,一个默认放在AL中,另一个放在8位reg或内存字节单元中;如果是16位,一个默认在AX中,另一个放在16位reg或内存字单元中。
- 结果:如果是8位乘法,结果默认放在AX中;如果是16位乘法,结果高位默认在DX中存放,低位在AX中放。
参数和结果传递的问题
可以将其放入内存中,传递存储数据的地址。
寄存器冲突的问题
子程序开始: 子程序中使用的寄存器入栈
子程序内容
子程序中使用的寄存器出栈
返回(ret,retf)