汇编语言(王爽)读书笔记
文章目录
基础知识
存储单元
最小信息单位是bit(比特、二进制位),8bit为1Byte(字节)。存储单元以字节为单位计。128个存储单元的存储器可以存储128字节。
CPU对存储器的读写
读写时CPU与外部器件进行3类信息交互:
-
存储单元的地址(地址信息)
-
器件的选择,读或写的命令(控制信息)
-
读或写的数据(数据信息)
读写过程
CPU从3号单元中读取数据过程
-
CPU通过地址线将地址信息3发出。
-
CPU通过控制线发出内存读取命令,选中存储芯片,并通知它,将要从中读取。
-
存储器将3号单元中的数据通过数据线送入CPU。
CPU向3号单元写入数据
-
CPU通过地址线将地址信息3发出。
-
CPU通过控制线发出内存写命令,选中存储器芯片,并通知它,要向其中写入数据。
-
CPU通过数据线将数据送入内存的3号单元中。
地址总线
一个CPU有N根地址线,则可以立这个CPU的地址总线宽度为N。这样CPU可以最多寻找2的N次方个内存单元。
数据总线
CPU与内存或其它器件之间的数据传递是通过数据总线进行的。数据总线宽度决定了CPU和外界的数据传送速度(宽度不足时将需要进行多次传送)。8088CPU数据总线宽度为8,8086CPU的数据总线宽度为16。
控制总线
控制总线是一些不同控制线的集合。有多少根控制总线,就意味着CPU提供了对外部器件的多少种控制。它决定了CPU对外部器件的控制能力。
地址空间概述
主板上的核心器件通过总线(地址总线、数据总线、控制总线)相连。这些器件有:CPU、存储器、外围芯片组、扩展插槽(插有RAM内存条和各类接口卡)等。
PC上的存储器芯片从读写属性上分为两类:RAM和ROM。从功能和连接上分为:RAM、BIOS及接口卡上的RAM(例如:显存)。
这些存储器在物理上是独立的器件,但它们都和CPU的总线相连;CPU对它们进行读或写的时候都通过控制线发出内存读写命令。即CPU在操纵和控制它们的时候,把它们都当作内存来对侍,把它们总的看作一个由若干存储单元组成的逻辑存储器,这个逻辑存储器就是我们所说的内存地址空间。
所有物理存储器被看作由若干存储单元组成的逻辑存储器,每个物理存储器在这个逻辑存储器中占有一个地址段。CPU在这段地址空间中读写数据,实际上就是在对相应的物理存储器中读写数据。
内存地址空间的大小受CPU地址总线宽度的限制。8086CPU的地址总线宽度为20,可以传送2的20次方个不同的地址信息(内存单元),地址空间大小为1MB。80386CPU的地址总线宽度为32,则内在地址空间最大约为4GB。
寄存器(CPU工作原理)
CPU由运算器、控制器、寄存器等器件构成,这些器件靠内部总线相连。前一章所说的总线相对于CPU内部来说是外部总线。在CPU中:运算器进行信息处理;寄存器进行信息存储;控制器控制各种器件进行工作;内部总线连接各种器件,在它们之间进行数据的传送。
对于汇编程序员来说,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四个寄存器通常用来存放一般性的数据,被称为通用寄存器。
8086CPU的上代CPU中寄存器是8位的,为保证兼容性,8086CPU中的AX、BX、CX、DX四个寄存器都可以分为两个独立的8位寄存器来使用:
-
AX可分为AH和AL
-
BX可分为BH和BL
-
CX可分为CH和CL
-
DX可分为DH和DL
8086CPU可以一次性处理两种尺寸的数据:byte(字节,8位),可存在8位寄存器中,word(字,16位),可存在16位寄存器中。
物理地址
CPU访问内存单元时,要给出内存单元的地址。所有内存单元构成的存储空间是一个一维的线性空间,每个内存单元在这个空间中都有惟一的地址,这个地址被称为物理地址。
CPU通过地址总线卷入存储器的必须是一个内在单元的物理地址。在CPU向地址总线上发出物理地址之前,必须在内部先形成这个物理地址。
16位结构的CPU
16位结构的CPU有下面几方面的结构特性:
-
运算器一次最多可以处理16位的数据
-
寄存器的最大宽度为16位
-
寄存器和运算器之间的通路为16位。
内存单元的地址在送上地址总线之前,必须在CPU中处理、传输、暂时存放,对于16位CPU,能一次处理、传输、暂存16位地址。
8086CPU给出物理地址的方法
8086CPU有20位地址总线,可以传送20位地址,达到1MB寻址能力。8086CPU又是16位结构,在内部一次性处理、传输、暂存的地址为16位。如果将地址从内部简单的发出,那就只能送出16位地址,表现出的寻址能力只有64KB。
8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。
当8086CPU要读写内存时:
-
CPU中的相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址
-
段地址和偏移地址通过内部总线送入一个称为地址加法器的部件
-
地址加法器将两个16位地址合并成为一个20位的物理地址
-
地址加法器通过内部总线将20位物理地址送入输入输出控制电路
-
输入输出控制电路将20位物理地址送上地址总线
-
20位物理地址被地址总线传送到存储器
地址加法器采用 物理地址=段地址X16+偏移地址 的方法用段地址和偏移地址合成物理地址。
段的概念
将若干地址连续内在单元看作一个段,用段地址X16的起始地址(基础地址),用领衔地址定位段中的内在单元。有两点需要注意:段地址X16必然是16的倍数,所以一个段的起始地址也一定是16的倍数;偏移地址为16位,16位的寻址能力为64KB,所以一个段的长度最大为64KB。
段寄存器
8086CPU有4个段寄存器:CS、DS、SS、ES。
CS和IP
CS和IP是8086CPU中两个最关键的寄存器,它们指示了CPU当前要读取指令的地址。CS主代码段寄存器,IP为指令指针寄存器。
在8086CPU中,任意时刻,设CS中内容为M,IP中的内容为N,8086CPU将从内存MX16+N单元开始,读取下一条指令并执行。
8086CPU加电启动或复位后CS和IP被设置为CS=F000H,IP=FFF0H,CPU从内存FFFF0H单元中读取指令执行。
修改CS、IP的指令
大部分寄存器的值,都可以用mov指令来改变,mov指令被称为传送指令。
但是mov指令不能用于设置CS、IP的值,因为8086CPU没有提供这样的功能。8086CPU提供了jmp指令来改变CS、IP的内容。
若想同时修改CS、IP的内容,可用指令“jmp 段地址: 偏移地址”完成,如:
|
|
若想仅修改IP的内容,可以用指令“jmp 某一合法寄存器”完成,如:
|
|
代码段
在编程时,可根据需要,将一组内存单元定义为一个段。长度为N(N<=64KB)
的一组代码,存在一组地址连续、起始地址为16的倍数的内存单元中,我们可以认为,这段内存是用来存放代码的,从而定义了一个代码段。
将一段内存当作代码段,仅仅是我们在编程时的一种安排,CPU并不会由于这种安排,就自动地将我们定义的代码段中的指令当作指令来执行。CPU只认被CS:IP指向的内存单元中的内容为指令。
Debug程序
-
R命令查看、改变CPU寄存器的内容,“r”查看,“r ax”修改ax的内容。
-
D命令查看内存中的内容,“d 段地址:偏移地址”将显示指定的内存单元开始的128个内存单元的内容。
-
E命令改写内存中的内容,“e 起始地址 数据 数据 数据……”,实际上也可以用它来直接写入机器码。
-
T指令逐条执行代码。
-
P执行中断调用。
-
A命令以汇编指令的形式在内存中写入机器指令。简单的A命令从一个预设的地址开始输入指令。“A 1000:0”从1000:0开始的内存中写入指令。
寄存器(内存访问)
内存中字的存储
CPU中,用16位寄存器来存储一个字。高8位存放高位字节,低8位存放低位字节。在内存中存储时,由于内存单元是字节单元,则一个字要用两个地址连续的内存单元来存放,这个字的低位字节放在低地址单元中,高位字节存放在高地址单元中。
字单元,即存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。
DS和[address]
CPu要读写一个内存单元的时候,必须先给出这个内存单元的地址,在8086PC中,内存地址由段地址和偏移地址组成。8086中有一个DS寄存器,通常用来存放要访问数据的段地址。
前面使用mov指令,可以完成两种传送:
-
将数据直接送入寄存器
-
将一个寄存器中的内容送入另一个寄存器
也可以使用mov将一个内存单元中的内容送入一个寄存器中。格式:mov 寄存器名,内在单元地址。
内存单元偏移地址放在[]中。段地址由CPU自动取ds中的数据。
不能使用类似mov ds, 1000h来将1000h送入ds。只能通过寄存器中转。
|
|
从寄存器送入内存单元。例如,使用mov [0],al可以将al的数据传送到当前数据段中偏移地址为0的内存单元里。
字的传送
我们只要在mov指令中给出16位寄存器就可以进行16位数据的传送了。
mov、add、sub指令
mov指令形式:
-
mov 寄存器, 数据 如:mov ax, 8
-
mov 寄存器, 寄存器 如:mov ax,bx
-
mov 寄存器, 内存单元 如:mov ax,[0]
-
mov 内存单元, 寄存器 如:mov [0],bx
-
mov 段寄存器, 寄存器 如:mov ds,bx
-
mov 寄存器, 段寄存器 如:mov bx,ds
-
mov 内存单元, 段寄存器 如:mov [0],cs
-
mov 段寄存器, 内存单元 如:mov ds,[0]
-
不能使用 mov 段寄存器, 数据
add和sub指令同mov一样,都操作两个对象。它们可以有以下几种形式:
-
add 寄存器, 数据 如:add ax,8
-
add 寄存器, 寄存器 如:add ax,bx
-
add 寄存器, 内存单元 如:add ax, [0]
-
add 内存单元, 寄存器 如:add [0],ax
-
sub 寄存器, 数据 如:sub ax,8
-
sub 寄存器, 寄存器 如:sub ax,bx
-
sub 寄存器, 内存单元 如:sub ax, [0]
-
sub 内在单元, 寄存器 如:sub [0],ax
数据段
将一段内存将作数据段,是编程时的一种安排,用ds存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。
栈
栈是一种具有特殊的访问方式的存储空间。栈的操作规则为LIFO(Last In First Out),先进先出。
CPU提供的栈机制
8086CPU提供的入栈和出栈指令为PUSH和POP。
8086CPU中有两个寄存器来存放栈顶的地址,段寄存器SS和寄存器SP,栈顶的段地址放在SS中,偏移地址放在SP中。栈为空时,栈中没有元素,也就不存在栈顶的元素,所以SS:SP只指向栈的最底部单元下面的单元,该单元的偏移地址为栈最底部的字单元的偏移地址+2。栈操作时变化的是SP的值,内存中的数据不会被清空,只会被覆盖。
栈顶超界的问题
当栈满时再使用push指令,或栈空时再使用pop指令都将发生栈顶超界的问题,这将覆盖栈空间外的数据。
8086CPU不保证对栈的操作不会超界。
在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,执行出入栈的时候要注意防止超界。
push和pop指令
指令格式:
-
push 寄存器
-
pop 寄存器
-
push 段寄存器
-
pop 段寄存器
-
push 内存单元
-
pop 内存单元
指令执行时,可以只给出内存单元的偏移地址,段地址在指令执行时,CPU从ds中取得。
push和pop指令同mov指令不同,CPU执行mov指令时只需要一步操作,就是传送,而执行push、pop指令时需要两步操作。执行push时,先改变SP,然后向SS:SP处传送。执行pop时,先读取SS:SP的数据,然后改变SP。
栈段
栈段也仅仅是编程时的一种安排。我们可以将长度为N(N<=64K)
的组地址连续,起始地址为16的倍数的内存单元当作栈空间来使用。
第一个程序
源程序
伪指令
汇编语言源程序中,包含两种指令,一种是汇编指令,一种是伪指令。汇编指令是有对应的机器码的指令,可以被编译的机器指令,是终为CPU所执行。而伪指令没有对应的机器指令,最终不被CPU所执行。伪指令由编译器来执行,编译器根据伪指令来进行相关的编译工作。
- segment和ends是一对成对使用的伪指令,这是在写可被编译的汇编程序时,必须要用到的一对伪指令。它的功能是定义一个段,segment说明段开始,ends说明段结束。使用格式为:
|
|
一个汇编程序由多个段组成,这些段被用来存放代码、数据或当作栈空间来使用。
一个有意义的汇编程序中至少有一个段,这个段用来存放代码。
-
end是一个汇编程序的结束标记,
-
assume这条伪指令的含义为“假设”。它假设某一段寄存器和程序中的某一个用segments…ends定义的段相关联。通过assume说明这种关联,在情况下,编译程序可以将段寄存器和某一个具体的段联系。assume并不是一条非要深入理解不可的伪指令,以后我们编程时,记着用assume将有特定用途的段和相关的段寄存器关联起来即可。
比如我们用codes segment … codesg ends定义了名为codseg的段。我们在程序开头,用assume cs:codeseg将用作代码段的段codesg和CPU中的段寄存器cs联系起来。
源程序中的“程序”
汇编语言写的源程序,包括伪指令和汇编指令,伪指令由编译器来处理,它们并不实现我们编程的最终目的。我们这里所说的程序是指源程序中最终由计算机执行、处理的指令或数据。
标号
汇编源程序中,除了汇编指令和伪指令外,还有一些标号,如“codesg”。一个标号指代了一个地址。比如codesg在segment的前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址。
程序的结构
源程序由一些段构成。我们可以在这些段中存放代码、数据、或将某个段当作栈空间。基本要素:
|
|
程序返回
通过下面的两条指令返回:
|
|
语法错误和逻辑错误
编译源程序
编译过程中将得到3个输出文件:目标文件(.obj)、列表文件(.lst)、交叉引用文件(.crf)。目标文件是我们最终要得到的结果,另外两个只是中间结果。
连接器的简单解释
-
当源程序很大时,可以将它分为多个源程序文件来编译,每个源程序编译成为目标文件后,再用连接程序将它们连接到一起,生成一个可执行文件
-
程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起,生成一个可执行文件
-
一个源文件编译后,得到了存有机器码的目标文件,目标文件中有些内容还不能直接用来生成可执行文件,连接程序将这些内容处理为最终的可执行信息。所以,在只有一个源程序文件,而又不需要调用某个库中的子程序的情况下,也必须用连接程序对目标文件进行处理,生成可执行文件。
以简化的方式进行编译和连接
编译和连接1.asm
|
|
可执行文件中的程序将入内存并运行的原理
DOS中如果用户要执行一个程序,则输入该程序的可执行文件名,command首先根据文件名找到可执行文件,然后将这个可执行文件中的程序加载入内存,设置CS:IP指向程序的入口,此后,command暂停运行,CPU运行程序。程序运行结束后,返回到command中,command再次显示当前盘符和当前路径组成的提示符,等侍用户的输入。
程序执行过程的跟踪
用Debug将程序加载入内存。
DOS中EXE加载的过程。
-
找到一段起始地址为SA:0000的容量足够的空闲内存区
-
在这段内存区的前256字节中,创建一个称为程序段的前缀 (PSP)的数据区,DOS要利用PSP来和被加载的程序进行通信。
-
从PSP的后面将程序装入,程序的地址被设置为SA+10H:0;(空闲的内存区从SA:0开始,0-255字节为PSP,从256字节处开始存放程序,为更好地区分PSP和程序。DOS一般将它们划分到不同的段中,所以,有了这样的地址安排:空闲内存区:SA:0,PSP区:SA:0,程序区SA+10H:0。注意:PSP区和程序区虽然物理地址连接,却有不同的段地址。)
-
将内存区的段地址存入DS中,初始化其它相关寄存器后,设置CS:IP指向程序入口。
程序加载后,ds中存放着程序所在内存区的段地址,这个内存区的偏移地址为9,则程序所在的内存区的地址为ds:0
这个内存区的前256个字节中存放的是PSP,dos用来和程序进行通信。从256字节处向后的空间存放的是程序。
[bx]和loop指令
- [bx]和内存单元的描述
[bx]同样表示一个内存单元,它的偏移地址在bx中。
- loop
循环指令
- 我们定义的描述性的符号“()”
在下面的内容中,将使用“()”来表示一个寄存器或内存单元中的内容。它所表示的内容有两种类型:字节或字。是哪种类型将由寄存器名或具体的运算决定。
- 约定符号idata表示常量
后面,将使用idata表示常量。
[bx]
- mov ax,[bx]
将bx中存放的数据作为一个偏移地址EA,段地址SA默认在ds中,将SA:EA处的数据送入ax中。
- mov [bx],ax
bx中存放的数据作为一个偏移地址,段地址SA默认在ds中,将ax中的数据送入SA:EA处。
Loop指令
指令格式是:loop 标号。CPU执行loop指令时,要进行两步操作:
-
(cx)=(cx)-1
-
判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。
在Debug中跟踪用loop指令实现的循环程序
-
在汇编程序中数据不能以字母开头。所以A000h要写为0A000h。
-
在Debug中使用G来继续执行程序。或者g 0012之类的指令转到cs:0012处开始跟踪。
Debug和汇编编译器Masm对指令的不同处理
Debug和编译器masm对形如“mov ax,[0]”这类的指令在解释上不同。Debug会将[0]解释为一个内存单元。而0是内存单元的偏移地址;而编译器将[0]就解释为了0。
当前我们将偏移地址送入bx寄存器,用[bx]的方式来访问内存单元。如:
|
|
如果希望像在Debug中那样,在“[]”中直接给出内存单元的偏移地址。只需要在“[]”的前面显式的给出段地址所在的寄存器。
|
|
段前缀
-
mov ax,ds:[bx]
-
mov ax,cs:[bx]
-
mov ax,ss:[bx]
-
mov as,es:[bx]
-
mov ax,ss:[0]
-
mov ax,cs:[0]
这些出现在访问内存单元的指令中,用于显式地指明内存单元的段地址的“ds:”、“cs”、“ss”或“es”,称为段前缀。
一段安全的空间
在8086模式中,随意向一段内存空间写入内容是很危险的,因为这段空间中可能存放着重要的系统数据或代码。
一般在PC机中,DOS和其他合法程序一般都不会使用0:200~0:300的256个字节的空间。所以我们使用这段空间是安全的。
包含多个段的程序
程序取得所需空间的方法有两种,一是在加载程序的时候为程序分配,再就是程序在执行过程中间向操作系统申请。
若要一个程序在加载的时候取得所需的空间,则必须要在源程序中做出说明。我们通过在源程序中定义段来进行内存空间的获取。
在代码段中使用数据
|
|
这里代码段定义了一些字型数据。因此代码段的起始位置不再是代码,代码的起始位置由start来标明,代码结束由end start来标明。
在代码段中使用栈
将数据、代码、栈放入不同的段
更灵活的定位内存地址的方法
and和or指令
以字符形式给出数据
|
|
[bx+idata]
它的偏移地址为(bx)+idata(bx中的数值加上idata)。也可以写作idata[bx]。
SI和DI
SI和DI是8086CPU中和bx功能相近的寄存器,SI和DI不能够分成两个8位寄存器来使用。
[bx+si]和[bx+di]
这两者的含义相似。表示一个内存单元,它的偏移地址为(bx)+(si),即bx中的数值加上si中的数值。
[bx+si+idata]和[bx+di+idata]
这两者的含义相似。表示一个内存单元,它的偏移地址为(bx)+(si)+idata,即bx中的数值加上si中的数值再加上idata。
不同的寻址方式的灵活应用
-
[idata]用一个常量来表示地址,可用于直接定位一个内存单元;
-
[bx]用一个变量来表示内存地址,可用于间接定位一个内存单元;
-
[bx+idata]用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元;
-
[bx+si]用两个变量表示地址;
-
[bx+si+idata]用两个变量和一个常量表示地址。
一般来说,在需要暂存数据的时候,我们都应该使用栈。
数据处理的两个基本问题
下面将使用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中,只有这4个寄存器可以用在“[…]”中来进行内存单元的寻址。
-
在[…]中,这4个寄存器可以单个出现,或只能以四种组合出现:bx和si、bx和di、bp和si、bp和di。
-
只要在[…]中使用寄存器bp,而指令没有显式的给出段地址,段地址默认在ss中。
机器指令处理的数据所在位置
绝大部分机器指令都是进行数据处理的指令,处理大致可分为三类:读取、写入、运算。在机器指令这一层来讲,关不关心数据的值是多少,而关心指令执行前一刻,它将要处理的数据所在的位置。指令在执行前,所要处理的数据可以在三个地方:CPU内部、内存、端口。
汇编语言中数据位置的表达
- 立即数(idata)
对于直接包含在机器指令中的数据,在汇编指令中直接给出,如:
|
|
-
寄存器
-
段地址(SA)和偏移地址(EA)
指令要处理的数据在内存中,在汇编指令中可用[X]的格式给出EA,SA在某个段寄存器中。存放段寄存器可以是默认的,如:
|
|
等指令,段地址默认在ds中:
|
|
等指令,段地址默认在ss中。
存放段地址的寄存器也可以显性的给出,如:
|
|
寻址方式
当数据存放在内在中的时候,我们可以用多种方式来给定这个内存单元的偏移地 址,这种定位内存单元的方法一般被称为寻址方式。
寻址方式 | 含义 | 名称 | 常用格式举例 |
[idata] | EA=idata;SA=(ds) | 直接寻址 | [idata] |
[bx] [si] [di] [bp] |
EA=(bx);SA=(ds) EA=(si);SA=(ds) EA=(di);SA=(ds) EA=(bp);SA=(ss) |
寄存器相对寻址 |
用于结构体: [bx].idata 用于数组: idata[si],idata[di] 用于二维数组 [bx][idata] |
文章作者 Jamsa
上次更新 2010-09-01