汇编实现电话本 Tips

汇编基础知识的实践

Posted by WuJ on April 20, 2020

汇编实现电话本 Tips

之前实现了汇编程序的电话本,其中遇到很多坑和很多新知识,这里稍微总结一下,给大家提供一些思路 (完整代码见最后)

0x01 .data 段

.data 段会存放常量、全局变量、输入输出提示符、打印信息等

换行

汇编中的换行符为 0dh, 0ah,同时需要在字符串结尾添加 0

例:

1
2
info db "Hello", 0dh, 0ah,
	"World!", 0

输出:

1
2
Hello
World

定义常量

name equ <表达式>

上述换行就可以简化定义成一个常量

endl EQU <0dh,0ah>

实现输入输出

汇编中实现输入输出有多种方式,此处使用第三种,因为它可以方便输出更多数据类型,也更类似 C 语法

1、使用 MASM 提供的 StdOut、StdIn 函数;

2、使用系统 API:

3、使用微软 C 标准库 msvcrt.dll 中的 crt_printf、crt_scanf 函数.

Win32汇编入门教程03控制台下的几种输出方式

输出

  1. 在 .data 段中定义要输出的字符串
  2. 在 .code 段中调用函数

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.data
	szShowMenu db "电话本 V1.0", endl, 
		"1. 添加数据", endl,
		"2. 显示数据", endl,
		"3. 查找数据", endl,
		"4. 删除数据", endl,
		"5. 修改数据", endl,
		"6. 退出程序", endl,
		"请输入选项: ", 0
.code
	push offset szInputNum	; 将字符串地址压入栈中
	call crt_printf			; 调用函数
	add esp, 4				; 平栈
	; 或者
	; invoke crt_printf, addr szInputNum
  ; ret

输出:

输入

  1. 在 .data 段定义格式化字符串
  2. 在 .code 段调用函数

例:

1
2
3
4
5
6
7
8
9
10
11
.data
	str_format_int	db	"%c", 0
	; 用于保存菜单的选项
	choose db 0	; 定义存放输入的变量
.code
	; 获取用户的输入
	push offset choose	; 存放输入的变量地址
	push offset str_format_int	; 格式化字符的地址此处表示获取一个字符
	call crt_scanf
	; 有两个参数需要平衡 2*4 字节
	add esp, 8

0x02 汇编中的函数

任何非 demo 都建议使用函数来增强代码复用和可读性,汇编中定义函数需要 使用 procendp

1
2
3
4
5
6
7
8
9
.code
	Func1 proc
		mov eax, 3
	Func1 endp
	
	main proc
		call Func1
	main endp
	end main

image-20200420120643558

0x03 循环

汇编中循环多用 标号jmploop 实现,也可以使用伪代码,但限制太多比较难实现复杂的循环

例:

1
2
3
4
5
6
7
8
9
mov eax, val1                 			; 把变量复制到 EAX
beginwhile:
        cmp eax, val2                  ; 如果非 val1 < val2
        jnl     endwhile               ; 退出循环
        inc    eax                     ; val1++;
        dec    val2                    ; val2--;
        jmp    beginwhile              ; 重复循环
endwhile:
        mov    val1, eax                ;保存 val1 的新值

使用汇编语言实现WHILE循环

0x04 实现数组遍历(查询功能)

实现数组遍历,即找到对应数组信息是非常重要的功能,除了查询,删除和修改都要用到这个功能

repe cmpsb 指令解析

repe 是一个串操作前缀,它重复串操作指令,每重复一次 ECX 的值就减一 一直到 CX 为 0 或 ZF 为 0 时停止。

cmpsb 是字符串比较指令,把 ESI 指向的数据与 EDI 指向的数一个一个的进行比较。如果两个字符相等,即 ZF 为 1,当不相等时 ZF 为 0

当 repe cmpsb 配合使用时就是实现字符串比较,当相同时继续比较,不同时(ZF = 0)停止比较

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
start_for:
	cmp edx, count
	jnb info_output

	; 获取结构体的大小
	mov eax, sizeof(Contact)
	; edx 相当于计数器 i计算 arr[i] 的偏移
	imul eax, edx
	; 获取数组首地址
	lea esi, TeleContacts
	; 计算对应下标 i 元素的首地址
	add esi, eax

	; username 里存放我们输入的想要查询的字符串
	lea edi,  username
	; 这里是要进行对比的第 i 个数组的字符串
	lea esi, [esi + Contact.user]
	; 设定最长对比长度
	mov ecx, 20h

	; 比较 edi  esi 里存的字符串
	repe cmpsb
	; 不相等就进入下一次循环此时 ZF=0 jnz会跳转
	jne next_for
	; 相等就打印信息
	xxx ; 
next_for:
	inc edx
	jmp start_for

0x05 删除功能

首先找到我们需要删除的信息,同上

之后,从后一个数组元素开始的地址,将每个字节都向前填充到前一个元素的地址,借此实现该数组元素的删除

rep movsb 指令解析

搬移字串指令有两种,分别是 MOVSB 和 MOVSW,先说 MOVSB。MOVSB 的英文是 move string byte,意思是搬移一个字节,它是把 DS:SI 所指地址的一个字节搬移到 ES:DI 所指的地址上,搬移后原来的内容不变,但是原来 ES:DI 所指的内容会被覆盖而且在搬移之后 SI 和 DI 会自动地址向下一个要搬移的地址。

通常,程序并不会只搬一个字节,都会重复许多次,如果要重复的话,就得把重复次数 ( 也就是字串长度 ) 先记录在 CX 寄存器,并且在 MOVSB 之前加上 REP 指令,REP 是重复 (repeat) 的意思。

一般而言汇编语言源文件的每一行都只有一个指令,但 REP MOVSB 却可以在同一行写两个指令,当然分开写也是一样的。

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
del_process:
	; 将序号后的字节都前移
	mov ebx, find_i
	mov ecx, sizeof(Contact)
	imul ebx, ecx
	lea edi, TeleContacts
	; 获取目的地址
	add edi, ebx
	; 获取源地址目的地址后一个
	mov esi, edi
	add esi, ecx

	; 计算拷贝数量
	mov eax, count
	sub eax, find_i
	; 需要前移的字节
	imul ecx, eax

	; 循环前移  DS:SI 所指地址的一个字节搬移到 ES:DI 所指的地址上
	rep movsb
	; 总数 -1
	dec count
	; 成功删除
	push offset szDelSuccess
	call crt_printf
	add esp, 4

0x06 完整代码

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
; .386
; .model flat,stdcall
; .stack 4096

include Irvine32.inc
includelib kernel32.lib
includelib Irvine32.lib
include msvcrt.inc
includelib msvcrt.lib
include masm32.inc
includelib masm32.lib



ExitProcess proto,dwExitCode:dword

; 定义需要的结构体
Contact struct
    ; 需要定义的数据
    user db 32 dup(0)
    tel db 20 dup(0)
Contact ends

.data

; 设置控制台标题
titleStr BYTE "汇编电话本 V1.0    Author: WuJ1n9", 0

endl EQU <0dh,0ah>            ; 行结尾

; 定义一个结构体数组用于存放所有的联系人
TeleContacts Contact 100 dup(<'0'>)
TempTeleContacts Contact <'0', '0'>

; 用于保存菜单的选项
choose db 0

; 清空屏幕
str_system_cls db "cls", 0

; 当前最多能存放多少个数据
max_count dd 20

; 暂停
str_system_pause db "pause", 0dh, 0ah, 0

; 已经存放了多少个数据
count dd 0

; 序号
find_i dd 0

; 用于保存用户名和电话
username db 32 dup(0)
telenum db 20 dup(0)

szShowMenu db "电话本 V1.0", endl, 
		 "1. 添加数据", endl,
         "2. 显示数据", endl,
         "3. 查找数据", endl,
		 "4. 删除数据", endl,
		 "5. 修改数据", endl,
         "6. 退出程序", endl,
         "请输入选项: ", 0
         ; 0dh 0ah == \r\n 0 字符串结尾
ShowMenuSize db	($ - szShowMenu)

szInputName db "请输入联系人的姓名:", endl, 0
InputNameSize db ($ - szInputName)
szInputNum db "请输入联系人的电话:", endl, 0
szInputError db "请重新输入格式正确的选项!(数字1-6)", endl, 0
szFenGe db ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", endl, 0
szFind db "请输入要查询联系人的姓名:", endl, 0
szFindNoone db "查无此人", endl, 0
szDelWho db "请输入要删除联系人的姓名:", endl, 0
szDelSuccess db "删除成功!", endl, 0
szModify db "请输入要修改联系人的姓名:", endl, 0
szModifySuccess db "修改成功!", endl, 0
szInputNewName db "请输入新的联系人姓名:", endl, 0
szInputNewNum db "请输入新的联系人电话:", endl, 0

str_printf_size db "超出容量", 0dh, 0ah, 0
str_format_str	db		"%s", 0
str_format_int	db		" %c", 0 ; 此处前面加一个空格用于把换行符吃掉
; 输出联系人信息
str_show_content db "%d: %-7s %-7s", 0dh, 0ah, 0

stdOutHandle	HANDLE	0     ; 标准输出设备句柄
stdInHandle		HANDLE	0

byteWritten	DWORD ?      ; 输出字节数
bytesRead   DWORD ?

.code

; 控制台标题
INVOKE SetConsoleTitle, ADDR titleStr


AddData proc
		; 获取当前已存放的个数
		mov ecx, count
		; 判断是否大于容量
		
		.if ecx == max_count
		; 如果超出了就报错
		jmp size_error
		.endif

		; 打印提示
		; 因为 printf 会修改 edx, ecx  eax 的值所以需要进行保存
		push edx
		push eax
		push ecx
		push offset szInputName
		call crt_printf
		add esp, 4
		pop ecx
		pop eax
		pop edx

		; 接收用户的输入
		; 获取结构体的大小
		mov eax, sizeof(Contact)
		; 计算偏移
		imul eax, ecx
		; 获取数组首地址
		lea esi, TeleContacts
		; 计算对应下标元素的首地址
		add esi, eax

		; 获取联系人
		lea ebx, [esi + Contact.user]
		push ebx
		push offset str_format_str
		call crt_scanf
		add esp, 8

		; 打印提示
		; 因为 printf 会修改 edx, ecx  eax 的值所以需要进行保存
		push edx
		push eax
		push ecx
		push offset szInputNum
		call crt_printf
		add esp, 4
		pop ecx
		pop eax
		pop edx

		; 获取联系人电话
		lea ebx, [esi + Contact.tel]
		push ebx
		push offset str_format_str
		call crt_scanf
		; 平栈
		add esp, 8

		; 自增计数器 count
		inc count
		jmp ret_addr

size_error:
		push offset str_printf_size
		call crt_printf
		add esp, 4

ret_addr:
		ret
AddData endp


ShowData proc
	; 打印分割符
	push offset szFenGe
	call crt_printf
	add esp, 4

	mov ecx, 0
	.while ecx < count
		; 获取结构体的大小
		mov eax, sizeof(Contact)
		; 计算偏移
		imul eax, ecx
		; 获取数组首地址
		lea esi, TeleContacts
		; 计算对应下标元素的首地址
		add esi, eax

		; 打印所有联系人信息
		push ecx    ; 保存原始 ecx
		lea ebx, [esi + Contact.tel]
		push ebx
		lea ebx, [esi + Contact.user]
		push ebx
		push ecx
		push offset str_show_content
		call crt_printf
		add esp, 10h
		pop ecx     ; 恢复 ecx

		; 自增下标
		inc ecx
	.endw

	; 打印分割符
	push offset szFenGe
	call crt_printf
	add esp, 4

ret
ShowData endp


FindData proc
	; 提示输入查找的姓名
    push offset szFind
	call crt_printf
	add esp, 4

	; 获取输入的用户名 scanf("%s", username)
	push offset username
	push offset str_format_str
	call crt_scanf
	add esp, 8

	mov byte ptr [username + 31],0

	mov edx, 0
start_for:
	cmp edx, count
	jnb info_output

	;同上
	mov eax, sizeof(Contact)
	imul eax, edx
	lea esi, TeleContacts
	add esi, eax

	lea edi,  username
	lea esi, [esi + Contact.user]
	mov ecx, 20h

	; 比较 edi  esi 里存的字符串
	repe cmpsb
	; 不相等就进入下一次循环
	jne next_for
	; 相等就打印信息
	push edx    ; 保存 edx
	; 恢复 esi
	lea esi, TeleContacts
	add esi, eax
	; 输出
	lea ebx, [esi + Contact.tel]
	push ebx
	lea ebx, [esi + Contact.user]
	push ebx
	push edx
	push offset str_show_content
	call crt_printf
	add esp, 10h
	pop edx    ; 恢复 edx
	jmp end_for		; 结束循环

next_for:
	inc edx
	jmp start_for

info_output:
	push offset szFindNoone
	call crt_printf
	add esp, 4

end_for:
	ret

FindData endp


DelData proc
	; 打印提示信息
	push offset szDelWho
	call crt_printf
	add esp, 4

	; 获取输入的用户名 scanf("%s", username)
	push offset username
	push offset str_format_str
	call crt_scanf
	add esp, 8

	; 查询该用户的序号
	mov edx, 0
start_for:
	cmp edx, count
	jnb info_output

	;同上
	mov eax, sizeof(Contact)
	imul eax, edx
	lea esi, TeleContacts
	add esi, eax

	lea edi,  username
	lea esi, [esi + Contact.user]
	mov ecx, 20h

	; 比较 edi  esi 里存的字符串
	repe cmpsb
	; 不相等就进入下一次循环
	jne next_for
	; 相等edx 就是该用户的序号
	mov find_i, edx
	jmp del_process		; 结束循环

next_for:
	inc edx
	jmp start_for

info_output:
	push offset szFindNoone
	call crt_printf
	add esp, 4
	jmp exit_prog

del_process:
	; 将序号后的字节都前移
	mov ebx, find_i
	mov ecx, sizeof(Contact)
	imul ebx, ecx
	lea edi, TeleContacts
	; 获取目的地址
	add edi, ebx
	; 获取源地址目的地址后一个
	mov esi, edi
	add esi, ecx

	; 计算拷贝数量
	mov eax, count
	sub eax, find_i
	; 需要前移的字节
	imul ecx, eax

	; 循环前移  DS:SI 所指地址的一个字节搬移到 ES:DI 所指的地址上
	rep movsb
	; 总数 -1
	dec count
	; 成功删除
	push offset szDelSuccess
	call crt_printf
	add esp, 4

exit_prog:
	ret
DelData endp


ModifyData proc
	; 打印提示信息
	push offset szModify
	call crt_printf
	add esp, 4

	; 获取输入的用户名 scanf("%s", username)
	push offset username
	push offset str_format_str
	call crt_scanf
	add esp, 8

	; 查询该用户的序号
	mov edx, 0
start_for:
	cmp edx, count
	jnb info_output

	;同上
	mov eax, sizeof(Contact)
	imul eax, edx
	lea esi, TeleContacts
	add esi, eax

	lea edi,  username
	lea esi, [esi + Contact.user]
	mov ecx, 20h

	; 比较 edi  esi 里存的字符串
	repe cmpsb
	; 不相等就进入下一次循环
	jne next_for
	; 相等edx 就是该用户的序号
	mov find_i, edx
	jmp modify_process		; 结束循环

next_for:
	inc edx
	jmp start_for

info_output:
	push offset szFindNoone
	call crt_printf
	add esp, 4
	jmp exit_prog

modify_process:
	; 输入新的姓名
	push offset szInputNewName
	call crt_printf
	add esp, 4
	; 获取输入的用户名 scanf("%s", username)
	push offset TempTeleContacts.user
	push offset str_format_str
	call crt_scanf
	add esp, 8
	; 输入新的电话
	push offset szInputNewNum
	call crt_printf
	add esp, 4
	; 获取电话号码
	push offset TempTeleContacts.tel
	push offset str_format_str
	call crt_scanf
	add esp, 8

	; 修改
	mov ecx, sizeof(Contact)
	mov eax, find_i
	lea esi, TempTeleContacts
	lea edi, TeleContacts
	imul eax, ecx
	add edi, eax
	;  DS:SI 所指地址的一个字节搬移到 ES:DI 所指的地址上
	rep movsb
	; 成功修改
	push offset szModifySuccess
	call crt_printf
	add esp, 4

exit_prog:
	ret
ModifyData endp


main proc

begin_while:

	; 清空屏幕
	push offset str_system_cls
	call crt_system
	add esp, 4	

	; 获得控制台输出句柄
	invoke	GetStdHandle, STD_OUTPUT_HANDLE
	mov		stdOutHandle, eax
	; 打印电话本首页
	invoke	WriteConsole, stdOutHandle,          ; 控制台输出句柄
						  addr szShowMenu,           ; 字符串指针
					      ShowMenuSize,            ; 字符长度
						  addr byteWritten,      ; 返回输出字节数
						  0                       ; 未使用
	
	; 获得控制台输入句柄
	; invoke	GetStdHandle, STD_INPUT_HANDLE
    ; mov		stdInHandle,  eax
	; 等待用户输入
    ; invoke	ReadConsole,  stdInHandle, 
	;					  ADDR choose,
	;					  1, 
	;					  ADDR bytesRead, 0
	;invoke	StdOut,       addr choose
    ;ret 
	
	; 获取用户的输入
	push offset choose
	push offset str_format_int
	call crt_scanf
    ; 有两个参数需要平衡 2*4 字节
	add esp, 8

	.if choose == "1"	
		call AddData
		jmp end_switch
	.elseif choose =="2"
		call ShowData
		jmp end_switch
	.elseif choose == "3"
		call FindData
		jmp end_switch
	.elseif choose == "4"
		call DelData
		jmp end_switch
	.elseif choose == "5"
		call ModifyData
		jmp end_switch
	.elseif choose == "6"
		jmp exit_prog 
	.else
		jmp input_error
	.endif

; 提示输入正确选项
input_error:
	push offset szInputError
	call crt_printf
	add esp, 4

; 添加暂停
end_switch:
	push offset str_system_pause
	call crt_system
	add esp, 4

	jmp begin_while

; 退出
exit_prog:
	INVOKE ExitProcess,0

main endp
end main