本系列是北航计算机学院于 2024 年春季学期开设的一般专业课《X86汇编程序设计》课程的实验报告记录,由于学习过程中掌握并不牢靠,如有错误请读者不吝赐教!

X86汇编程序设计第二次实验作业

本次作业共三道程序设计题,其中一道题含有选做一小问

冒泡排序程序设计

编写一道完整汇编程序,实现冒泡排序,并显示排序前后的结果。

要求(提示:参考讲义例题修改):

  • 建立样本数据区,其中包含两个字(分开,分别由学生本人的8位学号的16进制字组成:XXXXh,YYYYh)。排序后,这两个字可以分开。
  • 要显示排序前及排序后的字表,显示时每个字中间空一格。
  • 要求将排序、显示内存中的字(十六进制至十进制ASCII码)、显示字符、显示字符串等程序块改编为子程序或宏。

作为第一道自己编写的 x86 汇编程序而言,我们最应该注意的是程序的结构,其次就是将一些常用的结构拆解出来,形成子程序辅助后续作业。

还有一点就是要注意寄存器的用法,主要记住段寄存器不能直接转移到段寄存器和 OFFSET 什么时候用应该就够了,其他的不太容易犯错。

这个题写的内容不太多,主要工作在处理数字输出上,可以关注子程序 PrintAXAsDecimal 的实现,可以抽象出输出寄存器中存储的十进制整数的函数(可以用栈、缓冲区实现)

STACK		SEGMENT	PARA STACK
STACK_AREA DW 100h DUP(?)
STACK_TOP EQU $-STACK_AREA
STACK ENDS

DATA SEGMENT PARA
TABLE_LEN DW 16
TABLE DW 12, 23, 34, 80H, 45, 76, 67, 78
DW 2000H, 40, 42H, 1919H, 60, 1145H, 2, 3
ENTER DW 0AH
SPACE DW ' '
BEFORE DB "BEFORE SORT: $"
AFTER DB "AFTER SORT: $"
DATA ENDS

CODE SEGMENT
ASSUME CS:CODE,DS:DATA
ASSUME SS:STACK

PRINTCHAR MACRO ; DATA in DX
PUSH AX
MOV AH, 2
INT 21H
POP AX
ENDM

PRINTSTR MACRO ; OFFSET in DX
PUSH DX
MOV AH, 9
INT 21H
POP DX
ENDM

PrintAXAsDecimal PROC
PUSH AX
PUSH BX
PUSH CX
PUSH DX

MOV BX, 10
MOV CX, 0

convert_loop:
XOR DX, DX
DIV BX
PUSH DX
INC CX
TEST AX, AX
JNZ convert_loop

print_loop:
POP DX
ADD DL, '0'
MOV AH, 02H
INT 21H
LOOP print_loop

POP DX
POP CX
POP BX
POP AX
RET
PrintAXAsDecimal ENDP

PRINTTABLE PROC
XOR CX, CX
MOV DI, OFFSET TABLE
LP0: MOV AX, [DI]
CALL PrintAXAsDecimal

PUSH DX
MOV DX, SPACE

PRINTCHAR
POP DX


ADD DI, 2
MOV AX, TABLE_LEN
INC CX
CMP AX, CX
JNE LP0
MOV DX, ENTER
PRINTCHAR
RET
PRINTTABLE ENDP

MAIN PROC FAR

START: MOV AX,STACK
MOV SS,AX
MOV SP,STACK_TOP
MOV AX,DATA
MOV DS,AX ;SET SS,SP,DS

MOV DX, OFFSET BEFORE
PRINTSTR
CALL PRINTTABLE

JMP START1

l1: NOP
START1: NOP
LP1: MOV BX,1
MOV CX,TABLE_LEN
DEC CX
LEA SI,TABLE ;MOV SI,offset Table
LP2: MOV AX,[SI]
CMP AX,[SI+2]
JBE CONTINUE
XCHG AX,[SI+2]
MOV [SI],AX
MOV BX,0
CONTINUE:
ADD SI,2
LOOP LP2

CMP BX,1
JZ EXIT
JMP SHORT LP1

EXIT: MOV DX, OFFSET AFTER
PRINTSTR
CALL PRINTTABLE

MOV AX,4C00H
INT 21H
MAIN ENDP
CODE ENDS

END MAIN
END

读入数据实现乘法并输出

编程实现:从键盘输入一个两位及三位的十进制数,做乘法(假定乘积小于65535,不考虑溢出),并显示乘法结果的十进制 ASCII 码。

第二题类似,困难的点在于如何读入和输出,内部的乘法实际上很简单,只需要 AX * BX = DX: AX 就够用了(因为不会溢出)

是的,难点在于从控制台读入数字,虽然输出也是,不过我们已经在上一题拿到一个函数了,可以关注子程序 GET_NUMBER 的实现,具体做法是利用系统调用从控制台读取缓冲区的内容,并保存到一段内存中。我们从第一位开始,通过循环每次读取一个十进制下的 ASCII 数,转化为数字后和已经读出的值扩大 10 倍后进行加和(补充个位),直至循环结束读出数字的值

通过缓冲区读取到的字符串有固定的特点:

  • 第一个 BYTE 应该在读缓冲区之前设置好确定的值,它限制了一次读取的最大长度
  • 第二个 BYTE 在读取完成后被自动更新,数值为该次读取到的数据总长度(不包含最后的回车符),通常情况下,这个字符和字符串的循环等操作密切相关,也经常读取到 CX 里
  • 从第三个字符开始,才是真正读取到的数据

比如代码段中的 69 ~ 70 行就体现了缓冲区读取的这种特色。

STACK		    SEGMENT	PARA STACK
STACK_AREA DW 100h DUP(?)
STACK_TOP EQU $-STACK_AREA
STACK ENDS


DATA SEGMENT PARA
prompt1 db 'Enter first number: $'
prompt2 db 'Enter second number: $'
result_str db 'The result is: $'
input db 9 dup('$')
output db 9 dup('$')
enter db 0ah, '$'
DATA ENDS


CODE SEGMENT
ASSUME CS:CODE,DS:DATA
ASSUME SS:STACK

PRINTENTER MACRO
PUSH AX
MOV AH, 09H
LEA DX, enter
INT 21H
POP AX
ENDM

MAIN PROC FAR
start:
MOV AX,STACK
MOV SS,AX
MOV SP,STACK_TOP
MOV AX,DATA
MOV DS,AX ; SET SS,SP,DS

MOV AH, 09H
LEA DX, prompt1
INT 21H

CALL GET_NUMBER
MOV BX, AX
PRINTENTER

MOV AH, 09H
LEA DX, prompt2
INT 21H
CALL GET_NUMBER
XOR DX, DX
PRINTENTER

MUL BX ; AX * BX

CALL PRINT_NUMBER
PRINTENTER

MOV AX, 4C00H
INT 21H
MAIN ENDP

GET_NUMBER PROC
PUSH BX
MOV BX, 0
MOV AH, 0AH
LEA DX, input
INT 21H

XOR AX, AX
MOV SI, OFFSET input + 2
MOV CL, BYTE PTR [SI - 1]

MOV CH, 0 ; CH = 0

convert_loop:
MOV DX, 10
MOV BL, BYTE PTR [SI]
SUB BL '0'
MOV BH, 0

MUL DX
ADD AX, BX
INC SI
LOOP convert_loop
POP BX
RET
GET_NUMBER ENDP

PRINT_NUMBER PROC
MOV BX, 10
LEA SI, output
ADD SI, 8

print_loop:
XOR DX, DX
DIV BX
ADD DL, '0'
DEC SI
MOV [SI], DL
TEST AX, AX
JNZ print_loop

MOV AH, 09H
LEA DX, result_str
INT 21H
MOV AH, 09H
MOV DX, SI
INT 21H
RET
PRINT_NUMBER ENDP
CODE ENDS
END MAIN
END

64 位下的无符号数乘法

编程实现 32 位无符号数乘法。

  • 在内存中定义一个无符号数双字 XX,YY,做乘法,得到一个 64 位的乘积。
  • 显示该乘积的 16 进制 ASCII 码。
  • (选做):显示该乘积的十进制 ASCII 码。

提示:双字 XX 可拆分成两个字 XXH,XXL;双字 YY 可拆分成两个字 YYH,YYL; 双字或 64 位结果可用 DD 定义,也可以用 DW 定义,也可以定义为数组(间接寻址)。(XXH,XXL)*(YYH,YYL)=4 个字;乘法列竖式,注意到处都有进位!进位加法用 ADC 指令。

实际上也没说的那么玄乎,虽然我们在 16 位下的系统下进行操作,但是仍然可以通过多个字拼接的方式实现 64 位的乘法。

首先列一个竖式,将双字拆成两个单字再分别进行乘法,我们就会得到分占不同位置的四个局部乘积。将这些乘积再按单字划分,确定好最终的 64 位字的每一个 16 位都由哪些乘积的高位/低位组成,然后就可以开始进位加法了。这两部分分别在不同的标签下(multingadding

要注意的是 XOR 这种清空寄存器的方式还会清空标志位。所以如果要使用的话需要注意 XOR 和 ADD、ADC 的使用顺序。

那么最后还是一个输出操作,首先 16 进制的输出比较简单,按照内存中字节的顺序,自左向右逐个取出每个 BYTE 然后除以 16 分成两个 16 进制数就能直接输出了,注意 0-9 和 A-F 处理并不完全一致

对于 10 进制输出,做法也类似竖式,首先检查被除数是否为 0,若 0 则结束操作,否则开始从最高字运算,让每一个单字都除以 10,余数留给下一个字作为高位,再进行除法,最后一个字的余数是当前真正的输出值,然后存入内存字符串,循环操作即可。

为什么不用双字除法?因为一个较大的 32 位数字除以 10,可能商超过 16 位,这样就会造成溢出,为了避免溢出,上面的做法实际上只会进行({9,16位} / 10)的操作,这样是肯定不会溢出的,保证了程序的安全性。

总的来说还是比较繁琐的,寄存器别用错,函数记得压栈比较重要。

STACK           SEGMENT PARA STACK
STACK_AREA DW 100H DUP(?)
STACK_TOP EQU $-STACK_AREA
STACK ENDS

DATA SEGMENT PARA
X DD 12345678H
Y DD 87654321H
Z DB 8 DUP(?)

X1Y1 DD ?
X1Y2 DD ?
X2Y1 DD ?
X2Y2 DD ?

OUTPUT DB 20 DUP(?), "$"

PROMPT1 DB "OUTPUT IN HEX: ", 0AH, "$"
PROMPT2 DB "OUTPUT IN DEC: ", 0AH, "$"
DATA ENDS

CODE SEGMENT
ASSUME CS: CODE, DS: DATA, SS:STACK
MAIN PROC FAR
init:
MOV AX, STACK
MOV SS, AX
MOV SP, STACK_TOP
MOV AX, DATA
MOV DS, AX

multing:
MOV AX, WORD PTR [X] ; A2 * B2
MOV BX, WORD PTR [Y]
MUL BX
MOV WORD PTR [X2Y2 + 2], DX
MOV WORD PTR [X2Y2], AX

MOV AX, WORD PTR [X] ; A2 * B1
MOV BX, WORD PTR [Y + 2]
MUL BX
MOV WORD PTR [X2Y1 + 2], DX
MOV WORD PTR [X2Y1], AX

MOV AX, WORD PTR [X + 2] ; A1 * B2
MOV BX, WORD PTR [Y]
MUL BX
MOV WORD PTR [X1Y2 + 2], DX
MOV WORD PTR [X1Y2], AX

MOV AX, WORD PTR [X + 2] ; A1 * B1
MOV BX, WORD PTR [Y + 2]
MUL BX
MOV WORD PTR [X1Y1 + 2], DX
MOV WORD PTR [X1Y1], AX

adding:
MOV AX, WORD PTR [X2Y2]
MOV WORD PTR [Z], AX

XOR CX, CX
MOV AX, WORD PTR [X2Y2 + 2]
MOV BX, WORD PTR [X1Y2]
ADD AX, BX


ADC CX, CX
XOR DX, DX

MOV BX, WORD PTR [X2Y1]
ADD AX, BX
MOV WORD PTR [Z + 2], AX

MOV AX, WORD PTR [X1Y2 + 2]
MOV BX, WORD PTR [X2Y1 + 2]
ADC AX, BX


ADC DX, DX
XOR BX, BX

ADD AX, CX ; IF OV AGAIN
ADC BX, BX
ADD DX, BX

MOV BX, WORD PTR [X1Y1]
ADC AX, BX
MOV WORD PTR [Z + 4], AX

MOV AX, WORD PTR [X1Y1 + 2]
XOR BX, BX
ADD AX, DX
ADC AX, BX
MOV WORD PTR [Z + 6], AX

hex:
MOV AH, 09H
LEA DX, PROMPT1
INT 21H
CALL PRINT_CHAR
MOV AH, 02H
MOV DX, 0AH
INT 21H

decx:
MOV AH, 09H
LEA DX, PROMPT2
INT 21H
CALL PRINT_NUMBER
exit:
MOV AX, 4C00h
INT 21h

MAIN ENDP

; 打印数值的ASCII码
PRINT_CHAR PROC
MOV CX, 8

LOOP1:
XOR AX, AX ; 0790:00A6
XOR BX, BX
XOR DX, DX
MOV SI, CX
MOV AL, BYTE PTR [Z + SI - 1]
MOV BX, 010H
DIV BX ; AL = HIGH, DL = LOW
MOV BX, DX

CMP AL, 9
JG letter1

ADD AL, '0'
JMP print1

letter1:
ADD AL, 'A' - 10
PRINT1:
MOV AH, 02H
MOV DL, AL
INT 21H


CMP BX, 9
JG letter2

ADD BX, '0'
JMP print2

letter2:
ADD BX, 'A' - 10
PRINT2:
MOV AH, 02H
MOV DL, BL
INT 21H

LOOP LOOP1

RET
PRINT_CHAR ENDP

PRINT_NUMBER PROC
MOV SI, OFFSET OUTPUT
ADD SI, 19
LOOP2:
CALL CHECK_CLEAR
CMP BX, 1 ; 0790:00FF
JE PRINT3
MOV BX, 10

MOV AX, WORD PTR [Z + 6]
XOR DX, DX
DIV BX
MOV WORD PTR [Z + 6], AX

MOV AX, WORD PTR [Z + 4]
DIV BX
MOV WORD PTR [Z + 4], AX

MOV AX, WORD PTR [Z + 2]
DIV BX
MOV WORD PTR [Z + 2], AX

MOV AX, WORD PTR [Z]
DIV BX
MOV WORD PTR [Z], AX

ADD DX, 30H
MOV BYTE PTR [SI], DL
DEC SI

JMP LOOP2
PRINT3:
MOV DX, SI
ADD DX, 1
MOV AH, 09H
INT 21H
RET ; 0790:0132

PRINT_NUMBER ENDP

CHECK_CLEAR PROC
PUSH AX
PUSH BX
PUSH CX
PUSH DX
MOV BX, WORD PTR [Z + 6]
MOV AX, 0
CMP AX, BX
JNZ FALSE

MOV BX, WORD PTR [Z + 4]
MOV AX, 0
CMP AX, BX
JNZ FALSE

MOV BX, WORD PTR [Z + 2]
MOV AX, 0
CMP AX, BX
JNZ FALSE

MOV BX, WORD PTR [Z]
MOV AX, 0
CMP AX, BX
JNZ FALSE

TRUE: POP DX
POP CX
POP BX
POP AX
MOV BX, 1
RET
FALSE: POP DX
POP CX
POP BX
POP AX
MOV BX, 0
RET
CHECK_CLEAR ENDP
CODE ENDS

END MAIN