本文档是程序设计基础(II),即MIPS汇编语言大作业的实验报告。

实验要求

使用MIPS汇编语言编写程序,并使用MARS运行,尽量多地使用宏和函数调用以使主函数的结构清晰明了。

要求对于输入的最多不超过100个MIPS汇编机器码进行反汇编操作,如果输入的过程中出现位数不正确或不符合十六进制格式的数则跳过,直到输入了正确的MIPS汇编机器码为止。完成输入之后,输出一句话表达一共成功输入了多少条MIPS汇编机器码。随后对输入的汇编机器码进行反汇编,每一行的格式为:

[指令地址] 原指令(16进制) 标签: 指令

由于本学期所学的MIPS汇编语言仅涉及整数的处理,本次实验给出的MIPS指令表并非完整的MIPS汇编指令表,而是缺少一部分的表,对于表中没有出现的指令,程序运行过程中不作处理,在输出时以???表示这是一个未知指令。

下面是针对输入输出格式说明给出的样例:

Input MIPS machine code:3C011001 34240000 24080006 20850004 00084821 8C8A0000 8cab0000 016a082a 10200002 acaa0000 ac8b0000 20a50004 2129ffff 0009082a 1420fff6 20840004 2108ffff 0008082A 1420FFF0
19 instructions loaded
[00400000] 3c011001       lui $at,4097
...
[0040000c] 20850004 L001: addi $a1,$a0,4
[00400010] 00084821       addu $t1,$zero,$t0
[00400014] 8c8a0000 L002: lw $t2,0($a0)
...
[00400034] 0009082a       slt $at,$zero,$t1
[00400038] 1420fff6       bne $at,$zero,L002[00400014]
...
[00400048] 1420fff0       bne $at,$zero,L001[0040000c]

代码思路

数据段 (Data Segment)

在解决实际问题之前,我需要先考虑的问题是如何通过在数据段中预先存好数据以减少实际代码过程中的冗杂判断。

指令名称

我对指令和寄存器名的的字符串进行了一些预先处理,将所有指令拼接到一起,每个指令之间以\0符号隔开以方便直接输出字符串,为了方便地查找需要输出的字符串的起始位置,我对每一条指令的字符串相比所有字符串的偏移量做成了一个一一对应的数组。

放在了数据段中:

Op_Code_Low:	.asciiz	"j\0jal\0beq\0bne\0blez\0bgtz\0addi"
		.asciiz "addiu\0slti\0sltiu\0andi\0ori\0xori\0lui"
# Other Items
Op_Offset_Low:	.byte	-1, -2, 0, 2, 6, 10, 14, 19, 24, 29, 35
		.byte	40, 46, 51, 55, 60

那么,在遇到实际的寄存器和指令时,我就可以用类似如下的思路来输出(用C语言的方式表达)。

char *	str_Start	= Type_Code + Type_Offset[Offset_Value];
printf("%s", str_Start);

指令格式

除了对于指令和寄存器的输出外,我还需要考虑的内容是指令的格式。显然我不能在最后的输出函数中对每一个指令都用branch语句来控制它的输出格式,所以我在对指令表进行观察和总结之后对指令的格式作出了如下的定义:

Format Type Format
0 非法格式(指令不存在)
1 [Label] Imm
2 [Label] rs, Imm
3 [Label] rs, rt, Imm
4 rt, Imm
5 rt, Imm(rs)
6 rt, rs, Imm
7 rs
8 rd, rs
9 rd, rt, sa
10 rd, rt, rs
11 rd, rs, rt
12 rd
13 rs, rt
14 (None)

那么,我就可以根据给定指令表中的指令格式为每一个Op_CodeFunction_Code指定其对应的输出格式类别,对于输出格式的控制,则可以参考前文中的输出思路。

Format_Ctrl:	.ascii	"3g.3g4g.4gi.4gir.4g3gi.3.5g3.5g4gs.5g4g3."
		.ascii	"5g3g4.5.3g4."
# Other Items
Format_Offset:	.byte	0, 0, 0, 3, 8, 12, 17, 23, 25, 29, 35 
		.byte	41, 47, 49

其中.代表格式分隔符,当输出格式符检查到.时说明这个格式的输出结束,对于其他符号:

  • g表示Gap,也就是输出, ;

  • i表示Imm,表示此处需要输出立即数;
  • s表示Sa,表示此处应该输出的是Shift Amount寄存器存储的值,而非该值大小所对应的寄存器名
  • 数字作为从数据段Is_LabelCommand_RsCommand_RtCommand_Rd之间的偏移量的百分之一。

例如:3代表我需要查找的寄存器编号存储在当前所检查的Is_Label向后偏移300的内存地址,即对应的Command_Rs的地址。

那么相应的,4代表的就是Command_Rt5代表的就是Command_Rd,用这种方式来控制输出的指令的格式。

标签因需要进行特殊处理,在后续代码编写的过程中,考虑到传参的问题,我并没有在输出格式控制中对其进行处理,而是通过格式的编号进行判断,当目前处理的格式编号不大于3时,那在处理完正常的格式输出之后,我需要输出其跳转向的标签。

指令数据存储

对于每一条指令,根据其为R指令、I指令或J指令进行分解,并将分解到的数据存储在按如下方式分配的数据段中:

# Other Items
		.align	2
Is_Label:	.space	100
Command_Type:	.space	100
Command_Op:	.space	100
Command_Rs:	.space	100
Command_Rt:	.space	100
Command_Rd:	.space	100
Command_Sa:	.space	100
Command_Func:	.space	100
Command_Imm:	.space	400
Command_Array:	.space	400

这样的数据存储方式能允许我通过地址偏移的方式直接得到当前正在处理的指令的除立即数外的所有数据,所以在输出时我只需要传入Is_LabelCommand_Imm就可以得到我所有需要使用的数据,从而简化最后的输出代码。

符号类

输出指令时需要有方括号、小括号、冒号、空格、逗号等符号,这些内容包括输入缓冲区、输出时的十六进制数缓冲区、输出的标签前缀缓冲区一起存储于数据段中,下面只给出部分作为示例。

# Other Items
Unknown:	.asciiz "???"
Colon:		.asciiz ":"
Gap:		.asciiz	" "
NLine:		.asciiz	"\n"

完整数据段代码

下面给出我的完整数据段代码,在上传的代码文件中,我在数据段之前通过代码中文档的方式说明了数据段中各分配内存的用途。

.data
Op_Code_Low:	.asciiz	"j\0jal\0beq\0bne\0blez\0bgtz\0addi"
		.asciiz "addiu\0slti\0sltiu\0andi\0ori\0xori\0lui"
Op_Code_High:	.asciiz	"lb\0lh\0lwl\0lw\0lbu\0lhu\0lwr\0sb\0sh"
		.asciiz	"swl\0sw\0swr"
Op_Special:	.asciiz	"bgez\0bgezal\0bltz\0bltzal"
Function_Code:	.asciiz	"sll\0srl\0sra\0sllv\0srlv\0srav\0jr"
		.asciiz	"jalr\0syscall\0mfhi\0mthi\0mflo\0mtlo"
		.asciiz	"mult\0multu\0div\0divu\0add\0addu\0sub"
		.asciiz	"subu\0and\0or\0xor\0nor\0slt\0sltu"
Register_Name:	.asciiz	"$zero\0$at\0$v0\0$v1\0$a0\0$a1\0$a2\0$a3"
		.asciiz	"$t0\0$t1\0$t2\0$t3\0$t4\0$t5\0$t6\0$t7"
		.asciiz	"$s0\0$s1\0$s2\0$s3\0$s4\0$s5\0$s6\0$s7"
		.asciiz	"$t8\0$t9\0$k0\0$k1\0$gp\0$sp\0$fp\0$ra"
Format_Ctrl:	.ascii	"3g.3g4g.4gi.4gir.4g3gi.3.5g3.5g4gs.5g4g3."
		.ascii	"5g3g4.5.3g4."
Input_Prompt:	.asciiz	"Input MIPS machine code:"
Loaded_Notice:	.asciiz	" instructions loaded\n"
Label_Out:	.ascii	"L"
		.space	2
		.byte	0
Hex:		.space	8
		.byte	0
Buffer:		.space	1001
LSBracket:	.asciiz	"["
RSBracket:	.asciiz	"]"
Comma:		.asciiz ","
LBracket:	.asciiz "("
RBracket:	.asciiz ")"
Unknown:	.asciiz "???"
Colon:		.asciiz ":"
Gap:		.asciiz	" "
NLine:		.asciiz	"\n"
Op_Offset_Low:	.byte	-1, -2, 0, 2, 6, 10, 14, 19, 24, 29, 35
		.byte	40, 46, 51, 55, 60
Op_Offset_High:	.byte	0, 3, 6, 10, 13, 17, 21, -3, 25, 28, 31
		.byte	35, -3, -3, 38
Op_Offset_Special:
		.byte	0, 5, 12, 17
Format_Offset:	.byte	0, 0, 0, 3, 8, 12, 17, 23, 25, 29, 35 
		.byte	41, 47, 49
Function_Offset:
		.byte	0, -3, 4, 8, 12, -3, 17, 22, 27, 30, -3
		.byte	-3, 35, -3, -3, -3, 43, 48, 53, 58, -3
		.byte	-3, -3, -3, 63, 68, 74, 78, -3, -3, -3
		.byte	-3, 83, 87, 92, 96, 101, 105, 108, 112
		.byte	-3, -3, 116, 120
Register_Offset:
		.byte	0, 6, 10, 14, 18, 22, 26, 30, 34, 38, 42
		.byte	46, 50, 54, 58, 62, 66, 70, 74, 78, 82
		.byte	86, 90, 94, 98, 102, 106, 110, 114, 118
		.byte	122, 126
Op_Format_Low:	.byte	0, 2, 1, 1, 3, 3, 2, 2, 6, 6, 6, 6
		.byte	6, 6, 6, 4
		.space	16
Op_Format_High:	.byte	5, 5, 5, 5, 5, 5, 5, 0, 5, 5, 5, 5
		.byte	0, 0, 5
		.space	17
Func_Format:	.byte	9, 0, 9, 9, 10, 0, 10, 10, 7, 8, 0, 0, 14
		.byte	0, 0, 0, 12, 7, 12, 7, 0, 0, 0, 0, 13, 13
		.byte	13, 13, 0, 0, 0, 0, 11, 11, 11, 11, 11, 11
		.byte	11, 11, 0, 0, 11, 11
		.space	20
		.align	2
Is_Label:	.space	100
Command_Type:	.space	100
Command_Op:	.space	100
Command_Rs:	.space	100
Command_Rt:	.space	100
Command_Rd:	.space	100
Command_Sa:	.space	100
Command_Func:	.space	100
Command_Imm:	.space	400
Command_Array:	.space	400

宏定义(Macro)

很显然,对于这样的一份代码,有很多会重复做的事情,所以在代码开头我定义了若干个宏,使后半部分的代码更加简洁易懂。

安全调用(用于主函数)

定义如下宏,在主函数调用其他子程序时,将$s0寄存器中保存的总加载指令数量和$ra中保存的函数返回地址保存到堆栈区中,并调用函数,完成调用之后将数据恢复,进行下一步的操作。

.macro		SAFE_CALL	%FUNCTION_LABEL
		addi	$sp, $sp, -8
		sw	$s0, ($sp)
		sw	$ra, 4($sp)
		jal	%FUNCTION_LABEL
		lw	$s0, ($sp)
		lw	$ra, 4($sp)
		addi	$sp, $sp, 8
.end_macro

加载参数(用于主函数)

定义如下宏,在主函数调用其他子程序时,将%ADDRESS作为$a0$s0(即总加载的指令数量)作为$a1加载参数。

.macro		LOAD_PARA	%ADDRESS
		la	$a0, %ADDRESS
		move	$a1, $s0
.end_macro

打印寄存器的数值

定义如下宏,用于打印寄存器中的数值。

.macro		PRINT_NUM	%REGISTER
		move	$a0, %REGISTER
		li	$v0, 1
		syscall
.end_macro

打印符号(字符串)

定义如下宏,用于打印传入的地址所对应的字符串。

.macro		PRINT_SYMBOL	%ADDRESS
		la	$a0, %ADDRESS
		li	$v0, 4
		syscall
.end_macro

安全加载(仅用于Output)

定义如下宏,用于在Output函数中调用Set_LabelSet_Hex这两个函数时对$t8$t9$ra寄存器进行保护。

.macro		SAFE_SET	%FUNCTION_LABEL
		addiu	$sp, $sp, -12
		sw	$t8, ($sp)
		sw	$t9, 4($sp)
		sw	$ra, 8($sp)
		jal	%FUNCTION_LABEL
		lw	$t8, ($sp)
		lw	$t9, 4($sp)
		lw	$ra, 8($sp)
		addiu	$sp, $sp, 12
.end_macro

打印间隙(仅用于Command_Output函数)

定义如下宏,用于打印指令中寄存器名之间的间隙,即,

.macro		PRINT_GAP
		PRINT_SYMBOL	Comma
		PRINT_SYMBOL	Gap
.end_macro

打印寄存器名称(仅用于Command_Output函数)

定义如下宏,用于打印寄存器的名称,传入的参数是目标寄存器相对于Is_Label的偏移量。

.macro		PRINT_REG	%BASE %REGISTER
		addu	$t9, %BASE, %REGISTER
		lb	$t8, ($t9)
		la	$t9, Register_Offset
		addu	$t9, $t9, $t8
		lb	$t8, ($t9)
		la	$t9, Register_Name
		addu	$a0, $t8, $t9
		li	$v0, 4
		syscall
.end_macro

代码段 (Text Segment)

接下来是代码段,一开始是主函数,随后是其他子函数。

主函数

基于实验给出的要求,我的主函数代码思路如下:

graph LR
  A[处理输入]-->B[指令分解]
  B-->C[指令格式指定]
  C-->D[指令输出]

对应的主函数代码如下:

main:		jal		Handle_Input
		move		$s0, $v0		
		LOAD_PARA	Command_Array
		SAFE_CALL	BrkDwn_Command
		LOAD_PARA	Command_Type
		SAFE_CALL	Handle_Command
		LOAD_PARA	Is_Label
		SAFE_CALL	Output
		li		$v0, 10
		syscall

调用Handle_Input之后,函数将返回读取到的指令总数到$v0寄存器中,并将读到的所有机器指令转换为10进制数字格式存储于数据段Command_Array指令数组中。随后在指令分解BrkDwn_Command、指令格式并标签处理Handle_Command、输出指令Output时,分别将Command_ArrayCommand_TypeIs_Label地址作为$a0,总指令数量作为$a1,通过LOAD_PARA宏加载参数,再通过SAFE_CALL宏对指定函数进行安全调用。

输入处理函数(Handle_Input)

Handle_Input函数无传入参数,返回值$v0为加载的所有格式合法的8位16进制机器码数量,函数执行的流程图如下。

graph TD
	A[输出提示词<br>要求用户输入] --> B[使用8号系统服务<br>将输入内容存入缓冲区]
	B --> C[加载<br>缓冲区首地址]
	C --> D[初始化循环计数<br>及指令存储地址]
	D --> E[检查字符是否是单个指令的结尾,即空格、制表符、空字符和换行符]
	E -- 否 --> F[在本条指令中<br>是否已有非法字符]
	F -- 否 --> G[检查是否有非法字符<br>将合法大写字母转换为小写]
	G -- 是 --> H[置非法标记为1]
	G -- 否 --> I[将该个字符的值加入到<br>正在计算的指令值中]
	F -- 是 --> J
	I --> J[加载下一个字符]
	H --> J
	J --> E
	E -- 是空格或制表符 --> K[检查非法标记是否为1]
	K -- 是 --> L[初始化非法标记、长度标记和已计算的指令值]
	L --> J
	M -- 否 --> L
	K -- 否 --> M[检查已读到的指令长度是否为8]
	M -- 是 --> N[存储指令并增加有效指令计数]
	N --> J
	E -- 是换行或空字符 --> O[检查非法标记是否为1]
	O -- 否 --> P[检查已读到的指令长度是否为8]
	P -- 是 --> Q[存储指令并增加有效指令计数]
	Q --> R[将已读到的有效指令数返回]
	O -- 是 --> R
	P -- 否 --> R

以下是这个函数的汇编代码实现:

#################################################################
# Function:	Handle_Input					#
#################################################################
# Description:	Scan the MIPS machine code as a whole to a 	#
#		string buffer and check if they are legal codes	#
#		and store them.					#
#################################################################
# Parameter In:	None						#
#################################################################
# Output:	$v0 as the Number of Commands			#
#################################################################
# Cross Reference Table:					#
# $t0		The address of the character to be checked	#
# $t1		The address which we save the command to	#
# $t2		The character being checked			#
# $t3		Check if the command's length is correct	#
# $t4		Calculate and Store the Command Number		#
# $t5		Flag for Illegal Symbols in Machine Code	#
# $t6		Counter for Commands				#
# $t7		Temporary Register for Getting Actual Value	#
#################################################################
Handle_Input:
		la	$a0, Input_Prompt	# Output Notify
		li	$v0, 4
		syscall
		la	$a0, Buffer		# Input Buffer
		li	$a1, 1000		# Max Length Set
		li	$v0, 8
		syscall
		la	$t0, Buffer		# Start Check
		la	$t1, Command_Array	# Start Saving
		xor	$t6, $t6, $t6		# Total Commands
HI_Init_Loop:	xor	$t3, $t3, $t3		# Command Length
		xor	$t4, $t4, $t4		# Command Num
		xor	$t5, $t5, $t5		# Illegal Flag
HI_Loop:	lb	$t2, ($t0)
		beq	$t2, 9, Save_phrase	# Check Tab
		beq	$t2, 32, Save_phrase	# Check Space
		beq	$t2, 10, Save_phrase	# Check End
		beqz	$t2, Save_phrase	# Reach the End
		beq	$t5, 1, Next_Figure	# Skip if Illegal
		ble	$t2, 47, Illegal	# Illegal Check
		ble	$t2, 57, Num		# Num Check
		ble	$t2, 64, Illegal	# Illegal Check
		ble	$t2, 70, Upper_Case	# Upper_Case Check
		ble	$t2, 96, Illegal	# Illegal Check
		ble	$t2, 102, Lower_Case	# Lower_Case Check
		b	Illegal			# Illegal Check
Num:		sll	$t4, $t4, 4		# Shift Left 4
		addiu	$t7, $t2, -48		# Get Actual Num
		addu	$t4, $t4, $t7		# Add to Cmd Num
		b	Next_Figure
Upper_Case:	addiu	$t2, $t2, 32		# Convert to Lower
Lower_Case:	sll	$t4, $t4, 4		# Shift Left 4
		addiu	$t7, $t2, -87		# Get Actual Num
		addu	$t4, $t4, $t7		# Add to Cmd Num
		b	Next_Figure
Illegal:	li	$t5, 1			# Flag as Illegal
Next_Figure:	addiu	$t0, $t0, 1		# Next Character
		addiu	$t3, $t3, 1		# Length Update
		b	HI_Loop			
Save_phrase:	bne	$t3, 8, Invalid		# Check if Valid
		beq	$t5, 1, Invalid		# Check if Valid
		sw	$t4, ($t1)		# Save Command
		addiu	$t1, $t1, 4		# Move Forward 4
		addiu	$t6, $t6, 1		# Add Count
Invalid:	beqz	$t2, End		# End of String
		beq	$t2, 10, End		# End of String
		addiu	$t0, $t0, 1		# Move Forward
		b	HI_Init_Loop
End:		move	$v0, $t6		# Return Count
		jr	$ra
指令分解函数(BrkDwn_Command)

BrkDwn_Command函数的传入参数$a0Command_Array,也就是指令数组的地址,$a1是总的指令数量,对每一条指令,先判断它的类型,随后根据不同类型的指令格式分解其中所需要的数据,函数执行的流程图如下。

graph TD
	A[加载指令] --> B[取指令最高6位Op码]
	B -- Op码为1,是J指令 --> C[取指令最低26位立即数并存储]
	B -- 其他Op码 --> D[取指令由高到低低第7-11位为Rs并存储]
	D --> E[取指令由高到低第12-16位为Rt并存储]
    E -- Op码不为0,是I指令 --> F[取指令最低16位立即数]
    F --> G[修正立即数的符号并存储]
    E -- Op码为0, 是R指令 --> H[取指令由高到低第17-21位为Rd并存储]
    H --> I[取指令由高到低第22-26位为Sa并存储]
    I --> J[取指令最低6位为Func并存储]
    C --> K
    G --> K
    J --> K[检查已处理的指令条数]
    K -- 未分解完所有指令 --> A
    K -- 已分解完所有指令 --> L[返回]

以下是这个函数的汇编代码实现:

#################################################################
# Function:	BrkDwn_Command					#
#################################################################
# Description:	For each specific number command, break it down	#
#		according to MIPS Machine Code rule and save	#
#		them to the Specified Array.			#
#################################################################
# Parameter In:							#
#		$a0 as the Starting Address of the Command Num	#
#		$a1 as the Sum of Command Numbers		#
#################################################################
# Output:	None						#
#################################################################
# Cross Reference Table:					#
# $t0		Address currently being checked			#
# $t1		Loop Counter					#
# $t2		Machine Code Number				#
# $t3		Used for Depart Op_Code, Rd, Sa, Func, Imm	#
# $t4		Only Used for Departing Rs, Rt			#
# $t5		Current Command_Op's Address			#
# $t6		Current Command_Imm's Address			#
#################################################################
BrkDwn_Command:	move	$t0, $a0		# Start Address
		la	$t5, Command_Op		# Load Op Saver
		la	$t6, Command_Imm	# Load Imm Saver
		xor	$t1, $t1, $t1		# Init Counter
BC_Loop:	lw	$t2, ($t0)		# Load Command
		andi	$t3 ,$t2, 0xfc000000	# Get Op Code
		srl	$t3, $t3, 26
		sb	$t3, ($t5)		# Store Op Code
		beq	$t3, 2, BC_J_Command	# Check J Command
		beq	$t3, 3, BC_J_Command
		andi	$t4, $t2, 0x03e00000	# Check Rs
		srl	$t4, $t4, 21
		sb	$t4, 100($t5)		# Save Rs
		andi	$t4, $t2, 0x001f0000	# Check Rt
		srl	$t4, $t4, 16
		sb	$t4, 200($t5)		# Save Rt
		beqz	$t3, BC_R_Command	# Check R Command
		andi	$t4, $t2, 0x0000ffff	# Check Imm for I
		blt	$t4, 0x8000, BC_Imm_Pos# Not Negative
		addi	$t4, $t4, -0x10000
BC_Imm_Pos:	sw	$t4, ($t6)		# Save Imm for I
		b	BC_Next_Command
BC_J_Command:	andi	$t3, $t2, 0x03ffffff	# Imm for J Command
		sw	$t3, ($t6)		# Save Imm for J
		b	BC_Next_Command
BC_R_Command:	andi	$t3, $t2, 0x0000f800	# Check Rd
		srl	$t3, $t3, 11
		sb	$t3, 300($t5)		# Save Rd
		andi	$t3, $t2, 0x000007c0	# Check Sa
		srl	$t3, $t3, 6
		sb	$t3, 400($t5)		# Save Sa
		andi	$t3, $t2, 0x0000003f	# Check Func
		sb	$t3, 500($t5)		# Save Func
BC_Next_Command:
		addiu	$t5, $t5, 1		# Move Forward
		addiu	$t6, $t6, 4
		addiu	$t0, $t0, 4
		addiu	$t1, $t1, 1		# Increase Counter
		blt	$t1, $a1, BC_Loop	# Continue BrkDwn
		jr	$ra
指令格式及标签处理函数(Handle_Command)

Handle_Command函数的传入参数$a0Command_Type,也就是指令格式类型的存储地址,$a1是总的指令数量。对于每一条指令,根据其Op码、Func码唯一确定这个指令对应的输出格式,并根据格式对需要打标签的指令地址进行处理,随后完成标签的标号。函数执行的流程图如下:

graph TD
	A[加载当前指令Op码] -- Op码为0 --> B[加载当前指令Func码]
	A -- Op码不为0 --> C[加载当前Op码所对应的格式编号]
	C --> D[检查加载得到的格式编号]
	D -- 格式编号比3大,不需要计算标签 --> E[保存指令格式]
    D -- 格式编号不大于3,需要计算标签 --> F[加载当前指令的立即数]
    F -- 格式编号为1,立即数为绝对地址 --> G[将立即数减去0x00100000<br>得出需要打上标签的指令次序]
	F -- 格式编号不为1,立即数为相对地址 --> H[将立即数加上当前循环计数<br><font color = red>再加1</font>后得出需要打上标签的指令次序]
	G --> I[给需要打标签的指令打上标签]
	H --> I
	I --> E
	B --> J[加载当前Func码所对应的格式编号]
	J --> E
	E --> K[检查已处理的指令条数]
	K -- 未处理完所有指令 --> A
	K -- 已处理完所有指令 --> L[从头开始,将标签按指令顺序从小到大由1开始编号]
	L --> M[返回]

以下是这个函数的汇编代码实现:

#################################################################
# Function:	Handle_Command					#
#################################################################
# Description:	For Each Command, Get the Format Type for it, 	#
#		if its' Jump Command or Branch Command,		#
#		we calculate the Label for it.			#
#################################################################
# Parameter In:	$a0 as the Starting position of Command_Type	#
#		$a1 as the Total Amount of Commands		#
#################################################################
# Output:	None						#
#################################################################
# Cross Reference Table:					#
# $t0		The Address of Current Command's Type		#
# $t1		The Address of Current Command's Immediate Num	#
# $t2		Command Counter					#
# $t3		Op_Code to be checked, if zero, it should be	#
#		Func_Code					#
# $t4		The Address of the Format of Output		#
# $t5		Be used to Op_Code, check if it's high or low	#
# $t6		Format Type					#
# $t7		Immediate Number Loaded for Label Calculation	#
# $t8		Labeled Command Number, and the Address		#
# $t9		Flag if Command is Labeled, and Label Counter	#
#################################################################
Handle_Command:	move	$t0, $a0		# Load Save Addr
		addi	$t1, $t0, 700		# Load Imm Addr
		xor	$t2, $t2, $t2		# Init Loop Count
HC_Loop:	lb	$t3, 100($t0)		# Load Op Code
		beqz	$t3, HC_R_Command	# R Command
		la	$t4, Op_Format_Low	# Load Base Format Addr
		andi	$t5, $t3, 0x0020	# Check if High
		beqz	$t5, HC_Save_Format
		la	$t4, Op_Format_High	# Load Base Format Addr
		andi	$t3, $t3, 0x001f	# Get Offset
		b	HC_Save_Format
HC_R_Command:	lb	$t3, 600($t0)		# Load Func Offset
		la	$t4, Func_Format	# Load Base Format Addr	
HC_Save_Format:	addu	$t4, $t3, $t4		# Get Actual Format Addr
		lb	$t6, ($t4)		# Get Format Type
		bgt	$t6, 3, HC_Not_Label	# No Need Label Handle
		beqz	$t6, HC_Not_Label
		lw	$t7, ($t1)		# Load Imm Number
		bgt	$t6, 1, HC_Rela_Addr	# Relative Address Calc
		addi	$t8, $t7, -0x00100000	# Get Absolute Address
		b	HC_Abso_Label
HC_Rela_Addr:	add	$t8, $t2, $t7		# Get the Label Address
		addi	$t8, $t8, 1
HC_Abso_Label:	add	$t8, $t8, $a0
		li	$t9, 1			# Flag As Labeled
		sb	$t9, -100($t8)		# Save Flag
HC_Not_Label:	sb	$t6, ($t0)		# Save Format Type
		addiu	$t0, $t0, 1		# Next Command
		addiu	$t1, $t1, 4
		addiu	$t2, $t2, 1
		blt	$t2, $a1, HC_Loop	# Handle Next Command
		addi	$t0, $a0, -100		# Back to Start
		xor	$t2, $t2, $t2		# Reset Counter
		xor	$t9, $t9, $t9		# Label Counter
HC_Label_Loop:	lb	$t8, ($t0)		# Check if Labeled
		beqz	$t8, HC_Next_Label
		add	$t9, $t9, $t8
		sb	$t9, ($t0)		# Load Label ID
HC_Next_Label:	addi	$t0, $t0, 1		# Next Command
		addi	$t2, $t2, 1
		blt	$t2, $a1, HC_Label_Loop
		jr	$ra
输出函数(Output)

Output函数的传入参数$a0Is_Label,也就是指令格式对应的标签编号的存储地址,$a1是总的指令数量。在对输出函数的流程进行分析之前,我需要先对几个会在Output函数中用到的子程序进行说明。

设置16进制数(Set_Hex)

Set_Hex函数的传入参数仅有$a0,它的作用是将$a0中的数,以16进制字符串的形式存在数据段中的Hex中,以方便直接输出。函数执行的流程图如下:

graph LR
	A[设定存储地址为Hex + 7,循环变量为8] --> B[截取目标数据的的末4位转化为对应字符并存于存储地址中]
	B --> C[将数据向右移4位,存储地址向前移1位,循环变量减1]
	C -- 循环变量为0 --> D[返回]
	C -- 循环变量不为0 --> A

以下是这个函数的汇编语言实现:

#################################################################
# Function:	Set_Hex						#
#################################################################
# Description:	Convert a Number to Hex Format and Store it in	#
#		string in Hex in Data Segment			#
#################################################################
# Parameter In:	$a0 as the number to be converted		#
#################################################################
# Output:	None						#
#################################################################
# Cross Reference Table:					#
# $t0		Address currently to be filled			#
# $t1		Counter for loop				#
# $t2		The Number that Currently being handled		#
# $t3		The character to be saved			#
#################################################################
Set_Hex:	la	$t0, Hex		# Load Address of Hex
		addi	$t0, $t0, 7		# Store From Low End
		li	$t1, 8			# Init Loop Count
		move	$t2, $a0		# Check Num
SH_Loop:	andi	$t3, $t2, 0x000f	# Get Low End of Num
		blt	$t3, 10, SH_Num		# Check Num
		addi	$t3, $t3, 87		# Above 10
		b	SH_Next_Loop
SH_Num:		addi	$t3, $t3, 48		# Below 10
SH_Next_Loop:	sb	$t3, ($t0)		# Save Character
		srl	$t2, $t2, 4		# SR for Next Check
		addi	$t0, $t0, -1		# Next To Be Filled
		addi	$t1, $t1, -1
		bgtz	$t1, SH_Loop		# Next Loop
		jr	$ra
设置标签前导0(Set_Label)

Set_Label函数的传入参数$a0是需要处理前导0的标签编号,根据标签的编号设置对应数量的’0’字符以将其补成三位数,使标签的编号对齐。函数执行的流程图如下:

graph TD
	A[加载Out_Label地址并向后移一位]-->B[检查编号是否大于等于100]
	B -- 是 --> C[置0并返回]
	B -- 否 --> D[置'0'并将需要填充的地址向后移一位]
	D -- 是 --> E[检查编号是否大于等于10]
	E -- 是 --> C
	E -- 否 --> F[置'0'并将需要填充的地址向后移一位]
	F --> C

以下是这个函数的汇编语言实现:

#################################################################
# Function:	Set_Label					#
#################################################################
# Description:	To make it clear, it actually set the prefix	#
#		for a Label Number and store it in string in	#
#		Label_Out in Data Segment			#
#################################################################
# Parameter In:	$a0 as the number of the label			#
#################################################################
# Output:	None						#
#################################################################
# Cross Reference Table:					#
# $t0		Address currently to be filled			#
# $t1		'0' as the character to be filled as prefix	#
#################################################################
Set_Label:	la	$t0, Label_Out		# Load Address of Label
		li	$t1, 48
		addi	$t0, $t0, 1
		bge	$a0, 100, SL_Ret	# No Prefix 0
		sb	$t1, ($t0)		# Add Prefix 0
		addi	$t0, $t0, 1
		bge	$a0, 10, SL_Ret		# No Additional Prefix 0
		sb	$t1, ($t0)		# Add Prefix 0
		addi	$t0, $t0, 1
SL_Ret:		sb	$zero, ($t0)		# As the End of Prefix
		jr	$ra
得到指令字符地址(Get_Command_Str)

Get_Command_Str函数的传入参数$a0是当前指令对应的Op_Code$a1是当前指令对应的Func_Code$a2当前指令的Rt值。返回结果$v00(代表在此时检查出了非法指令)或指令字符串的地址。

在前面,我们通过Op_CodeFunc_Code是否有对应的格式编号,已经筛除掉了一部分不合法的指令,但这样的格式码并没有针对Op_Code为1的情况进行特别检查。当Op_Code1Rt寄存器的值并非之前我们的指令表中给出的已知值$1, 17, 0, 16$时,这个指令是没有办法正确对应的,因此,在此函数中进行了额外检查,并通过特殊返回值的方式表达在此处检查出错误的结果。

函数执行的流程图如下:

graph TD
	A[检查Op_Code] -- Op_Code为0 --> B[检查Func_Code]
	A -- Op_Code为1 --> C[检查传入的Rt值是否合法]
	C -- Rt值合法 --> D[根据Op_Special及Op_Offset_Special得到当前指令对应的指令字符串地址]
	C -- <font color = red>Rt值不合法</font> --> E[<font color = red>返回0表示指令无效</font>]
	A -- Op_Code大于1 --> F[根据Op_Code字符串及Op_Offset得到当前指令对应的指令字符串地址]
	B --> G[根据Function_Code字符串及Function_Offset得到当前指令对应的指令字符串地址]
	G --> H[返回得到的字符串地址]
	F --> H
	D --> H

以下是这个函数的汇编语言实现:

#################################################################
# Function:	Get_Command_Str					#
#################################################################
# Description:	For Op_Code, Func_Code, and Rt Value, return	#
#		The Command's Output Address.			#
#################################################################
# Parameter In:	$a0 as Op_Code of the Command			#
#		$a1 as Func_Code of the Command			#
#		$a2 as the Rt Register Value of Command		#
#################################################################
# Output:	$v0 as the String Address for Target Command	#
#################################################################
# Cross Reference Table:					#
# $t0		Offset address for command string		#
# $t1		Base address for command string			#
#################################################################
Get_Command_Str:
		beqz	$a0, GCS_Func_Code
		bgt	$a0, 1, GCS_Normal
		la	$t0, Op_Offset_Special
		beq	$a2, 1, GCS_Offset_1
		beq	$a2, 17, GCS_Offset_2
		beqz	$a2, GCS_Offset_3
		beq	$a2, 16, GCS_Offset_4
		move	$v0, $zero
		jr	$ra
GCS_Offset_4:	addiu	$t0, $t0, 1
GCS_Offset_3:	addiu	$t0, $t0, 1
GCS_Offset_2:	addiu	$t0, $t0, 1
GCS_Offset_1:	la	$t1, Op_Special
		b	GCS_End
GCS_Normal:	la	$t0, Op_Offset_Low
		la	$t1, Op_Code_Low
		blt	$a0, 16, GCS_Not_High
		la	$t0, Op_Offset_High
		la	$t1, Op_Code_High
		andi	$a0, $a0, 0x000f
GCS_Not_High:	addu	$t0, $t0, $a0
		b	GCS_End
GCS_Func_Code:	la	$t0, Function_Offset
		addu	$t0, $t0, $a1
		la	$t1, Function_Code
GCS_End:	lb	$t0, ($t0)
		addu	$v0, $t0, $t1
		jr	$ra
按格式输出指令(Command_Output)

Command_Output函数的传入参数$a0是需要输出的指令的Is_Label地址,$a1是需要输出的指令的Command_Imm地址,$a2是格式编号。根据格式编号加载对应的格式字符串,通过格式字符的规则输出对应格式中寄存器名及数值。函数执行的流程图如下:

graph TD
	A[加载当前指令对应的格式编号] --> B[加载该格式编号对应的格式字符串地址]
	B --> C[加载地址对应的格式字符]
	C --> D[检查格式字符]
	D -- 格式字符为'.' --> E[结束输出并返回]
	D -- 格式字符为'g' --> F[输出间隔字符', ']
	D -- 格式字符为'r' --> G[输出带括号的Rs寄存器名]
	D -- 格式字符为's' --> H[输出Sa寄存器的值]
	D -- 格式字符为'i' --> I[输出立即数的值]
	D -- 格式字符为'3','4','5' --> J[根据数字进行Is_Label偏移量计算后取寄存器编号后输出对应的寄存器名]
	F --> K[加载下一个格式字符]
	G --> K
	H --> K
	I --> K
	J --> K
	K --> D

以下是这个函数的汇编语言实现:

#################################################################
# Function:	Command_Output					#
#################################################################
# Description:	For Each Command, based on its format, output	#
#		the command string which obeys the format rule.	#
#################################################################
# Parameter In:	$a0 as the Is_Label Address for Current Command	#
#		$a1 as the Imm Address for Current Command	#
#		$a2 as the Format				#
#################################################################
# Output:	None						#
#################################################################
# Cross Reference Table:					#
# $t0		Calculated Format Offset Address		#
# $t1		Format Offset Value				#
# $t2		Current position in Format_Ctrl string		#
# $t3		Current character from Format_Ctrl		#
# $t4		Saved Is_Label Address				#
# $t5		Saved Imm Address				#
# $t6		Temporary register for calculations/values	#
# $t7		Multiplier for register index calculation	#
#################################################################
Command_Output:	move	$t4, $a0		# Save Address
		move	$t5, $a1		# Save Imm Address
		la	$t0, Format_Offset
		addu	$t0, $t0, $a2
		lb	$t1, ($t0)		# Get Format Offset
		la	$t2, Format_Ctrl
		addu	$t2, $t2, $t1		# Start Format Check
CO_Loop:	lb	$t3, ($t2)
		beq	$t3, '.', CO_End_Loop	# End of Format
		beq	$t3, 'g', CO_Gap	# Gap -> ', '
		beq	$t3, 'r', CO_Bracket	# Bracket -> '(Rs)'
		beq	$t3, 's', CO_Shift	# Shift -> sa
		beq	$t3, 'i', CO_Imm	# Imm Num
		addi	$t6, $t3, -48		# Handle Register
		li	$t7, 100		# Multiplier
		mult	$t6, $t7
		mflo	$t6
		PRINT_REG	$t4, $t6	# Print Register Name
		b	CO_Next_Loop
CO_Imm:		lw	$t6, ($t5)
		PRINT_NUM	$t6		# Print Imm Num
		b	CO_Next_Loop
CO_Gap:		PRINT_GAP			# Print ', '
		b	CO_Next_Loop
CO_Bracket:	li	$t6, 300
		PRINT_SYMBOL	LBracket
		PRINT_REG	$t4, $t6	# Print '(Rs)'
		PRINT_SYMBOL	RBracket
		b	CO_Next_Loop
CO_Shift:	lb	$t6 ,600($t4)		# Print Value of Sa
		PRINT_NUM	$t6
		b	CO_Next_Loop
CO_Next_Loop:	addi	$t2, $t2, 1
		b	CO_Loop
CO_End_Loop:	jr	$ra
函数执行流程及汇编代码实现

了解完前文中这些子函数的执行流程后,对于Output函数而言,其执行流程就非常明了了,见如下的流程图(其中,调用子程序的地方已经用蓝色标出):

graph TD
	A[输出当前已加载的总指令数] --> B[初始化循环变量]
	B --> C[输出当前指令的地址及<font color = blue>调用Set_Hex函数处理16进制格式的原始机器码</font>并输出]
	C --> D[检查当前指令是否有标签]
	D -- 有标签 --> E[<font color = blue>调用Set_Label处理标签编号的前导0</font>]
	E --> F[输出标签前缀、标签编号和冒号]
	D -- 无标签 --> G[输出5个空格以与有标签的情况对齐]
	F --> H[检查当前指令的格式编号]
	G --> H
	H -- 当前指令的格式编号为0 --> I[此指令为未知指令,输出'???']
	H -- 当前指令的格式编号不为0 --> J[<font color = blue>调用Get_Command_Str函数获得指令的字符串地址</font>并判断返回值是否为0]
	J -- 返回值为0 --> I
	J -- 返回值不为0 --> K[输出得到的指令字符串]
	K --> L[检查当前指令的格式编号是否为1]
	L -- 格式编号为1 --> M[输出标签<br><font color=blue>需要调用Set_Label函数和Set_Hex函数</font>]
	L -- 格式编号不为1 --> N[<font color = blue>调用Command_Output函数根据指令编号按格式输出指令</font>]
	N --> O[检查格式编号是否大于3]
	O -- 否 --> M
	O -- 是 --> P[移动至下一指令,并检查是否已经处理完所有指令]
	M --> P
	I --> P
	P -- 否 --> C
	P -- 是 --> Q[完成处理并返回]

以下是这个函数的汇编语言实现:

#################################################################
# Function:	Output						#
#################################################################
# Description:	Output commands Loaded, then for each command	#
#		based on its format, output the original MIPS	#
#		code.						#
#################################################################
# Parameter In:	$a0 as the address of the Label Check		#
#		$a1 as the Total Amount of Commands		#
#################################################################
# Output:	None						#
#################################################################
# Cross Reference Table:					#
# $s0		Current Label Address				#
# $s1		Total Command Amount				#
# $s2		Current Imm Address				#
# $s3		Text Address Check				#
# $s4		Loop Counter					#
# $s5		Check if Labeled				#
# $s6		Format Checker					#
# $s7		Op_Code						#
# $t8		Temporary for label check/address		#
# $t9		Temporary for immediate/label calculation	#
#################################################################
Output:		move	$s0, $a0		# Load Label Address
		move	$s1, $a1		# Load Command Count
		PRINT_NUM	$s1
		PRINT_SYMBOL	Loaded_Notice
		addi	$s2, $s0, 800		# Load Imm Address
		li	$s3, 0x00400000		# For Text Address
		xor	$s4, $s4, $s4		# Init Loop Count
O_Loop:		move	$a0, $s3		# Handle Text Address
		SAFE_SET	Set_Hex
		PRINT_SYMBOL	LSBracket
		PRINT_SYMBOL	Hex
		PRINT_SYMBOL	RSBracket
		PRINT_SYMBOL	Gap
		lw	$a0, 400($s2)
		SAFE_SET	Set_Hex
		PRINT_SYMBOL	Hex
		PRINT_SYMBOL	Gap
		lb	$s5, ($s0)		# Check if Labeled
		beqz	$s5, O_Add_Gap		# Not Labeled
		move	$a0, $s5
		SAFE_SET	Set_Label
		PRINT_SYMBOL	Label_Out
		PRINT_NUM	$s5
		PRINT_SYMBOL	Colon
		b	O_Next_Gap
O_Add_Gap:	PRINT_SYMBOL	Gap
		PRINT_SYMBOL	Gap
		PRINT_SYMBOL	Gap
		PRINT_SYMBOL	Gap
		PRINT_SYMBOL	Gap
O_Next_Gap:	PRINT_SYMBOL	Gap
		lb	$s6, 100($s0)		# Load Format
		beqz	$s6, O_Unknown		# Unknown Format
		la	$t8, Is_Label
		lw	$t9, ($s2)
		lb	$a0, 200($s0)		# Load Op Code
		lb	$a1, 700($s0)		# Load Func Code
		lb	$a2, 400($s0)		# Load Rt Code
		SAFE_SET	Get_Command_Str
		beqz	$v0, O_Unknown		# Illegal Command
		move	$a0, $v0		# Print Command Name
		li	$v0, 4
		syscall
		beq	$s6, 14, O_Next_Command
		PRINT_SYMBOL	Gap
		beq	$s6, 1, O_Out_Abso	# Check Absolute Addr
		move	$a0, $s0
		move	$a1, $s2
		move	$a2, $s6
		SAFE_SET	Command_Output
		bgt	$s6, 3, O_Next_Command
		addiu	$t9, $t9, 1
		addu	$t9, $t9, $s4
		b	O_Out_Label
O_Out_Abso:	addi	$t9, $t9, -0x00100000
O_Out_Label:	la	$t8, Is_Label
		addu	$t8, $t8, $t9
		lb	$t8, ($t8)
		move	$a0, $t8
		SAFE_SET	Set_Label
		PRINT_SYMBOL	Label_Out
		PRINT_NUM	$t8
		addiu	$t9, $t9, 0x00100000
		sll	$a0, $t9, 2
		SAFE_SET	Set_Hex
		PRINT_SYMBOL	LSBracket
		PRINT_SYMBOL	Hex
		PRINT_SYMBOL	RSBracket
		b	O_Next_Command
O_Unknown:	PRINT_SYMBOL	Unknown
O_Next_Command:	PRINT_SYMBOL	NLine
		addi	$s0, $s0, 1
		addi	$s2, $s2, 4
		addi	$s3, $s3, 4
		addi	$s4, $s4, 1
		blt	$s4, $s1, O_Loop
		jr	$ra

运行结果

将老师提供的3个样例数据输入到程序中,分别得到了如下结果:

样例1

Input MIPS machine code:00840018 00004812 00094840 00095821 00094880 012b4821 00421026 0044102a 01202025 0000000c 03e00008
11 instructions loaded
[00400000] 00840018       mult $a0, $a0
[00400004] 00004812       mflo $t1
[00400008] 00094840       sll $t1, $t1, 1
[0040000c] 00095821       addu $t3, $zero, $t1
[00400010] 00094880       sll $t1, $t1, 2
[00400014] 012b4821       addu $t1, $t1, $t3
[00400018] 00421026       xor $v0, $v0, $v0
[0040001c] 0044102a       slt $v0, $v0, $a0
[00400020] 01202025       or $a0, $t1, $zero
[00400024] 0000000c       syscall
[00400028] 03e00008       jr $ra

样例2

Input MIPS machine code:3C011001 34240000 24080006 20850004 00084821 8C8A0000 8cab0000 016a082a 10200002 acaa0000 ac8b0000 20a50004 2129ffff 0009082a 1420fff6 20840004 2108ffff 0008082A 1420FFF0
19 instructions loaded
[00400000] 3c011001       lui $at, 4097
[00400004] 34240000       ori $a0, $at, 0
[00400008] 24080006       addiu $t0, $zero, 6
[0040000c] 20850004 L001: addi $a1, $a0, 4
[00400010] 00084821       addu $t1, $zero, $t0
[00400014] 8c8a0000 L002: lw $t2, 0($a0)
[00400018] 8cab0000       lw $t3, 0($a1)
[0040001c] 016a082a       slt $at, $t3, $t2
[00400020] 10200002       beq $at, $zero, L003[0040002c]
[00400024] acaa0000       sw $t2, 0($a1)
[00400028] ac8b0000       sw $t3, 0($a0)
[0040002c] 20a50004 L003: addi $a1, $a1, 4
[00400030] 2129ffff       addi $t1, $t1, -1
[00400034] 0009082a       slt $at, $zero, $t1
[00400038] 1420fff6       bne $at, $zero, L002[00400014]
[0040003c] 20840004       addi $a0, $a0, 4
[00400040] 2108ffff       addi $t0, $t0, -1
[00400044] 0008082a       slt $at, $zero, $t0
[00400048] 1420fff0       bne $at, $zero, L001[0040000c]

样例3

Input MIPS machine code:0601001b 0810001c 18a00019 1cc00018 2862ff9c 2d490064 32300001 3af70004 80a8000a 84d0000a 9209ff9c a32a0000 b88b0003 00084082 000a4bc3 01b98004 01988006 03e02009 00800011 00850019 0211001a 012a4020 03b3e822 00840824 00432025 00a50826 039e2827 035b882b 0500ffe3 40f70004 44f70004 7Fa52827 9c31882b B4b2e822 F0000000 00840001 00004805 0009484F 00095835 00094828 0084002c 0084003f
42 instructions loaded
[00400000] 0601001b L001: bgez $s0, L002[00400070]
[00400004] 0810001c       j L002[00400070]
[00400008] 18a00019       blez $a1, L002[00400070]
[0040000c] 1cc00018       bgtz $a2, L002[00400070]
[00400010] 2862ff9c       slti $v0, $v1, -100
[00400014] 2d490064       sltiu $t1, $t2, 100
[00400018] 32300001       andi $s0, $s1, 1
[0040001c] 3af70004       xori $s7, $s7, 4
[00400020] 80a8000a       lb $t0, 10($a1)
[00400024] 84d0000a       lh $s0, 10($a2)
[00400028] 9209ff9c       lbu $t1, -100($s0)
[0040002c] a32a0000       sb $t2, 0($t9)
[00400030] b88b0003       swr $t3, 3($a0)
[00400034] 00084082       srl $t0, $t0, 2
[00400038] 000a4bc3       sra $t1, $t2, 15
[0040003c] 01b98004       sllv $s0, $t9, $t5
[00400040] 01988006       srlv $s0, $t8, $t4
[00400044] 03e02009       jalr $a0, $ra
[00400048] 00800011       mthi $a0
[0040004c] 00850019       multu $a0, $a1
[00400050] 0211001a       div $s0, $s1
[00400054] 012a4020       add $t0, $t1, $t2
[00400058] 03b3e822       sub $sp, $sp, $s3
[0040005c] 00840824       and $at, $a0, $a0
[00400060] 00432025       or $a0, $v0, $v1
[00400064] 00a50826       xor $at, $a1, $a1
[00400068] 039e2827       nor $a1, $gp, $fp
[0040006c] 035b882b       sltu $s1, $k0, $k1
[00400070] 0500ffe3 L002: bltz $t0, L001[00400000]
[00400074] 40f70004       ???
[00400078] 44f70004       ???
[0040007c] 7fa52827       ???
[00400080] 9c31882b       ???
[00400084] b4b2e822       ???
[00400088] f0000000       ???
[0040008c] 00840001       ???
[00400090] 00004805       ???
[00400094] 0009484f       ???
[00400098] 00095835       ???
[0040009c] 00094828       ???
[004000a0] 0084002c       ???
[004000a4] 0084003f       ???

总结与感想

在书写本次大作业的代码时,我使用了Git进行了版本管理,在每次提交时均会附上本次更新、修改的内容说明。而在完成大作业后回看这些提交记录,感慨万千。

事实上,初版能完成此实验目标的汇编代码的执行逻辑相比实验报告中提交的版本更加复杂。一开始我觉得最大的难度在于指令的拆解及指令字符串的得到上,为了节省内存空间,我将Op_Code根据其是否大于$31$分成了Op_Code_LowOp_Code_High,拼接成字符串后,通过Op_Offset来控制指令字符串中实际指令字符串的偏移量,对于Function_Code也是同样的处理方式,在LLM工具的辅助下确认这些字符串及对应的偏移量正确后,这个问题被解决。

虽然事后想来,这似乎并不是最简洁的解决方案,毕竟这本质上相当于二重指针调用,倘若一开始在数据段中存储的不是偏移量,而直接是Op_Code_Low + Offset的值,少一层解引用的操作,后续的代码段应该会得到极大的简化。

完成这次大作业时遇到的第二个问题就是如何控制指令输出的格式,由于一开始我只是给每个指令赋了格式编号,并未通过字符串的方式对指令输出格式进行控制,于是初版代码中出现了在Output函数中判断输出格式时连写$15$个beq语句后对每种格式编号单独进行处理的地狱绘图,并且由于初版代码在这方面太复杂,一些细小的问题很难被快速定位,导致该代码的输出结果并不完全正确,不过好歹有了输出,至少通过样例1和样例2,并正确处理样例3中绝大多数的指令,这一结果对我来说是比较振奋的。

为了解决这样的问题,我改进了格式编号的定义,并引入了指令字符串,对每个指令格式编号通过统一的标准处理,通过Command_Output统一处理指令字符串中的符号,避免大量无意义的重复操作,在此次重构中我还将“得到指令字符地址”这一功能单独做成一个函数,进一步地使Output函数的代码可读性上升。

image-20250519234313219

重构代码时对格式编号的重新定义

完成这一次重构之后,第一版代码中没有解决的陈旧问题也一个接一个地浮出了水面。

首先,在初版代码中我在分解指令时并没有对I指令的$16$位立即数进行符号判别,而对J指令而言,从机器码中拆分出的$26$位立即数并不会出现负数的情况,导致我在后期每次加载立即数时都要差别是否为J指令,这样繁杂的判定方式也导致了刚修好这里,其他地方又出现新的问题的现象。在多次进行修补还是没有办法解决问题时,我选择直接在分解指令时直接判定是否为负数,而解决了这一个问题。

其次,初版对于非法指令的容错能力是不足的,由于我在初版代码中使用了比较省内存的操作去存储Op_CodeFunction_Code等,我对每个指令编号对应的Op_FormatFunc_Format同样也只预留了对应较少有效指令的空间,这导致的问题是,当存在Op码为$16\to31$或$47\to63$的或Func码大于$43$的情况时,其在计算格式编号偏移量时会出现异常,为了避免引入更加复杂的判断来解决这个问题,我最终选择将Op_FormatFunc_Format均补全其大小到$64$,由于数据段中.space的内存默认赋值为$0$,这样做使得这些非法数据都能对应上没有符合条件的格式的结果。

image-20250520000052358

对于立即数为负的情况的特判以及对非法数据的额外防范

另一个一直埋着的雷就是在前文中Get_Command_Str函数中被专门进行特殊判定的Op_Code为$1$,而Rt寄存器的值不合法的情况,在进行格式检查时,因为Op_Code为$1$时,指令的输出格式本身就已固定,所以忽略掉了在这里作文章的情况。发现这个特殊情况后,我立即追加了这样的修复,并在Output中新增了特判,以防止在这样的非法数据下翻车。

image-20250520000649020

Op_Code为1的情况下新增的特判,并修复了Rt寄存器内存偏移量错误的问题

编程就是这样,在不断的实践中积累经验,从以往的错误中汲取教训。其实一开始我在听到以反汇编作为大作业时想到的最麻烦的问题其实是将已展开的宏还原回去,如果问题是这样的话,那么代码中就需要有额外的检查,并且将宏还原后,PC的检查也将会是一大麻烦的问题,我想这应该是我后续学习中需要思考的内容,并尝试用简洁的方式去将其解决。

查阅资料得知,通过“跳表”代替繁杂的branch语句,应该也能对代码进行进一步的简化,这也是我后续代码的优化方向。