x86汇编语言实践(4)
0 写在前面
为了更深入的了解程序的实现原理,近期我学习了IBM-PC相关原理,并手工编写了一些x86汇编程序。
在2017年的计算机组成原理中,曾对MIPS体系结构及其汇编语言有过一定的了解,考虑到x86体系结构在目前的广泛应用,我通过两个月左右的时间对x86的相关内容进行了学习。
在《x86汇编语言实践》系列中(包括本篇、x86汇编语言实践(1)、x86汇编语言实践(2)、x86汇编语言实践(3)以及x86汇编语言复习笔记),我通过几个具体案例对x86汇编语言进行实践操作,并记录了自己再编写汇编代码中遇到的困难和心得体会,与各位学习x86汇编的朋友共同分享。
我将我编写的一些汇编代码放到了github上,感兴趣的朋友可以点击屏幕左上角的小猫咪进入我的github,或请点击这里下载源代码。
1 程序设计复习1
1-1 练习要点
-
字符串中查找指定字符
-
字符串中删除指定字符(使用快慢指针)
-
子程序调用的堆栈参数传递
1-2 实现思路
-
在数据段存储好待查找的CHAR,和目标字符串STR1,并将二者初始化
-
主程序中首先将CHAR和STR1压栈
-
调用FIND_CH子程序查找是否有CHAR
-
若找到CHAR则调用DELX删除STR1中的X
-
为了在STR1的原内存空间上操作字符串的修改动作,采用快慢指针的方式进行删除。
1-3 重点难点
-
参数传递:使用堆栈进行参数传递,需要将参数压栈,注意子程序返回时,必须增加一个常数偏移量RET X。这里的X为压入参数所占的字节数,通常为2的倍数,以保证堆栈平衡
-
子程序保存现场:在子程序中,往往要用到很多寄存器,但我们希望在子程序返回时,调用子程序位置处周围的变量仍能恢复,这就需要在调用的子程序中保存现场,即子程序中所用到或修改的所有寄存器,都必须压栈处理
-
子程序中的堆栈寻址:使用BP寄存器寻址,这是为了不修改SP指针,避免弄乱堆栈栈顶指针SP
-
快慢指针:与高级语言程序设计中的思路类似,首先将快慢指针指向STR1的头部,之后循环STR1的长度LEN次,若快指针SI指向的位置的字符不为CHAR,则将SI复制到慢指针DI,否则只将SI++。这里用到的技巧是可以使用LODSB和STOSB自动实现快指针SI与慢指针DI的自增操作。
1-4 代码实现
1 STACK SEGMENT PARA STACK 2 DW 100H DUP(?) 3 STACK ENDS 4 5 DATA SEGMENT PARA 6 LEN EQU 12 7 CHAR DB 'X' 8 STR1 DB 'CHENQIXIAN',13,10,'$' 9 MSG1 DB 'X IN CHENQIXIAN',13,10,'$' 10 MSG2 DB 'NOT FOUND X IN CHENQIXIAN',13,10,'$' 11 STR2 DB LEN DUP(?) 12 DATA ENDS 13 14 CODE SEGMENT PARA 15 ASSUME CS:CODE,DS:DATA,SS:STACK 16 OUTPUT MACRO MSG 17 PUSH AX 18 PUSH DX 19 MOV DX,OFFSET MSG 20 MOV AH,9 21 INT 21H 22 POP DX 23 POP AX 24 ENDM 25 26 DELX PROC 27 DELETEX: 28 PUSH BP 29 MOV BP,SP 30 PUSH SI 31 PUSH DI 32 PUSH CX 33 PUSH AX 34 35 MOV SI,[BP+4] 36 MOV DI,[BP+6] 37 MOV CX,LEN 38 39 DELX_LP: 40 LODSB 41 CMP AL,'X' 42 JE DELX_CONTINUE 43 STOSB 44 DELX_CONTINUE: 45 LOOP DELX_LP 46 47 DELETEXRET: 48 MOV AL,'$' 49 STOSB 50 POP AX 51 POP CX 52 POP DI 53 POP SI 54 POP BP 55 RET 4 56 DELX ENDP 57 58 FIND_CH PROC 59 FINDCHAR: 60 PUSH BP 61 MOV BP,SP 62 PUSH AX 63 PUSH DI 64 PUSH CX 65 66 MOV AX,[BP+4] 67 MOV DI,[BP+6] 68 MOV CX,LEN 69 70 CLD 71 REPNZ SCASB 72 JZ FOUND 73 OUTPUT MSG2 74 JMP SHORT FINDCHARRET 75 FOUND: 76 OUTPUT MSG1 77 MOV DX,OFFSET STR1 78 PUSH DX 79 PUSH DX 80 CALL DELX 81 FINDCHARRET: 82 POP CX 83 POP DI 84 POP AX 85 POP BP 86 RET 4 87 FIND_CH ENDP 88 89 MAIN PROC FAR 90 MAINPROC: 91 MOV AX,DATA 92 MOV DS,AX 93 MOV ES,AX 94 95 MOV DX,OFFSET STR1 96 PUSH DX 97 MOV DL,CHAR 98 XOR DH,DH 99 PUSH DX 100 CALL FIND_CH 101 102 EXIT: 103 MOV AX,4C00H 104 INT 21H 105 MAIN ENDP 106 107 CODE ENDS 108 END MAIN
1-5 实现效果截图
1-5-1 程序运行结果
经验证,发现输出结果符合预期
1-5-2 查看删除后内存中新的字符串
经验证,发现内存中的结果符合预期
2 程序设计复习2
2-1 练习要点
-
字符的输入输出
-
数字读入存储逻辑
-
数字的最优输出方式
2-2 实现思路
-
首先为读入字符和输出数字分别单独编写子程序
-
主程序中循环调用读入字符,由于题目固定读入两位十进制数,因此读入的第一个数乘10加上第二个读入的数,即为读入的数字
-
在输出上的改进:仍是除10显示,但这次保存余数。为了得到正序输出,将每次的余数压栈,这样在显示的时候就是从高位向低位显示了。此外,在输出时对前导0进行了过滤处理,需要注意的是当遇到第一个非0数字后,需要将标志位置1,这样以后的数字0就可以正常显示
2-3 代码实现
1 STACK SEGMENT PARA STACK 2 DW 100H DUP(?) 3 STACK ENDS 4 5 DATA SEGMENT PARA 6 LEN EQU 5 7 X DB 0 8 Y DB 0 9 Z DB ? 10 NL DB 13,10,'$' 11 DATA ENDS 12 13 CODE SEGMENT PARA 14 ASSUME CS:CODE,DS:DATA,SS:STACK 15 NEWLINE MACRO 16 PUSH AX 17 PUSH DX 18 MOV DX,OFFSET NL 19 MOV AH,9 20 INT 21H 21 POP DX 22 POP AX 23 ENDM 24 GETNUM PROC 25 INPUT: 26 MOV AH,1 27 INT 21H 28 SUB AL,30H 29 XOR AH,AH 30 RET 31 GETNUM ENDP 32 33 OUTPUT PROC 34 PRINT: 35 PUSH DX 36 PUSH CX 37 PUSH BX 38 PUSH AX 39 NEWLINE 40 MOV CX,LEN 41 MOV BX,10 42 PRINT_LP1: 43 XOR DX,DX 44 DIV BX 45 PUSH DX 46 LOOP PRINT_LP1 47 48 MOV CX,LEN 49 MOV BX,0 50 PRINT_LP2: 51 POP DX 52 CMP DL,0 53 JNE PRINT_LP2_1 54 CMP BX,0 55 JZ PRINT_LP2_2 56 PRINT_LP2_1: 57 MOV BX,1 58 MOV AH,2 59 OR DL,30H 60 INT 21H 61 62 PRINT_LP2_2: 63 LOOP PRINT_LP2 64 POP AX 65 POP BX 66 POP CX 67 POP DX 68 RET 69 OUTPUT ENDP 70 71 72 MAIN PROC FAR 73 MAINPROC: 74 MOV AX,DATA 75 MOV DS,AX 76 MOV ES,AX 77 78 CALL GETNUM 79 MOV BL,10 80 MUL BL 81 MOV X,AL 82 CALL GETNUM 83 ADD X,AL 84 85 CALL GETNUM 86 MOV BL,10 87 MUL BL 88 MOV Y,AL 89 CALL GETNUM 90 ADD Y,AL 91 92 MOV AL,X 93 MOV BL,Y 94 MUL BL 95 96 CALL OUTPUT 97 98 EXIT: 99 MOV AX,4C00H 100 INT 21H 101 MAIN ENDP 102 103 CODE ENDS 104 END MAIN
2-4 运行结果
显然,运行结果符合预期。
3 程序设计复习3
3-1 练习要点
-
字符串读取:0AH号中断调用
-
字符串拷贝
-
子程序调用参数的传递与保持
3-2 实现思路
-
首先为读入字符串和输出字符串分别单独编写子程序
-
输入待插入字符串后,首先调用第一次拷贝字符串子程序,判断条件为读取到空格即停止拷贝。注意边界条件的判断,以及最后一次拷贝后SI与DI的保持
-
紧接着在主程序中将SI压栈保存,将SI指向待插入字符串首地址,调用插入子程序。将待插入字符串拼接到目标串尾部
-
最后将SI弹出栈恢复,即又指向原列表空格后的第一个字符的位置处,调用第二次拷贝字符串子程序。此时边界判断条件为’$’符号
-
输出目标串
3-3 代码实现
1 STACK SEGMENT PARA STACK 2 DW 100H DUP(?) 3 STACK ENDS 4 5 DATA SEGMENT PARA 6 LEN EQU 32 7 LIST DB 'ABOVE ZEBRA$' 8 TEMP DB LEN DUP(?) 9 NL DB 13,10,'$' 10 STR1 DB LEN-1 11 DB ? 12 DB LEN DUP(?) 13 DATA ENDS 14 15 CODE SEGMENT PARA 16 ASSUME CS:CODE,DS:DATA,SS:STACK 17 NEWLINE MACRO 18 PUSH DX 19 PUSH AX 20 MOV DX,OFFSET NL 21 MOV AH,9 22 INT 21H 23 POP AX 24 POP DX 25 ENDM 26 27 OUTPUT MACRO MSG 28 PUSH DX 29 PUSH AX 30 NEWLINE 31 MOV DX,OFFSET MSG 32 MOV AH,9 33 INT 21H 34 POP AX 35 POP DX 36 ENDM 37 38 INPUT PROC 39 INPUTSTR1: 40 PUSH DX 41 PUSH AX 42 PUSH SI 43 44 MOV DX,OFFSET STR1 45 MOV AH,0AH 46 INT 21H 47 MOV SI,OFFSET STR1+2 48 MOV AL,STR1+1 49 XOR AH,AH 50 ADD SI,AX 51 MOV BYTE PTR [SI],'$' 52 53 POP SI 54 POP AX 55 POP DX 56 RET 57 INPUT ENDP 58 59 COPY PROC 60 STRCPY: 61 LODSB 62 CMP AL,20H 63 JE COPYRET 64 STOSB 65 JMP STRCPY 66 COPYRET: 67 STOSB 68 RET 69 COPY ENDP 70 71 INSERT PROC 72 INSERT_STR1: 73 MOV CL,STR1+1 74 XOR CH,CH 75 INSERT_LP: 76 LODSB 77 STOSB 78 LOOP INSERT_LP 79 MOV AL,20H 80 STOSB 81 RET 82 INSERT ENDP 83 84 COPY2 PROC 85 STRCPY2: 86 LODSB 87 CMP AL,'$' 88 JE COPYRET2 89 STOSB 90 JMP STRCPY2 91 COPYRET2: 92 STOSB 93 RET 94 COPY2 ENDP 95 96 MAIN PROC FAR 97 MAINPROC: 98 MOV AX,DATA 99 MOV DS,AX 100 MOV ES,AX 101 OUTPUT LIST 102 CALL INPUT 103 MOV SI,OFFSET LIST 104 MOV DI,OFFSET TEMP 105 CALL COPY 106 PUSH SI 107 MOV SI,OFFSET STR1+2 108 CALL INSERT 109 POP SI 110 CALL COPY2 111 OUTPUT TEMP 112 113 EXIT: 114 MOV AX,4C00H 115 INT 21H 116 MAIN ENDP 117 118 CODE ENDS 119 END MAIN
3-4 运行结果
4 程序设计复习4
4-1 练习要点
-
16进制输出方式
-
从10向A的转化
-
2号中断调用输出单个字符
4-2 实现思路
-
首先在数据段初始化一个64位数字
-
注意由于一个字是2个字节16位,因此在输出时,要依次在基地址的基础上+2
-
由于是循环访问数据段中的除数,因此用SI寄存器记录数据段中除数的位置,每次循环都要使用两次INC指令,保证访问到下一个字中的内容。
-
访问除数必须用WORD PTR [SI],否则会提示 ’must have size’
-
判断16进制输出的数字是否大于10,若不大于则直接输出,否则需要加7(在ASCII数值上‘9’和‘A’之间差8),注意从数字转换为ASCII码此处必须用ADD 30H 来代替 OR 30H,否则会出现错误。
4-3 代码实现
1 STACK SEGMENT PARA STACK 2 DW 100H DUP(?) 3 STACK ENDS 4 5 DATA SEGMENT PARA 6 NUM DW 1606H,1160H,1234H,0FFFFH 7 DIVISOR DW 1000H,100H,10H,1H 8 DATA ENDS 9 10 CODE SEGMENT PARA 11 ASSUME CS:CODE,DS:DATA,SS:STACK 12 13 OUTPUT PROC 14 PRINT: 15 MOV SI,OFFSET DIVISOR 16 MOV CX,4 17 OUTPUT_LP: 18 XOR DX,DX 19 DIV WORD PTR [SI] 20 PUSH DX 21 CMP AL,10 22 JB OUTPUT_CONTINUE 23 ADD AL,7 24 OUTPUT_CONTINUE: 25 ADD AL,30H 26 MOV DL,AL 27 MOV AH,2 28 INT 21H 29 INC SI 30 INC SI 31 POP AX 32 LOOP OUTPUT_LP 33 RET 34 OUTPUT ENDP 35 36 MAIN PROC FAR 37 MAINPROC: 38 MOV AX,DATA 39 MOV DS,AX 40 MOV ES,AX 41 42 MOV AX,NUM 43 CALL OUTPUT 44 45 MOV AX,NUM+2 46 CALL OUTPUT 47 48 MOV AX,NUM+4 49 CALL OUTPUT 50 51 MOV AX,NUM+6 52 CALL OUTPUT 53 54 EXIT: 55 MOV AX,4C00H 56 INT 21H 57 MAIN ENDP 58 59 CODE ENDS 60 END MAIN
4-4 运行结果
【数据段】
【运行结果】