汇编语言程序设计学习笔记
汇编语言程序设计学习笔记
参考书目:汇编语言第四版 王爽
2.1 通用寄存器
8086cpu寄存器共有14个,分别是AX BX CX DX SI DI SP BP CS SS DS ES PSW
每个寄存器都是16位的,可以存储16个bit
其中AX BX CX DX为通用寄存器,存储一些一般的数据,为了兼容基于上一代的cpu的汇编
程序,这四个寄存器都分别可以被分为两个8位寄存器去操作,AX变成AL AH 其中AL占据原
AX的0到7 剩下的位AH,BX也可分为BL,BH,其他通用寄存器也相同
2,2 字在寄存器中的存储
8086 cpu 可以一次性处理两种尺寸的数据
1 字节:byte 就是一个字节,可以存在八位寄存器中
2 字:word 一个字由两个字节构成,这两个字节分为高位字节和低位字节
一个字可以存储在一个16位寄存器中,其中AL存储了低位字节,AH存储了高位字节
应为cpu可以存储和处理的数据最低单位为byte,用16表示更为直观,所以我们一般采用16
进制去表示
2.3 几条汇编指令
常见的汇编指令有mov add
mov用于将数据存入寄存器比如:
mov ax,18
就是将18存入寄存器ax
mov ax,bx
就是将bx寄存器的数据送入寄存器AX
add用于在原有的寄存器数据上叠加
add ax,18
用于将18加到ax寄存器的原数据中
add ax,bx
用于将bx寄存器的内容加到ax寄存器的数据中
我们需要主义的是寄存器的位数有限,要是叠加后的结果超过其范围,超出范围的位会溢
出,没有超出的部分会被留下,但溢出的这部分并不是真正的被丢弃,以后会涉及相关的内
容 ,在我们对寄存器进行操作时,如果直接去操作al寄存器,此时cpu将其看作一个独立的
寄存器,认为其和ax,ah寄存器没有直接关系,所以即使在al中发生了进位,也不会将进位
部分存入ah,如果我们直接对ax进行操作,即使超过低位寄存器的范围,超出的部分也会
进入高位寄存器中,因为在cpu在执行指令时只认为有一个16为寄存器ax
注意: 在进行数据传送或者运算时,需要注意两个操作对象的位数应该一致,否则就是错
误的指令
2.4 物理地址
内存单元在逻辑空间中线性的放在一起,每一个内存单元在该
空间中被分配唯一的地址,所有的地址一个一维线性空间,我
们称给内存单元分配的那个唯一地址为物理地址
这个物理地址并非本来就存在的 ,而是由cpu生成的,不同的
cpu可以有不同的生成地址的方式
2.5 16位结构
8086是16位cpu,16位cpu拥有以下的特性
1 运算器一次最多可以处理16位数据
2 寄存器的最大宽度位16位
3 寄存器和运算器之间的通路为16位
2.6 8086cpu给出物理地址的方法
8086cpu的地址总线宽度位20位,一次可传输长度位20bit的地
址,但cpu内部一次处理的最大地址长度为16,,那20位地址
总线剩下的位难道就用不到了吗?显然不是这样
8086处理器内部采用将两个16位地址合成为一个20位地址的
方’式来解决该问题
1 cpu的内部提供两个地址,一个为段地址,另一个为偏移地址
2 两个地址同时送入地址加法器
3 地址加法器将这两个地址合成为一个20位的物理地址
4 地址加法器通过内部总线将20位物理地址送到输入输出控制的电路
5 输入输出将地址送上地址总线
6 20位物理地址通过地址总线到达存储器
在地址加法器中,物理地址=段地址*16+偏移地址,16位的段
地址在乘以16后就变成了20位再加上构造好的偏移地址就形成
了20位的物理地址
可能此时就会产生疑问,这样相加的结果不是有可能超过20位
吗?事实上段地址和偏移地址并非随便给出的,给出时设定好
了相加的结果不会超过20位
2.9 段寄存器
8086的段地址在8086的段寄存器中存放,8086cpu共有四个段寄存器,CS,DS,SS,ES,当
8086cpu访问内存时由这资格段寄存器提供内存单元的段地址
2.10 CS和IP
CS和IP是8086CPU中两个最关键的寄存器,CS为代码段寄存器,IP为指令指针寄存器
在8086cpu中,假设CS寄存器中存放了M,IP中的内容为N,cpu会从M*16+N单元开始,读
取一条指令,CS中存放的是段地址,IP中存放的是偏移地址,需要注意的是8086cpu中存在指
令预存,因为一个指令往往不仅仅是由一个字节构成的,即虽然我们发过去一个地址,但实际
上会将与其相邻的存储单元所存储的同样构成该命令的数据,也,如果遇到不是该命令的数
据,则不会一起发送,而是等上一个命令的数据发送完成再进行。当一次读取完成时,根据你
传输过来的字节数,ip的值进行变化,需要注意的时,当一次读取结束后,是ip先加值,再执
行命令,而执行的命令可能会对ip寄存器进行更改从而出现跳地址的情况
8086cpu断电启动或复位后,CS IP会被设置为 CS=FFFFH IP=0000H,由此算出的存储单元的
指令是开机后执行的第一条指令,cpu可以将存储单元中存储的二进制数看作指令,也可以将
其看作二进制的数据,那么怎么区分呢?当需要将其以指令形式执行时,一定是被IP 和CS指向
过
2.11 修改CS IP的指令
我们之前学习了可以使用mov来修改寄存器的值,但对于cs ip这两个寄存器来说,却不能使用
这两个寄存器,能够改变CS IP内容的指令被称为转移指令,一个最简单的修改CS IP的指令,
JMP指令具体用法为: jmp 段地址:偏移地址
比如:jmp 2AE3:3
执行完成后CS寄存器变为:2AE3H IP寄存器变成0003H
如果只想更改ip寄存器,只需要 jmp 其他寄存器
就可以把其他寄存器的值赋值给ip寄存器
2.12 代码段
上面我们已经明白了命令是如何被执行的了,如果我们自己有一个存放在内存里的代码段,即
一段连续存放的命令,我们想要cpu去执行,应该怎么操作呢?我们已知cpu并不会分辨哪些内
存区域存放的是代码,哪些内存区域存放的是数据,而是会根据cs ip寄存器的指向去执行命
令,所以我们想要cpu去执行,只需要将cs ip寄存器进行修改,使得计算得出的地址是我们代
码段存放区域的首地址即可
2.13 运行环境的搭建以及工具的使用
由于当前我们使用的cpu和8086差别太大,加上8086的汇编程序必须在dos系统下运行,所以
我们在win11系统下想要运行8086的汇编程序,需要用到dos模拟器,即dosbox,还需要下载
编译工具 masm.exe 将编译出的,link.exe需要把编译出来的obj文件链接起来,比如我们有多
个汇编asm程序,此时通过masm编译成多个obj文件,我们需要一次执行所有的文件,使用
link.exe就可以将这几个文件链接起来,形成一个可执行文件.exe,当然,如果我们只有一个汇
编文件,想要执行也得去使用link.exe ,即链接器将其变成.exe文件去执行,除了编译器和连
接器,还有调试工具debug.com,用来调试汇编程序
我们需要注意的是masm处理的是asm程序,也就是没编译的原始汇编程序,link.exe处理的是
obj程序,debug.com处理的是link连接好的exe程序
在debug中,存在不同的debug指令:
r
查看cpu寄存器的内容
d
查看内存的内容
e
改写内存的内容
u
将机器指令翻译成汇编指令
t
执行一条机器指令,注意是一条
a
以汇编指令的格式在内存中写入一条机器指令
还有一些其余的指令没有给出,现在对上面这些指令的用法进
行说明
d指令
当我们使用debug中的d命令时,会返回起始地址开始的128
个字节的内容,每个字节中存储的数据会用两个16进制数去表
示,每行有16个内存单元的数据,会在中间以-
来进行分割
最右边是存放数据对应的ASCII字符,如果没有对应的ASCII字
符,则会以.
形式来输出
如果我们要指定D命令的查看范围,则可以使用:d 短地址:
偏移地址 结尾偏移地址
这样就可以查看到从起始偏移地址到结
尾偏移地址的内存
如果想要单独查看某个内存单元内的数据,直接让d指令中的
起始偏移地址和末尾偏移地址相同即可
e指令
e指令用于修改地址指向的内存单元中的内容,从起始地址开
始,依次向后进行修改,你输入多少数就会一次修改多少内存
单元内的值
具体的用法为:e 段地址:偏移地址 数据1 数据2 数据3 ...
除了这样直接一次性赋值完成,我们也可以一个个的去赋值,
决定是否改变其的值,具体的做法为
e 段地址:偏移地址
然后按回车
就会出现第一个地址指向的内存单元的值,后面跟着一个.
如
果我们需要更改就在后面输入然后按空格,进入和他相邻的第
二个内存单元中,决定是否更改其的值,如果不想更改,直接
按空格即可跳到下一个的内存单元中,最后如果不想再继续,
直接按回车即可退出更改模式
我们还可以利用e命令向内存中写入字符,比如 e 1000:0
'a'
输入的字符将以ASCII码的形式存储,还可以直接给内存中输
入字符串,不过输入字符串等价于向内存中依次输入字符,比
如 e 1000:0 'a' 'b' 'c'
就等价于e 1000:0 'abc'
3.7 cpu提供的栈机制
8086中也有栈的设计,将一段内存当作栈来使用,我们进行栈
操作的两个基础的指令就是push 和 pop
这两个操作的意义很显然的,一个是入栈一个是出出栈
具体的使用,比如push ax
即将ax的数据送入栈中
pop ax
表示将从栈顶取出的数据送入ax中,8086cpu的入栈
和出栈操作都是以字节为单位进行的
注意,我们如果将一段内存区域作为栈来使用,那么高地址的
为栈底,低地址的为栈顶
需要注意的是,我们在像栈中压入数据时栈顶是不断在变化
的,cpu怎么知道栈顶在哪呢?这时候就需要使用到段寄存器
SS和寄存器SP,栈顶的段地址和偏移地址分别记录在这两个寄
存器之中,由于栈操作的基本单位是字,所以一次入栈操作会
使SP寄存器的值减2,同理,出栈会让sp寄存器的值减去2
一次push操作依次由 更改sp寄存器地址,压入数据这两步构
成如果我们使用pop操作,过程为:先将栈顶的元素放入指定的寄
存器中,然后让sp寄存器的值加二,栈内元素的pop为了节省时间,
实际上只改变寄存器,而没有真的去删除空间的内容,而是等着被覆盖
3.8 栈超界问题
我们所分配的栈空间的大小是有限的,如果pop或者push不停,肯定
会超出给定的栈空间范围,从而对非栈内的数据进行操作,这样肯定是
不行的。在8086cpu中,cpu只知道栈顶在哪,并不知道我们安排的栈空
间有多大。所以如果我们不加注意,是很容易出现栈超界现象的。
3.9 push 和pop的用法
和之前我们学的mov等类似,pop和push的对象不仅可以是寄存器,还可以是内存单元,
但需要注意的是,立即数是不能直接进行操作的。
比如push [0] 就是将段地址在ds寄存器中,偏移地址为0的内存单元存储的值压入栈中
我们在使用时需要先设置栈指针,比如如果想要设置10000H-1000FH这段空间为栈空间
mov ax,1000H
mov ss,ax
mov sp,10H; 栈底的下一格
设置好了栈寄存器后,就可以对栈进行各种操作了
4.1 源程序从写出到执行的过程
想要编写汇编程序,首先我们要去了解一下什么是伪指令。
我们写的汇编程序中可以直接转换为机器码执行的称为汇编指令,而不能转换
成机器码,而是由编译器来识别,根据其内容做出操作的指令称作伪指令
一个汇编程序是有多个段组成的,这些段用来存放代码,数据或者当作栈空间。
一个有意义的汇编程序中至少要有一个段来存放代码,我们怎么去标识一段呢?
段名 segment
代码
段名 ends
段的开头和结尾标识都是伪指令,这里的段其实和我们之前学习的段是同一概念
与ends很像的一个伪指令为end,用于表明汇编程序的结束,在我们写程序时,如果写完了
就需要在最后加上end,否则编译器不知道程序在哪里结束
我们在这还需要区分源程序和程序的区别,我们写的彪悍汇编指令和伪指令的程序被称为源程序
而其中由计算机直接去执行,处理的指令或者数据,被称为数据。
思考一下我们通过编译器,连接器生成的可执行文件怎么去允许,首先余姚另一个正在运行的程序将
我们想要允许的可执行文件加载人内存,然后将cpu的控制u按交给它,该程序开始运行,最开始的
程序暂时停止,当第二个程序运行完成后,将cpu的控制权还给第一个程序,然后第一个程序继续运
行,我们称一个程序结束后,将cpu的控制权交换给使它运行的程序的过程为程序返回
比如
mov ax,4c00H
int 21H
虽然我们现在并不知道这两行指令在干什么,但这两行语句实现了程序的返回
我们现在来写一个基础的汇编程序:
assume cs:abc ;将代码段和cs段寄存器关联起来,告知cpu这是一个存放命令代码的段
abc segment
mov ax,2
add ax,ax
add ax,ax
mov ax,4c00H
int 21H
abc ends
end
这样就完成了一个基础的汇编程序
4.3 编辑源程序
我们想要运行一个源程序,首先要使用edit进行编辑masm进行编译,然后使用连接器进行连接,生
成可执行文件
注意,如果我们使用masm和link在命令结尾加上分号,则会默认不去生成中间文件等操作,否则要
自己去确认是否生成
对于最后生成的exe程序,直接输入文件名即可弯沉执行,但我们发现程序运行完没有任何显示,这
是因为我们之前写的程序没有向显示器输出任何信息,而是只对寄存器和假发的操作,在以后的学习
中,我们也会学习向屏幕输出的方式,那时候就能看出了
还记的我们之前说的想要运行一个exe需要另一个程序将其写入内存,并给其cpu操作权限,这另一
个程序就是shell,任何通用的操作系统,都要提供一个shell,在早期的dos系统中,shell是
command.com 该程序干了这些工作,将执行程序加载进内存,并设置cs ip寄存器,最后程序执行
完成回到ccommand中,cpu继续运行command
4.9 程序执行过程的跟踪
我们在debug某可执行程序是,debug并不放弃对cpu的控制,这样debug就可以单步执行程序,查
看每一步的结果,当我们用debug将程序从可执行文件载入内存后cx寄存器存放的是程序的长度
注意,在调试时,当执行int 21时,我们需要使用p指执行而不是t指令
5.2 loop指令
loop其实就是汇编中的循环指令,具体的用法为
标号: 需要循环的指令
loop 标号
在汇编中,循环的次数是根据cx寄存器的值判定的,当我们开
始循环时,执行一次循环的过程为
先正常执行,当执行到loop 标号这行进行判定,首先将cx寄存
器的值减去一,如果减去后的结果不为1,则跳转到标号位置,
如果减去后的结果为0,则退出循环,继续执行下面的语句
在这我们举个实际的循环的例子,用于计算2的8次方吧
asume cs:code
code segment
mov cx,7
mov ax,2
s: add ax,2
loop s
mov ax,4c00h
int 21h
code ends
end
这就是一个简单的运用循环的程序
5.3 与循环相关的debug指令
如果我们现在只想观察循环的运行情况,不想管循环之前的代
码执行的如何,此时我们如果像之前一样只是用t指令,则会一
条条的执行,这显然和我们的目的不符,此时要使用g指令对命
令执行进行跳转,我们先使用u指令查看代码段中的各个命令
存放的地址,最后直接g 想要停止的偏移地址
此时就会执行到
该命令之前的那一行。如果循环要循环较多次,我们在debug
时看个别就行,其他的不想看了,这时直接使用p命令,会自动
执行到循环结束的那一行
p
像这样直接用即可