【8086汇编入门】《零基础入门学习汇编语言》汇编语言第4版
1基础知识
机器语言是机器指令的集合,由0和1组成,但是很长很复杂,汇编语言因此产生。
汇编语言的主体是汇编指令。汇编指令是机器指令的便于记忆的书写格式。
程序员写完汇编指令通过编译器转换为机器码,机器码再传到计算机执行。
汇编语言有以下三类:
1汇编指令:助记符,有对应机器码
2.伪指令:没有对应机器码,编译器执行计算机不执行
3.其他符号:+ -等由编译器识别,没有对应机器码
汇编语言的核心是汇编指令,决定了汇编语言的特性
CPU是计算机的核心部件,他控制整个计算机的运作并运算,指令和数据在存储器中存放,也就是内存。CPU离不开内存。内存中指令和数据没区别,都是二进制。CPU来识别是信息还是指令。
一个存储单元存储1Byte
CPU从内存中读写书,要指定地址,指定进行哪种操作,CPU通过总线连接其他芯片,传输信息
存储单元的地址(地址信息)->地址总线
器件的选择,读或写的命令(控制信息)->控制总线
读或写的数据(数据信息)->数据总线
]()
地址总线
一根导线有两种稳定状态代表0和1,那么10根导线就有2^10次方个不同数据,从0到1023
地址总线的宽度决定了CPU的寻址能力
数据总线
8根数据总线可传送一个8位二进制数据 8bits = 1byte
数据总线的宽度决定了CPU与其他器件进行数据传送时的一次数据量
控制总线
控制总线的宽度决定CPU对外部器件的控制能力
主板上都是核心器件,CPU、存储器等,CPU通过总线向接口卡发送指令,接口卡控制外设进行工作
随机存储器RAM 只读存储器ROM
BIOS(Basic input/putput system)
CPU将各类存储器看作一个逻辑存储器,所有的物理存储器被看作一个由若干个存储单元组成的逻辑存储器,每个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间。
内存地址空间的大小受CPU地址总线宽度的限制。不同计算机系统内存地址分配情况不同
2.寄存器
CPU由运算器、控制器、寄存器等器件构成,器件靠内部总线连,与之前总线(外)不同
寄存器程序员可以用指令读写
8086CPU的14个寄存器
AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW
通用寄存器:
用来存放一般性数据 AX BX CX DX
16位寄存器可以拆成两个8位寄存器使用 AH AL,高8位和低8位
两字节byte构成一个字word,一个字节8bit放在8位寄存器
汇编指令
mov ax,18
mov ah,78
add ax,8
mov ax,bx
add ax,bx
...
16位寄存器只能存放4位十六进制数据,1044CH最高位的1就不能保存再ax中004CH
独立使用AL这个寄存器如果进位是不会储存在AH中
数据传送和运算时 指令的两个操作对象的位数应当一致
物理地址
所有内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间中都有唯一的地址
CPU向地址总线发出物理地址前,要在内部形成这个物理地址
16位的CPU能够一次性处理、传输、暂时存储16位地址
8086CPU有20位地址总线,内部用两个16位地址合成一个20位 的物理地址
物理地址 = 段地址 x 16(基础地址) + 偏移地址
段地址 x 16 常用说法左移4位(二进制位)相当于16进制左移1位
X 进制左移1位相当于乘 X
因为内部结构是这样,为了达到20位寻址能力,利用两个16位地址可以达到目的
接着段地址,内存没有分段,段的划分来自cpu
段地址 x 16是16的倍数,所以一个段的起始地址也是16的倍数;偏移地址位16位,16位地址的寻址能力位64kb,所以一个段的长度最大为64kb
CPU可以用不同的段地址和偏移地址形成同一个物理地址
8086的4个段寄存器CS、DS、SS、ES。当访问内存由这四个段寄存器提供内存单元段地址
CS为代码段寄存器 IP为指令指针寄存器
任意时刻,设CS内容为M,IP中内容为N,8086CPU从内存M x 16 + N单元开始,读取指令并执行
读取一条指令后,IP中的值自动增加(指令长度),以使CPU可以读取下一条指令
CPU将CS:IP指向的内存地址单元内容看作指令
同时修改CS、IP的内容可以用 jmp 段地址 :偏移地址
仅修改IP可用 jmp 某一合法寄存器(用寄存器中的值修改IP)?
8086机编程时,可以根据需要,将一组内存单元定义为一个段。
将长度为N<=64的代码,存在一组地址连续、起始地址为16的倍数的内存单元作代码段
用CS:IP指向的内容就能让代码段的内容执行
3.寄存器(内存访问)
在内存中存储时,内存单元是一个字节byte单元,则一个字Word要用两个地址连续的内存单元来存放,低位字节存放在低地址单元,高位字节存放在高地址单元
字单元:由两个地址连续的内存单元组成,起始地址为N的字单元简称为N地址字单元
0地址字单元4E21H,1地址字单元124EH......
DS和[address]
8086中有一个DS寄存器,通常用来存放要访问数据的段地址
mov bx,1000H
mov ds,bx
mov al,[0]
将10000H(1000:0)中的数据读到al中
这里[...]表示一个内存单元,括号里面表示偏移地址,指令执行时8086CPU自动读取ds中数据作为内存单元的段地址
8086CPU不支持将数据直接送入段寄存器,只好用一个寄存器中转
16位结构,有16根数线,所以一次可以传送16位数据,也就是一个字
mov add sub都是带有两个操作对象的指令,而jump具有一个
编程时根据需要定义数据段,可以在具体操作的时候用ds存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元
栈
栈是一种具有特殊访问方式(最后进入空间的数据,最先出去)的存储空间
栈的两个基本操作:入栈和出栈,入栈就是将新的元素放到栈顶,出栈就是将栈顶元素取出一个
栈的操作规则被称为:LIFO(Last In First Out,后的进先出来)
编程时,可以将一段内存当作栈
push ax 将ax中数据入栈
pop ax 从栈顶取出数据到ax
操作都是以字为单位进行的
8086CPU中任意时刻,段寄存器SS(栈的段地址):寄存器SP(偏移地址)指向栈顶元素,push指令和pop指令执行时,CPU从SS和SP中得到栈顶地址
push ax两步走
1> SP=SP-2,SS:SP指向当前栈顶前面的单元,作为新栈顶
2> 将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶
入栈时,栈顶从高地址向低地址方向增长。
pop ax两步走
1> 将SS:SP指向的内存单元处数据送入ax
2> SP=SP+2,SS:SP指向当前栈顶下面的单元,作为新的栈顶
值得注意的时,出栈后pop操作前的栈顶元素仍然存在,但是它已经不在栈中,再次push后会在那里写入新数据覆盖
由上面数据不在栈中进而可以思考栈顶超界的问题
8086CPU不保证我们对栈的操作不会超界
当我们把一段内存当作栈空间,当栈满时再执行push栈顶超出栈空间,栈空间外数据被覆当栈空时再次执行pop栈顶超出了栈空间,而超出的地方的数据会被覆盖,自己需要注意
用栈暂存以后需要恢复寄存器中的内容时,出栈顺序和入栈顺序相反
push和pop实质上是一种内存传送指令
编程时根据需要可定义栈段
段的综述
对于数据段,将它的段地址放在DS中,用mov、add、sub等访问内存单元的指令时,CPU就将我们定义的数据段中内容当作数据来访问
对于代码段,将它的段地址放在CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码段中的指令
对于栈段,将它的地址放在SS中,将栈顶单元的偏移地址放在SP中,这样CPU进行栈操作时,将我们定义的栈段当作栈空间用
由此可见CPU将内存中内容当作什么,是因为相应的段寄存器指向了那里
4.第一个程序
一个源程序从写出到执行的过程
第一步:编写汇编源程序
第二步:对源程序进行编译链接
使用汇编语言编译程序对源程序文件中的源程序进行编译,产生目标文件;再用链接程序对目标文件进行链接,生成可在操作系统中直接运行的可执行文件。
可执行文件包含两部分内容
1)程序(从源程序中的汇编指令翻译过来的机器码)和数据(源程序定义的数据)
2)相关的的描述信息(比如,程序多大、占多少内存空间等)
第三步:执行可执行文件中的程序
操作系统依照可执行文件的描述信息,将可执行文件中的机器码和数据载入内存,开始相关初始化,然后由CPU执行
源程序
汇编语言写的源程序,包括伪指令和汇编指令,其中伪指令由编译器来处理,程序是指源程序中由计算机执行、处理的指令或数据
伪指令:
1>segment 和 ends成对使用,功能是定义一个段,segment 说明一个段开始,ends 说明一个段结束
格式: 段名 segment
.
段名 ends
2>end是一个汇编程序的结束标志,编译器碰到它就结束编译,注意区分ends
3>assume 含义为假设。它假设某一段寄存器和程序中的某一个用segment...ends定义的段相关联。
比如code segment ... code ends就定义了一个名为code的段
在程序开头,用assume cs:code 将用作代码段的段code和CPU中的段寄存器cs 联系起来
DOS(一个单任务操作系统)
一个程序p2在可执行文件中,则必须有一个正在运行的程序p1,将p2从可执行文件中加载入内存,将CPU的控制权交给p2,p2才能运行。p2开始运行后,p1暂停运行
而当p2运行完,CPU控制权应交还给p1
这个过程叫做:程序返回
任何通用的操作系统,都要提供一个称为shell(外壳)的程序,用户使用这个程序来操作计算机系统进行工作
DOS启动时,先完成其他重要初始化工作,然后运行command.com,command.com运行后,执行完其他的相关任务后,在屏幕上显示出由当前盘符和当前路径组成的提示符
比如C:\
用户输入的指令 cd dir 等由command执行
执行一个程序,command 首先根据文件名找到可执行文件,然后将可执行文件加载入内存,设置CS:IP指向程序的入口。此后,command 暂停运行,CPU运行程序。程序运行结束后,返回到command中,command 再次显示由当前盘符和当前路径组成的提示符,等待用户输入
在DEBUG中,command将debug加载入内存,而debug将程序加载入内存,所以程序结束后返回到debug中,Q可以返回到command
mov ax,4c00h
int 21h
这两条指令所实现的就是程序返回,在程序末尾使用
edit
编辑程序
masm
汇编编译器,接收默认文件扩展名为 .asm,如果不是就要将文件扩展名写出
输入源程序文件名要指明路径,除非它就在当前路径下
masm 1t.asm / masm 1t
简化过程,最后加上 ; 忽略中间文件的生成
link
链接器,接收默认文件扩展名 .obj,如果不是就要将文件扩展名写出
对编译生成的目标文件进行链接,从而得到可执行程序。 输入目标文件名要指明路径,除非它就在当前路径下
link 1t.obj / link 1t
简化过程,最后加 ; 忽略中间文件的生成
学习汇编主要目的,通过用汇编语言进行编程而深入地理解计算机底层的基本工作机理,达到可以随心所欲控制计算机的目的。汇编语言编程用到的工具在操作系统是运行,暂时不做过多探究
Debug
数据在Debug中默认所有数据用十六进制表示
遇到int 21h时要用P命令执行
载入.EXE
DOS系统中.EXE文件中程序加载,cx 中存放了程序的长度
.exe装入内存后,程序被装入内存的什么地方?
5.[BX]和loop指令
1.bx
用[0]表示一个内存单元时,0表示单元的偏移地址,段地址默认在ds中,单元的长度(类型)可以由具体指令中的其他操作对象(比如寄存器)中指出
[bx]同样也表示一个内存单元,它的偏移地址在bx中
inc bx的含义是bx中的内容加1,执行后bx = 2
2.loop
正如它的意思循环
loop指令的格式是: loop 标号
CPU执行 loop指令时两步走(这里圆括号代表一个寄存器或内存单元的内容)
1>(cx)=(cx)-1
2>判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行
通常我们用loop指令来实现循环功能,cx中存放循环次数
程序框架如下
mov cx,循环次数
s:
循环执行的程序段
loop s
调试/执行程序时
大于9FFFh的十六进制数据A000H、A001H...FFFFH在书写时以字母开头,但在汇编源程序中,数据不能以字母开头,所以前面要加0,比如0A001H
在程序执行时 loop s 中的标号 s 已经变为了一个地址
我们只想跟踪循环的过程时,可以用DUBUG里命令G来达到目的,一次执行完标号前的内容,g 0012表示执行程序到当前代码段(段地址在cs中)的0012h处
当进入循环后,我们想要循环一次执行完,可以用p命令来达到目的,DEBUG就会自动重复执行循环中指令,直到(cx)=0为止
Debug和汇编编译器masm对指令不同处理
在Debug中,mov ax,[0] 表示将ds:0处的数据送入ax中
但在汇编源程序中,这个指令被编译器当作指令mov ax,0处理
Dubug将它解释为idata是一个内存单元
编译器将[idata]解释为 idata
目前的方法是将偏移地址送入bx寄存器中,用[bx]的方式来访问内存单元
但是这样比较麻烦,还有一种方法是在[ ]的前面显式地给出段地址所在的段寄存器
段前缀
用于显式地指明内存单元的段地址的ds: cs: ss: es:,在汇编语言中成为段前缀
比如访问2000:0单元
mov ax,2000h
mov ds,ax
mov al,ds:[0]
loop和[bx]的联合应用
在实际编程中,经常会遇到用同一种方法处理地址连续的内存单元中的数据问题。我们需要每次循环的时候,按照同一种方法来改变要访问的内存单元的地址
mov al,[bx] 中bx就可以看作一个代表内存单元地址的变量,我们可以通过改变 bx 中的数值,改变访问的内存单元
计算ffff:0~ffff:b单元中的数据和,结果存储在ds中
1.运算和的结果是字节型数据,范围在0-255之间,12个结果相加不会大于65535,dx能放下
2.不能将ffff:0~ffff:b中的数据累加到ds中,在这里面数据是8位的,不能直接加到16位寄存器dx中,也不能累加到dl中,dl会进位丢失
解决方案
用一个16位寄存器做中介,将内存单元中的8位数据赋值到一个16位寄存器ax中,再将ax中的数据加到dx上,从而使两个运算对象的类型匹配并且结果不会超界
assume cs:code
code segment
mov ax,0ffffh
mov ds,ax
mov bx,0
mov dx,0
mov cs,12
s:
mov al,[bx]
mov ah,0
add dx,ax
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end
一段安全的空间
8086模式中,随意向一段空间写入内容很危险,这段空间可能存放重要的系统数据或代码
dos和其他合法程序一般都不会使用0:200~0:2ff的256字节空间,使用这段空间是安全的,谨慎起见我们还可以debug查看
6.包含多个段的程序
在代码段中使用数据
之前提到的那一段安全的空间只有256字节,我们需要超过256个字节的空间该怎么办?在操作系统的环境中,合法地通过操作系统取得地空间都是安全的,因为操作系统不会让一个程序所用的空间和其他程序以及系统自己的空间相冲突。
程序取得所需空间的两种方法:
1.在加载程序的时候为程序分配
2.在程序执行的过程中向系统申请
我们若要一个程序在被加载的时候取得所需的空间,则必须要在源程序中做出说明
当可执行文件中的程序被加载入内存时,这些定义的数据同时也被加载入内存。与此同时,我们要处理的数据自然而然地获得了存储空间
dw 0123h,"dw"的含义是定义字形数据即define word
编程计算0123h、0456h、0789h、0abch、0defh、0cbah、0987h的和,结果存在ax中
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0cbah,0987h
start: mov bx,0
mov ax,0
mov cx,8
s: add ax,cs:[bx]
add bx,2
loop s
mov ax,4c00h
int 21h
code ends
end start
start这个标号在end后出现。伪指令end除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。end指令指明了程序的入口在标号start处
伪指令end描述了程序的结束和程序的入口。在编译、链接后,由"end start"指明的程序入口,被转化为一个入口地址,存储在可执行文件的描述信息中
利用这种方法可以安排程序框架
assume cs:code
code segment
数据
start:
代码
code ends
end start
在代码段中使用栈
我们需要栈空间,当然也要由系统分配,正如上面定义数据,数据就能载入内存。所以可以在程序中通过定义数据来取得一段空间,然后将这段空间作为栈空间使用
dw 0,0,0,0,0,0,0,0
之后合理设置栈顶ss:sp,这段空间就可以当作栈空间
所以描述dw的作用时,可以说用它定义数据,也可以说用它开辟内存空间
将数据、代码、栈放入不同空间
上述内容将他们放在一起程序显得混乱,用到栈空间也小,代码不长,放在一个段没问题(8086模式一个段的容量不能大于64kb)
所以考虑用多个段存放数据、代码
assume cs:code,ds:data,ss:stack
data segment
dw 0123h,0456h,0789h,0abch,0defh,0cbah,0987h
data ends
stack segment
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,20h
mov ax,data
mov ds,ax
mov bx,0
mov cx,8
s: push [bx]
add bx,2
loop s
mov bx,0
mov cx,8
s0: pop [bx]
add bx,2
loop s0
mov ax,4c00h
int 21h
code ends
end start
定义多个段的方法和定义一个段方法一样
对段地址的引用:在程序中段名就相当于一个标号,它代表了段地址
我们定义了三个段,作用如同的名字含义,但是计算机不知道
我们只需设置start的位置在code段(cs:ip),ss:sp指向栈顶,ds指向data段,其他寄存器如bx存放data段中数据的偏移地址,即可按照我们的含义分段了
7.更灵活的定位内存地址的方法
and指令:逻辑与指令,按位进行与运算(对应位全1为1,不然为0)
例如指令
mov al,01100011B
and al, 00111011B
执行后al = 00100011B
通过该指令可将操作对象的相应位设位0,其他位不变
or指令:逻辑或指令,按位进行或运算(对应位有1为1,全0为0)
例如指令
mov al,01100011B
or al, 00111011B
执行后al = 01111011B
通过该指令可将操作对象的相应位设为1,其他位不变。
我们要把人能看懂的信息存储在计算机中,就要对其及进行编码,将其转化为二进制信息进行存储。计算机要将存储的信息显示出来看就需要对其进行解码。
一个文本编辑过程中,就包含着按照ASCII编码规则进行编码和解码:
键入"a"计算机用ASCII码的规则对其进行编码,将转化为61H存储在内存指定的空间中;文本编辑软件从内存中取出61H,将其送到显卡的显存中;工作在文本模式下的显卡,用ASCII码的规则解释显存中的数据,61H当作字符"a",显卡驱动显示器,将字符"a"图像画在屏幕上
汇编程序中'......'的方式指明数据是以字符的形式给出的,编译器将其转换对应ASCII码
db 'unIX'相当于db 75H,6EH,49H,58H
大小写转换问题
首先分析:每个小写字母的ASCII码值比大写字母ASCII码值大20H,通过此方法可以将小写字母转化为大写,可这要先判断字母本身是否是大小写,但目前水平没有达到
再分析:字母ASCII码的二进制,除第五位(从0开始计数)外,大写字母和小写字母其他各位都一样。大写字母第五位为0,小写字母第五位为1
因此可以用and 和 or指令将第五位置0或置1来改变大小写
[bx+idata]
[bx]可以指明一个内存单元,[bx+idata]更灵活的指明内存单元,偏移地址(bx)+idata
段地址在ds中
db 'BaSiC'
我们要把这个字符串全部转为大写,可以把这个字符串看作一个数组,首地址就是B
SI和DI
si和di是8086CPU和bx功能相近的寄存器,si和di不能分成两个8位寄存器来使用。
[bx+si]和[bx+di]
都表示一个内存单元,偏移地址是(bx)+(si),段地址在ds中
[bx+si+idata]和[bx+di+idata]
都表示一个内存单元,偏移地址位(bx)+(si)+idata,段地址在ds中
以上就是CPU提供的多种寻址方式
当我们需要汇编中的循环嵌套时,可以将外层循环的cx数值保存起来,在执行外层循环loop指令前,再恢复外层循环cx数值。
一般来说,在需要暂存数据的时候,我们应该用栈
寻址方式的适当使用,是我们可以以更合理的结构来看待所要处理的数据。而为所要处理的看似杂乱的数据设计一种清晰的数据结构是程序设计的一个关键问题