7.3 数组、查找表与字符串
第 7.2 节建立了循环和状态转移,却还没有规定它们遍历什么数据。现在把控制流连接到连续存储器。处理器只看见带地址的字节;数组、查找表或字符串之所以存在,是因为程序遵守某种布局契约。
数组是基址加上一组等宽元素
数组(array)在连续存储器中保存固定数量、宽度相同的元素。它有三个必要事实:
- 元素 0 的基址(base address)。
- 每个元素占用的字节数,也就是元素宽度。
- 决定合法下标范围的元素数量。
对于从零开始的下标 ,字节地址为:
8086 的有效地址表达式可以相加基址寄存器、变址寄存器与位移,但没有通用的缩放下标项。字节数组不需要缩放;字数组下标必须先在临时寄存器中乘 2:
; SI 是 0 到 count-1 的元素下标
CMP SI,count
JAE out_of_bounds
MOV BX,SI
SHL BX,1
MOV AX,word_array[BX]若字数组从 1000H 开始,下标 3 从 1006H 开始。保存的 BEEFH 在 1006H 放置 EFH,在 1007H 放置 BEH;处理器按小端顺序重建这个字。
地址本身不携带边界信息。对只有六个元素的数组读取下标 8,8086 不会触发“数组错误”,而是直接读取数组之后碰巧存在的字节。程序必须在访问前把下标与元素数量比较。
查找表用数据替代重复判断
查找表(lookup table)用较小输入作为下标,保存预先计算的输出。显示程序不必把一个数字与十种情况逐个比较,而可以直接用数字选择七段显示编码。
对于 DS:BX 指向的字节表,8086 的 XLAT 指令把无符号 AL 作为下标,读取 DS:[BX+AL],再用读出的字节替换 AL。指令很紧凑,却不会检查边界:
; AL 必须是 0 到 9 的数字
CMP AL,10
JAE invalid_digit
MOV BX,OFFSET seven_segment
XLAT查找表把策略显式保存成数据。修改数字 6 的编码只需改一个表项,不必修复多条分支。它还建立了清楚的测试边界:每个合法下标都应映射到预期字节,第一个非法下标必须进入拒绝路径。
字符串必须规定停止方式
字符串(string)是字符编码序列。第 2 章介绍过 ASCII;这里每个 ASCII 字符占一个字节。存储器仍然不会记录序列在哪里结束,所以程序需要采用两种常见契约之一:
- 计数字符串(counted string)另外携带长度。包括 00H 在内的任何字节值都可以作为数据。
- 终止字符串(terminated string)保留一个特殊的哨兵值(sentinel),通常是 00H,用于标记结尾。哨兵不能同时作为该字符串内部的普通字符。
有边界的搜索可以同时使用终止字节与最大计数:
; DS:BX 指向字节,AL 是目标,CX 是最大可读长度
XOR SI,SI
search:
JCXZ not_found
MOV DL,[BX+SI]
CMP DL,0
JE not_found
CMP DL,AL
JE found
INC SI
DEC CX
JMP search零字节处理正常的较短字符串;CX 处理终止字节缺失的损坏或不可信数据。任一条件都能在 SI 离开批准区域前停止循环。
边界情况真正定义了契约:
- 计数为空的字符串长度为零,不能读取第一个地址。
- 终止形式的空字符串以 00H 开头,读取这个哨兵后立即结束。
- 目标不存在时,搜索在长度或哨兵处停止,不能继续到无关存储器中碰巧出现的匹配值。
- 复制操作需要足够的目的容量;如果格式需要终止字节,容量还必须包含它。
只有代码保持宽度、数量与终止规则时,数组和字符串才会把地址变成结构化数据。第 7.4 节把同样思想用于另一种存储结构:调用者与过程共同遵守的栈布局。