这是2021暑假短学期计算机系统概论实验部分的最后一个lab,lab有两个选择:assembler或者executer,我在做lab时对LC3的数据通路还不太熟悉,故先选择写了assembler,后面有时间了把executer补上。
理论基础
简易汇编器的理论部分比较简单,就是汇编的两遍扫描(first pass and second pass),除此之外,好像并没有什么新奇的方法和知识(?)
First Pass
第一遍扫描的作用是建立符号表,也即标签(label)和地址的一一对应关系。
LABEL | Address |
---|---|
Kenshin | x300F |
Is | x4000 |
Handsome | xEFFF |
… | … |
Second Pass
实现细节
一些准备工作
考虑到要对LC3的每一条指令的汇编形式进行翻译,所以可以考虑使用switch-case
语句作为翻译部分的主体结构,因此对于指令集可以考虑enum
枚举类型。
enum ins {
BR = 0, ADD, LD, ST, JSR,
AND, LDR, STR, RTI, NOT,
LDI, STI, JMP, RET, LEA, TRAP
};
同时建立指令名、伪指令、部分Trap命令字典用于索引。
static char* dir[] = {
".ORIG", ".END", ".BLKW", ".STRINGZ", ".FILL"
};
static char* pseudo[] = {
"HALT", "IN", "OUT", "GETC", "PUTS","PUTSP"
};
对于符号表的储存,用结构数组,
struct label {
char symbol[60];
int address;
};
struct label SymbolTable[MAXMEM];
开始处理!
预处理
首先对读入的汇编代码转化为大写形式,注意.STRINGZ
指令内的字符串要保留原始状态不转换。
for (int i = 0; i < asmLines; i++) {
strcpy(temp, AsmCode[i]);
LowerToUpper(temp);
if (strstr(temp, ".STRINGZ") == NULL) { //not .STRINGZ instruction
LowerToUpper(AsmCode[i]);
}
else {
for (int j = 0; j < strlen(AsmCode[i]); j++) {
if (AsmCode[i][j] != '\"') { //match with '"', do not convert
if (AsmCode[i][j] >= 'a' && AsmCode[i][j] <= 'z') {
AsmCode[i][j] = AsmCode[i][j] - 'a' + 'A';
}
}
else break;
}
}
} //convert to upper case for check
Important
现在遇到了最重要的一个问题,如何将一条指令里的字段读出来并且进行分析?以LDR
和ADD
指令为例,简要介绍一下我的处理方法。
HERE LDR R0, R1, #0
ADD R0, R1 ,#5
可以注意到指令的操作数之间以,
和`分割,因此可以考虑对一条指令用
strtok`函数进行分割,对于取出的各字段(标签、指令头、操作数)再进行分析。也即,
LABEL | Instruction | arg1 | arg2 | … |
---|---|---|---|---|
例如LDR
指令在用strtok
进行切割后,就会产生HERE
,LDR
,R0
,R1
,#0
五个字段,ADD
指令在切割后会产生ADD
,R0
,R1
,#5
四个字段,再具体分析即可。
建立符号表
进行第一遍扫描,借用中心思想,可以将一条指令切割出的第一个字段进行分析,若和字典中任何词条匹配,说明不是标签,反之则是标签。这里还要正确计算.STRINGZ
和.BLKW
占多个位置时的标签地址。
//part of first pass
for (i = 1; i < asmLines - 1; i++) {
if (IfRealLabel(AsmCode[i])) {
strcpy(copy, AsmCode[i]);
CodeToken = strtok(copy, " ");
SymbolTable[j].address = LC;
strcpy(SymbolTable[j].symbol, CodeToken); //construct symbol table
j++;
}
}
扫描生成机器码
继续沿用中心思想,如3~13
行所示,取出指令头和操作数以备分析。
for (i = 1; i < asmLines - 1; i++, k++) {
PC = LC + 1;
//get string of instruction
strcpy(copy, AsmCode[i]);
opcode = strtok(copy, " ,");
if (IfRealLabel(AsmCode[i])) {
opcode = strtok(NULL, " ,");
}
do {
arg[j] = strtok(NULL, " ,");
j++;
} while (arg[j - 1] != NULL); //get all arguments
j = 0; //set counter to 0
}
主体分析用switch-case
进行分析,对取出的指令头和操作数逐个分析并转化成对于机器码,在一条完整指令分析完毕后将机器码字段进行连接形成完整指令,这里只列出两条指令的分析代码段,完整C文件会贴在文末。
//part of second pass
switch (WhichIns(opcode)) {
case BR:
opcode = strtok(opcode, "BR"); //get nzp bits
if (opcode == NULL || strcmp(opcode, "NZP") == 0) strcpy(nzpBits, "111");
else if (strcmp(opcode, "NZ") == 0) strcpy(nzpBits, "110");
else if (strcmp(opcode, "NP") == 0) strcpy(nzpBits, "101");
else if (strcmp(opcode, "ZP") == 0) strcpy(nzpBits, "011");
else if (strcmp(opcode, "N") == 0) strcpy(nzpBits, "100");
else if (strcmp(opcode, "Z") == 0) strcpy(nzpBits, "010");
else if (strcmp(opcode, "P") == 0) strcpy(nzpBits, "001");
if (*arg[0] != '#') {
PCoffset9 = GetLabelAddress(arg[0]) - PC;
arg[0] = DecToBinString(PCoffset9, 9);
}
else arg[0] = DecToBinString(StringToNum(arg[0]), 9);
strcat(MachCode[k], "0000");
strcat(MachCode[k], nzpBits);
strcat(MachCode[k], arg[0]);
break;
case ADD:
arg[0] = RegToBinString(arg[0]);
strcpy(dst, arg[0]);
arg[1] = RegToBinString(arg[1]);
strcpy(src1, arg[1]);
strcat(MachCode[k], "0001");
strcat(MachCode[k], dst);
strcat(MachCode[k], src1);
if (strncmp(arg[2], "R", 1) == 0) { //ADD DR, SR1, SR2
arg[2] = RegToBinString(arg[2]);
strcpy(src2, arg[2]);
strcat(MachCode[k], "000");
strcat(MachCode[k], src2);
}
else { //ADD DR SR1, imm5
arg[2] = DecToBinString(StringToNum(arg[2]), 5);
strcpy(imm5, arg[2]);
strcat(MachCode[k], "1");
strcat(MachCode[k], imm5);
}
break;
/*other cases*/
}
反思与总结
由于是简易汇编器(实际上就只会翻译),目前只能对一对.ORIG
和.END
构成的LC3汇编代码进行翻译,无法对有.EXTERNAL
的多文件进行翻译。也不能处理含注释的汇编代码,代码实现也肯定有很多丑陋的地方。总的来说,还是一个很粗糙的东西。
不过在写的过程中对C
处理字符串的库函数有了进一步的了解,对汇编过程有了进一步的理解。除此之外,在观摩了@sloth用大蟒蛇re
正则表达式实现的汇编器(代码行数是我的1/3!)后,我决定弃暗投明,这就去学大蟒蛇!
main.c