8086汇编语言基础
前言
本篇文章是对王爽老师的《汇编语言(第三版)》所做的笔记与总结,格式比较随意,权当复习参考之用
基础
编译器:将汇编语言转换为机器语言
汇编语言的组成
- 汇编指令:机器码的助记符,有对应的机器码
- 伪指令:没有对应的机器码,由编译器执行
- 其他符号:如+,-,*,/等,由编译器识别,没有对应的机器码
指令和数据:一段二进制信息,可以被看做数据或者看做程序
CPU读写数据:CPU读写数据时,与外部设备交互存储单元的地址(地址信息),器件信息,读或写的命令(控制信息),读或写的数据(数据信息)
总线:CPU与其他芯片的导线,分为地址总线,控制总线,数据总线
地址总线
地址总线宽度为N,则CPU寻找的范围为2^N^个内存单元
数据总线
数据总线的宽度决定了数据传输的速度
控制总线
控制总线的宽度决定了CPU对外部设备的控制能力,其中有一根读信号输出线,低电平表示将要读取数据,写信号输出线负责传输写信号
存储器芯片
- 随机存储器(RAM):用于存放CPU使用绝大部分程序和数据,分为主板上的RAM和插在扩展插槽上的RAM
- 装有BOIS的ROM(装有基本输入输出系统的只读存储器):可以通过各类主板和接口卡上的BOIS使用该设备进行基本的输入输出
- 接口卡上的RAM:接口卡对大批量输入输出数据进行暂时存储,用于与设备进行输入输出
内存地址空间:CPU地址总线可以寻到的所有存储单元组成CPU的内存地址空间,对于各类接口卡,CPU通过总线控制它们,将它们看做一个由多个存储单元组成的逻辑存储器
寄存器
通过改变各种寄存器中的内容来实现对CPU的控制
通用寄存器:用于存放一般性数据
对于一个16位寄存器来说,存储的数据可以看做16位二进制,也可以看做一个字
字分为高位字节和低位字节,如ax分为ah,al,两个字节为8位,可以将它们看做单独的两个寄存器,做运算时产生的进位不影响其他位
16位机的含义:位数描述了CPU的几个结构特性
- 运算器最多一次可以处理16为的数据
- 寄存器的最大宽度为16位
- 寄存器和运算器之间的通路为16位
CPU给出物理地址的方法
CPU在地址总线上传输的是内存单元的物理地址,CPU首先需要在内部生成这个物理地址,CPU给出物理地址的方法可以概括为基础地址+偏移地址=物理地址,基础地址(段地址)由段寄存器提供(8086CPU有4个段寄存器:CS,DS,SS,ES)
流程:CS,IP中的段地址和偏移地址传入到地址加法器,将物理地址传入输入输出控制电路,从内存中读取指令,传回指令寄存器,执行指令,CPU只认为CS和IP所指的地址为指令
修改CS和IP的指令:jmp 段地址:偏移地址
,该指令只能使用在调试模式
仅改变IP的指令:jmp ax
内存访问
内存中字的存储
字单元:存放一个字形数据(16位)的内存单元,由两个地址连续的内存单元组成,高地址单元存放高位字节,低地址单元存放低位字节
DS和[address]
8086CPU有一个DS寄存器,用于存放要访问数据的段地址
mov ax [0]
:将DS中的段地址+中括号里的偏移地址所指定的内存单元传入ax
设置DS中的地址:需要将地址传入其他寄存器,再从其他寄存器传入DS,mov ds ax
mov,add,sub指令
mov指令
mov register data
mov register1 register2
mov register memory
,内存单元用中括号包围mov memory register
mov segment-register register
add指令
add register data
add register1 register2
add register memory
add memory register
sub指令
sub register data
sub register1 register2
sub register memory
sub memory register
栈
8086CPU提供出栈和入栈的指令:push/pop
出栈和入栈的单位为字
8086CPU中的两个寄存器SS(段寄存器)和SP存储栈顶元素的地址,入栈时,栈顶从高地址向低地址增长
SS:SP初始时指向栈空间最高地址(栈底)的下一个单元
SS:SP不会记忆栈空间的大小,只知道当前执行的地址,需要注意操作时不要越界
创建栈(创建10000到1000f为栈空间)
1
2
3
mov ax,1000
mov ss,ax
mov sp,0010
在debug模式调用t指令时,执行mov ss,ax
后会继续执行mov sp,0010
push指令
push register
push segment-register
push memory
,注意栈操作以字为单位(16位,两个8位字节)
pop指令
pop register
pop segment-register
pop memory
Debug模式
- r指令:查看当前寄存器状态,
r register
,修改寄存器中的值 - d指令:查看指定内存的内容,显示为十六进制
d 1000:0 f
,显示1000:0000到1000:000f的内容d cs:0
,查看当前代码段中的内容
- e指令:改写内存中的内容
e 1000:0 data1 data2 ...
,从1000:0000开始写入数据e 1000:0
,以提问形式逐个单元写入十六进制
- u指令:将内存中的机器码翻译为汇编语言,
u 1000:0
,查看1000:0000开始的汇编代码 - a指令:在内存中写入汇编代码,
a 1000:0
,从1000:0000开始写入汇编代码 - t指令:执行当前CS:IP指向的内存地址中的代码
程序编写
- 编写汇编代码源文件
- 对源文件进行编译和链接,产生机器码,存储在可执行文件
- 执行可执行文件
伪指令
伪指令没有对应的机器指令,由编译器执行,编译器根据伪指令来进行编译
segment-ends用于标注一个段,格式为
1
2
3
code_name segment
...
code_name ends
end标识整个程序的结束,在end处结束编译
assume表示将某一段寄存器与程序中的某一个段相关联,assume segment_register:code_name
程序返回:mov ax,4c00h int 21h
,汇编中十六进制数字后面加h,数据不能以字母开头,需要在前面加0
程序执行过程
汇编程序从编写到执行的过程
编写(1.asm)——编译masm(1.obj)——连接link(1.exe)——加载——内存中的程序——运行(CPU)
调试程序指令:debug 1.exe
调试时使用p命令执行int 21h
,q命令退出debug模式
[bx]和loop指令
[bx]
[bx]表示bx中的偏移地址,与DS的段地址组成物理地址
inc bx
:将bx的内容+1
dec bx
:将bx的内容-1
编译器和debug模式对[idata]的执行情况不同
- debug模式将[idata]解释为用常量表示偏移地址
- 编译器将[idata]解释为数字常量,在[idata]前加段地址
mov ax,ds:[0]
即可表示偏移地址 - 两个模式对[bx]的解释相同
loop
标号:s: add ax,ax
,标号标识了一个地址,该地址处有一条指令
使用loop:loop s
,将cx作为循环计数器(loop指令默认cx为计数器),执行的代码段放在标号和loop之间,loop放在标号之后,注意在loop之前,代码已经执行过一次,相当于do-while
- (cx)=(cx)-1
- 判断cx的值,若不为0,则跳转到标号处,否则跳出循环
debug模式跳转到指定位置执行:g 偏移地址(ip)
自动结束循环:p,在loop指令处调用
多个段的程序
定义字型数据:dw 0123h,0456h
,系统在该指令地址处分配空间存放数据
定义字节型数据:db 'abc'
在程序前定义数据会出现程序入口不确定的问题
使用标号标注程序的入口,end start
表示结束start处的程序
定义多个段分别存放数据、代码、栈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
assume cs:code,ds:data,ss:stack
data segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,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
定位内存地址
and和or指令
1
2
and al,00111011b
or al,00111011b
将输入值与寄存器中的值进行按位与/或运算,存储在第一个寄存器参数中
字符串处理
字符和字符串以ASCII码的形式存储在寄存器中,数据段中使用db定义字符串空间
大小写转换:大写字母的二进制第5位为0(从0开始),小写字母的二进制第5位为1,运用and和or指令将字母的二进制第5位设置为0或1,即可转换大小写
[bx+idata]
可以使用bx中的地址加上一个偏移量表示一个内存单元,物理地址为(ds)*16+(bx)+idata
其他格式:[200+bx],200[bx],[bx].200
实现数组操作:idata[bx]
,ds为起始段地址,idata为数组相对于ds的起始地址,bx作为索引
si,di寄存器:si和di为8086CPU中与bx功能相近的寄存器,它们不能分为两个8位寄存器
其他偏移格式
[bx+si]表示一个内存单元,物理地址为
(ds)*16+(bx)+(si)
其他格式:
[bx][si]
[bx+si+idata]
,物理地址为(ds)*16+(bx)+(si)+idata
其他格式:
[bx+si+200],[200+bx+si],200[bx][si],[bx].200[si],[bx][si].200
当程序中多个数据(如多层循环计数器)需要暂存时,应该使用栈
数据处理
寄存器类型
reg:ax,bx,cx,dx,ah,al,bh,bl,ch,cl,dh,dl,sp,bp,si,di
sreg:ds,ss,cs,es
bx,si,bp,di可用于偏移地址寻址,bx和bp不能同时在一个中括号中寻址,si和di同理
数据的表达
数据位置的表达
立即数
在汇编指令中直接给出的数据,包括数字和字符串,执行前数据存放在CPU内部的指令缓冲器
寄存器
使用寄存器中存储的数据,执行前数据存储在CPU内部的寄存器
段地址:偏移地址(SA:EA)
使用SA:EA索引内存中的数据,执行前数据存储在内存中相应的内存单元中
SA默认为ds中的地址
数据尺寸的表达
8086CPU可以处理两种尺寸的数据:byte,word,byte为8位,word为16位
通过寄存器名指明操作的数据尺寸
通过ax,bx等表明操作的是word,通过ah,al等表明操作的是byte
使用操作符X ptr指明数据尺寸
1 2
mov word ptr ds:[0],1 ;按字操作 mov byte ptr ds:[0],1 ;按字节操作
操作默认的尺寸
栈操作push,pop默认按word操作
其他指令
div指令
用于做除法操作
- 除数:除数为8位或16位,存储在内存或寄存器中
- 被除数:若除数为8位,则被除数为16位,默认存放在ax中,若除数为16位,则被除数为32位,默认存放在dx和ax中,dx存放高16位,ax存放低16位
- 结果:若除数为8位,则结果存放在ax中,al存储商,ah存储余数,若除数为16位,则结果存放在dx和ax中,dx存储余数,ax存储商
1
div reg/memory
提前在ax和dx中设置被除数,div指令传入除数所在的寄存器或内存单元
伪指令dd
dd用于定义双字型数据,占两个word的大小(32位)
dup操作符
dup操作符同db,dw,dd等指令配合使用,用于数据重复
1
2
db 3 dup (0) ;定义了3个字节,内容都为0
db 3 dup (0,1,2) ;定义了9个字节,内容重复0,1,2
转移指令
可以修改IP或同时修改CS和IP的指令称为转移指令
- 只修改IP时,称为段内转移
- 同时修改cs和ip,称为段间转移
转移种类:无条件转移,条件转移,循环指令,过程,中断
操作符offset
offset由编译器处理,用于取得标号的偏移地址
1
mov ax,offset s
jmp指令
jmp为无条件跳转指令,可只修改IP或同时修改cs:ip
1
jmp short label
无条件跳转到标号处执行指令,实现段内短位移,转移的偏移量为8位(-128~127)
CPU在执行jmp指令时不需要知道转移的目的地址,该指令的机器码中包含了目的地址到jmp指令的下一条指令的偏移量
1
jmp near ptr label
该指令表示段内近转移,偏移量为16位(-32768~32767)
1
jmp far ptr label
表示段间转移,机器码中包含标号所在的cs和ip
1
jmp reg
reg为16位寄存器,修改ip为寄存器中的地址
转移地址在内存中的jmp指令
jmp word ptr addr
:指定一个内存单元地址,该内存单元存放一个字大小的目的偏移地址,实现段内转移jmp dword ptr addr
:指定一个内存单元地址,该内存单元存放两个字大小的目的偏移地址,实现段间转移,高位的字作为cs,低位的字作为ip
jcxz指令
jcxz为条件跳转指令,条件跳转指令都是短转移,机器码中包含偏移量(同jmp指令)
1
jcxz label
当cx=0时,跳转到指定标号处,否则向下执行
call和ret指令
call和ret都是转移指令,他们都修改ip或同时修改cs和ip,可用于子程序调用和返回
ret,retf
- ret指令用栈中的数据,修改ip,实现近转移
- retf用栈中的数据,修改cs和ip,实现远转移,先从栈中弹出ip再弹出cs
两个指令使用栈中数据时会同时弹出栈元素
call
1
call label
执行call指令时,CPU将当前的ip压入栈中,之后转移到标号处,实现段内近转移
call指令转移通过偏移量转移类似jmp short label
1
call far ptr label
该指令执行时,CPU先压入当前cs再压入ip,之后转移到标号处,实现段间转移
机器码中包含目的cs和ip,类似jmp far ptr label
1
call reg(16bit)
CPU先压入IP,再执行jmp reg
跳转
转移地址在内存中的call指令
call word ptr memory
:先压入当前ip,再执行jmp word ptr memory
跳转call dword ptr memort
:先压入cs,再压入ip,执行jmp dword ptr memory
跳转
子程序设计
1
2
3
4
5
6
7
8
9
10
11
12
assume cs:code
code segment
main: statements
call s
statements
mov ax,4c00h
int 21h
s: statements
ret
code ends
end main
mul指令
用于乘法,两个操作数应同时为8位或16位
同时为8位时,一个默认放在al中,一个放在8位寄存器或内存单元中,结果默认存放在ax中
同时为16位时,一个默认存放在ax,一个放在16位寄存器或内存单元中,结果高位存放在dx中,低位存放在ax中
参数和结果传递
可以通过寄存器传递参数和返回值
对于多个参数,可以将数据存储在内存中,在子程序中通过数据首地址访问
当主程序寄存器和子程序寄存器冲突时,可以使用栈保存冲突的寄存器内容,返回时弹出内容
标志寄存器
标志寄存器用于存储相关指令的某些执行结果,为CPU执行相关指令提供行为依据,控制CPU的工作方式
标志寄存器中的信息被称为程序状态字
add,sub,mul,div,inc,or,and等运算影响标志位
mov,push,pop等传送指令不影响标志位
- ZF标志:零标志位,若执行结果为0,则ZF=1,否则为0
- PF标志:奇偶标志位,若执行结果每一bit中1的个数为偶数,则PF=1,若为奇数,则PF=0
- SF标志:符号标志位,若执行结果为负数,则SF=1,否则为0
- CF标志:进位标志位,记录了无符号数运算低位向高位的进位值或借位值
- OF标志:溢出标志位,若有符号数运算发生溢出,则OF=1,否则为0
adc指令
adc指令为带进位加法指令,它利用了CF位的进位值
1
adc obj1,obj2 ;obj1=obj1+obj2+CF
sbb指令
sbb指令为带借位减法指令
1
sbb obj1,obj2 ;obj1=obj1-obj2-CF
cmp指令
cmp为比较指令,对标志寄存器产生影响从而影响其他指令的判断结果,立即数必须放在第二个操作数
1
cmp obj1,obj2
执行obj1-obj2,根据结果设置标志寄存器
对无符号数比较的结果
zf=1
:obj1=obj2zf=0
:obj1!=obj2cf=1
:obj1<obj2cf=0
:obj1>=obj2cf=0&&zf=0
:obj1>obj2cf=1||zf=1
:obj1<=obj2
对有符号数比较的结果
sf=1&&of=0
:obj1<obj2sf=1&&of=1
:obj1>obj2sf=0&&of=1
:obj1<obj2sf=0&&of=0
:obj1>=obj2
条件转移指令
配合cmp指令跳转
- je:相等跳转,
zf=1
- jne:不相等跳转,
zf=0
- jb:小于跳转,
cf=1
- jnb:不小于跳转,
cf=0
- ja:大于跳转,
cf=0&&zf=0
- jna:不大于跳转,
cf=1||zf=1
DF标志和串传送指令
DF为方向标志位,在串传送指令中,控制每次操作后si,di的增减
- df=0,si,di递增
- df=1,si,di递减
movsb指令
相当于((es)*16+(di))=((ds)*16+(si))
,之后根据df标志,si,di递增或递减,传输的单位为字节
movsw指令
与movsb同理,传输的单位为字
两个指令与rep指令配合使用
1
2
rep movsb
rep movsw
表示根据cx的值循环执行movsb/movsw
设置df的指令
- cld:令df=0
- std:令df=1
pushf和popf
pushf将标志寄存器压栈,popf将栈中数据弹出到标志寄存器中
flag在debug中的表示
flag | value=1 | value=0 |
---|---|---|
of | OV | NV |
sf | NG | PL |
zf | ZR | NZ |
pf | PE | PO |
cf | CY | NC |
df | DN | UP |
内中断
在CPU执行完当前的指令后,可以检测到从CPU外部或内部发送来的一种特殊信息,使CPU立即对这种特殊信息进行处理,而不继续进行接下来的指令
内中断是CPU内部产生了中断信息
当CPU内部发生以下情况时产生中断信息
- 除法错误,执行div产生除法溢出
- 单步执行,当TF=1时触发
- 执行into指令
- 执行int指令
中断类型码标识了中断信息的来源,用于定位中断处理程序的位置
- 除法错误:0
- 单步执行:1
- into:4
- int n:n为提供给CPU的类型码
中断向量表:记录了中断类型码对应的中断处理程序的入口地址(cs:ip),一个入口地址为两个字,高地址字存放段地址,低地址字存放偏移地址,设类型码为n,偏移地址存放的偏移地址为4n,段地址为4n+2
中断过程
- 从中断信息中取得中断类型码
- 标志寄存器入栈
- 将flag的第8位TF和第9位IF设为0
- cs入栈
- ip入栈
- 从中断向量表中读取处理程序的入口地址
编写中断处理程序
- 使用到的寄存器入栈
- 处理中断
- 使用到的寄存器出栈
- 用iret指令返回,相当于自动完成
pop ip pop cs popf
使一段程序成为中断处理程序
将一段中断处理子程序传输到内存中存储
使用
rep movsb
传输,ds:si
指向源地址,es:di
指向目的地址,cx为传输长度(处理程序的代码长度),cld
设置传输方向为正编译器可以处理立即数表达式,使用加减乘除等符号,表达式中不能存在寄存器
在处理程序的开头和结束设置一个标号,
mov cx,offset func_end-offset func_begin
可以获取处理程序的长度处理程序中需要的字符串等数据应该存放在不会被覆盖的区域,可在处理程序的开头跳转到真正的处理程序,跳转指令后分配字符串等数据的内存空间
设置中断向量表,将对应中断类型的表项设置为子程序的入口地址
int中断
1
int n
n为中断类型码,int指令可以引发指定的中断过程
可手动编写中断例程,将例程的入口地址设置到中断向量表中,通过int指令,手动触发中断,执行中断例程
BIOS和DOS的中断例程
BIOS主要包括
- 硬件系统的检测和初始化程序
- 外部中断和内部中断的中断例程
- 用于对硬件设备进行IO操作的中断例程
- 其他和硬件系统相关的中断例程
中断例程的安装过程
- CPU从ffff:0000开始执行程序,ffff:0有一条跳转指令,跳转到BIOS中的硬件系统检测和初始化
- 初始化程序建立BIOS的中断向量,记录在中断向量表中
- 调用
int 19h
进行操作系统的引导,此后计算机由操作系统控制 - DOS启动后,将DOS提供的中断例程装入内存,建立中断向量
BIOS和DOS的中断例程用ah来传递中断例程中执行的子程序的编号,用int指令触发
int 21h
程序返回例程
21h号中断例程中的4ch号子程序为程序返回程序,使用ah传输子程序的编号,al传输程序的返回值
端口
主板上的接口芯片和各种接口卡的接口芯片都有一组由CPU读写的寄存器,它们都和CPU的总线相连,CPU对它们进行读写时通过控制线向它们所在的芯片发出端口读写命令
端口读写命令只有in,out两条指令,分别向端口读取数据和向端口写入数据
只能使用ax或al来存放从端口读取的数据和要向端口写入的数据,访问8位端口时使用al,16位端口使用ax
shl和shr指令
shl和shr是逻辑移位指令
1
2
shl reg,reg1
shr reg,reg1
reg1中的值为n,将寄存器中的值左/右移n位,最后移出的一位写入CF中,最低/最高位补0
移位位数必须放在cl中
CMOS RAM芯片
该芯片包含一个实时钟和一个有128个存储单元的RAM存储器,RAM中0-0dh用来保存时间信息,其余大部分用于保存系统配置信息,系统启动时供BIOS读取
该芯片提供两个端口,分别是70h和71h,通过这两个端口来读写CMOS,70h为地址端口,存放要访问的RAM单元的地址,71h为数据端口,存放从选定单元中读取的数据或向选定单元写入的数据
外中断
由CPU外部产生的中断信息,外中断源共有两类
可屏蔽中断
CPU可以不响应的中断,根据TF来决定是否响应中断,IF=1响应中断,IF=0不响应中断,在进入中断例程时使TF=0,IF=0可以屏蔽其他可屏蔽中断,若需要在进入中断例程后处理可屏蔽中断,可将IF=1
- sti:IF=1
- cli:IF=0
不可屏蔽的中断
CPU必须处理的外中断,对8086CPU,不可屏蔽中断的中断类型码固定为2
键盘的处理过程
键盘扫描
键盘中的芯片对键盘上的每一个键进行扫描,按下按键,芯片产生一个通码,松开按键,芯片产生一个断码,两种扫描码被送入芯片的寄存器中,寄存器端口地址为60h,读取键盘输入
in al,60h
扫描码的长度为一个字节,通码的第7位为0,断码的第7位为1,断码=通码+80h
引发9号中断
执行9号中断例程
直接定址表
定义数据长度的标号
1
2
3
4
5
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dd a,b
data ends
这种标号表示了地址,还表示了单元长度,如a表示了地址code:0
,还表示了从该地址开始之后的单元都是字节单元
加冒号的标号只能在代码段中使用,在其他段中不能使用
标号还可以表示一个或一组内存单元,在指令中标号b表示code:[8]
的内存单元,标号a表示一组内存单元,通过类似数组索引的方式访问a[idata]
,等价于code:0[idata]
,标号c处存储a和b处的偏移地址和段地址,等价于
1
c dw offset a,seg a,offset b,seg b
seg运算符取得某一标号的段地址
若为dw类型,则存储偏移地址
使用标号访问数据段中的数据必须将标号所在的段与段寄存器联系起来
直接定址表
直接获取或计算得到要查数据所在的位置的表称为直接定址表
将表定义在数据段中,通过标号可直接获取到数据,简化程序或加快运算速度
表中存储不同子程序的入口地址,从而实现方便地调用不同的子程序
BIOS进行键盘输入和磁盘读写
int9中断例程
int9中断例程将从键盘读出的扫描码转化为相应的ASCII码或状态信息,存储在键盘缓冲区或状态字节中
键盘缓冲区的逻辑结构为循环队列
int9例程从端口读出按键的通码,然后检测状态字节,查看是否有Shift、Ctrl等控制键按下,将按键的扫描码和对应的ASCII码存到缓冲区的字单元中
该例程在按键按下时调用
int16h中断例程
int16h中断例程中的0号子程序从键盘缓冲区中读取数据,读取的数据存放在ax中,高位存放扫描码,低位为ASCII码
例程先检查缓冲区中是否存在数据,有数据时读取第一个数据,存入ax,同时将缓冲区中的数据删除
该例程在程序需要读取键盘时调用,与int9调用的时刻不同
int13h中断例程
int13h例程用于对磁盘进行访问,通过控制磁盘控制器来访问磁盘,读写以扇区为单位
参数
- ah:功能号,2表示读扇区,3表示写扇区
- al:操作的扇区数
- ch:磁道号
- cl:扇区号
- dh:磁头号(面号)
- dl:驱动器号
- es:bx:指向接收从扇区读入的数据的内存区
- 返回值
- 成功:ah=0,al=操作的扇区数
- 失败:ah=出错代码