16位汇编语言
学习操作系统前对汇编语言进行快速学习,参考汇编语言进行总结。
Before Learning
-
进制;
-
字节和字;
- 字节,即Byte,是由八个位组成的一个单元,也就是8个bit组成1个Byte;
- 字,即Word,代表计算机处理指令或数据的二进制数位数,对于不同位的CPU来说,字也是不相同的(如:8086CPU一次能处理的数据最大就是16位,两个字节。即:1表示为:0000 0000 0000 0001);
-
汇编的组成:
- 汇编指令;
- 伪指令:编译器所识别的指令,机器(CPU)并不识别;
- 其他符号;
-
Debug命令
- R命令:用来查看、改变CPU寄存器中的内容;
- D命令:用来查看内存中的内容;
- E命令:用来改写内存中的内容;
- U命令:将内存中的机器指令翻译成汇编指令;
- T命令:执行一条机器指令,即单步调试;
- A指令:以汇编指令的格式在内存中写入一条机器指令。
CPU
CPU的作用及组成
-
地址计算:8086CPU的寻址方式为“基地址+偏移地址”;
-
内存地址空间:不管是显示屏还是音响,CPU统统都是将他们看成一个个的内存地址,下图展示了8086CPU中大致的内存分配:
-
三大外部总线(CPU上面的针脚):地址总线、控制总线、数据总线;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sMhmrx57-1661760975909)(img/汇编/image-20220823143948073.png)]
-
CPU的内部组成:运算器、控制器、寄存器、内部总线;
寄存器
-
以8086CPU为准,它的寄存器全部都是16位的,可存放两个字节的数据(一个字节是八位);
-
其中主要包括通用寄存器:AX,BX,CX,DX;以及标志寄存器flag;
-
以AX为例,16位结构如下:
-
AX中0-7八位为低位,表示为AL寄存器;8-15八位为高位,表示为AH寄存器;
-
注意区分好每一个通用寄存器和它们的两个八位寄存器的关系。
-
汇编指令
-
赋值指令 MOV a, b:把b的值给a;
-
a可以是一个寄存器,一个内存单元,b可以是一个直接数,或者寄存器和内存单元;
-
当操作数为一个字节的时候,仅会改变对应的那个八位寄存器(AL)的值,而不会影响另外一个八位寄存器(AH)的值,但是会影响整体16位寄存器(AX)的值:
MOV AX, 2E40H ;执行结束后,AX值为2E40H,则AH值为2E,AL值为40H;
add ax,[0] ;含义为:将内存地址DS:0下的字单元内容与AX寄存器中的值进行相加,运算结果放到AX寄存器中。 add al,[0] ;含义为:将内存地址DS:0下的字节单元内容与AL寄存器中的值进行相加,运算结果放到AL寄存器中。 ;注意区分上述两个指令中操作的数据长度,AX是十六位寄存器,对应的数据长度为一个字,数据就是由DS:[0]、DS:[1]这两个字节组成的字内容;AL是八位寄存器,对应的数据长度为一个字节,数据就是DS:[0]下的字节内容。 add [0],ax ;含义为:将内存地址DS:0下的字单元内容与AX寄存器中的值进行相加,运算结果放到内存地址DS:[0]下的字单元中。 add [0],al ;含义为:将内存地址DS:0下的字节单元内容与AL寄存器中的值进行相加,运算结果放到内存地址DS:[0]下的字节单元中。
-
-
跳转指令;
-
JMP 内存地址:会同时修改CS寄存器值和IP寄存器值,达到程序执行逻辑跳转的目的;
jmp 2AE3:3 ;执行完毕后,CS值为2AE3H,IP值为0003H,CPU将执行2AE33H处的代码;
-
jmp也可以配合通用寄存器使用,格式为:JMP 寄存器名,此时仅修改IP中的值。
-
JMP 内存单元:只修改IP寄存器值
jmp [0] ;含义为:将IP寄存器修改为内存地址DS:0下的字单元内容。
-
-
与栈有关的指令
-
入栈指令 PUSH:它有一个操作对象,可以是寄存器,也可以是内存单元;
push ax ;含义:将AX寄存器中的值放入栈中 push [0] ;含义:将内存地址DS:0下的字单元内容送入栈中。
- PUSH指令一次操作的数据长度为一个字,所以 push [0] 操作的数据为DS:0、DS:[1]这两个字节单元组成的字单元内容。由于数据长度为一个字的限制,所以 push al 是非法指令。
-
出栈指令 POP:有一个操作对象,可以是寄存器,也可以是内存单元;
pop ax ;含义:将当前栈顶的字数据放入AX寄存器中 pop [0] ;含义:将当前栈顶的字数据放入内存地址DS:0下的字单元中。
- POP指令一次操作的数据长度也是一个字。
-
段
定义
- 段是人为对内存上的一段区域的划分,CPU中是没有段的概念;
- 基地址就是段地址,段的起始地址可以表示为:段地址×16+偏移地址;
段寄存器
- 在8086CPU中,共有四个段寄存器:
-
CS,Code segment,它指明了指令段(代码)所在内存地址;
-
DS,Data segment,它指明了数据段(变量)所在内存地址;
-
SS,Stack segment,它指明了栈段所在的内存地址;
-
ES,Empty segment,预留段,当出现段寄存器不够用的情况下,可以使用它来作为补充;
-
8086CPU中无法使用MOV直接给段寄存器赋值,只能先把数据赋值给通用寄存器,然后再由通用寄存器赋值给段寄存器;
-
MOV指令也无法操作CS和IP;
-
CS和IP
- CS是代码段寄存器,存放的就是代码段的段地址,IP是指令指针寄存器,里面存放的是代码段的偏移地址;
- 任意时刻CPU将CS:IP指向的数据当作指令进行执行,也就是说,CS:IP指向的内容就是代码;
- **同一个目的地址,**可以使用不同的基地址和偏移地址去表达;
访问内存
内存中字的存储
-
CPU中使用寄存器来存储数据,8086CPU是十六位寄存器,所以它的寄存器可存放一个字;
-
内存由一个个内存单元组成,每个单元存储一个字节的数据,那么在内存中存放一个字,则需要两个内存单元,而这两个内存单元遵守高位对应高位,低位对应低位规则;
-
eg:将20000(十六进制4E20H)存放到内存单元0开始的内存中,0下标对应“20H”,1下标对应“4EH”;
-
即:高位对应高位,这两个高位分别指数据的高位和内存空间的高位;低位对应低位,这两个低位分别指数据的低位和内存空间的低位。
-
DS段寄存器
-
代码和变量组成了一个程序,但CPU执行程序是,严格将两者分割开来:程序中的代码告诉了CPU如何执行如何操作,而程序中的变量就是CPU需要执行操作的数据;
- CS:IP指明了代码;
- DS声明了变量:DS段寄存器中存放的是数据段的段地址,任意时刻CPU将DS中地址下的数据当作真实数据来看待处理,而[…] 在指令中表示为一个内存单元, [0] 中0则表示该内存单元的偏移地址;
-
访问内存:
-
eg:读取内存地址单元10000H下的数据到寄存器AL中;
- 目的地址为:1000:0,段地址为1000H,也就是说DS寄存器中存放的数据为1000H,可以使用MOV指令来设置DS寄存器值:
;MOV不可以直接给DS段寄存器赋值,只能先把数据赋值给通用寄存器,然后再由通用寄存器赋值给段寄存器 mov bx,1000H mov ds,bx ;[...] 在指令中表示为一个内存单元, [0] 中0则表示该内存单元的偏移地址 ;为了加强理解,可以写成 mov al,ds:[0] mov al,[0]
-
栈
定义
- 栈空间实际上是一段被人为定义的内存地址空间(固定的且明确的起始地址和结束地址组成),可以称为栈段;
- 栈中存放的数据长度都是一个字的长度;
- 单核CPUDOS下只存在一个栈;
访问栈
-
SS段寄存器:
- 十六位寄存器,它和CS、DS它们存放的数据类型一样,也是一个地址;
- CPU在访问栈数据时,会默认把SS段寄存器中的地址当作栈段的段地址来进行寻址,除了基地址之外还需要偏移地址,故需要SP寄存器;
-
SP寄存器:
-
十六位寄存器,存放栈的栈顶元素偏移地址;
-
SS+SP,则是栈顶元素的地址任意时刻,CPU将 SS:SP 指向的数据当作栈顶元素;
-
栈为空的情况下,SP指向栈底的下一个内存单元地址,即SP = 栈底+1;
-
eg:将内存地址10000H~1000FH这段内存地址空间当作栈段
- 当栈为空时,SS段寄存器中值为1000H,SP寄存器中值为10010FH;
- 放入一个字数据后SP寄存器值为000EH;
-
汇编开发
环境搭建
- Notepad++、Debug、MASM(编译)、LINK(链接);
程序开发
assume cs:code;定义一个代码段,代码段的标号为:code
code segment ;标号为code的段从这里开始
mov ax,0123H
mov bx,0456H
add ax,bx
add ax,ax
mov ax,4c00H
int 21H
code ends ;标号为code的段从这里结束
end
-
伪指令:编译器所识别的指令,机器(CPU)并不识别
-
assume:定义一个段;
- eg:
assume cs:code
定义一个代码段,代码段的标号为:code。
- eg:
-
segment:标示一个段的开始
- 标号一定要和assume中定义的标号保持一致;
-
ends:end segment的缩写,标示一个段的结束;
- segment和ends这两个伪指令是成对出现的;
-
end:表示整个汇编程序的结束;
-
-
源程序中的“程序”
- 源程序是指我们.asm文件中的代码程序;
- 将源程序去除伪指令去除关键字后,剩下的汇编指令,才是真正意义上的程序;
-
标号:标号的作用就是在源代码中代指某一处地址,例如标号code,它就代指了CS代码段的段地址;
-
程序返回:
mov ax,4c00H int 21H
这两条汇编语句是固定形式,必须这样写才能确保本次程序结束运行并返回。
程序编译及链接
参考:https://blog.csdn.net/qq_34149335/article/details/123121428
通用寄存器
AX寄存器
- 主要用于各种赋值、数据传递;
BX寄存器的特殊用途
- 配合DS进行便捷可变寻址;
- mov ax,[bx](mov ax,ds:[bx] 的简略写法);
- 将内存地址ds:bx下的字单元数据送入AX寄存器中;
- 通过在代码中动态修改BX寄存器的值,就可以动态寻址;
- 尽量使用ds:[idata] 这种强指定,可以避免编译结果出错,且增强了源程序的可读性;
循环和CX寄存器
-
循环
-
loop指令:loop 标号(标号是我们在程序中某一处设置的标号);
-
CPU在执行Lopp指令时的顺序:
执行顺序: 1、CX = CX - 1 2、判断此时CX是否为 0 3、如果CX不为 0,则转到标号处执行(jmp 标号) 4、如果CX为 0,则继续向下执行
-
-
CX寄存器
-
通常情况下我们使用loop指令和CX寄存器相互配合,来实现某段程序的循环。loop和标号之间为循环体,CX寄存器中存放的是循环次数;
-
循环编写结构:
mov cx,循环次数 s: 循环逻辑 loop s
-
eg:计算2^12
assume cs:code code segment mov ax,2 mov cx,11 ;会循环11次 s:add ax,ax ;循环逻辑 loop s ;loop 指令中的标号,一定要是循环开始处的标号:s mov ax,4c00H int 21H code ends end
-
-
Loop和[bx]结合应用
-
eg:编程实现将 ffff:0~ffff:d 这段内存空间中的数据加在一起,结果存放在dx中
-
数据累加:我们使用AX寄存器和DX寄存器进行配合
-
内存字节数据访问:使用BX寄存器和DS段寄存器来配合
-
循环实现:使用CX寄存器和Loop指令进行配合
1、首先,将AX寄存器、DX寄存器赋值为0
2、将字节数据放到AL寄存器中,AH寄存器赋值为0,这样就把字节数据变成了字数据
3、将AX寄存器和DX寄存器相加,值放到DX寄存器中
4、完成一次相加(循环)后,BX寄存器值加1,访问下一个字节单元数据。
assume cs:code ; 声明代码段 code segment ; 代码段开始 mov ax,0FFFFH ; 注意,源程序中,无法识别纯字母,需要字母前加上数字0 mov ds,ax ; 设置数据段为 FFFH mov ax,0H ; 初始化AX寄存器值为0 mov dx,0H ; 初始化DX寄存器值为0 mov bx,0H ; 初始化BX寄存器值为0,这一步是为了从偏移位置 0 开始寻址 mov cx,12 ; 12个字节数据相加,初始值为0,相加12次 s:mov al,ds:[bx] ; 循环开始,将字节数据放到AL寄存器中 mov ah,0H ; 设置AH寄存器为0,将八位数据变成了十六位数据 add dx,ax ; 将DX、AX相加,值放到DX中 add bx,1 ; 将BX加1,访问接下来的字节数据 loop s ; 判断循环是否完成 mov ax,4c00H ; int 21H ; 程序返回 code ends ; 代码段结束 end ; 源程序结束
-
-
具有包含多个段的代码
申请内存
-
伪指令 dw:全称为define word,用于申请以字为单位的内存空间;
-
伪指令 db: 全称为define byte,即申请以字节为单位的内存空间;
- eg:编程计算,将以下八个数据相加:0123H、0456H、0789H、0abcH、0defH、0fedH、0cbaH、0987H,和放到AX寄存器中;
assume cs:code code segment ;使用dw定义了8个字型数据,它们所占内存空间大小为16个字节。 dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H start: ;使用标号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 ;编译结束
在代码段中使用栈
-
eg:利用栈,将程序中定义的数据逆序存放。
assume cs:code code segment dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H ;使用dw定义16个字型数据,并使用0来做填充 ;由于这段内存空间位于代码段内,所以栈的段地址也就是代码段的段地址:SS=CS dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 start: mov ax,cs mov ss,ax mov sp,30H mov bx,0 mov cx,8 s:push cs:[bx] ;第一个循环是将8个字数据依次入栈 add bx,2 loop s mov bx,0 ;BX偏移是从0开始,所以最后一个字数据便放到了CS:0处 mov cx,8 s0:pop cs:[bx] ;第二个循环则是将栈中的数据依次出栈 add bx,2 loop s0 mov ax,4c00H int 21H code ends end start
-
栈不仅仅只是用了存放了我们程序中待操作的16个字节数据,它同时还被别的程序(这里的程序指:Debug)使用存放数据,如果我们栈空间只有16个字节,那么势必就会发生栈溢出<一般建议栈空间为代码中要操作的数据大小的两倍即可。
定义数据段
-
上文中数据段和栈段都在代码段内,它们共用同一个段地址,然而一个段最大为64KB,所以需要定义数据段,来专门存放待操作的数据;需要定义栈段, 来专门进行栈访问。
-
定义数据段(与DS有关)
assume cs:code,ds:data;定义DS段,使用标号data来表示代替
-
使用数据段(与DS有关)
assume cs:code,ds:data data segment ;表示数据段的开始 ;使用dw申请了8个字型数据大小的空间并做了初始化 dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H data ends ;表示数据段结束 ;在代码段中操作 code segment mov ax,data ;将源程序中定义的数据段的段地址送入AX寄存器中 mov ds,ax ;设置数据段的段地址 code ends
定义栈段
-
定义栈段(与SS有关)
assume cs:code,ss:stack;定义SS段,使用标号 stack来表示代替
-
使用栈段(与SS有关)
assume cs:code,ds:data stack segment ;dw 申请了16个字型数据大小的内存空间,并用0对空间进行初始化填充 dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 stack ends ;在代码段中操作 code segment ;栈段开始的地方是stack:0H,栈底是stack:1FH,所以SP就要设置为20H。 mov ax,stack mov ss,ax mov sp,20H ;栈段的访问是由SS:SP决定的 code ends
注意
- 每个段的段地址都是不相同的,但是起始偏移位置相同;
- 在源程序中没有指明程序入口的情况下,将会默认把源程序中最先定义的那个段的起始地址当作程序入口地址;
- 在源程序中指明程序入口的情况下,将会把入口标号所在处当作程序的入口地址。CS设置为标号所在段的段地址,IP设置为标号所在的偏移地址;
- 必须要整除16才能当作段的段地址。所以,申请的内存空间大小默认都是16的整数倍,不足16的整数倍,会自动补上以符合要求。
灵活寻址
大小写转换
-
and 指令:只有两个都为1的情况下才为1,其余都是0;
- 通过与运算,我们可以指定修改二进制数据中某一位的值为0:
a:10110110; b:11111011; 将a与b进行与运算,可以修改二进制数据a的第2位为0:10110010 即:如果想修改二进制数据某位数据为0,那么我们就需要准备一个等位长度的二进制数:其中需要修改的位数据为0,其余位数据全部是1。将之与待修改的二进制数进行与运算,便可以将二进制数据指定位设置为0。
-
or 指令:只有两个都为0的情况下才为0,其余都是1;
- 通过或运算,我们可以指定修改二进制数据中某一位的值为1:
a:10110110; b:00001000; 将a与b进行或运算,可以修改二进制数据a的第3位为1:c:10111110 即:如果想修改二进制数据某位数据为1,那么我们就需要准备一个等位长度的二进制数:其中需要修改的位数据为1,其余位数据全部是0。将之与待修改的二进制数进行与运算,便可以将二进制数据指定位设置为1。
-
大小写转换
-
小写字母的二进制数据第5位为1,大写字母的二进制数据第五位为0;
-
大写转成小写,需要将该位修改成1,则使用:or指令,0100000
and al,5fH ; 将英文字符转为大写
-
小写转成大写,需要将该位修改成0,则使用:and指令,1011111
or al,20H ; 将英文字符转为小写
-
数组形式寻址
-
[bx+idata]
- [bx] 表示为一个内存单元,[bx+idata]同样也表示为一个内存单元,那么它的偏移地址就为:bx+idata;
-
编程实现将data段中定义的两条字符串,第一条字符中的英文字母转为大写,第二条字符串中的英文字母转为小写字母;
-
通过 [bx] 实现
assume cs:code,ds:data ; 定义代码段、数据段 data segment ; 数据段开始 db 'BaSiC' ; 申请内存空间并初始化第一条字符串 db 'MinIX' ; 申请内存空间并初始化第二条字符串 data ends ; 数据段结束 code segment ; 代码段开始 start: ; 程序指令开始 mov ax,data mov ds,ax ; 设置数据段 mov bx,0 ; 偏移从0开始 mov cx,5 ; 循环5次 s:mov al,[bx] ; 取一个英文字符到AL寄存器中 and al,5fH ; 将英文字符转为大写 mov [bx],al ; 将大写后英文字符放入原地址下 add bx,1 ; 偏移加1 loop s ; 判断循环是否结束 mov cx,5 ; 循环5次 s1:mov al,[bx] ; 取一个英文字符到AL寄存器中 or al,20H ; 将英文字符转为小写 mov [bx],al ; 讲小写后英文字符放入原地址下 add bx,1 ; 偏移加1 loop s1 ; 判断循环是否结束 mov ax,4c00H int 21H ; 程序返回 code ends ; 代码段结束 end start ; 源程序结束,并标明程序入口
-
使用 [bx+idata]:两条长度相等的字符串,它们唯一的区别就是起始地址不同,第一条字符串起始地址为:ds:0,第二条字符串起始地址为:ds:5,可以在访问第一条字符串的同时访问第二条字符串;
assume cs:code,ds:data data segment db 'BaSiC' db 'MinIX' data ends code segment start: mov ax,data mov ds,ax mov bx,0 mov cx,5 s:mov al,[bx] ; 取第一个字符串中的字符到AL寄存器中 and al,5fH ; 将英文字符转为大写 mov [bx],al ; 将大写后的英文字符放到原地址下 mov al,[bx+5] ; 当前偏移bx加5,取到第二个字符串中的字符到AL寄存器中 or al,20H ; 将英文字符转为小写 mov [bx+5],al ; 将小写后的英文字符放到原地址下 add bx,1 ; 可变偏移bx加1 loop s ; 判断循环是否结束 mov ax,4c00H int 21H code ends end start
-
与bx作用相似的寄存器
-
bx的功能:配合DS段寄存器实现可变地址寻址;
-
si寄存器和di寄存器在使用上和bx寄存器等效;
- 在数据拷贝复制场景中,si寄存器指向源地址下的数据;
- 在数据拷贝复制场景中,di寄存器指向目的地址下的数据;
-
eg:使用si、di 实现将字符串“welcome to masm!”复制到它后面的数据区中
assume cs:code,ds:data ; 定义代码段、数据段 data segment ; 数据段开始 db 'welcome to masm!' ; 申请16个字节空间,并赋初始值 db '................' ; 申请16个字节空间 data ends ; 数据段结束 code segment ; 代码段开始 start: ; 程序开始 mov ax,data ; mov ds,ax ; 设置数据段 mov si,0 ; 复制源的数据偏移从0开始 mov di,16 ; 复制的目的地址数据偏移从16开始 mov cx,16 ; 循环16次 s:mov al,[si] ; 从复制源地址取出一个字节放入al寄存器中 mov [di],al ; 将al寄存器中的数据写入复制目的地址下 add si,1 ; 复制源地址的偏移加1 add di,1 ; 复制目的地址的偏移加1 loop s ; 判断循环是否结束 mov ax,4c00H int 21H ; 程序返回 code ends ; 代码段结束 end start ; 源程序结束,并指明程序入口地址