第八章 数据处理的两个基本问题
引言
本章对前面的所有内容是具有总结性的。我们知道,计算机是进行数据处理、运算的机器,那么有两个基本的问题就包含在其中:
- 处理的数据在什么地方?
- 要处理的数据有多长?
这两个问题,在机器指令中必需给以明确或隐含的说明,否则计算机就无法工作。本章中,我们就要针对8086CPU对这两个基本问题进行讨论。虽然讨论是在8086CPU的基础上进行的,但是这两个基本问题却是普遍的,对任何一个处理器都存在。
我们定义的描述性符号:reg
和 sreg
。即,寄存器(register)和段寄存器(segment register)。
为了描述上的简洁,在以后的课程中,我们将使用两个描述性的符号 reg 来表示一个寄存器,用 sreg 表示一个段寄存器。
- reg 的集合包括:ax、bx、cx、dx、ah、al、bh、bl、ch、cl、dh、dl、sp、bp、si、di;
- sreg 的集合包括:ds、ss、cs、es。
8.1 bx、si、di、bp
前三个寄存器我们已经用过了,现在我们进行一下总结。
-
在8086CPU中,只有这4个寄存器(bx、bp、si、di)可以用在“[…]”中来进行内存单元的寻址。
以下我们来区分好正确和错误的用法。
正确的指令:
mov ax,[bx] mov ax,[bx+si] mov ax,[bx+di] mov ax,[bp] mov ax,[bp+si] mov ax,[bp+di]
错误的指令:
mov ax,[cx] mov ax,[ax] mov ax,[dx] mov ax,[ds]
-
在“[…]”中,这4个寄存器(bx、bp、si、di)可以单个出现,或只能以四种组合出现:
- bx 和 si
- bx 和 di
- bp 和 si
- bp 和 di
错误的用法(bx 和 bp 不能组合;si 和 di 不能组合):
mov ax,[bx+bp] mov ax,[si+di]
正确的用法:
mov ax,[bx] mov ax,[si] mov ax,[di] mov ax,[bp] 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]
-
只要在 [...] 中使用寄存器 bp,而指令中没有显性的给出段地址,段地址就默认在 ss 中(从这里就可以看出 bp 是帮忙 sp 来减轻负担的)。比如:
mov ax,[bp] ;含义:(ax) = ((ss)*16+(bp)) mov ax,[bp+idata] ;含义:(ax) = ((ss)*16+(bp)+idata) mov ax,[bp+si] ;(ax) = ((ss)*16+(bp)+(si)) mov ax,[bp+si+idata] ;(ax) = ((ss)*16+(bp)+(si)+idata)
8.2 机器指令处理的数据所在位置
绝大部分机器指令都是进行数据处理的指令,处理大致可分为三类:读取、写入、运算。
在机器指令这一层来讲,并不关心数据的值是多少,而关心指令执行前一刻,它将要处理的数据所在的位置。指令在执行前,所要处理的数据可以在三个地方:
- CPU内部
- 内存
- 端口(端口我们将在后面的课程中进行讨论)
举例:
机器码 | 汇编指令 | 指令执行前数据的位置 |
---|---|---|
8E1E0000 | mov bx,[0] | 内存,ds:0 单元 |
89C3 | mov bx,ax | CPU内部,ax 寄存器 |
BB0100 | mov bx,1 | CPU内部,指令缓冲器 |
8.3 汇编语言中数据位置的表达
在汇编语言中如何表达数据的位置?
汇编语言中用三个概念来表达数据的位置
- 立即数(idata)
- 寄存器
- 段地址(SA)和偏移地址(EA)
立即数(idata)
对于直接包含在机器指令中的数据(执行前在CPU的指令缓冲器中),在汇编语言中称为:立即数(idata),在汇编指令中直接给出。例如:
mov ax,1 ;对应机器码:B80100
执行结果:(ax) = 1
寄存器
指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名。例如:
mov ax,bx ;对应机器码:89D8
执行结果:(ax) = (bx)
段地址(SA)和偏移地址(EA)
指令要处理的数据在内存中,在汇编指令中可用 [X] 的格式给出 EA,SA 在某个段寄存器中。存放段地址的寄存器可以是默认的,或者显性的给出。
我们来看示例:
-
存放段地址的寄存器是默认的
-
示例:
mov ax,[0] mov ax,[bx] mov ax,[bx+8] mov ax,[bx+si] mov ax,[bx+si+8]
注:
mov ax,[0]
这么写是可以的,此处相当于mov ax,ds:[0]
。若写为mov ax,[9]
则不可以。只有中括号里数字为0时,才是特殊情况。此时写
mov ax,[0]
或mov ax,ds:[0]
均可;若中括号数字大于零,则正确的写法是mov ax,[9]
或者mov bx,9 mov ax,[bx]
段地址默认在 ds 中!
-
示例:
mov ax,[bp] mov ax,[bp+8] mov ax,[bp+si] mov ax,[bp+si+8]
段地址默认在 ss 中!
-
-
显性的给出存放段地址的寄存器
-
示例
mov ax,ds:[bp] ;含义:(ax) = ((ds)*16+(bp)),段寄存器为 ds。 mov ax,es:[bx] ;含义:(ax) = ((es)*16+(bx)),段寄存器为 es。 mov ax,ss:[bx+si] ;含义:(ax) = ((ss)*16+(bx)+(si)),段寄存器为 ss。 mov ax,cs:[bx+si+8] ;含义:(ax) = ((cs)*16+(bx)+(si)+8),段寄存器为 cs。
-
8.4 寻址方式
当数据存放在内存中的时候,我们可以用多种方式来给定这个内存单元的偏移地址,这种定位内存单元的方法一般被称为寻址方式。
8086CPU有多种寻址方式,我们在前面的课程中都已经用到了,这里我们进行一下总结。
寻址方式 | 含义 | 名称 | 常用格式举例 |
---|---|---|---|
[idata] | EA = idata; SA = (ds) | 直接寻址 | [idata] |
[bx] | EA = (bx); SA = (ds) | 寄存器间接寻址 | [bx] |
[si] | EA = (si); SA = (ds) | 寄存器间接寻址 | |
[di] | EA = (di); SA = (ds) | 寄存器间接寻址 | |
[bp] | EA = (bp); SA = (ss) | 寄存器间接寻址 | |
[bx+idata] | EA = (bx)+idata; SA = (ds) | 寄存器相对寻址 | 用于结构体:[bx].idata |
用于二维数组:[bx][idata] | |||
[si+idata] | EA = (si)+idata; SA = (ds) | 寄存器相对寻址 | 用于数组:idata[si] |
[di+idata] | EA = (di)+idata; SA = (ds) | 寄存器相对寻址 | 用于数组:idata[di] |
[bp+idata] | EA = (bp)+idata; SA = (ss) | 寄存器相对寻址 | |
[bx+si] | EA = (bx)+(si); SA = (ds) | 基址变址寻址 | 用于二维数组:[bx][si] |
[bx+di] | EA = (bx)+(di); SA = (ds) | 基址变址寻址 | |
[bp+si] | EA = (bp)+(si); SA = (ss) | 基址变址寻址 | |
[bp+di] | EA = (bp)+(di); SA = (ss) | 基址变址寻址 | |
[bx+si+idata] | EA = (bx)+(si)+idata; SA = (ds) | 相对基址变址寻址 | 用于表格(结构)中的数组项:[bx].idata[si] |
用于二维数组:idata[bx][si] | |||
[bx+di+idata] | EA = (bx)+(di)+idata; SA = (ds) | 相对基址变址寻址 | |
[bp+si+idata] | EA = (bx)+(si)+idata; SA = (ss) | 相对基址变址寻址 | |
[bp+di+idata] | EA = (bx)+(di)+idata; SA = (ss) | 相对基址变址寻址 |
注:相对对应 idata;基址对应 [bx] 或 [bp];变址对应 [si] 或 [di]。具体举例可参考:[39、数据处理的两个基本问题02][https://www.bilibili.com/video/av28132657/?p=39]
8.5 指令要处理的数据有多长
8086CPU的指令,可以处理两种尺寸的数据,byte 和 word。所以在机器指令中要指明,指令进行的是字节操作还是字操作。
对于这个问题,汇编语言中用一下方法处理。
- 通用寄存器名指明要处理的数据的尺寸。
- 在没有寄存器名存在的情况下,用操作符 X ptr 指明内存单元的长度,X 在汇编指令中可以为word或byte。(新方法)
- 其它方法。
通过寄存器名指明要处理的数据的尺寸
-
下面的指令中,寄存器指明了指令进行的是字操作:
mov ax,1 ;0001H放入16位寄存器ax中 mov bx,ds:[0] ;取ds高8位ds:[1]充当bx高8位;取ds低8位ds:[0]充当bx低8位 mov ds,ax mov ds:[0],ax ;同理,取ax高8位充当ds:[1];取ax低8位充当ds:[0] inc ax add ax,1000
-
下面的指令中,寄存器指明了指令进行的是字节操作:
mov al,1 ;01H放入8位寄存器al中 mov al,bl mov al,ds:[0] mov ds:[0],al inc al add al,100
新方法
-
下面的指令中,用 word ptr 指明了指令访问的内存单元是一个字单元:
mov word ptr ds:[0],1 ;0001H inc word ptr [bx] inc word ptr ds:[0] add word ptr [bx],2
-
下面的指令中,用 byte ptr 指明了指令访问的内存单元是一个字节单元:
mov byte ptr ds:[0],1 ;01H inc byte ptr [bx] inc byte ptr ds:[0] add byte ptr [bx],2
-
在没有寄存器参与的内存单元访问指令中,用 word ptr 或 byte ptr 显性地指明所要访问的内存单元的长度是很必要的。否则,CPU无法得知所要访问的单元是字单元,还是字节单元。
假如我们用Debug查看内存的结果如下:
2000:1000 FF FF FF FF FF FF FF ......
那么指令:
mov ax,2000H mov ds,ax mov byte ptr [1000H],1 ;将内存偏移地址1000H处的字节单元值修改为1
将使内存中的内容变为:
2000:1000 01 FF FF FF FF FF ......
而指令:
mov ax,2000H mov ds,ax mov word ptr [1000H],1 ;将内存偏移地址1000H处的字单元值修改为1
将使内存中的内容变为:
2000:1000 01 00 FF FF FF FF ......
这是因为
mov byte ptr [1000H],1
访问的是地址为ds:1000H
的字节单元,修改的是ds:1000H
单元的内容;而mov word ptr [1000H],1
访问的是地址为ds:1000H
的字单元,修改的是ds:1000H
和ds:1001H
两个单元的内容。
其它方法
有些指令默认了访问的是字单元还是字节单元。比如:push [1000H]
就不用指明访问的是字单元还是字节单元,因为push指令只进行字操作。
8.6 寻址方式的综合应用
-
一般来说,我们可以用
[bx+idata+si]
的方式来访问结构体中的数据。用 bx 定位整个结构体,用 idata 定位结构体中的某一个数据项,用 si 定位数组项中的每个元素。
为此,汇编语言提供了更为贴切的书写方式。
如:
[bx].idata
、[bx].idata[si]
。
8.7 div 指令
-
div 是除法指令(division),使用 div 作除法的时候:
-
除数:8位或16位,在寄存器或内存单元中
-
被除数:(默认)放在 AX 或 DX 和 AX 中
除数 被除数 8位 16位(AX) 16位 32位(DX+AX,高16位存放在DX中,低16位存放在AX中) -
结果
运算 商 余数 8位 AL AH 16位 AX DX
-
-
div 指令格式:
- div reg
- div 内存单元
-
现在我们可以用多种方法来表示一个内存单元了
-
div 指令示例
-
div byte ptr ds:[0]
含义为:
(al) = (ax) / ((ds)*16+0)的商;
(ah) = (ax) / ((ds)*16+0)的余数
-
div word ptr es:[0]
含义为:
(ax) = [(dx)*10000H+(ax)]/((ds)*16+0)的商;
(dx) = [(dx)*10000H+(ax)]/((ds)*16+0)的余数
注:(dx)*10000H 解读为被除数的高16位。类似于,比如十进制数107可以写成1*100(高位)+7(低位)。
-
div byte ptr [bx+si+8]
(al) = (ax) / ((ds)*16+(bx)+(si)+8)的商;
(ah) = (ax) / ((ds)*16+(bx)+(si)+8)的余数
-
div word ptr [bx+si+8]
(ax) = [(dx)*10000H+(ax)] / ((ds)*16+(bx)+(si)+8)的商;
(dx) = [(dx)*10000H+(ax)] / ((ds)*16+(bx)+(si)+8)的余数
-
8.8 伪指令 dd(define double)
-
前面我们用 db 和 dw 定义字节型数据和字型数据。
-
dd 是用来定义 dword(double word双字)型数据的(2*16=32位)。
-
说一台电脑某软件拥有64位的性能,必须满足:
- CPU是64位;
- 操作系统是64位;
- 该软件是64位
缺一不可。否则,低于64位。
-
示例:
data segment db 1 dw 1 dd 1 data ends
在 data 段中定义了三个数据:
第一个数据为 01H,在 data:0 处,占1个字节;
第二个数据为 0001H,在data:1 处,占1个字;
第三个数据为 00000001H,在data:3 处,占2个字节;
8.9 dup
dup 是一个操作符,在汇编语言中同 db、dw、dd 等一样,也是由编译器识别处理的符号。它是和 db、dw、dd 等数据定义伪指令配合使用的,用来进行数据的重复。
-
dup 示例
-
db 3 dup (0)
定义了3个字节,它们的值都是0,相当于 db 0,0,0
-
db 3 dup (0,1,2)
定义了9个字节,它们是
0、1、2、0、1、2、0、1、2
相当于 db 0,1,2,0,1,2,0,1,2
-
db 3 dup ('abc','ABC')
定义了18个字节,它们是
'abcABCabcABCabcABC'
相当于 db 'abcABCabcABCabcABC'
-
可见,dup 的使用格式如下:
- db 重复的次数 dup(重复的字节型数据)
- dw 重复的次数 dup(重复的字型数据)
- dd 重复的次数 dup(重复的双字型数据)
dup 是一个十分有用的操作符。比如我们要定义一个容量为 200 个字节的栈段,如果不用 dup,则必须用这样的格式:
stack segment
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
stack ends
当然,读者可以用 dd,使程序变得简短一些,但是如果要求定义一个容量为1000字节或10000字节的呢?
如果没有 dup,定义部分的程序就变得太长了。有了dup就可以轻松解决。如下:
stack segment
db 200 dup (0)
stack ends
实验七 寻址方式在结构化数据访问中的应用
Power idea 公司从 1975 年成立一直到 1995 年的基本情况如下。
年份 | 收入(千美元) | 雇员(人) | 人均收入(千美元) |
---|---|---|---|
1975 | 16 | 3 | ? |
1976 | 22 | 7 | ? |
1977 | 382 | 9 | ? |
1978 | 1356 | 13 | ? |
1979 | 2390 | 28 | ? |
1980 | 8000 | 38 | ? |
... | |||
1995 | 5937000 | 17800 | ? |
下面的程序中,已经定义好了这些数据:
assume cs:codesg
data segment
db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
db '1993','1994','1995'
;以上是表示 21 年的 21 个字符串
db 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
db 345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000
;以上是表示 21 年公司总收入的 21 个 dword 型数据
dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226
dw 11542,14430,15257,17800
;以上是表示 21 年公司雇员人数的 21 个 word 型数据
data ends
table segment
db 21 dup ('year summ ne ?? ')
table ends
编程,将 data 段中的数据按如下格式写入到 table 段中,并计算 21 年中的人均收入(取整),结果也按照下面的格式保存在 table 段中。
提示:可将 data 段中的数据看成是多个数组,而将 table 中的数据看成是一个结构型数据的数组,每个结构型数据中包含多个数据项。可用 bx 定位每个结构型数据,用 idata 定位数据项,用 si 定位数组项中的每个元素,对于 table 中的数据的访问可采用 [bx].idata 和 [bx].idata[si] 的寻址方式。
注意,这个程序是到目前为止最复杂的程序,它几乎用到了我们以前学过的所有知识和编程技巧。所以,这个程序是对我们从前学习的最好的实践总结。请认真完成。
简答参考:42、寻址方式在结构化数据访问中的应用