汇编语言-基础功能

在之前我们见过了mov,pop,push,add等指令,很显然这些都是最基础的指令,只能执行一些很简单的功能,若要想实现复杂的功能,只用那这些指令是很难办到的,接下来将继续介绍更多的基础指令

[bx]寄存器和loop指令

在之前,我们从内存中取数据到寄存器都是固定数字,如mov ax,[idata],除此之外,还可以mov ax,[bx],这条指令的作用是将DS:(bx中储存的数据)所指向的内存单元的值赋给AX。默认的段地址是DS,也可以手动设定,如mov ax,ES:[bx]

Loop指令的格式为 Loop 标号, CPU执行了loop指令时,会执行两步操作,首先将CX寄存器中的值减一,随后判断CX寄存器中的值是否为零,如果为不零则跳到标号处,如果为零则向下直行。

可以发现CX寄存器控制着loop指令执行的次数,通常我们用loop指令来实现循环,在CX寄存器中储存着循环次数。

在之前的时候,我们想实现二的三次方的计算,会用到下面的代码:

mov ax,2
add ax,ax
add ax,ax

当我们想实现更高次方的计算高次方的计算,如2的12次方计算,就需要不断重复add ax,ax

如果改用loop指令实现,发现代码大大减少:

mov ax,2
mov cx,11
s : add ax,ax
	loop s

注意: 标号一定要在 Loop 标号前被定义。

debug看看loop是怎么执行的

比如我们要实现将内存单元 ffff:6所指向的字节乘以三倍放在DX中

assume cs:code
	code segment
		mov ax,0ffffh ;数据不能以字母开头,记得加0
		mov ds,ax
		mov bx,6
		mov al,[bx]
		mov ah,0
		mov dx,0
		mov cx,3
		s : add dx,ax
			loop s
		mov ax,4c00h
		int 21H
	code ends
end

Debug程序之后,我们可以用U命令来查看载入的程序,我们发现 LOOP 标号此时已经变成了LOOP 0012,通过观察我们可以看到add dx,ax的IP地址为0012H,也就是说,在载入loop指令后,IP指向了下一条指令,随后又经过对CX的判断,将IP修改为0012H。

在之前的程序里,在debug的过程中,程序很简短,我们用单步式调T命令执行完了整个程序,现在有了循环功能,很有可能按段手指也跑不完,G这一命令可以直接执行到程序结束。

在debug中直接写程序和masm编译程序的区别

在之前的debug中,我们想将DS:6所指向的内存数据,放到AX,会用到以下代码,在之前也测试过确实是可行的,但在masm中,这样的写法是无法达到我们的目的。
debug模式:如愿以偿

mov ax,0FFFFH
mov ds,ax
mov ax,[6]

masm编译:会吧6直接赋值给AX

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

如果想解决这个问题我们有两种选择:

  1. 就是像上面的循环程序一样,把bx赋值,再用mov ax,[bx]
  2. 显式的给出段前缀,如mov ax,ds:[6]

有四个段前缀,分别是SS,DS,CS,ES

安全的使用内存

那之前的代码例子中为了方便,使用debug直接向内存中写入数据,但是在实际的电脑中,这样的行为是非常危险,你无法确定你所选择的内存单元是否被其他程序占用。

比如以下程序

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

运行到mov ds:[26h],ax会造成系统死机(dosbox会卡死,无法再输入操作)。可见,在不能确定一段内存空间中是否存放着重要的数据或代码的时候,不能随意向其中写入内容。我们是在操作系统的环境中工作,操作系统管理所有的资源,也包括内存。如果我们需要向内存空间写入数据的话,要使用操作系统给我们分配的空间,而不应直接用地址任意指定内存单元,向里面写入。

一般情况下00200h——002ffh这段内存单元不包含代码和数据。

使用段前缀

尝试将内存ffff:0-ffff:b单元中的数据复制到0:20-0:20b单元中。
在四个段前缀中,DS指向数据段,SS指向堆栈段,CS指向代码转。
ffff:0和0:20相差超过64kb,无法用同一个段前缀表示,想要实现复制,可以把DS反复赋值为FFFFh和0020h,这样做明显不聪明,注意到,四个段前缀中我们只使用了三个,还有ES扩展段没有用到。

assume cs:code
	code segment
		mov ax,Offffh
		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

		movax,4c00h
		int 21h
	code ends
end

多个段的程序

前面的程序中,只有一个代码段。现在有一个问题是,如果程序需要用其他空间来存放数据,使用哪里呢?前面,我们讲到要使用一段安全的空间。可哪里安全呢我们说0:200-0:2FF是相对安全的,可这段空间的容量只有256个字节,如果我们需要的空间超过256个字节该怎么办呢?

在操作系统的环境中,合法地通过操作系统取得的空间都是安全的,因为操作系统不会让一个程序所用的空间和其他程序以及系统自己的空间相冲突。在操作系统允许的情况下,程序可以取得任意容最的空间。

程序取得所需空间的方法有两种,一是在加载程序的时候为程序分配,再就是程序在执行的过程中向系统申请。加载程序的时候为程序分配空间,我们在前面已经有所体验,比如我们的程序在加载的时候,取得了代码段中的代码的存储空间。

考虑一个问题,实现多个在内存中的数据累加,结果保存在ax中:
数据为0123h、0456h、0789h、0abch、0defh、0fedh、0cbah、0987h
可以用下面这段代码实现

assume cs:code
	code segment
		dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
		mov ax,0
		mov bx,0
		mov cx,8
		s:add ax,cs:[bx]
			add bx,2
			loop s

		mov ax,4c00h
		int 21h
	code ends
end

在上面的这段代码中,我们将需要用的数据保存在了代码段中,debug上面的程序可以验证。但这样会有一个问题, IP默认指向CS段开始的数据,会错误的将我们上面的数据当做是代码去执行。为了解决这一问题,一种做法是用start伪指令强制标识程序开始。(dw 也是伪指令,是告诉编译器,分配一个word=2byte的空间来保存一个数据,上面的8个数据,在内存中占用了16byte=8word,除了dw 外还有db申请单byte,dd用来申请双字数据2word=4byte)

assume cs:code
	code segment
		dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
		start:	mov ax,0
				mov bx,0
				mov cx,8
				s:add ax,cs:[bx]
					add bx,2
					loop s

				mov ax,4c00h
				int 21h
	code ends
end start

在程序的第一条指令的前面加上了一个标号start,而这个标号在伪指令end的后面出现。这里,我们要再次探讨end的作用。end除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。在程序中我们用end指令指明了程序的入口在标号start处,也就是说,"mov ax,0"是程序的第一条指令。

下面尝试一下在代码段中使用栈,实现储存在CS:0—F的数据0123h、0456h、0789h、0abch、0defh、0fedh、0cbah、0987h,逆序存放。一种在代码段中使用栈的操作是,dw出一块空的区域,用来当做堆栈。
可以用下面这段代码实现:

assume cs:code
	code segment
		dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
		dw 0,0,0,0,0,0,0,0
		start:	mov ax,cs
				mov ss,ax
				mov sp,20h

				mov bx,0
				mov cx,8
			s:  push cs:[bx]
				add bx,2
				loop s

				mov bx,0
				mov cx,8
			s1: pop cs:[bx]
				add bx,2
				loop s1

				mov ax,4c00h
				int 21h
	code ends
end start

在前面的内容中,我们在程序中用到了数据和栈,将数据、栈和代码都放到了一个段里面。我们在编程的时候要注意何处是数据,何处是栈,何处是代码。这样做显然有两个问题:

  1. 把它们放到一个段中使程序显得混乱;
  2. 面程序中处理的数据很少,用到的栈空间也小,加上没有多长的代码,放到一个段里面没有问题。但如果数据、栈和代码需要的空间超过64KB,就不能放在一个段中(一个段的容量不能大于64KB,是我们在学习中所用的8086模式的限制,并不是所有的处理器都这样)。

所以,应该考虑用多个段来存放数据、代码和栈。我们用和定义代码段一样的方法来定义多个段,然后在这些段里面定义需要的数据,或通过定义数据来取得栈空间。具体做法如下面的程序所示,这个程序实现了和上面程序一样的功能,
不同之处在于它将数据、栈和代码放到了不同的段中。

assume cs:code,ss:stack,ds:data
	data segment
		dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
	data ends
	stack segment
		dw 0,0,0,0,0,0,0,0
	stack ends
	code segment
		start:	mov ax,stack
				mov ss,ax
				mov sp,20h

				mov ax,data
				mov ds,ax

				mov bx,0
				mov cx,8
			s:  push [bx]
				add bx,2
				loop s

				mov bx,0
				mov cx,8
			s1: pop [bx]
				add bx,2
				loop s1

				mov ax,4c00h
				int 21h
	code ends
end start

更灵活的定位内存地址的方法

AND和OR指令,这里短暂介绍下,有计算机基础的都会很容易想到,这2个指令的作用,直接给例子

mov al, 011OOO11B
and al, 00111011B
;执行后:al = 00100011B
;可将操作对象的相应位设为,其他位不变

mov al,01100011B
or  al,00111011B
;执行后:a1 =01111011B
;通过该指令可将操作对象的相应位设为1,其他位不变。

任何数据在计算机中都是二进制储存的,字符也不例外,这里说一下ASCII码在计算机中的储存。世界上有很多编码方案,有一种方案叫做ASCII编码,是在计算机系统中通常被采用的。简单地说,所谓编码方案,就是一套规则,它约定了用什么样的信息来表示现实对象。比如说,在ASCII编码方案中,用61H表示"a",62H表示"b"。一种规则需要人们遵守才有意义。

一个文本编辑过程中,就包含着按照ASCII编码规则进行的编码和解码。在文本编辑过程中,我们按一下键盘的a键,就会在屏幕上看到"a"。这是怎样一个过程呢?我们按下键盘的a键,这个按键的信息被送入计算机,计算机用ASCII码的规则对其进行编码,将其转化为61H存储在内存的指定空间中;文本编辑软件从内存中取出61H,将其送到显卡上的显存中。

工作在文本模式下的显卡,用ASCII码的规则解释显存中的内容,61H被当作字符"a"'显卡驱动显示器,将字符"a"的图像画在屏幕上。我们可以看到,显卡在处理文本信息的时候,是按照ASCII码的规则进行的。这也就是说,如果我们要想在显示器上看到"a"'就要给显卡提供"a"的ASCII码,61H。如何提供?当然是写入显存中。

下面给出如何保存字符串在数据段中:

assume cs:code,ss:stack,ds:data
	data segment
		db 'unIX'
		db 'foRK'
	data ends
	code segment
		start:	mov ax,'a'
				mov ax,4c00h
				int 21h
	code ends
end start

在上面的代码中,db 'unIX'相当于db 75H,6EH,49H,58H,分别对应了每个字符的ascii码。mov ax,'a'相当于mov ax,61H

通过上面的知识储备,已经可以实现字符串的大小写转换了,可能突然有点懵,但确实如此,仔细一想自己的变成经历,你会发现当初做大小写转换,用到过'A'和'a'之间正好差了32=2^5,也就是二进制的第六位。'A' = 41H(0100 0001),'a' = 61H(0110 0001),所以只要检查二进制的第6位就可以确定是大写还是小写字母。
把第一行数据变为大写,第二行变成小写的程序

assume cs:code,ds:data
	data segment
		db 'BaSic'
		db 'iNfOrMaTiOn'
	data ends
	code segment
		start:	mov ax,data
				mov ds,ax

				mov cx,5
				mov bx,0
			s:  mov al,[bx]
				and al,11011111B
				mov [bx],al
				inc bx
				loop s

				mov cx,11
			s1: mov al,[bx]
				or al,00100000B
				mov [bx],al
				inc bx
				loop s1

				mov ax,4c00h
				int 21h
	code ends
end start

对内存操作除了前面说到的立即数(idata)和[bx]之外,还有很多其他的方式,下面一一介绍:

  1. [bx+idata]在bx的基础上加上一个立即数对应的地址

对前面的字符串大小写转换,如果两个字符串长度是相同的,可以用[bx+idata]实现一个循环就可解决。

assume cs:code,ds:data
	data segment
		db 'BaSic'
		db 'iNfOr'
	data ends
	code segment
		start:	mov ax,data
				mov ds,ax

				mov cx,5
				mov bx,0
			s:  mov al,[bx]
				and al,11011111B
				mov [bx],al
				mov al,[bx+5]
				or al,00100000B
				mov [bx+5],al
				inc bx
				loop s

				mov ax,4c00h
				int 21h
	code ends
end start
  1. SI和DI

SI和DI是8086CPU中和bx功能相近的寄存器,SI和DI不能够分成两个8位寄存器来使用。下面的3组指令实现了相同的功能。

mov bx,0
mov ax,[bx]

mov si,0
mov ax,[si]

mov di,0
mov ax,[di]

下面的3组指令也实现了相同的功能

mov bx,0
mov ax,[bx+123]

mov si,0
mov ax,[si+123]

mov di,0
mov ax,[di+123]
  1. [bx+si]或[bx+di]

在前面,我们用[bx(si或di)]和[bx(si或di)+idata]的方式来指明一个内存单元,我们还可以用更为灵活的方式:[bx+si]和[bx+di
[bx+si]和[bx+di]的含义相似,我们以[bx+si]为例进行讲解。[bx+si]表示一个内存单元,它的偏移地址为(bx)+(si)(即bx中的数值加上si中的数值)。
指令mov ax,[bx+si]的含义如下:

将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上si中的数值,段地址在ds中。该指令也常被写作mov ax,[bx][si]

  1. [bx+si+idata]和[bx+di+idata]

相当于在上面的基础是又加了立即数。

mov ax,[bx+200+si]
mov ax,[200+bx+si]
mov ax,200[bx][si]
mov ax,[bx].200[si]
mov ax,[bx][si].200

一个使用寻址小试验

介绍了这么多的寻址方式,现在尝试通过这些来实现一个任务,尽可能用最少的代码:
在数据段中保存了这些字符串,每个字符串长16Byte,尝试把他们的前4个字母变成大写。

db '1. display      '
db '2. brows        '
db '3. replace      '
db '4. modify       '

分析一下问题,有4个字符串,每个字符串需要修改4位,需要循环嵌套使用,循环依靠cx中的值实现控制次数,如果我们单纯的直接从外循环进入内循环,没有保存进入内循环前的cx值,等到内循环结束继续外循环会发现进行不下去了(退出内循环的条件是cx=0,若为保存进入内之前的cx值,外循环只进行一次),所以进入内循环前cx的值需要保存,那么保存到哪里?在简单的情况下,可以直接保存在寄存器中,当所有的寄存器都被用了的时候很这个办法行不通,一个好的方法是使用堆栈去保存cx,使用栈出入不需要其他的寄存器,只用到cx。

观察可以发现第一个字母都在第4个byte,下标为3,所以我们用bx保存每个字符串的开头,si指向要修改的第几个字母,用立即数3修正下标。

assume cs:code,ss:stack,ds:data
	data segment
		db '1. display      '
		db '2. brows        '
		db '3. replace      '
		db '4. modify       '
	data ends
	stack segment
		dw 0,0,0,0,0,0,0,0
	stack ends
	code segment
		start:	mov ax,stack
				mov ss,ax
				mov sp,60h

				mov ax,data
				mov ds,ax

				mov bx,0
				mov cx,4
			s:  push cx
				mov cx,4
				mov si,0

			s1: mov al,[bx+si+3]
				and al,11011111B
				mov [bx+si+3],al
				inc si
				loop s1

				add bx,16
				pop cx
				loop s

				mov ax,4c00h
				int 21h
	code ends
end start

总结

  1. bx,si,di,bp

前 3 个寄存器我们已经用过了, 现在我们进行一下总结。在 8086CPU 中, 只有这4 个寄存器可以用在"[...]" 中来进行内存单元的寻址。比如下面的指令都是正确的:

mov ax ,  [ bx ]
mov ax ,  [ bx +si]
mov ax ,  [ bx +di]
mov ax ,  [ bp ]
mov ax ,  [ bp +si]
mov ax ,  [ bp+di ]

而下面的指令是错误的:

mov  a x ,  [ cx ]
mov  a x ,  [ ax ]
mov  a x ,  [ dx ]
mov  a x ,  [ ds ]

在[...]中,这4个寄存器可以单个出现,或只能以4种组合出现:bx和si、bx和di、bp和si、bp和di。比如下面的指令是正确的:

mov ax,[bx]
mov ax,[si]
mov ax,[di]
mov ax,[bp]
mov ax,[bx+si]
mov ax,[bx+di]
mov ax,[bp+si]
mov ax,[bp+di]
mov ax,[bx+si+idata]
mov ax,[bx+di+idata]
mov ax,[bp+si+idata]
mov ax,[bp+di+idata]

下面的指令是错误的:

mov ax,[bx+bp]
mov ax,[si+di]

只要在[...]中使用寄存器bp,而指令中没有显性地给出段地址,段地址就默认在ss中。

  1. 处理数据的位置

绝大部分机器指令都是进行数据处理的指令,处理大致可分为3类:读取、写入、运算。在机器指令这一层来讲,并不关心数据的值是多少,而关心指令执行前一刻,它将要处理的数据所在的位置。指令在执行前,所要处理的数据可以在3个地方:CPU内部、内存、端口(端口将在后面的课程中进行讨论)。

  1. 汇编语言中数据位置的表达
mov ax,10h     ;立即数
mov ax,bx  	   ;寄存器
mov ax,[bx]	   ;内存
  1. 寻址方式
  1. 指令要处理的数据有多长
    有三种方式:
mov ax,[1834]    ;通过寄存器指定大小,ax为2byte
mov al,[1834]    ;通过寄存器指定大小,al为1byte

mov word ptr ds:[0], 1   ;word ptr指定是字操作
mov byte ptr ds:[0], 1   ;word ptr指定是byte操作

push ax          ;这种指令指定了数据长度为word
  1. div指令

div是除法指令,使用div做除法的时候应注意以下问题。

(1)除数:有8位和16位两种,在一个reg或内存单元中。
(2)被除数:默认放在AX或DX和AX中,如果除数为8位,被除数则为16位,默认在AX中存放;如果除数为16位,被除数则为32位,在DX和AX
中存放,DX存放高16位,AX存放低16位。
(3)结果:如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数;如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数。

32位的被除数可以用dd来申请。

  1. dup操作符

dup也是一个操作符,和dd,dw,db一样由编译器识别处理,他和dd,dw,db一起使用,用来实现数据的重复。

db 3 dup(0)  ; 相当于db 0,0,0
db 3 dup(1,2); 相当于db 1,2,1,2,1,2

;像之前去申请堆栈一样
dw 8 dup(0)  ; 相当于dw 0,0,0,0,0,0,0,0,0
05-03 13:15