这是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 instructionLowerToUpper(AsmCode[i]);}else {for (int j = 0; j < strlen(AsmCode[i]); j++) {if (AsmCode[i][j] != '\"') { //match with '"', do not convertif (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, #0ADD 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 passfor (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 tablej++;}}
扫描生成机器码
继续沿用中心思想,如3~13行所示,取出指令头和操作数以备分析。
for (i = 1; i < asmLines - 1; i++, k++) {PC = LC + 1;//get string of instructionstrcpy(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 argumentsj = 0; //set counter to 0}
主体分析用switch-case进行分析,对取出的指令头和操作数逐个分析并转化成对于机器码,在一条完整指令分析完毕后将机器码字段进行连接形成完整指令,这里只列出两条指令的分析代码段,完整C文件会贴在文末。
//part of second passswitch (WhichIns(opcode)) {case BR:opcode = strtok(opcode, "BR"); //get nzp bitsif (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, SR2arg[2] = RegToBinString(arg[2]);strcpy(src2, arg[2]);strcat(MachCode[k], "000");strcat(MachCode[k], src2);}else { //ADD DR SR1, imm5arg[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
