汇编语言学习

以8086CPU为例来进行学习

汇编语言的基础知识

汇编语言的主体是汇编指令。汇编指令和机器指令的差别在于指令的表示方法上。汇编指令是机器指令便于记忆的书写格式。

每一种CPU都有自己的汇编指令集

汇编指令有一下三类指令组成

(1)汇编指令:机器码的助记符,有对应的机器码

(2)伪指令:没有对应的机器码,由编译器执行,计算机并不执行

(3)其他符号:如+,-,*,/等,由编译器识别,没有对应机器码


CPU在内存中读数据,需要和存储器进行3类信息的交互(CPU可以直接使用的信息在存储器中存放)

  • 存储单元的地址(地址信息)
  • 器件的选择,读或写的命令(控制信息)
  • 读或写的数据(数据信息)

tip:在存储器中指令和数据没有任何区别,都是二进制信息

每一个CPU都有许多管脚,这些管脚和总线相连,所以说CPU可以引出3中总线的宽度标志了这个CPU的不同方面的性能:

  • 地址总线的宽度决定了CPU的寻址能力
  • 数据总线的宽度决定了CPU与其他器件进行数据传送时的依次数据传送量
  • 控制总线的宽度决定了CPU对系统中其他器件的控制能力

存储器:

从读写属性上看分为两类:RAM(随机存储器),ROM(只读存储器)

其中,在功能和连接上又分为:

  • 随机存储器
  • 装有BIOS的ROM
  • 接口卡的RAM

内存地址空间

存储器,在物理上是独立的器件,但在以下两点上相同

  • 都和CPU的总线相连
  • CPU对他们进行读或写的时候都通过控制线发出内存读写命令

也就是说,CPU在操纵他们的时候,把他们都当做内存来对待,把他们总的看做一个由若干储存单元组成的逻辑存储器,这个逻辑存储器就是我们所说的内存地址空间。

每个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间。CPU在这段地址空间中读写数据,实际上就是在相对应的物理存储器中读写数据。

寄存器

1.通用寄存器

8086CPU的所有寄存器都是16位的,可以存放两个字节

AX,BX,CX,DX这四个寄存器都是8位的,并且通用,为了保证兼容,使原来基于上代CPU编写的程序稍加修改就可以运行在8086CPU上,这4个寄存器都可以分为两个可独立使用的8位寄存器来用:

  • AX可分为AH和AL
  • BX可分为BH和BL
  • CH可分为CH和CL
  • DX可分为DH和DL

数据:18

二进制表示:10010

2.字在寄存器中的储蓄

  • 字节:记为Byte,一个字节由8个bit组成,可以存在8位寄存器中
  • 字:结尾word,一个字由两个字节组成,这两个字节分别被称为这个字的高位字节和低位字节(前8位为高位字节,后8位为低位字节)

3.几条汇编指令

mov ax,18                        #将18写入寄存器AX中
mov ah,78					   #将78写入寄存器AH中
add ax,8					   #将寄存器AX中的数值加上8
mov ax,bx					   #将寄存器BX中的送入AX中
add ax,bx					   #将AX和BX中的数值相加,并存在AX中

4.物理地址

8086cpu是16位结构的cpu,具有下面几方面的结构特性

  • 运算器依次最多可以处理16位的数据
  • 寄存器的最大宽度为16位
  • 寄存器和运算器之间的通路为16位

8086CPU有20位地址总线,可以传送20位地址,达到1MB的寻址能力

8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址,当8086CPU要读写内存时:

(1)CPU中相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址

(2)段地址和偏移地址通过内部总线送入一个称为地址加法器的部件

(3)地址加法器将两个16位地址合成一个20位的物理地址

(4)地址加法器通过内部总线将20位的物理地址

(5)输入输出控制电路将20位物理地址送上地址总线

(6)20位物理地址被地址总线传送到储存器

地址加法器采用 物理地址 = 段地址 * 16 + 偏移地址的方法用段地址和偏移地址合成物理地址

这段公式的本质含义是:CPU访问内存时,用一个基础地址(段地址*16)和一个相对于基础地质的偏移地址相加,给出内存单元的物理地址


5.段寄存器

8086CPU有4个段寄存器:CS,DS,SS,ES

6.CS和IP

CS和IP是8086CPU中最重要的两个寄存器,其中,CS是代码寄存器,IP是指令指针寄存器

设CS中的内容为M,IP中的内容为N,8086CPU将从内存M*16+N单元开始,读取一条指令并执行(任意时刻,CS:IP指向的内容当做指令执行)

通过上面的过程,8086CPU的工作过程可以简要描述成:

(1)从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器

(2)IP = IP + 所读取指令的长度,从而指向下一条指令

(3)执行指令。转到步骤1,重复这个过程

修改CS,IP的指令

mov不能用于设置CS:IP的值,如果想同时修改CS:IP的内容,可用形如“jmp 段地址:偏移地址”的指令完成,如:

jmp 2AE3:3   #执行后:CS=2AE3H ,IP=0003h ,CPU将从2AE33H处读取指令
jmp 3:0B16   #执行后:CS=0003H ,IP=0B16H ,CPU将从00B46H处读取指令

“jmp 段地址:偏移地址”指令的功能为:用指令中给出的段地址修改CS,偏移地址修改IP

若想仅修改IP的内容,可以用形如”jmp 某一合法寄存器”的指令完成

jmp ax    #指令执行前:ax=1000h,CS=2000H,IP=0003H
		 #指令执行后: ax=1000H,CS=2000H,IP=0B16H

在含义上,好似”mov IP ax”


Debug模式

debug是Dos,Windows都提供的实模式(8086方式)程序的调试工具

会经常用到的Debug功能:

  • R命令查看,改变CPU寄存器的内容
  • D命令查看内存中的内容
  • E命令改写内存中的内容
  • U命令将内存中的机器指令翻译成汇编指令
  • T命令执行一条机器指令
  • A命令以汇编指令的格式在内存中写入一条机器命令

-r指令,-r 寄存器,即可修改寄存器中的内容

如图,输入指令后,会提示当前AX中的内容,用一个冒号提示修改后的值,即可修改


-d 查看内存中的内容,如图


输入-a后,即可以在内存中输入汇编指令

可以看见,已经切换到某个内存单元中去了

输入

mov ax,4e20
add ax,1416
mov bx,2000
add ax,bx
mov bx,ax
add ax,bx
mov ax,001A
mov bx,0026
add al,bl
add ah,bl
add bh,al
mov ah,0
add al,bl
add al,9c

输入-u 138c:0100即可查询到刚刚输入的汇编代码


寄存器(内存访问)

字单元

字单元,及存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。我们将起始地址为N的字单元简称为N地址字单元。

DS和[address]

mov bx,1000H
mov ds,bx
mov al,[0]

上面的3条指令将10000H(1000:0)中的数据读到al中

其中[…]表示一个内存单元,[…]中的0表示内存单元的偏移地址,程序执行时候8086CPU自动取ds中的数据作为段地址

字的传送

mov bx,1000H
mov ds,bx
mov al,[0]
mov [0],cx                ;cx中的16位数据送到1000:0处

mov,add,sub指令

add和sub指令同mov一样,都有两个操作对象。他们也可以有一下几种形式

add 寄存器,数据
add 寄存器,寄存器
add 寄存器,内存单元
add 内存单元,寄存器
sub 寄存器,数据
sub 寄存器,寄存器
sub 寄存器,内存单元
sub 内存单元,寄存器

数据段

对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。我们可以将一组长度为N(N<=64KB),地址连续,起始地址为16倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。可以在具体操作的时候,用ds存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元

mov ax,123BH
mov ds,ax
mov ax,0                ;用ax存放累计结果
add ax,[0]              ;将数据段第一个字(偏移地址为0),加到ax中
add ax,[2]              ;将数据段第二个字(偏移地址为2),加到ax中
add ax,[4]              ;将数据段第三个字(偏移地址为4),加到ax中

注意,一个字型数据占两个单元,所以偏移地址是0,2,4

进出规则为:“Last In First Out”(后进先出)

PUSH进栈指令 POP出栈指令

栈操作示意:

mov ax,0123H
push ax
mov bx,2266H
push bx
mov cx,1122H
push cx
pop ax
pop bx
pop cx

注:字型数据用两个单元存放,高地址单元存放高8位,低地址单元存放低8位

8086cpu中,有两个寄存器,段寄存器SS和寄存器SP,栈顶的段地址存放在SS中,偏移地址存放在SP中

任意时刻,SS:SP指向栈顶元素push指令和pop指令执行时,CPU从SS和SP中得到栈顶的位置,但是当栈空的时候,栈中没有元素,也就不存在栈顶元素,所以SS:SP只能指向栈的最底部单元下面的单元,该单元的偏移地址为栈最底部的字单元的偏移地址+2,栈最底部字单元的地址为1000:000E,所以栈空时,SP=0010H

pop指令的执行过程和push刚好相反,由以下两步完成

(1)将SS:SP指向的内存单元处的数据送入ax中

(2)SP=SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶

栈顶超界问题

栈顶超界是危险的,如果我们在入栈出栈的时候不小心,而将这些数据,代码意外地改写,将会引发一系列的错误

PUSH和POP指令

push 寄存器/段寄存器/内存单元                 ;将一个寄存器/段寄存器/内存单元中的数据入栈
pop 寄存器/段寄存器/内存单元				  ;出栈,用一个寄存器/段寄存器/内存单元接收出栈的数据

我们也可以使用push和pop在内存单元和内存单元之间传送数据

push 内存单元      ;将一个内存字单元处的字入栈(注:栈操作都是以字为单位)
pop 内存单元       ;出栈,出战的数据送入1000:2处

CPU要知道内存单元的地址,可以在push,pop指令中只给出内存单元的偏移地址,段地址在指令执行时,CPU从ds中获得

注:push,pop等栈操作指令,修改的只是SP。也就是说,栈顶变化范围最大为:0~FFFFH

栈的综述:

(1)栈根据SS:SP指示的地址以栈的方式访问内存

(2)push指令的执行步骤:1.SP=SP-2 向SS:SP指向的字单元送入数据

(3)pop:SP=SP+2,也是从SS:SP指向的的字单元中读取数据

栈段

我们可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元

mov ax 1000H
mov ss,ax
mov sp,0020H             ;初始化栈项
mov ax,cs
mov ds,ax                ;设置数据段段地址
mov ax,[0]
add ax,[2]
mov bx,[4]
add bx,[6]
push ax
push bx
pop ax
pop bx

第一个程序

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

其中assume cs:codesg是把codesg与cs寄存器连接起来

sagement表示以codesg为名称的段开始

codesg ends表示codesg段结束

end表示整个汇编函数结束

mov ax,4c00H
int 21H

实现的是程序返回(调用了INT 21H的4CH号中断)

编译


[BX]和loop命令

[bx]表示一个内存单元,它的偏移地址在bx中,比如:

mov ax,[bx]

将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址在bx中,段地址在ds中

loop意为循环,这个函数和循环有关

[BX]

mov bx,1
inc bx              ;bx的内容+1

Loop指令

loop指令的格式:loop标号,CPU执行loop指令时,要进行两步操作

1.(cx)=(cx)-1

2.判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行

例:

假如说我们要进行计算2^3(2*2*2),那么可以这样实现

assume cs:code
code segment
	mov ax,2
	add ax,ax
	add ax,ax
	mov ax,4C00H
	INT 21H
code ends
end

那如果要计算2^10,如果直接进行计算,那么重复次数就有点多了,可以用loop指令来化简

assume cs:code
code segment
	mov ax,2
	mov cx,10              ;循环次数
s: add ax,ax               ;定义标号s
	loop s                 ;循环s
	mov ax,4C00H
	INT 21H
code ends
end

cx是循环计数寄存器,所以循环次数的值写入cx中

如果我们在debug中追踪循环的值,如果循环次数过大,我们要用相对应次数的t命令才能从循环中出来,所以我们可以用p命令来一次性完成循环的内容

loop和[bx]的联合使用

如果计算ffff:0~ffff:b单元中数据的和,结果存储在dx中

有两种方法

  • (dx)=(dx)+内存中的8位数据;
  • (dl)=(dl)+内存中的8位数据。

但是,第一种方法两个运算对象不匹配,第二种方法是可能会超界

可以用一个16位寄存器作为中介,编写以下程序

assume cs:code
code segment
	mov ax,0ffffh
	mov ds,ax
	mov dx,0               ;初始化累加寄存器,(dx)=0
	
	mov al,ds:[0]          
	mov ah,0               ;(ax)=((ds)*16+0)=(ffff0h)
	add dx,ax              ;向dx中加上ffff:0单元的数值
	
	mov al,ds:[1]
	mov ah,0                ;(ax)=((ds)*16+1)=(ffff1h)
	add dx,ax               ;向dx中加上ffff:1单元的数值
	
	...
	
	mov al,ds:[0bs]
	mov ah,0                 ;(ax)=((ds)*16+0bh)=(ffffbh)
	add dx,ax                ;向dx中加上ffff:b单元的数值

程序写得很长,为了简化,我们可以用数学语言来描述这个累加的运算

用程序运行可以描述成这样:

s:(a1)=((ds)*16+(bx))
  (ah)=0
  (dx)=(dx)+(ax)
  (bx)=(bx)+1
  loop s

最后,程序可以改成这样

assume cs:code
code segment
	mov ax,0ffffh
	moc ds,ax
	mov bx,0            ;初始化ds:bx指向ffff:0
	mov dx,0            ;初始化累加寄存器,dx,(dx)=0
	mov cx,12           ;初始化循环计数寄存器cx,(cx)=12
	
s: mov a1,[bx]
	mov ah,0
	add dx,ax            ;间接向dx中加上((ds)*16+(bx))单元的数值
	inc bx               ;ds:bx指向等下一个单元
	loop s
	
	mov ax,4c00h
	int 21h
	
	code ends
end

段前缀

顾名思义,就是可以为偏移地址加上段前缀

例:mov ax,ds:[bx]意为:将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址在bx中,段地址在ds中

一段安全的空间

在8086中,随意向一段空间写入内容是很危险的,因为这段空间可能存放着重要的系统数据或者代码,从而引发报错

如下面这一串代码

assume cs:code
code segment
	mov ax,0
	mov ds,ax
	mov ds:[26h],ax
	
	mov ax,4c00h
	int 21h
	
	code ends
end

可以看到源程序经过masm翻译,再经过debug解释,最后得到的是”mov [0026],ax”

发生了报错,如果是在原生dos模式下则会发生死机,可见,不能确定一段内存空间中是否存放盒重要数据或代码的时候,不能随意向里面写入内容

  • DOS模式下,一般情况,0:2000:2ff这段空间没有系统或其他程序的数据或代码,所以我们需要往内存里面写内容时,就用0:2000:2ff这段空间

包含多个段的程序

在代码段使用数据

将以下8个数据的和,结果存在ax寄存器中:

0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h

assume cs:code
code segment
	dw 0123h,0456h,......,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”的含义是定义字型数据。dw即define word,在这里,使用dw定义了8个字型数据,占用内存空间大小为16个字节(每个数据占2个字节)