x86汇编语言实践(3)

时间:2022-03-20 19:05:14

0 写在前面

  为了更深入的了解程序的实现原理,近期我学习了IBM-PC相关原理,并手工编写了一些x86汇编程序。

  在2017年的计算机组成原理中,曾对MIPS体系结构及其汇编语言有过一定的了解,考虑到x86体系结构在目前的广泛应用,我通过两个月左右的时间对x86的相关内容进行了学习。

  在《x86汇编语言实践》系列中(包括本篇、x86汇编语言实践(1)x86汇编语言实践(2)x86汇编语言实践(4)以及x86汇编语言复习笔记),我通过几个具体案例对x86汇编语言进行实践操作,并记录了自己再编写汇编代码中遇到的困难和心得体会,与各位学习x86汇编的朋友共同分享。

  我将我编写的一些汇编代码放到了github上,感兴趣的朋友可以点击屏幕左上角的小猫咪进入我的github,或请点击这里下载源代码。

1 递归调用计算N!

1-1 练习要点

  • 递归调用

  • 栈指针的维护

  • 子程序编写与调用

1-2 实现思路

  • 在数据段存储好待计算的N,和用以存储计算结果的RESULT

  • 主程序中首先将N和RESULT压栈

  • 调用CALCULATE进行阶乘的递归计算

  • 结果返回至RESULT

  • 调用DISP_VALUE打印输出阶乘计算结果

1-3 重点难点

  • 参数传递:使用堆栈进行参数传递,需要将参数压栈,注意子程序返回时,必须增加一个常数偏移量RET X。这里的X为压入参数所占的字节数,通常为2的倍数,以保证堆栈平衡

  • 子程序保存现场:在子程序中,往往要用到很多寄存器,但我们希望在子程序返回时,调用子程序位置处周围的变量仍能恢复,这就需要在调用的子程序中保存现场,即子程序中所用到或修改的所有寄存器,都必须压栈处理

  • 子程序中的堆栈寻址:使用BP寄存器寻址,这是为了不修改SP指针,避免弄乱堆栈栈顶指针SP

  • 中间一直困扰我的就是在子程序中获取参数N的方式MOV BX,[BP+6],为什么是BP+6呢?我们来看,BP保存的是子程序中的SP指针,但是距离我们将N压栈之间,我们经历了:将RESULT压栈、调用时将调用处的IP+2压栈以及将BP压栈,三个过程。因此当前的BP和N之间相差了6个字节的距离,故采用[BP+6]的方式进行参数N的寻址

  • 输出上的改进:仍是除10显示,但这次保存余数。为了得到正序输出,将每次的余数压栈,这样在显示的时候就是从高位向低位显示了。此外,在输出时对前导0进行了过滤处理,需要注意的是当遇到第一个非0数字后,需要将标志位置1,这样以后的数字0就可以正常显示。

1-4 代码实现

 STACK     SEGMENT    PARA    STACK
DW 100H DUP(?)
STACK ENDS DATA SEGMENT PARA
N DW
RESULT DW ?
DATA ENDS CODE SEGMENT PARA
ASSUME CS:CODE,DS:DATA,SS:STACK
CALCULATE PROC NEAR
CAL_PART:
PUSH BP
MOV BP,SP
PUSH DX
PUSH BX MOV BX,[BP+]
CMP BX,
JNZ CAL1
MOV AX,
JMP SHORT CAL2
CAL1:
PUSH BX
DEC BX
PUSH BX
PUSH RESULT
CALL CALCULATE
POP BX
MUL BX
CAL2:
MOV RESULT,AX
POP BX
POP DX
POP BP
RET
CALCULATE ENDP DISP_VALUE PROC
DISPLAY:
PUSH DX
PUSH CX
PUSH BX
PUSH AX MOV CX,
MOV BX, DLP1:
XOR DX,DX
DIV BX
PUSH DX
LOOP DLP1 MOV BX,
MOV CX,
DLP2:
POP DX
CMP DL,
JNZ DLP2_1
CMP BX,
JZ DLP2_2
DLP2_1:
MOV BX,
OR DL,30H
MOV AH,
INT 21H
DLP2_2:
LOOP DLP2 POP AX
POP BX
POP CX
POP DX
RET
DISP_VALUE ENDP MAIN PROC FAR
MAINPROC:
MOV AX,DATA
MOV DS,AX MOV AX,N
PUSH AX
PUSH RESULT
CALL CALCULATE
MOV AX,RESULT
CALL DISP_VALUE EXIT:
MOV AX,4C00H
INT 21H
MAIN ENDP
CODE ENDS
END MAIN

1-5 实现效果截图

1-5-1 计算N=7时的阶乘计算结果

   x86汇编语言实践(3)

  经验证,发现输出结果符合预期。

1-5-2 查看递归调用到N=4时的堆栈信息

  x86汇编语言实践(3)

  从上面单步执行的寄存器结果中可以看出,BX=4即此时已经执行到N=4,此时堆栈指针SP位于01d2。我们来分析一下,当前堆栈中的内容:

  • ss:1d2 压入RESULT作为参数向递归函数中传递,值为0

  • ss:1d4 压入BX(这里也就是N=4)作为参数向递归函数中传递,值为4

  • ss:1d6 保存的减一之前的N,这是为了在子程序返回时能计算N*AX返回结果

  • ss:1d8 子程序开始是压入的BX保存的值,值为5

  • ss:1da 子程序开始是压入的DX保存的值,值为0

  • ss:1dc 子程序开始是压入的BP保存的值,值为1ea

  • ss:1de CALL子程序会保存调用处下一条指令的IP并压栈,值为1c,即该子程序返回后会跳转至1c(+偏移值)

2 练习子程序参数传递的两种方法

2-1 练习要点

  • 子程序的编写

  • 使用寄存器向子程序传递参数

  • 使用堆栈向子程序传递参数

  • 复习乘法计算子程序,字符串拷贝子程序,字符串比较子程序,查找子程序

  • 选做部分我练习的是将字符串中全部的大写字母替换成小写字母

2-2 重点难点

  • 寄存器传参比较简单,将用到参数的寄存器保存为相应的参数值即可完成参数传递

  • 堆栈传参需要注意以下几点

    • 压栈顺序一定要注意,在压入多个参数时,需要记住其相对于SP的相对位置,从而避免取出参数时的混乱

    • 在子程序中对参数的索引采用BP指针代替SP指针进行寻址,从而避免改变栈顶SP指针引发的紊乱现象发生

    • 返回时需要加上一个常数偏移量,将压入栈中的参数位置地址恢复,从而维持堆栈平衡

2-3 实现思路

  • 首先为输入和输出单独编写子程序,程序主体采用跳转表实现

  • 为每一个条件单独编写一个子程序,有10中条件(A-E为堆栈传参子程序,a-e为寄存器传参子程序),因此共需编写10个子程序分别对应着实现响应功能

  • 在最外层设置循环结构,使得程序能够处理多组输入

  • 字符串、数据、参数等初始化设置在数据段完成即可

2-4 代码实现

 STACK     SEGMENT    PARA    STACK
DW 100H DUP(?)
STACK ENDS DATA SEGMENT PARA
LEN EQU
N EQU ;TIMES OF LOOP
X DW
Y DW
Z DW ?
STRING1 DB 'QIQI',20H,,'$'
STRING2 DB 'CHEN',20H,,'$'
CHAR DB 'C'
OP DB ?
NL DB ,,'$'
MSGEQ DB 'STRING1=STRING2',,,'$'
MSGGT DB 'STRING1>STRING2',,,'$'
MSGLT DB 'STRING1<STRING2',,,'$'
DOFOUND DB 'CHAR FOUND IN STRING2',,,'$'
NOTFOUND DB 'CHAR NOT FOUND IN STRING2',,,'$'
DATA ENDS CODE SEGMENT PARA
ASSUME CS:CODE,DS:DATA,SS:STACK
;PRINT A NEWLINE
NEWLINE MACRO
PUSH DX
PUSH AX
MOV DX,OFFSET NL
MOV AH,
INT 21H
POP AX
POP DX
ENDM
;GET OPERATION TO OP
GETOP MACRO
GETOPM:
MOV AH,
INT 21H
MOV OP,AL
ENDM
;OUTPUT MSG
OUTPUT MACRO MSG
PUSH DX
PUSH AX
MOV DX,OFFSET MSG
MOV AH,
INT 21H
POP AX
POP DX
ENDM
;DISPLAY VALUE IN AX
DISP_VALUE PROC
DISPLAY:
PUSH DX
PUSH CX
PUSH BX
PUSH AX MOV CX,
MOV BX, DLP1:
XOR DX,DX
DIV BX
PUSH DX
LOOP DLP1 MOV BX,
MOV CX,
DLP2:
POP DX
CMP DL,
JNZ DLP2_1
CMP BX,
JZ DLP2_2
DLP2_1:
MOV BX,
OR DL,30H
MOV AH,
INT 21H
DLP2_2:
LOOP DLP2 NEWLINE
POP AX
POP BX
POP CX
POP DX
RET
DISP_VALUE ENDP DISP_STR2 PROC
PRINTSTR2:
PUSH DX
MOV DX,OFFSET STRING2
MOV AH,
INT 21H
NEWLINE
POP DX
RET
DISP_STR2 ENDP MULTIPLE PROC
MULTI:
PUSH BP
MOV BP,SP
PUSH AX
PUSH BX MOV AX,[BP+]
MOV BX,[BP+]
MUL BX
MOV Z,AX POP BX
POP AX
POP BP RET
MULTIPLE ENDP MULTIPLE2 PROC
MULTI2:
MUL BX
MOV Z,AX
RET
MULTIPLE2 ENDP STRCPY PROC
STRCPYPROC:
PUSH BP
MOV BP,SP PUSH DI
PUSH SI
MOV SI,[BP+]
MOV DI,[BP+] CLD
CPYLP:
LODSB
STOSB
CMP AL,
JNZ CPYLP
POP SI
POP DI
POP BP
RET
STRCPY ENDP STRCPY2 PROC
STRCPY2PROC:
CLD
CPYLP2:
LODSB
STOSB
CMP AL,
JNZ CPYLP2
RET
STRCPY2 ENDP STRCMP PROC
STRCMPROC:
PUSH BP
MOV BP,SP PUSH DI
PUSH SI MOV SI,[BP+]
MOV DI,[BP+]
CALL STRCMP2 POP SI
POP DI
POP BP
RET
STRCMP ENDP STRCMP2 PROC
STRCMP2PROC:
PUSH CX
PUSH SI
CLD
PUSH SI
MOV CX,
CMPLP2:
LODSB
CMP AL,
JZ CMPLPBEG2
INC CX
JMP SHORT CMPLP2
CMPLPBEG2:
POP SI
REPE CMPSB
JA L2_1
JB L2_2
OUTPUT MSGEQ
JMP SHORT CMPRET2
L2_1:
OUTPUT MSGGT
JMP SHORT CMPRET2
L2_2:
OUTPUT MSGLT
CMPRET2:
POP SI
POP CX
RET
STRCMP2 ENDP FIND PROC
FINDCHAR:
PUSH BP
MOV BP,SP
PUSH CX MOV DI,[BP+]
MOV CX,LEN
DEC CX
MOV AX,[BP+]
CLD
REPNZ SCASB
JZ FOUND
OUTPUT NOTFOUND
JMP SHORT FIND_RETURN
FOUND:
OUTPUT DOFOUND
FIND_RETURN:
POP CX
POP BP
RET
FIND ENDP FIND2 PROC
FIND2PROC:
PUSH CX
PUSH DI
MOV CX,LEN
DEC CX
CLD
REPNZ SCASB
JZ FOUND2
OUTPUT NOTFOUND
JMP SHORT FIND2RETURN
FOUND2:
OUTPUT DOFOUND
FIND2RETURN:
POP DI
POP CX
RET
FIND2 ENDP TOLOWER PROC
TOLOW:
PUSH BP
MOV BP,SP
PUSH SI
PUSH DI
PUSH CX
PUSH AX MOV SI,[BP + ]
MOV DI,SI
MOV CX,LEN
CLD
TOLOW_LP:
LODSB
CMP AL,'A'
JB TOLOW_CONTINUE
CMP AL,'Z'
JA TOLOW_CONTINUE
ADD AL,20H
TOLOW_CONTINUE:
STOSB
LOOP TOLOW_LP POP AX
POP CX
POP DI
POP SI
POP BP
RET
TOLOWER ENDP TOLOWER2 PROC
TOLOW2:
PUSH SI
PUSH DI
PUSH CX
PUSH AX
MOV DI,SI
MOV CX,LEN
DEC CX
CLD
TOLOW_LP2:
LODSB
CMP AL,'A'
JB TOLOW_CONTINUE2
CMP AL,'Z'
JA TOLOW_CONTINUE2
ADD AL,20H
TOLOW_CONTINUE2:
STOSB
LOOP TOLOW_LP2
POP AX
POP CX
POP DI
POP SI
RET
TOLOWER2 ENDP SWITCH PROC
SWITCHPROC:
PUSH CX
S0:
CMP OP,'A'
JNE S1
PUSH X
PUSH Y
CALL MULTIPLE
MOV AX,Z
CALL DISP_VALUE
JMP CONTINUE
S1:
CMP OP,'B'
JNE S2
MOV DX,OFFSET STRING2
PUSH DX
MOV DX,OFFSET STRING1
PUSH DX
CALL STRCPY
OUTPUT STRING2
NEWLINE
JMP CONTINUE
S2:
CMP OP,'C'
JNE S3
MOV DX,OFFSET STRING2
PUSH DX
MOV DX,OFFSET STRING1
PUSH DX
CALL STRCMP
JMP CONTINUE
S3:
CMP OP,'D'
JNE S4
MOV DX,OFFSET STRING2
PUSH DX
MOV DL,CHAR
XOR DH,DH
PUSH DX
CALL FIND
JMP CONTINUE
S4:
CMP OP,'E'
JNE S5
MOV DX,OFFSET STRING1
PUSH DX
CALL TOLOWER
OUTPUT STRING1
NEWLINE
JMP CONTINUE
S5:
CMP OP,'a'
JNE S6
MOV AX,X
MOV BX,Y
CALL MULTIPLE2
MOV AX,Z
CALL DISP_VALUE
JMP CONTINUE
S6:
CMP OP,'b'
JNE S7
MOV SI,OFFSET STRING1
MOV DI,OFFSET STRING2
CALL STRCPY2
OUTPUT STRING2
NEWLINE
JMP CONTINUE
S7:
CMP OP,'c'
JNE S8
MOV SI,OFFSET STRING1
MOV DI,OFFSET STRING2
CALL STRCMP2
JMP CONTINUE
S8:
CMP OP,'d'
JNE S9
MOV DI,OFFSET STRING2
MOV AL,CHAR
CALL FIND2
JMP CONTINUE
S9:
CMP OP,'e'
JNE CONTINUE
MOV SI,OFFSET STRING2
CALL TOLOWER2
OUTPUT STRING2
NEWLINE
CONTINUE:
POP CX
RET
SWITCH ENDP MAIN PROC FAR
MAINPROC:
MOV AX,DATA
MOV DS,AX
MOV ES,AX MOV CX,N
MAINLOOP:
GETOP
NEWLINE
CALL SWITCH
LOOP MAINLOOP EXIT:
MOV AX,4C00H
INT 21H
MAIN ENDP CODE ENDS
END MAIN

2-5 运行结果

为了验证程序符合预期,需要设计以下样例进行测试。设置循环次数为10次

设置数据区如下:

  x86汇编语言实践(3)

 数据分别表示

  • LEN 字符串长

  • N 外循环次数

  • X,Y,Z  执行A/a操作时的乘数和结果

  • STRING1,STRING2 待操作的两个字符串

  • CHAR 待寻找的字符串

  • OP 读入的操作指令符

  • NL 回车换行标志

  • MSGEQ,MSGGT,MSGLT,DOFOUND,NOTFOUND 输出提示信息

  运行程序,得到如下结果

   x86汇编语言实践(3)

  显然,运行结果符合预期。