基础知识
【8086汇编入门】
引言
汇编课程的研究重点放在如何利用硬件系统的编程结构和指令集有效的控制系统进行运作。
==机器语言==
机器语言是机器指令的集合。展开来说就是一台机器可以正确执行的命令
==汇编语言==
- 汇编语言的主体是汇编指令
- 汇编指令和机器指令的差别在于指令的表示方法上,汇编指令是机器指令便于记忆的书写格式
==寄存器==
寄存器简单来说就是CPU中可以存储数据的器件,一个CPU中有多个寄存器。
- CPU由运算器、控制器、寄存器等器件构成,这些器件靠片内总线相连。
- 运算器进行信息处理;控制器控制各种器件进行工作;寄存器进行信息存储;
汇编语言的组成
汇编语言由一下三类组成:
- 汇编指令(机器码的助记符)
- 伪指令(由编译器执行)
- 其他符号(由编译器识别,没有对用的机器码)
汇编语言的核心是汇编指令,它决定了汇编语言的特性。
存储器
CPU 是计算机的核心部件,控制计算机的运作并进行运算。指令和数据再存储器中存放,也就是平时说的内存
一台PC机中内存的作用仅次于CPU,离开了内存,CPU无法运作。而磁盘不同于内存,磁盘上的数据库程序如果不读到内存中,就无法被CPU使用。
- 随机存储器(RAM)在程序的执行过程中可读可写,必须带电存储
- 只读存储器(ROM)在程序的执行过程中只读,关机数据不丢失
指令和数据
- 指令和数据是应用上的概念
- 在内存或磁盘上,指令和数据没有任何区别,都是二进制信息。
存储单元
存储器被划分为若干个存储单元,每个存储单元从0开始编号
存储容量转换
1 | 1KB = 1024B |
CPU对存储器的读写
CPU要从内存中读取数据,首先要指定存储单元的地址。也就是它要确定读取哪一个存储单元的数据。
另外,在一台微机中,不只有存储器这一种器件,CPU在读写数据时还要指明,它要对哪一个器件进行操作。
还要确定进行的是那种操作,是从中读出数据,还是向里面写入数据。
可见,CPU要想进行数据的读写,必须和外部器件(标准的说法是芯片)进行三类信息的交互:
- 存储单元的地址(地址信息)
- 器件的选择,读或写命令(控制信息)
- 读或写的数据(数据信息)
CPU是通过什么将地址、数据和控制信息传到存储器芯片中的呢?计算机中专门有连接CPU和其他芯片的导线,称为总线。逻辑上分为三类:地址总线,控制总线和数据总线
地址总线
- CPU是通过地址总线指定存储单元的
- 地址总线上能传送多少个不同的信息,CPU就可以对多少和存储单元进行寻址
- 一个CPU有N根地址总线,则可以说这个CPU的地址总线的宽度为N
- 这样的 CPU最多可以寻找2的N次方个内存单元
数据总线
- CPU与内存或其他器件之间的数据川总是通过数据总线来进行的
- 数据总线的宽度决定了CPU和外界的数据传送数据
控制总线
- CPU对外部器件的控制是通过控制总线来进行的,在这里控制总线是一个总称,控制总线是一些不同控制线的集合
- 有多少根控制总线,就意味着CPU提供了对外部前进的多少种控制,控制总线的宽度决定了CPU对外部器件的控制能力
小结
- 汇编指令是机器指令的助记符,同机器指令一一对应
- 每一种CPU都有自己的汇编指令集
- CPU可以直接使用的信息是在存储器中存放的
- 在存储器中指令和数据没有任何区别,都是二进制信息
- 一个CPU可以引出三种不同的总线的宽度标志了这个CPU的不同方面的性能
- 地址总线的宽度决定了CPU的寻址能力
- 数据总线的宽度决定了CPU与其他器件进行数据传传送的一次数据传送量
- 控制总线宽度决定了CPU对系统中其他器件的控制能力
寄存器
CPU内部工作原理
CPU概述
一个典型的CPU由运算器,控制器,寄存器等器件组成,这些器件由内部总线相连。而第一章说的总线对于CPU来说是外部总线。
- 内部总线实现CPU内部各个器件之间的联系
- 外部总线实现CPU和主板上其他器件的联系
在CPU内部:
- 运算器进行信息处理
- 寄存器进行信息存储
- 控制器控制着各种器件进行工作
寄存器概述
8086CPU有14个寄存器,它们的名称为:
AX,BX,CX,DX,SI,DI,SP,BP,IP,CS,SS,DS,ES,PSW
通用寄存器
[AX,BX,CX,DX]
8086CPU所有的寄存器都是16位的,可以存放两个字节(一个字)
- 为保持兼容性,这四个通用寄存器都可以分为两个独立的8位寄存器使用
AX,BX,CX,DX通常用来存放一般性数据,被称为通用寄存器
- AX可以分为AH和AL(兼容性考虑) 其他三个同理
16位结构CPU具有下面几方面的结构特性。
- 运算器一次最多可以处理16位的数据;
- 寄存器的最大宽度为16位;
- 寄存器和运算器之间的通路为16位。
几条汇编指令
汇编指令 | 控制CPU完成的操作 | 用高级语言的语法描述 |
---|---|---|
mov ax , 18 | 将18送入AX | AX = 18 |
mov ah , 78 | 将78送入AH | AH = 78 |
add ah , 8 | 将寄存器AX中的数值加8 | AX = AX + 8 |
mov ax , bx | 将寄存器BX中的数据送入AX | AX = BX |
add ax , bx | 将AX,BX中的内容相加,结果放在AX中 | AX = AX + BX |
物理地址
- CPU访问内存单元时要给出内存单元的地址。所有的内存单元构成的存储空间是一个一维的线性空间
- 我们将这个唯一的地址称为物理地址
一个问题
8086CPU有20位地址总线,可以传送20位地址,达到1MB寻址能力。
8086CPU又是16位结构,在内部一次性处理、传输、暂时存储的地址为16位。
从8086CPU的内部结构来看,如果将地址从内部简单地发出,那么它只能送出16位的地址,表现出的寻址能力只有64KB。
8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址
- CPU中的相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址;
- 地址加法器将两个16位地址合成为一个20位的物理地址;
地址加法器合成物理地址的方法:
物理地址 = 段地址 × 16 + 偏移地址
其中段地址× 16
有一个更常用的说法就是数据左移4位,16进制的话就是左移1位。
段寄存器
段的概念
内存并没有分段,段的划分来自于CPU,由于8086CPU用物理地址 = 段地址 × 16 + 偏移地址
的方式给出内存单元的物理地址,使得我么可以用分段的方式来管理内存。
- 段地址 × 16 必然是16的倍数,所以一个段的起始地址也一定是16的倍数
- 偏移地址为16位,16位地址的寻址能力为64kb,左移一个段的长度最大为64kb
段寄存器
段寄存器就是提供段地址的。8086CPU有4个段寄存器,CS,DC,SS,ES
当8086CPU要访问内存是,由这4个段寄存器提供内存单元的段地址
CS和IP
CS和IP是8086CPU中最关键的寄存器,它们指示了CPU当前要读取指令的地址
- CS为代码段寄存器
- IP为指令指针寄存器
CS为代码段寄存器,IP为指令指针寄存器,
CPU将CS、IP中的内容当作指令的段地址和偏移地址,用它们合成指令的物理地址,
CPU将CS : IP指向的内容当作指令执行。(即PC)
==工作过程==
- 从CS:IP指向内存单元读取指令,读取的指令进入指令缓冲器。
- IP = IP + 读取的指令的长度,从而指向下一条指令。
- 执行指令,转到步骤1,重复这个过程。
==修改CS和IP的指令==
在CPU中,程序员能够用指令读写的部件只有寄存器,程序员可以通过改变寄存器中的内容实现对CPU的控制
CPU从何处执行指令是由CS、IP中的内容决定的,程序员通过改变CS、IP中的内容来控制CPU
MOV
指令可以改变8089CPU大部分寄存器的值,被称为传送指令,但是不能通过MOV
指令改变CS、IP的值8086提供了专门的转移指令来修改CS、IP的值
同时修改CS、IP的内容:
jmp 段地址:偏移地址
jmp 2AE3:3 -> 从2AE33H处读取指令
jmp 3:0B16 -> 从00B46H处读取指令功能:用指令中给出的段地址修改CS,偏移地址修改IP
仅修改IP的内容
jmp 某一合法寄存器
jmp ax (类似于 mov IP,ax)
功能:用寄存器中的值修改IP
代码段
- 对于8086CPU机,在编程时,可以根据需要,将一组内存单元定义为一个段
- 可以将长度为N的一组代码,存在一组地址连续、起始地址为16的倍数的内存单元中,这段内存是用来存放代码的,从而定义了一个代码段
寄存器(内存访问)
DS和[address]
- CPU要读取一个内存单元的时候,必须先给出这个内存单元的地址
- 在8086CPU中,内存地址由段地址和偏移地址组成
- 8086CPU中有一个DS寄存器,通常用来存放要访问的数据的段地址
例如:我们要读取10000H单元的内容可以用如下程序进行:
1 | mov bx,1000H |
解释:
- 前面已知mov指令可以将数据直接送入寄存器,或者将一个寄存器中的内容送入另一个寄存器
- 除此之外,mov指令还可以将一个内存单元中的内容送入一个寄存器
- 格式:
mov 寄存器名,内存单元地址
- [···]表示一个内存单元,“[···]”中的0表示内存单元的偏移地址
- 而CPU自动读取DS中的数据作为内存单元的段地址
- 此时,读取到的内存单元就是:DS:偏移地址
mov、add、sub指令
mov已学的几种指令如下:
1 | mov 寄存器,数据 ;mov ax 8 |
add和sub同mov一样,都有两个操作对象
1 | add 寄存器,数据 ;add ax,8 |
数据段
对于8086CPU,我们可以根据需要将一组内存单元定义为一个段(可以是代码段、数据段等)
我们可以将一组长度为N、地址连续、起始地址为16的倍数的内存单元当做专门存储数据的内存空间,从而定义了一个数据段。
比如用123B0H~123B9H这段空间来存放数据:
- 段地址:123BH
- 长度:10字节
数据段的访问
例如:将123B0H~123B9H的内存空间定义为数据段,并累加这个数据段中前三个单元中的数据:
1 | mov ax,123BH |
栈
特性:先进后出(LIFO) last in first out
8086CPU提供入栈和出栈的指令(最基本的):[入栈和出栈的操作都是以字节为单位进行的]
PUSH (入栈)
push ax
将寄存器ax中的数据送入栈中
POP (出栈)
pop ax
从栈顶去除数据送入ax。
cpu如何知道当前要执行的指令所在的位置?
答:寄存器CS和IP中存放着当前指令的段地址和偏移地址
8086CPU中,有两个寄存器:
- 段寄存器SS 存放栈顶的段地址
- 寄存器SP 存放栈顶的偏移地址
任意时刻:SS:SP指向栈顶元素
==pop指令的执行过程[push同理]==
pop ax
- 将SS:SP指向的内存单元处的数据送入ax中
- SP=SP+2[push的话就是SP-2],SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶
栈顶越界
8086CPU的工作机理:只考虑当前的情况
- 当前栈顶在何处
- 当前要执行的指令是那一条
- CPU并不会知道栈顶在哪里 —> 存在栈溢出的问题
push、pop
==push和pop指令的格式(1)==
push 寄存器
将一个寄存器中的数据入栈 ;push axpop 寄存器
出栈,用一个寄存器接收出栈的数据 ;pop bx
==push和pop指令的格式(2)==
push 段寄存器
将一个段寄存器中的数据入栈 ;push dspop 段寄存器
出栈,用一个段寄存器接收出栈的数据 ;pop es
==push和pop指令的格式(3)==
push 内存单元
将一个内存单元处的字入栈[栈操作都是以字为单位] ;push [0]pop 内存单元
出栈,用一个内存字单元接收出栈的数据 ;pop [2]
指令实行时,CPU要知道内存单元的地址,可以在push,pop指令中给出内存单元的偏移地址,段地址在指令执行的手CPU从ds中取得
练习
将10000H~1000FH这段空间当做栈,初始状态是空的,设置AX=002AH,BX=002BH,利用栈,交换AX和BX中的数据
1 | mov ax,1000H |
栈段
我们可以将一组长度为N、地址连续、起始地址为16的倍数的内存单元当做栈来使用,从而定义了一个栈段。
第一个程序
流程
一个汇编语言程序从写出到最终执行的简要过程:
- 编写 —> .asm
- 编译连接 —> 对源程序文件中的源程序进行编译产生目标文件 —> .ogj —> 再用连接程序对目标文件进行连接,生成可执行文件 —> .exe
- 可执行文件包含两个部分:
- 程序(从源程序中的汇编指令翻译过来的机器码)和数据(源程序中定义的数据)
- 相关的描述信息(比如:程序的大小和占用的内存空间等)
- 可执行文件包含两个部分:
- 执行 —> 操作系统依照客户自行文件中的描述信息,将可执行文件中的机器码和数据加载入内存,并进行相关的初始化(比如:设置CS:IP指向第一条要执行的指令),然后由CPU执行程序
源程序
1 | ;1.asm |
伪指令
在汇编语言源程序中,包含两种指令,一种是汇编指令,一种是。伪指令没有对应的机器码的指令,最终不被CPU执行。伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作。
==定义一个段==
- **
segment
和ends
**是一对成对使用的伪指令,这是在写可以被编译器编译的汇编程序时,必须用到的一对伪指令 segment
和ends
的功能是定义一个段,segment说明一个段的开始,ends说明一个段的结束- 一个段必须有一个名称来标识,使用格式为:
段名 segment
段名 ends
- 一个汇编程序由多个段组成,这些段用来存放代码、数据或被当做栈空间使用。但是一个程序至少有一个段
==End==
- End是一个汇编程序的结束标记,编译器在编译汇编程序的过程中,如果碰到了伪指令end,就结束对源程序的编译
- 注意不要搞混了end和ends。ends是和segment成对使用的标记一个段的结束,ends可以理解为
end segment
==assume==
assume
:含义为’假设’- 它假设某一段寄存器和程序中的某一个用
segment···ends
定义的段相关联 - 通过
assume
说明这种关联,在需要的情况下,编译程序可以将带寄存器和某个具体的段相关联 - assume并不是一条非要深入理解不可的伪指令,以后我们编程的时候,记得用特定用途的段和相关的段寄存器关联起来即可
assume cs:codesg
将用作代码段的段codesg和段寄存器cs联系起来。
汇编指令
有对应的机器码的指令,可以被编译为机器指令,最终为CPU所执行
1 | mov ax,0123H |
这些就是一些汇编指令,可以和机器指令一一对应
标号
汇编源程序中,除了汇编指令和伪指令外,还有一些标号,比如”codesg”
- 一个标号指代了一个地址
- codesg:放在segment的前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址
1 | assume cs:psych |
程序返回
我们的程序最先以汇编指令的形式存在源程序中,经编译、连接后转变为机器码,存储在可执行文件中,那么它怎么得到运行呢?
- DOS中的程序运行
DOS是一个单任务操作系统。一个程序P2在可执行文件中,则必须有一个正在运行的程序P1,将P2从可执行文件中加载入内存后,将CPU的控制权交给P2,P2才能得以运行。P2开始运行后,P1暂停运行。当P2运行完毕后,应该将CPU的控制权交换给它使它得以运行的程序P1,此后P1继续运行。
现在我们知道,一个程序结束后,将CPU的控制权交还给使他得以运行的程序,我们称这个过程为:程序返回。
1 | ;在程序的末尾天界返回的程序段。 |
==段结束、程序结束、程序返回==
目的 | 相关指令 | 指令性质 | 执行执行者 |
---|---|---|---|
通知编译器一个段结束 | 段名 ends | 伪指令 | 编译时,由编译器执行 |
通知编译器程序结束 | end | 伪指令 | 编译时,由编译器执行 |
程序返回 | mov ax,4c00H int 21H | 汇编指令 | 编译时,由CPU执行 |
[BX]和loop指令
[bx]
[bx] 的含义:[bx]和[0]类似,同样表示一个内存单元,它的偏移地址在bx中,段地址默认在ds中
mov ax, [bx]
功能:bx中存放的数据作为一个偏移地址EA,段地址SA默认在ds中,将SA:EA处的数据送入ax中。即(ax) =((ds)*16+(bx))
注:在以后的课程中,我们将使用一个描述性的符号”( )”来表示一个寄存器或一个内存单元的内容。比如:(ax)表示ax中的内容、(20000H)表示2000H中的内容;
loop
英文单词’loop’有循环的含义,显然这个指令和循环有关。
loop指令的格式是:loop 标号
,CPU执行loop指令的时候,要进行两步操作
- (cx) = (cx) - 1;
- 判断 cx 中的值,不为零则转至标号处执行程序,如果为零则向下执行。
我们可以发现,cx中的值影响着loop指令的执行结果。通常我们用loop指令来实现循环功能,cx中存放循环次数。
应用
任务:编程计算2^12,结果存放在ax中。
分析:
N^2可以用N+N表示,设(ax) = 2,(ax) = (ax)+(ax),
2^4可以用2*2*2*2
表示,上一步执行得到的(ax)再次执行两次(ax) = (ax) + (ax)
也就是2^12就是2乘11次2,总共执行11次(ax)=(ax)+(ax);
1 | assume cs:code |
- 程序中标号
s
实际上标示了一个地址,这个地址由一条指令:add ax,ax; - loop s 程序在执行这条指令时,先cx-1,不为零,执行标号的指令,再继续cx-1,一直到cx=0,总共循环了10次,但是刚开始执行了一次,故总共执行了11次,正好等于cx
loop和[bx]的联合
问题:计算ffff:0~ffff:b单元中的数据和,结果存储在dx中
分析:
- ffff:0
ffff:b中的数据都是8位的,不能直接加到寄存器dx中,而先累加到dl中设置(dh)=0也不行,ffff:0ffff:b数据范围在0~255,12个8位数据相加可能导致进位丢失 - 目前的方法是用一个16位的寄存器来做中介。将内存单元中的8位数据赋值到16位寄存器ax中,再将ax中的数据加到dx中,从而使这两种运算对象的类型匹配且不会发生越界
- 还有一个问题就是所有数据和加起来构成循环,可以使用loop来简化程序
1 | assume cs:code |
段前缀
指令mov ax,[bx]
中,内存单元的偏移地址由bx给出,而段地址默认在ds中。我们可以在访问内存单元的指令中显式的给出内存单元的段地址所在的段寄存器。比如:
mov ax,ds:[bx]
将一个内存单元的内容送入ax,这个内存单元的长度为2字节,存放一个字,偏移地址在bx中,段地址在ds中。
类似的还有
1 | mov ax, ds:[bx] |
这些出现在访问内存单元的指令中,用于显示的指明内存单元的段地址的”ds”,”cs”,”ss”,”es”,在汇编语言中称为段前缀。
段前缀的使用
问题:将内存ffff:0 ~ ffff:b
单元中的数据复制到0:200 ~ 0:20b
单元中。
分析:
- 复制的过程应用循环实现
- x=0
- 循环十二次:
- 将ffff:x中的数据送入0020:X(需要用一个寄存器中转)
- X=X+1
- 在循环中,X是变量,我们哟bx存放
1 | assume cs:code |
包含多个段的程序
概述
- 程序取得所需空间的方式有两种,一是在加载程序的时候为程序分配,再就是程序在执行的过程中向系统申请。这里我们主要研究第一种方式,加载程序的时候为程序分配空间
- 我们若要一个程序在被加载的时候取得所需的空间,则必须要在源程序中作出说明。我们通过源程序中定义段来进行内存空间的获取。
程序中对段名的引用,被编译器处理为一个表示段地址的数值。
1 | mov ax, data |
在代码段中使用数据
需求
计算8个数据的和,结果存储在AX寄存器中。
代码
1 | ;计算 8 个数据的和存到 ax 寄存器 |
在代码段中使用栈
1 | ;利用栈,将程序中定义的数据逆序存放。 |
将数据、代码、栈放入不同的段
1 | assume cs:code,ds:data,ss:stack ;定义三个段 |
更灵活的定位内存地址的方法
前面,我们用[0]、[bx]的方法,在访问内存的指令中,定位内存单元的地址。本章我们主要通过具体的问题来讲解一些更灵活的定位内存地址的方法和相关的编程方法。
And和Or指令
And:逻辑与,按位进行与运算
1 | mov al,01100011B |
执行后,al=00100011B
Or:逻辑或,按位进行或运算
1 | mov al,01100011B |
执行后,al=01111011B
应用:大小写转换
小写字母的ASCII码值比大写字母的ASCII码值大20H
大写字母ASCII码的第5位为0,小写字母的第5位为1(其他一致)
1 | assume cs:codesg,ds:datasg |
[bx+idata]
前面,我们用[bx]的方式来指明一个内存单元,还可以用一种更为灵活的方式来指明内存单元;[bx+idata]表示一个内存单元,它的偏移地址为(bx)+idata(bx中的数值加上idata)
mov ax,[bx+200]
含义:将一个内存单元的内容送入ax,这个内存单元的长度为2个自己(字单元),存放一个字,偏移地址为bx总的数值加上200,段地址在ds中。
以下写法也很常见:
mov ax,[200+bx]
mov ax,200[bx]
mov ax,[bx].200
这个可以理解为数组,我们利用这个可以用更高级的结构来实现大小写转换程序
1 | assume cs:codesg,ds:datasg |
对比以下汇编和C语言对数组的写法
C语言: a[i], b[i]
汇编: 0[bx], 5[bx]
SI和DI
si和di是8086CPU中和bx功能相近的寄存器,si和di不能够分成两个8位寄存器来使用。下面的3组指令实现了相同的功能
1 | mov bx,0 |
下面的三组指令也实现了相同的功能
1 | mov bx,0 |
不同寻址方式的灵活应用
如果我们比较前面提到了几种定位内存地址的方法(可称为寻址方式),就可以发现
- [idata]用一个常量表示地址,可用于直接定位一个内存单元
- [bx]用一个变量来表示内存地址,可用于间接定位一个内存单元
- [bx+idata]用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元
- [bx+si]用两个变量表示地址
- [bx+si+idata]用两个变量和一个常量表示地址
一般来说,在需要暂存数据的时候,我们都应该使用栈。
数据处理的两个基本问题
我们知道,计算机是尽心数据处理、运算的机器。那么有两个基本的问题就包含在其中:
- 处理的数据在什么地方
- 要处理的数据有多长
这两个问题,在机器指令中必须给以明确或隐含的说明,否则计算机就无法工作。
我们定义的描述性符号:reg
和sreg
为了描述上的简洁,在以后的课程中,我们将使用描述性的符号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个寄存器可以单个实现,或只能以4中组合出现:
bx和si
、bx和di
、bp和si
、bp和di
。只要在[···]中使用寄存器bp,而指令中没有显性地给出段地址, 段地址就默认在ss中
mov ax,[bp]
含义(ax)=((ss)*16+(bp))
汇编中数据位置的表达
汇编语言中用3个概念来表达数据的位置
- 立即数(idata)
- 寄存器
- 段地址(SA)和偏移地址(EA)
立即数(idata)
对于直接包含在机器指令中的数据(执行前在CPU是直接指令缓冲器中),在汇编语言中称为:立即数(idata),在汇编指令中直接给出。例如:
mov ax,1
寄存器
指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名。例如:
mov ax,bx
段地址(SA)和偏移地址(EA)
指令要处理的数据在内存中,在汇编指令中可用[X]的格式给出EA,SA在某个段寄存器中。
- 存放在段地址的寄存器可以是默认的
- 存放在段地址的寄存器也可以是显性给出的
1 | mov ax, [0] |