汇编笔记
基础知识
汇编语言指令组成
汇编指令:机器码的助记符,有对应的机器码。
伪指令:没有对应的机器码,编译器执行,机器不执行。
其他符号:如+-*/有编译器识别,无对应机器码。
CPU与外部器件交互需要
存储单元地址(地址信息)
器件选择,读写命令(控制信息)
数据(数据信息)
总线
总线就是一根根导线的集合,分为
地址总线,越宽(数量越多)代表可以寻址的范围越大
数据总线,越宽代表一次性读写的数据越多(8根1字节)
控制总线,越宽代表对器件控制操作越多
小结
汇编指令和机器指令一一对应
每一种cpu都有自己的汇编指令集
在存储器中指令和数据都是二进制,没有任何区别
CPU可以直接使用的信息存放在存储器中(内存)
接口卡
CPU无法直接控制显示器,键盘等的外围设备,但CPU通过直接控制这些外围设备在主板上的接口卡来控制这些设备。
存储器
随机存储器(RAM):带电存储,关机丢失,可读可写
用于存放CPU使用的绝大部分程序和数据,主随机存储器由装在主板上的RAM和扩展插槽的RAM组成。
其他接口卡上也可能有自己的RAM
只读存储器(ROM):关机不丢,只能读取
主板上的ROM装有系统的BIOS(基本输入输出系统)。
其他接口卡上也可能有自己的ROM,一般装着相应的BIOS。
(P10图)
内存地址空间
以上这些内存都和CPU总线相连,CPU都通过控制总线向他们发出内存读写命令。所以CPU都把他们当内存对待,看做一个一个由若干存储单元组成的逻辑存储器,即内存地址空间(一个假想的逻辑存储器P11图)。
内存地址空间中的各个不同的地址段代表不同的存储设备,内存地址空间大小收到CPU地址总线长度限制。
寄存器
内部总线
之前讨论的总线是CPU控制外部设备使用的总线,是将CPU和外部部件连接的。而CPU内部由寄存器,运算器,控制器等组成,由内部总线相连,内部总线负责连接CPU内部的部件。
通用寄存器
8086CPU寄存器都是16位的,一共14个,分别是AX,BX,CX,DX,SI,DI,SP,BP,IP,CS,SS,DS,ES,PSW。其中AX,BX,CX,DX四个寄存器通常存放一般性的数据,称为通用寄存器。
而且为了兼容上一代的8位寄存器,这四个寄存器可以拆开成两个8位的寄存器来使用。称为AH,AL,BH,BL,CH,CL,DH,DL。低八位(编号0-7)构成L寄存器,高八位构成H寄存器。
字
8086CPU可以处理以下两种数据
字节byte,8位
字word,连个字节,16位。分别称为高位字节和低位字节。
简单的汇编指令
指令 |
操作 |
高级语言 |
mov ax,18 |
将18存入AX寄存器 |
AX=18 |
add ax,8 |
将AX寄存器中的数加8 |
AX=AX+8 |
mov ax,bx |
将BX中的数据存入AX |
AX=BX |
add ax,bx |
将AX中的数据和BX中的数据相加存入AX |
AX=AX+BX |
汇编指令或寄存器名称不区分大小写。
注:AX寄存器当做两个8位寄存器al和ah使用的时候,CPU就把他们当做两个8位寄存器使用,而不会看成是一个16未分开,即如果al进行加法运算C5+93=158,即add al,93,al会变成58,ax则是0058而不是0158。
CPU位结构
16位结构的CPU指的是运算器一次最多处理16位数据,寄存器宽度16,寄存器和运算器之间通路也是16位。
CPU表示物理地址
如果物理总线宽度超过寄存器宽度,CPU寻址方法是两个寄存器输出一个地址,当地址总线宽度20的时候,P21图。一个寄存器输出短地址,另一个输出偏移地址。然后通过地址加法器合并为一个20位的地址,然后通过内部总线送给控制电路,控制电路通过地址总线送给内存。
公式:物理地址=段地址x16+偏移地址(这里的x16其实就是左移四位,P21图)
虽然这么表示,但内存并没有被分为一段一段的,是CPU划分的段。段地址x16称为基础地址,所以我们可以根据需求把任意的基础地址加上不超过一个寄存器表示的最长(64KB)的偏移地址来表示地址。而且一个实际地址往往可以有各种不同的方法表示,通常我们表示21F60H这个地址通过下面方法:
2000:1F60
2000H段中的1F60单元中
段寄存器与指令指针寄存器
8086CPU有四个段寄存器:CS,DS,SS,ES
除此之外,IP寄存器称为指令指针寄存器,所以任意时刻可以读取从CSx16+IP单元开始,读取一条指令执行。也就是说,CPU将IP指向的内容当做指令执行。
P26图,CPU执行一段指令。另外,8086CPU开机时CS被置为FFFFH,IP被置为0000H,也就是说刚开机的第一条指令从FFFF0H开始读取执行。
CPU将CS:IP指向的内存中的内容当做指令,一条指令被执行了,那一定被CS:IP指向过。
修改CS,IP
CS和IP寄存器不可以使用传送指令mov来改变,而能改变CS,IP内容的指令是转移指令。
jmp指令用法:
jmp 段地址:偏移地址 同时修改CS和IP的值 如jmp 2AE3:3 结果CS=2AE3H IP=0003H
jmp 某一合法寄存器 只修改IP的值 如jmp ax,将IP的值置为AX中的值(AX不变)
小结
8086CPU有四个段寄存器,CS是用来存放指令的段地址的段寄存器
IP用来存放指令的偏移地址
CS:IP指向的内容在任意时刻会被当做指令执行
使用转移指令修改CS和IP的内容
实验
Debug命令:
R:查看,改变CPU寄存器内容
o直接-r查看寄存器内容
o-r 寄存器名,改变寄存器内容
D:查看内存中内容
o-d直接查看
o-d 段地址:偏移地址 查看固定地址开始的内容
o-d 段地址:偏移地址 结尾偏移地址 查看指定范围内存
E:改写内存中内容
o-e 起始地址 数据 数据 数据 …
o提问方式修改 -e 段地址:偏移地址 从这个地址开始一个一个改,空格下一个,回车结束
o也可以写入字符 ‘a’
U:将内存中的机器指令翻译成汇编指令
o-u 段地址:偏移地址
T:执行一条机器指令
o-t 执行cs:ip指向的命令
A:以汇编指令格式在内存中写入一条机器指令
o-a 段地址:偏移地址 从这个地址开始一行一行的写入汇编语句
寄存器(内存访问)
内存到寄存器的储存
寄存器是16位的,可以存放一个字即两个字节,而内存中的一个存储单元是一字节。所以一个寄存器可以存两个存储单元的内容,高地址存储单元存在高位字节中,低地址存储单元存在低位字节中。
字单元:存放一个字型数据的两个地址连续的内存单元。
DS寄存器
与CS类似,DS寄存器存放的是要从内存中读取的数据的段地址。我们想要使用mov指令从内存10000H(1000:0)中的数据送给AL时,如下:
mov al,[0]
后面的[0]指的是内存的偏移地址是0,CPU会自动从DS寄存器中提取段地址,所以应该首先将段地址1000H写入DS寄存器中。但却不能直接使用mov ds,1000指令,只能从其他寄存器中转传入DS寄存器。所以完整命令如下:
mov bx,1000
mov ds,bx
mov al,[0]
当然,从AL寄存器中将数据送入内存只要反过来使用mov就可以了,mov [0],al
如果需要传输字型数,只要使用对应的16位寄存器就可以了,传输的是以相应地址开始的一个字型数据(连续两个字节)。如mov [0],cx。
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,sub常见语法:
add 寄存器,数据 add ax,8
add 寄存器,寄存器 add ax,bx
add 寄存器,内存单元 add ax,[0]
add 内存单元,寄存器 add [0],ax
sub和add一样
注意,add,sub不可以操作段寄存器。
栈
栈是一种后进先出的存储空间,从栈顶出栈入栈。LIFO(last in first out)
入栈指令:push ax ax中的数据送入栈顶
出栈指令:pop ax 栈顶送入ax
入栈和出栈指令都是以字为单位的。P58图
栈寄存器SS,SP与push,pop
CPU通过SS寄存器和SP寄存器来知道栈的范围,段寄存器SS存放的是栈顶的段地址,SP寄存器存放的是栈顶的偏移地址。所以,任意时刻SS:SP指向栈顶元素。
指令push ax执行过程:
1.SP=SP-2,SP指针向前移动两格代表新栈顶
2.AX中的数据送入SS:SP目前指向的内存字单元,P59图
所以栈顶在低地址,栈底在高地址。初始状态下,SP指向栈底的下一个单元。
反之pop ax执行过程相反。
8086CPU并不会自己检测push是否会超栈顶,pop是否会超栈底。
push和pop可以加寄存器,段寄存器,内存单元(直接偏移地址[address])
指定栈空间通常通过指定SS来进行,如:
指定10000H~1000FH为栈空间
mov ax,1000
mov ss,ax
mov sp 0010
注:将一个寄存器清零 sub ax,ax 两个字节,mov ax,0 三个字节
注:若设定一个栈段为10000H~1FFFFH,栈空的时候SP=0(要知道入栈操作先SP-2,然后再送入栈)
实验
Debug中的t命令一次执行一条指令,但如果执行的指令修改了ss段寄存器,下一条命令也会紧跟着执行(中断机制)。
简单编程
一个汇编语言程序
1.编写
2.编译(masm5.0)
3.连接
一些伪指令功能
assume cs:codesg codesg segment mov ax,0123 mov bx,0456 add ax,bx add ax,ax mov ax,4c00 int 21 codesg ends end
涉及到的一些知识:
XXX segment···XXXends
osegment和ends成对出现,代表一个段的开始和结束。
o一个汇编程序可以有多个段,代码,数据和栈等,至少要有一个段。
end
oend代表一个汇编程序结束,遇到end编译器停止编译。
assume
oassume 假设,假设某一个段寄存器和程序中的一个段关联。
o可以理解为将这个段寄存器指向程序段的段地址
标号(codesg)
o一个标号代表一个地址
程序返回mov ax,4c00 int 21
o暂时记住这两条指令代表程序返回
编译和连接方法,P83。
注:编译器只能发现语法错误而无法发现逻辑错误。
CPU执行一个程序,需要有另一个程序将它加载进内存(即将CS:IP指向它),一般情况下我们通过DOS执行这个.exe,所以是DOS程序将它加载进入内存。当这个程序运行结束,再返回DOS程序继续执行。如果是DOS调用Debug调用.exe,那么先返回Debug再返回DOS。
DOS加载一个.exe时,先在内存中找到一段内存,起始段地址SA,然后分配256字节的PSP区域,用来和被加载程序通信。在之后的段地址SA+10就是程序开始的段地址。CS:IP指向它,DS=SA。
注:在Debug中,最后的int 21指令要使用P命令执行。
[BX]和loop指令
内存单元的描述
内存单元可以使用[数字]表示,当然也可以使用[寄存器]表示,如[bx],mov ax,[bx],mov al,[bx]
为了表示方便,使用()来表示一个内存单元或寄存器中的内容,如(ax),(20000H),或((dx)*16+(bx))表示ds:bx中的内容,但不可写为(1000:0),((dx):0H)。而(X)中的内容由具体寄存器名或运算来决定。
我们使用idata来表示常亮。所以以下语句可以这么写:mov ax,[idata] mov ax,idata。
loop指令
loop指令格式:loop 标号。
loop指令通常用来实现循环功能,当执行loop指令时,CPU进行两步操作:
1.(cx)=(cx)-1
2.(cx)不为零则跳至标号处执行程序。
所以CX中存放的是循环次数,一个简单的例子如下(计算2^12):
assume cs:code code segment mov ax,2 mov cx,11 s:add ax,ax loop s mov ax,4c00h int 21h code ends end
所以使用loop注意三点:
1.先设置cx的值 mov cx,循环次数
2.设置标号与执行循环的程序段 s:执行程序段
3.在程序段最后写loop loop
注:在汇编语言中,数据不能以字母开头,所以大于9fffH的数据,要在开头加0,如0A000H
注:debug中G命令 g 0012表示CPU从当前CS:IP开始一直执行到0012处暂停。P命令可以将loop部分一次执行完毕,直到(CX)=0,或使用g loop的下一条命令。
Debug和masm编译器对指令的不同处理
mov ax,[0]这条指令在Debug和masm中有着不同的解释,Debug是将DS:0内存中的数据送给AX,而masm中则是mov ax,0,即将0送入AX。
解决方法1:先将偏移地址送入BX,然后再使用mov ax,[bx]
解决方法2:直接显式给出地址,如mov al,ds:[0] (相应的段寄存器还有CS,SS,ES这些在汇编语言中可以称为“段前缀”)当然,这种写法通过编译器之后会变成Debug中的mov al,[0]
注:inc bx bx值加一
安全的编程空间
在之前没有提到的一个问题,如果在写程序之前不看一眼要操作的内存,就直接开始使用的话,万一改写了内存中重要的系统数据,可能会引起系统崩溃。所以我们一般在一个安全的内存空间中操作。一般操作系统和合法程序都不会使用0:200~0:2ff这256字节的空间,所以我们可以在这里操作。
学习汇编语言的目的就是直接和硬件对话,而不理会操作系统,这在DOS(实模式)下是可以做到的,但在windows或Unix这种运行与CPU保护模式的操作系统上却是不可能的,因为这种操作系统已经将CPU全面严格的管理了。
段前缀的使用
将ffff:0~ffff:b中的数据转存入0:200~0:20b中:
assume cs:code code segment mov ax,0ffffh mov ds,ax mov ax,0020h mov es,ax mov bx,0 mov cx,12 s:mov dl,[bx] mov es:[bx],dl inc bx loop s mov ax,4c00h int 21h code ends end
[bx]直接使用的时候默认段前缀是ds,但要使用其他的段前缀,如es就要在前面加上。
程序的段
数据段
一般一个程序想要使用内存空间,有两种方法,在程序加载的时候系统分配或在需要使用的时候向系统申请,我们先考虑第一种情况。所以我们应事先将所需的数据存入内存中的某一段中,但我们又不可以随意的指定内存地址,以下面的求8个数据累加和的代码为例:
assume cs:code code segment dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h 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 代码第一行的dw
是定义字类型数据,define word的意思。这里定义了8个字类型数据,占16字节。由于是在程序最开始定义的dw,所以数据段的偏移地址为0,也就是说第一个数据0123h的地址是CS:[0]第二个0456h的地址是CS:[2]以此类推。
所以这个程序加载之后CS:IP指向的是数据段的第一个数据,我们要是想成功执行,需要把IP置10,指向第一条指令mov bx,0,所以我们想要直接执行(不在Debug中调整IP)的话,需要指定程序开始的地方:
··· dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h start:mov bx,0 ··· code ends end start
在第一条指令前加start,后面的end变成end start,end除了通知编译器程序在哪里结束之外,也可以通知程序的入口在哪,也就是第一条语句,在这里编译器就知道了mov bx,0是程序的第一条指令。也就是说,我们想要CPU从何处开始执行程序,只要在源程序中使用end 标号指定就好了。
所以有如下框架:
assume cs:code code segment ···数据··· start: ···代码··· code ends end start
栈段
看下面一段使8个数逆序存放的代码:
assume cs:codesg codesg segment dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h 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] add bx,2 loop s mov bx,0 mov cx,8 s0:pop cs:[bx] add bx,2 loop s0 mov ax,4c00h int 21h codesg ends end start
在定义了8个字型数据之后,又定义了16个取值为0的字型数据,用作栈空间。所以dw这个定义不仅仅用来定义数据,也可以用来开辟内存空间留给之后的程序使用。
数据,代码,栈的程序段
在8086CPU中,一个段的长度最大为64KB,所以如果我们将数据或栈空间定义的比较大,就不能像前面一样编程了。我们需要将代码,数据,栈放入不同的段中:
assume cs:codesg codesg segment dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h 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] add bx,2 loop s mov bx,0 mov cx,8 s0:pop cs:[bx] add bx,2 loop s0 mov ax,4c00h int 21h codesg ends end start
我们可以这样在写代码时就将程序分为几个段,这段代码中,mov ax,data的意思是将data段的段地址送入ax寄存器。但我们不可以使用mov ds,data这样是错误的,因为在这里data被编译器视为一个数值。
在这里将数据命名为data,代码命名为code,栈命名为stack只是为了方便阅读,CPU并不能理解,和start,s,s0一样,只在源程序中使用。而assume cs:code,ds:data,ss:stack这段代码也并不能让CPU的cs,ds,ss指向对应的段,因为assume是伪指令,CPU并不认识,它是由编译器执行的。源程序中end start语句指明了程序的入口,在这个程序被加载后,CS:IP被指向start处,开始执行第一条语句,这样CPU才会将code段当做代码执行。而当CPU执行
mov ax,stack mov ss,ax mov sp,20h
这三条语句后才会将stack段当做栈空间开使用。也就是说,CPU如何区分哪个段的功能,全靠我们使用汇编指令对ds,ss,cs寄存器的内容设置来指定。
灵活定位内存地址
and和or指令
and:逻辑与指令,按位与运算,如:
mov al,01100011B
and al,00111011B
执行结果是al=00100011B,所以我们想要把某一位置零的时候可以使用and指令。
or:逻辑或指令,按位或运算,如:
mov al,01100011B
or al,00111011B
执行结果是al=01111011B,or指令可以将相应位置1。
ASCII码和字符形式的数据
在汇编语言中我们可以使用’···’的方式指明数据是以字符形式给出的,编译器会自动将它们转化为ASCII码。例如:
assume cs:code,ds:data data segment db 'unIX' db 'foRK' data ends code segment start:mov al,'a' mov bl,'b' mov ax,4c00h int 21h code ends end start
db和dw类似,只不过定义的是字节型数据,然后通过’unIX’相继在接下来四个字节中写下75H,6EH,49H,58H即unIX的ASCII值。同理,mov al,’a’也是将’a’的ASCII值61H送入al寄存器。
使用and和or指令改变一串字符串字母的大小写,将第一串全变为大写,第二串全变为小写:
首先分析ASCII码:
大写 十六进制 二进制 小写 十六进制 二进制
A 41 01000001 a 61 01100001
B 42 01000010 b 62 01100010
C 43 01000011 c 63 01100011
可见,只有第5位(从右往左数,从0开始计数)在大写和小写的二进制中是不一样的,所以我们只要把所有字母的二进制第五位置零,那就是大写,置1就是小写。代码如下:
assume cs:codesg,ds:datasg datasg segment db 'BaSiC' db 'iNfOrMaTiOn' datasg ends codesg segment start:mov ax,datasg mov ds,ax mov bx,0 mov cx,5 s:mov al,[bx] and al,11011111B mov [bx],al inc bx loop s mov bx,5 mov cx,11 s0:mov al,[bx] or al,00100000B mov [bx],al inc bx loop s0 mov ax,4c00h int 21h codesg ends end start
[bx+idata]的内存表示方法与数组处理
除了使用[bx]来表示一个内存单元外,我们还可以使用[bx+idata]来表示一个内存单元,他表示的意思是偏移地址为(bx)+idata(bx中的数值加idata)的内存单元。当然也可写为[idata+bx],除此之外还可写为,200[bx],[bx].200。
既然有了这种表示方法,我们就可以使用这种方法来操作数组,刚才将两个字符串改变大小写的代码的循环部分可以如下优化:
··· s:mov al,[bx] and al,11011111B mov [bx],al mov al,[5+bx] or al,00100000B mov [5+bx],al inc bx loop s ···