汇编学习笔记(by 小白奋斗ing)
参考《汇编语言(第三版)》王爽著
1.基础知识
1.1 机器语言
二进制编码
1.2 汇编语言
1.3 汇编语言的组成
1.4进制表示符
二进制(B),十六进制(H)
2.寄存器
2.1通用寄存器
8086CPU的所有寄存器都是16位的,可以存放两个字节。AX、BX、CX、DX这4个寄存器通常用来存放一般性的数据,被称为通用寄存器。
8086CPU上一代CPU中的寄存器是8位的,为保证兼容,上述四个寄存器可以分为两个可独立使用的8位寄存器使用:
例如:AX可分为AH和AL
8个高位构成AH,8个低位构成AL
例如:
2.2字在寄存器中的存储
字节:byte,1 byte=8 bit,刚好存在8位寄存器中
字:word,1 word=2 byte,两个字节分别占用字的高位和低位,一个字刚好存在16位寄存器中
2.3几条汇编指令
例题运算:
ax=ax+bx=8226H+8226H=1044CH,保留044CH
al=C5H+93H=158H,保留58H,故ax=0058H
此处有规律,比如ax=2640H,则ah=26H,al=40H,在做运算时,可以将ax拆成ah
和al进行运算。
2.4物理地址
我们知道,CPU访问内存单元时,要给出内存单元的地址。所有的内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间中都有唯一的地址,我们将这个唯一的地址称为物理地址。
内存单元的存储空间的线性空间地址就是物理地址
2.5 16位结构的CPU
2.6 8086CPU给出物理地址的方法
补充:一个X进制的数据左移1位,相当于乘以X
2.7 “段地址x16+偏移地址=物理地址”的本质含义
CPU最大为16位,不能直接传出20位的数字信息,利用地址偏移将20位数字拆分为两个16位的数字信息进行传输
2.8 段的概念
CPU对内存进行分段处理(内存本身没有分段)
2.9 段寄存器
段地址需要存放在8086CPU的段寄存器中。8086CPU有4个段寄存器:CS、DS、SS、ES。8086CPU访问内存是,从这四个段寄存器中获取内存单元段地址
CS 代码寄存器 CS:IP指定CPU处理的地址
DS 数据段寄存器 DS:[···]指定内存段的位置
SS 堆栈段寄存器 SS:SP指定栈顶的位置
ES 附加段寄存器
2.10CS和IP
CS为代码段寄存器,IP为指令指针寄存器。其中设CS中内容为M,IP中内容为N,8086CPU将从内存Mx16+N单元开始,读取一条指令执行。
运行示例:
初始状态获取CS:2000H,IP:0000H
CS、IP放入地址加法器运算2000Hx16+0000H=20000H
数据传入输入输出控制电路、再传入地址总线
CPU通过数据总线从内存20000H
开始读取数据
输入输出控制电路将机器指令送入指令缓冲器
读取一条指令后,IP自动增加(当前读入B82301
字节大小为3,故增加3字节)。也就是CPU控制内存从哪里开始读取
执行控制器执行指令,则AX被赋值为0123
。这里可以看出CPU并非在结果出现后才进行控制下一次运算的,而是进入指令缓冲器后就开始执行下一次。
8086CPU工作简要概括为:
8086CPU刚开始工作(或复位时),CS=FFFFH,IP=0000H,故FFFF0H单元中的指令是8086PC机开机执行的第一条指令
2.11 修改CS、IP的指令
mov被称为传送指令
可以修改CS、IP内容的指令被称为转移指令
jmp指令可以修改CS、IP的内容
格式jmp 段地址:偏移地址
即jmp CS:IP
jmp ax
含以上类似mov IP,ax
,作用是只修改IP的值
例题:
该题注意要点==jmp只改变IP的值
==,这题最后会陷入循环中
2.12 代码段
代码段是人为定义的,想要让CUP执行这段代码,就必须指定CS=123BH,IP=0000H
实验debug
debug参数:
R查看、改变CPU寄存器的内容
D查看内存中的内容
E改写内存中的内容
U将机器指令翻译成汇编指令
T执行一条机器指令
A以汇编指令的格式在内存中写入一条机器指令
3.寄存器(内存访问)
3.1 内存中字的储存
CPU中用16位寄存器存储一个字,8个高位存放在高位字节,8个低位存放在低位字节。而在内存中存储,则是一个单元存放一个字节,一个字要用两个地址连续的内存单元来存放,低位字节放在低地址单元中,高位字节放在高地址单元中。
在上图中,若存放一个字,由0、1两个字节单元组成,对于字单元来说,0号单元是低地址单元,1号单元是高地址单元。以此类推,起始地址相对后续地址单元为低地址值单元,后续单元以此类推
字单元:即存放一个字型数据(16位)的内存单元,有两个地址连续的内存单元组成。高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。
例题:对于图3.1
3.2 DS和[address]
ds 为内存单元的段地址
[···]表示一个内存单元,[0]表示内存单元偏移地址为0
mov al,[0] //8086CPU中将(ds:0-ds:1)的数据读到al中
mov [0],al //将数据从寄存器送入内存单元,al的16进制值赋值给10000H的内存地址
ds属于段寄存器,在8086CPU中不能直接将数据送入ds,需要使用一个寄存器进行中转
3.3 字的传送
8086CPU是16位结构,有16根数据线,可以一次性传送16位的数据。
sub ax,bx //ax的值减去bx
ds的值设定后,后续内存单元[···]自动认为在ds段地址中,这里看ds有点像全局变量
3.4 mov、add、sub指令
mov指令的形式
mov 寄存器,数据 mov ax,8
mov 寄存器,寄存器 mov ax,bx
mov 寄存器,内存单元 mov ax,[0]
mov 内存单元,寄存器 mov [0],ax
mov 段寄存器,寄存器 mov ds,ax
mov 寄存器,段寄存器 mov ax,ds
add指令的形式
add 寄存器,数据 add ax,8
add 寄存器,寄存器 add ax,bx
add 寄存器,内存单元 add ax,[0]
add 内存单元,寄存器 add [0],ax
sub指令的形式
sub 寄存器,数据 sub ax,8
sub 寄存器,寄存器 sub ax,bx
sub 寄存器,内存单元 sub ax,[0]
sub 内存单元,寄存器 sub [0],ax
3.5 数据段
一组长度为N(N<=64KB)、地址连续、起始地址为16的倍数的内存单元当作专门存储数据的内存空间,定义为一个数据段。
检测点3.1
(1)
mov ax,1 //ax=0001H
mov ds,ax //ds=0001H
mov ax,[0000] //ax=2662H (ax等于0001:0000-0001:0001地址位的值,0001:0000-0001:0001地址位等于00010H-00011H,同时可以发现0000:0010-0000:0011)
mov bx,[0001] //bx=E626 (同上)
mov ax,bx //ax=bx=E626
mov ax,[0000] //ax=2662H
mov bx,[0002] //bx=D6E6H
add ax,bx //ax=ax+bx=FD48H
add ax,[0004] //ax=ax+2ECCH=2C14H
mov ax,0 //ax=0000H
mov al,[0002] //al=E6H,故ax=00E6H
mov bx,0 //bx=0000H
mov bl,[000c] //bl=26,故bx=0026H
add al,bl //al=al+bl=E6+26=0CH,故ax=000CH
通过检测点3.1发现一个有趣的现象:0000:0010
=0001:0000
(2)
①②
初始值为CS=2000H,IP=0,DS=1000H,AX=0,BX=0,程序从CS:IP开始运行
mov ax,6622H //ax=6622H
jmp 0ff0:0100 //CS:IP=0ff0:0100,跳转内存地址10000H
mov ax,2000H //ax=2000H
mov ds,ax //ds=2000H
mov ax,[0008] //ax=C389
mov ax,[0002] //ax=EA66
3.6 栈
栈的特点是:先进后出,后进先出
3.7 CPU提供的栈机制
PUSH(入栈)和POP(出栈)
push ax
表示将寄存器ax中的数据送入栈
pop ax
表示从栈顶取出数据送入ax
段寄存器SS和寄存器SP存放栈顶的位置,其中栈顶的段地址存放在SS中,偏移地址存放在SP中,任意时刻,SS:SP指向栈顶元素,在执行push和pop指令时,CPU从SS和SP中得到栈顶的地址。
在8086CPU中,入栈时,栈顶从高地址向低地址方向增长。
注意,在pop指令中,虽然在SP=SP+2时,1000C-1000D中的值已经赋值给ax了,但它依然存在内存单元中,只是已经不在栈中,在下一次入栈时,将会被覆盖
问题3.6
当栈为空时,SS=1000H,SP=10H
3.8 栈顶超界的问题
在8086CPU中,没有单独的寄存器负责栈是否超界的问题,需要开发者自行规范代码
超出栈的后果:pop在栈超界后,会导致大于10020H的地址从其他栈中释放出来,后续再用push时,将会出现数据覆盖的现象。
3.9 push、pop指令
push 寄存器 ;将一个寄存器中的数据入栈
push 段寄存器 ;将一个寄存器中的数据入栈
push 内存单元 ;将一个内存字单元处的字入栈(栈操作以字为单位)
pop 寄存器 ;出栈,用一个寄存器接收出栈的数据
pop 段寄存器 ;出栈,用一个段寄存器接收出栈的数据
pop 内存单元 ;出栈,用一个内存单元接收出栈的数据
问题3.7
mov ax,1000
mov ss,ax ;设置栈的段地址,不能直接向ss中传数据
mov sp,0010 ;设置栈顶的偏移地址,sp=0010H
push ax
push bx
push cx
问题3.8
mov ax,1000
mov ss,ax
mov sp,0010
mov ax,001a
mov bx,001b
push ax
push bx
sub ax,ax ;将ax清零
;sub ax,ax的机器码为2个字节
;mov ax,0的机器码为3个字节
sub bx,bx
pop bx
pop ax
问题3.9
mov ax,1000
mov ss,ax
mov sp,1010
mov ax,001a
mov bx,001b
push ax
push bx
pop ax
pop bx
问题3.10
# 补充代码
mov ax,1000
mov ss,ax
mov sp,2 ;根据sp-2写入原则,将初始地址定义到10002H上,则写入时,可以从10000H开始写入
3.10 栈段
在8086CPU中将一组内存单元定义为一个段。例如,将长度为N(N<=64KB)的一组地址连续、起始地址为16的倍数的内存单元,当做栈空间,从而定义一个栈段。
比如,将10010H~1001FH这段长度为16字节的内存空间作为栈来用,以栈的方式访问,则这个空间就成称为一个栈段,段地址为1001H,大小为16字节
问题3.11
问题3.12
根据问题3.11的结论,可以明显看出出,一个栈的大小,取决于SP的容量(相对于SP)
检测点3.2
(1)
mov ax,2000
mov ss,ax
mov sp,0010
(2)
mov ax,1000
mov ss,ax
mov sp,0000
实验2 用机器指令和汇编指令编程
1、预备知识:Debug的使用
d 段寄存器:偏移地址
-r ds
:1000
-d ds:0 ;查看从1000:0开始的内存区间中的内容
-d ds:10 18
-d cs:0 ;查看当前代码段中的指令代码
-d ss:0 ;查看当前栈段中的内容
-e ds:0 11 22 33 44 55 66 ;在从1000:0开始的内存区间中写入数据
-u cs:0 ;以汇编指令的形式,显示当前代码段中的代码
-a ds:0 ;以汇编指令的形式,向从1000:0开始的内存单元中写入指令
mov ax,2000
mov ss,ax
mov sp,10 ;设定2000:0000~2000:000F为栈空间,初始化栈顶
#在栈中压入两个数据
mov ax,3123
push ax
mov ax,3366
push ax
==debug的T命令在执行mov ss,ax
命令后,紧接着mov sp,10
自动执行了,这又被称为终端机制
(2)
原因是mov ss,ax mov sp,10两个指令时使用了中断机制,执行中断例程时,cpu会将一些中断例程使用的变量自动压栈到栈中。
4、第一个程序
4.1一个源程序从写出到执行的过程
第一步:编写汇编源程序
第二步:对源程序进行编译连接
第三步:执行可执行文件中的程序
4.2源程序
程序4.1
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
1、伪指令
汇编指令有对应的机器码,可以编译成机器指令,从而由CPU执行。伪指令没有对应的机器指令,最终不被CPU所执行。
(1)
段名 segment //说明一个段开始
段名 ends //一个段结束
一个有意义的汇编程序至少要有一个段,这个段用来存放代码。
(2)
end //一个汇编程序的结束标记
segment···ends
标记一个段的结束
end
标记整个程序结束
(3)
assume //将代码段codesg与段寄存器cs联系起来
假设某一段寄存器和程序中的某一个用segment…ends定义的段相关联
2、源程序中的“程序”
3、标号
一个标号指代了一个地址。比如codesg
4、程序的结构
编程运算2^3
assume cs:abc ;(可选)abc当做代码段使用
;定义一个段,名称abc
abc segment
mov ax,2 ;写入汇编指令
add ax,ax
add ax,ax
abc ends
end
5、程序返回
#在程序4.1中的命令返回代码
mov ax,4c00H
int 21H
6、语法错误和逻辑错误
在上述4、程序的结构
中,代码运行会引发一些问题,因为程序没有返回。这个错误在编译时不会表现出来。
修改后:
assume cs:abc ;(可选)abc当做代码段使用
;定义一个段,名称abc
abc segment
mov ax,2 ;写入汇编指令
add ax,ax
add ax,ax
mov ax,4c00H
int 21H
abc ends
end
4.3 编辑源程序
edit 1.asm
assume cs:codesg
mov ax,0123h
mov bx,0456h
add ax,bx
add ax,ax
mov ax,4c00h
int 21h
codesg ends
end
4.4 编译
该笔记采用微软masm汇编编译器
目标文件(.obj)、列表文件(.lst)、交叉引用文件(.crf)
其中目标文件是我们最终要得到的结果
4.5 连接
上一节中,将1.asm编译得到了1.obj,现在将1.obj连接为1.exe
利用link.exe进行连接,在bin目录下。
nul.map
提示输入映象文件的名称,该文件是连接程序将目标文件连接为执行文件过程中产生的中间结果,直接回车可以不生成这个文件
.lib
提示输入库文件的名称。库文件里包含了一些可以调用的子程序,如果程序中调用了某一个库文件中的子程序,就需要在连接的时候,将库文件和目标文件连接到一起,生成可执行文件。
nul.def
定义文件
连接的作用
4.6 以简化的方式进行编译和连接
masm 1.asm;
link 1.obj;
4.7 1.exe的执行
这个程序只做了将数据送入寄存器和加法的操作。
4.8 谁将可执行文件中的程序装载进入内存并使它运行
4.9 程序执行过程的跟踪
进入debug流程
debug 1.exe
在DOS系统中,exe文件程序的加载过程
(1)程序加载后,ds中存放这程序所在内存区的段地址(DS=SA),这个内存区的偏移地址为0,则程序所在的内存区的地址为ds:0;
(2)内存区前256个字节中存放的是PSP,DOS用来和程序进行通信。从256字节处向后的空间存放的是程序。
ds可得到PSP的段地址SA,PSP的偏移地址为0,则物理地址为SAx16+0
因为PSP占256(100H)字节,所以程序的物理地址是:
SAx16+0+256=SAx16+16x16+0=(SA+16)x16+0
用段地址和偏移地址表示为:CS:IP=SA+10H:0
继续上面用debug调试1.exe
通过r和u可以查看寄存器状态和内存内占用情况
利用t可以一步一步执行,同时在执行到INT 21
时,使用p
命令,可以得到Program terminated normally(程序正常终止)
的提示消息
实验3 编译、汇编、连接、跟踪
程序最初运算的起点
之后的每一步
-u ss:0
ss:sp=2000H:000AH
pop ax
ss:sp=2000H:000AH
pop bx
ss:sp=2000H:000CH
push ax
ss:sp=2000:000EH
push bx
ss:sp=2000:000CH
pop ax
ss:sp=2000:000AH
pop bx
ss:sp=2000:000CH
这里有一个现象,pop取走空栈的内容,栈溢出了,导致栈的内容也改变了
assume cs:codesg ;将代码段codesg与cs联系起来
codesg segment ;代表一个段开始
mov ax,2000H ;ax=2000H
mov ss,ax ;ss=2000H,栈的段地址
mov sp,0 ;sp=0,栈的偏移地址
add sp,10 ;sp=0+10=10,栈顶为ss:sp,栈桥大小为sp=10
pop ax ;取ss:sp位置的值,ax=0
pop bx ;取ss:sp+2
push ax ;取ss:sp+2+2-2
push bx ;ss:sp+2+2-2-2
pop ax
pop bx
mov ax,4c00H
int 21H
codesg ends ;代表一个段结束
在源程序中,若不加H,则代表十进制
PSP的内容应该是在CS-10H:IP
的位置