汇编基础

汇编基础

字符串和数组

如果可以有效的处理字符串和数组,就能掌握代码优化中的最常见情况。研究表明,绝大多数程序用90%的运行时间执行其10%的代码。毫无疑问,这10%通常发生在循环中,而循环正是处理字符串和数组所要求的结构。本节以编写高效代码为目的,阐释字符串和数组的处理技术

串处理

字符串操作指令的实质是对一片连续存储单元进行处理,这片存储单元是由隐含指针DS:SI或ES:DI来指定的。字符串操作指令可对内存单元按字节或字进行处理,并能根据操作对象的字节数使变址寄存器SI(和DI)增减1或2。

字符串基本指令

x86指令集有5组指令用于处理字节,字,双字数组。虽然他们被称为字符串原语(string primitives),但它们并不局限于字符数组。

指令 说明
MOVSB,MOVSW,MOVSD 传送字符串数据:将ESI寻址的内存复制到EDI寻址的内存位置
CMPSB,CMPSW,CMPSD 比较字符串:比较分别由ESI和EDI寻址的内存数据
SCASB,SCASW,SCASD 扫描字符串:比较累加器(AL,AX,EAX)与EDI寻址的内存数据
STOSB,STOSW,STOSD 保存字符串:将累加器内容保存到EDI寻址的内存位置
LODSB,LODSW,LODSD 从字符串加载到累加器:将ESI寻址的内存数据加载到累加器

使用重复前缀

指令 说明
REP ECX>0时重复
REPZ,REPE 零标志位置1且ECX>0时重复
REPNZ,REPNE 零标志位清零且ECX>0时重复

实例:复制字符串

1
2
3
4
5
cld      ;清除方向标志位
mov esi, OFFSET string1
mov edi, OFFSET string2
mov ecx, 10
rep movsb

上面例子,MOVSB从string1传10字节到string2,每次rep前先测试ECX是不是0,若为0则跳过这句,若ECX>0则将ECX减1执行MOVSB,而MOVSB会自动增加ESI和EDI,这个操作由CPU的方向标志位控制。
MOVSB减1,MOVSW减2,MOVSD减4

方向标志位

方向标志位的值 对ESI和EDI的影响 地址顺序
0 增加 低到高
1 减少 高到低

可以使用CLD和STD显示修改方向标志位
CLD ;方向标志位清零(正向)
STD ;方向标志位置1(反向)

比较

CMPSB, CMPSW,SMPSD用于比较字节,字,双字,同样方向标志位决定EDI和ESI增加还是减少
实例:比较双字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.data
source DWORD 1234h
target DWORD 5678h
.code
mov esi, OFFSET source
mov edi, OFFSET target
cmpsd ;比较双字
ja L1 ;大于跳转

;比较多个的时候
mov esi, OFFSET source
mov edi, OFFSET target
cld
mov ecx, LENGTHOF source
repe cmpsd ;相等则重复

REPE前缀重复比较操作,自动增加ESI和EDI直到ECX=0或者发现了一对不相等的双字

扫描

SCASB,SCASW,SCASD指令分别将AL,AX,EAX中的值与EDI寻址的一个字节,字,双字进行比较。这些指令可以用于在字符串或数组中寻找一个数值,结合REPE。
实例:扫描是否有匹配字符

1
2
3
4
5
6
7
8
9
10
.data
alpha BYTE "ABCDEFGH", 0
.code
mov edi, OFFSET alpha
mov al, 'F' ;检索字符F
mov ecx, LENGTHOF alpha
cld ;标志位正向
repne scasb ;不相等就重复
jnz quit ;未发现字符,退出
dec edi ;发现字符,EDI-1指向该位

需要记得减1才是F的位置,不然指向的是后一位,循环之后添加了JNZ,以避免出现由于ECX=0而没有找到AL中的字符结束循环的可能性

填充

LODSB,LODSW,LODSD指令分别从ESI指向的内存地址加载一个字节或一个字到AL,AX,EAX。ESI按照方向标志位的状态递增或递减。LODS很少与REP前缀一起使用。原因是,加载到累加器的新值会覆盖其原先的内容。所以LODS常常被用于加载单个数值
实例:数组乘法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
INCLUDE Irvine32.inc
.data
array DWORD 1,2,3,4,5,6,7,8,9,10
multiplier DWORD 10
.code
main PROC
cld
mov esi, OFFSET array
mov edi, esi
mov ecx, LENGTHOF array
L1:
lodsd ;[ESI]加载到EAX
mul multipier
stosd ;EAX保存到[EDI]
loop L1
main ENDP
END main

部分字符串过程

用Irvine32链接库中的几个过程来处理空字节结束的字符串。这些过程和标准C库有明显的相似性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
;将源串复制到目的串
Str_copy PROTO,
source: PTR BYTE,
target: PTR BYTE
;用EAX返回串长度(包括0字节)
Str_length PROTO,
string1:PTR BYTE
string2:PTR BYTE
;从字符串尾部去除特定的字符
;第二个参数是要去除的字符
Str_trim PROTO,
pString:PTR BYTE
char:BYTE
;将字符串转为大写
Str_ucase PROTO,
pString:PTR BYTE

Irvine64库中的字符串过程

  • Str_compare
    比较两个字符串
    输入参数:RSI为源指针,RDI为目的指针
    返回值:若源串<目的串,进位标志位CF=1,若源串=目的串,ZF=1,若源串>目的串,ZF=0且CF=0
  • Str_copy
    将源串复制到目的指针指向的位置
    输入参数:RSI为源串指针,RDI指向被复制串将要存储的位置
  • Str_length
    返回以空字节结束字符串的长度
    输入参数:RCX为字符串指针
    返回值:RAX为该字符串的长度

二维数组

在汇编语言的程序员看来,二维数组是一维数组的高级抽象。高级语言有两种方法在内存中存放数组的行和列:行主序列主序,就是按行的顺序来排列或者按列的顺序来排列

二维数组的定义和访问

1
2
3
4
5
6
7
8
9
10
11
12
13
.data
tableB BYTE 10h, 20h, 30h, 40h, 50h
Rowsize = ($-tableB)
BYTE 60h, 70h, 80h, 90h, 0Ah
BYTE 0Bh, 0Ch, 0Dh, 0Eh, 0Fh
;访问行1列2的80h
row_index = 1
column_index = 2
.code
mov ebx, OFFSET tableB
add ebx, RowSize * row_index
mov esi, column_index
mov al, [ebx+esi*TYPE tableB]

64位模式下的基址-变址操作数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Crlf PROTO
WriteInt64 PROTO
ExitProcess PROTO

.data
table QWORD 1,2,3,4,5
RowSize = ($ - table)
QWORD 6,7,8,9,10
QWORD 11,12,13,14,15
.code
main PROC
mov rax, 1
mov rsi, 4
call get_tableVal
call WriteInt64
call Crlf

mov ecx, 0
call ExitProcess
main ENDP

get_tableVal PROC USES rbx
;--------------------------------
;返回四字二维数组中给定的行列的值的元素
;参数:RAX=行数,RSI=列数
;返回:RAX存储找到的元素
;--------------------------------
mov rbx, RowSize
mul rbx
mov rax, table[rax+rsi*TYPE table]
ret
get_tableVal ENDP
end

需要注意的是,如果使用寄存器索引操作数,就必须使用64位寄存器

Author

Ctwo

Posted on

2019-08-13

Updated on

2020-10-25

Licensed under

Comments