用Java实现汇编器+链接器

设计功能描述

使用Java编写的将MiniSys汇编程序转换到 Minisys 体系机器码的汇编器,同时兼具链接功能。

模块功能

  • main.java:程序入口,实现命令行,-l表示是否与bios,中断程序链接。例: $ java -jar minisys-java.jar -l
  • Assembler.java:实现对数据段、指令段的汇编,先对指令进行宏展开,将指令与label分离并储存label以及其对应的地址,遍历所有指令进行汇编,最后输出AsmProgram(包含了处理后的数据段、指令段数据)。
  • MiniInstructions.java:实现57条指令,保存其相应的正则格式,调用toBinary方法即可实现指令转二进制。
  • Converter.java:将输入的AsmProgram转为相应的Coe文件,也实现了将数据段、指令段的Coe文件合并生成可以直接在开放板上运行的serial。
  • Linker.java:根据MiniSys的MEM布局(BIOS 区域、用户程序区域、中断处理程序入口、中断处理程序)对二进制数据实现内存的布局、地址的重定位。
  • MacroExpansionRules:实现宏指令,可以将push、pop、jg、jge、jl、jle、move宏指令转为常用指令。
  • Instruction.java:实现了指令类的底层逻辑,为了实现对57条指令的兼容,包含了自定义正则、指令componentList自定义、toBinary自定义(每种指令的构成、正则格式都是不同的,这样实现简单易用)。
  • Register.java:实现了寄存器的转换,可以对32个通用寄存器进行映射。
  • Utils:通用工具类,实现了label转二进制(Assember里已经保存了label的数据),变量转二进制以及各种通用的十六转二进制,十进制转二进制,计算偏移地址,获取数据类型大小等工具。

设计的主要特色

宏指令支持

通过对宏指令转换为MiniSys支持的57条常用指令,实现对push、pop、jg、jge、jl、jle、move宏指令的支持。

内存的布局、地址的重定位

链接器根据MiniSys的MEM布局(BIOS 区域、用户程序区域、中断处理程序入口、中断处理程序)对二进制数据实现内存的布局、地址的重定位,并生成可以直接在开放板上使用的serial文件。

易用性

在Instruction类实现中,由于正则、componentList、toBinary都可以自定义。因此,仅需在MinisysInstructions类中加入新指令对应的正则、指令格式、二进制转译规则,即可实现对新指令的支持。

体系结构

汇编器的结构如图所示:Assembler接收一个包含汇编代码的文件(.asm格式)。首先,进行宏指令扩展,并在此过程中处理标签(label),将处理后的结果存储在TextSegLabel中。随后,逐行处理指令,并将处理后的信息整合到AsmProgram中。AsmProgram包括数据段(DataSeg)和指令段(TextSeg)。数据段保存了所有变量的名称、类型以及相应的值,而指令段保存了所有指令的详细信息。

设计与特色概述

指令类的底层逻辑:Instruction.java

该类涵盖了当前指令的各个方面,包括指令类型、描述、指令名称、指令正则模式,以及构成列表(List<InstructionComponent>)。在这个上下文中,InstructionComponent用于定义指令的构成。具体而言,MIPS指令在图示中规定了其格式和含义,而InstructionComponent则指明了从lBit到rBit位置的二进制数据的类型type。toBinary方法定义了如何生成二进制val值,若不需要生成,则为null。每个指令包含多个InstructionComponent,如图中所示,其中第一个和最后一个是固定的,而中间的三个通过toBinary方法生成相应的二进制val值。举例来说,通过正则表达式解析输入的字符串,然后提取值并将其转换为二进制,最后填入相应的位置。这种设计能够有效地实现指令的解析和二进制表示,使其在汇编器中发挥作用。

1
2
3
4
5
6
7
8
public String toBinary() {
for (InstructionComponent component : components) {
if (component.getVal().trim().isEmpty()) {
throw new IllegalStateException("尝试将不完整的指令转为2或16进制。");
}
}
return components.stream().map(InstructionComponent::getVal).reduce(String::concat).orElse("");
}

在这个类中,实现了toBinary方法,其主要功能是提取之前InstructionComponent中存储的已转换为二进制的val值,并将它们合并成一个二进制流。值得注意的是,此处的toBinary方法与前文中Component中的toBinary方法并不相同。这里的toBinary方法专注于将Component中通过toBinary生成的二进制值(如果toBinary为null,则直接使用val)整合成一个二进制流。

通用指令的实现:MiniInstructions.java

该类为Instruction的上层类,构建了57条MIPS通用指令。

1
2
3
4
5
6
7
newInstruction("add", "按字加法", "(rd)←(rs)+(rt)", paramPattern(3), new InstructionComponent[]{
new InstructionComponent(31, 26, "op", null, InstructionComponentType.FIXED, "000000"),
new InstructionComponent(25, 21, "rs", m -> Register.regToBin(m.group(2)), InstructionComponentType.REG, ""),
new InstructionComponent(20, 16, "rt", m -> Register.regToBin(m.group(3)), InstructionComponentType.REG, ""),
new InstructionComponent(15, 11, "rd", m -> Register.regToBin(m.group(1)), InstructionComponentType.REG, ""),
new InstructionComponent(10, 6, "shamt", null, InstructionComponentType.FIXED, "00000"),
new InstructionComponent(5, 0, "func", null, InstructionComponentType.FIXED, "100000")

根据上图所示,通过newInstruction方法构建新指令。此方法接受指令名称、指令描述、指令正则和指令构成列表等参数,其中指令构成列表的格式按照MIPS指令的规范输入。这使得我们能够轻松地构建新的指令。值得注意的是,toBinary方法,在已经输入了val或者type为FIXED的情况下会为null。而在其他情况下,它定义了如何处理指令正则匹配后的字符串。

1
2
3
4
new InstructionComponent(31, 26, "op", null, InstructionComponentType.FIXED, "100100"),
new InstructionComponent(25, 21, "rs", m -> Register.regToBin(m.group(3)), InstructionComponentType.REG, ""),
new InstructionComponent(20, 16, "rt", m -> Register.regToBin(m.group(1)), InstructionComponentType.REG, ""),
new InstructionComponent(15, 0, "offset", m -> Utils.varToAddrBin(m.group(2), 16, true), InstructionComponentType.OFFSET, "")

例如,在处理add指令时,正则匹配后的参数为寄存器,因此,我们需要对matcher m的group进行regToBin的操作。如果是匹配后是offset的参数,则需要varToAddrBin或labelToBin,immediate则为literalToBin。同理,可以得到其他56条指令。

Register.java:寄存器的实现

1
2
3
4
5
6
7
8
9
10
public static String regToBin(String reg) {
reg = reg.replace("$", "").trim();
int regNumber;
if (reg.matches("\\d+")) {
regNumber = Integer.parseInt(reg);
} else {
regNumber = indexOfRegister(reg);
}
return Utils.decToBin(regNumber, 5,false);
}

实现寄存器功能并不复杂。通过接收输入的寄存器名称(reg),我们可以轻松找到相应的寄存器索引(Index),然后通过decToBin方法将其转换为二进制表示,最终进行输出。

MacroExpansionRules.java:宏指令的实现

1
2
expansionRules.put("push", new MacroExpansionRule("^push\\s+(\\$\\w{1,2})$", new String[]{"addi $sp, $sp, -4", "sw ${RegExp.$1}, 0($sp)"}));
expansionRules.put("pop", new MacroExpansionRule("^pop\\s+(\\$\\w{1,2})$", new String[]{"lw ${RegExp.$1}, 0($sp)", "addi $sp, $sp, 4"}));
1
2
3
4
5
6
7
8
9
10
11
12
13
private static String replaceGroups(Matcher matcher, String input) {
// Check for presence of ${RegExp.$3}, ${RegExp.$2}, ${RegExp.$1} before replacing
if (input.contains("${RegExp.$3}")) {
input = input.replace("${RegExp.$3}", getGroup(matcher, 3));
}
if (input.contains("${RegExp.$2}")) {
input = input.replace("${RegExp.$2}", getGroup(matcher, 2));
}
if (input.contains("${RegExp.$1}")) {
input = input.replace("${RegExp.$1}", getGroup(matcher, 1));
}
return input;
}

宏指令的展开过程首先涉及对相关指令的参数获取。在获取到这些参数之后,通过对这些参数的合成操作,生成新的指令序列,这些指令即为通用的MIPS指令。最终,将这一新生成的指令序列返回,这相当于将原始宏指令在语义上替换为相应的MIPS通用指令。

Assembler.java:汇编功能的主要模块

该模块主要涉及两个关键方面的处理,即数据段和指令段,这两者在该模块中进行详细处理。

数据段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String startAddr = asm.get(0).split("\\s+").length != 1 ? asm.get(0).split("\\s+")[1] : "0";
if (asm.get(0).split("\\s+").length > 2) {
throw new RuntimeException("数据段首声明非法");
}
//初始化
List<DataSegVarComp> comps = new ArrayList<>();
vars = new ArrayList<>();
String name = null;
int i = 1;
int addr;
if(startAddr.startsWith("0x")){
addr=Integer.parseInt(startAddr.substring(2),16);
}else{
addr=Integer.parseInt(startAddr);
}
AtomicInteger nextAddr = new AtomicInteger(addr);

首先,我们考虑数据段。数据段的处理分为三个状态:初始化、新增变量、变量中继。在初始化状态中,首先判断文件的第一行是否包含.data,然后获取起始地址startAddr和下一个地址nextAddr。

1
2
Matcher varStartMatcher = VAR_START_PATTERN.matcher(asm.get(i));
Matcher varContdMatcher = VAR_CONTD_PATTERN.matcher(asm.get(i));

如上图所示,通过正则表达式判断当前状态是新增变量还是变量中继状态。在新增变量状态中,即处理最初的变量声明,将上一次获取的信息存入变量集合vars中(如果上一次没有信息,则跳过)。然后,创建一个DataSegVar对象,将正则匹配得到的参数填入name和type字段,而最后一个参数value由于asm文件中可能使用逗号分隔多个变量,需要使用parseInitValue方法来处理。parseInitValue方法能够解析并存储数据到DataSegVar中。下图即为parseInitValue的调用,输入type和Matcher的最后一个参数,获得到的数据将其存入DataSegVar中。

1
2
3
4
parseInitValue(type, varStartMatcher.group(3)).forEach(val -> {
finalComps.add(new DataSegVarComp(type, val.trim()));
nextAddr.addAndGet(size * (Objects.equals(type, "ascii") ? val.length() : 1));
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
private static List<String> parseInitValue(String type, String init) {
assert !(!Objects.equals(type, "ascii") && init.contains("\"")) : "字符串型数据只能使用.ascii类型";
init = init.trim();
assert init.charAt(0) != ',' && init.charAt(init.length() - 1) != ',' : "数据初始化值头或尾有非法逗号";

if (!Objects.equals(type, "ascii")) {
return Stream.of(init.split("\\s*,")).toList();
} else {
boolean inQuote = false;
boolean nextEscape = false;
List<String> res = new ArrayList<>();
StringBuilder buf = new StringBuilder();
char prev = '\0';

for (int i = 0; i < init.length(); i++) {
char ch = init.charAt(i);
if (!inQuote && Character.isWhitespace(ch)) {
continue;
}
if (ch == '"') {
if (nextEscape) {
assert inQuote : "有非法字符出现在引号以外";
buf.append('"');
nextEscape = false;
} else {
inQuote = !inQuote;
}
} else if (ch == '\\') {
assert inQuote : "有非法字符出现在引号以外";
if (nextEscape) {
buf.append('\\');
nextEscape = false;
} else {
nextEscape = true;
}
} else if (ch == ',') {
if (inQuote) {
buf.append(','); // 引号内逗号可不escape
nextEscape = false;
} else {
assert prev != ',' : "数据初始化值存在连续的逗号分隔";
res.add(buf.toString());
buf = new StringBuilder();
}
} else {
assert inQuote : "有非法字符出现在引号以外";
if (nextEscape) {
buf.append(StringProcessor.unraw(ch));
} else {
buf.append(ch);
}
nextEscape = false;
}
prev = ch;
}
res.add(buf.toString());
return res;
}
}

以上为parseInitValue的具体实现,此函数接受两个参数:“type”和“init”,type是变量类型,init是变量的初始值。 “parseInitValue”函数首先声明,如果变量类型不是“ascii”,则初始值不应包含双引号字符。判断初始值是否以逗号开头或结尾。 如果变量类型不是“ascii”,则函数会用逗号分隔初始值,并返回一个修剪后的值数组。 如果变量类型为“ascii”,则函数将进入更复杂的解析过程。 它初始化几个变量以跟踪解析状态,包括它当前是否在带引号的字符串(“inQuote”)内,是否应转义下一个字符(“nextEscape”)、结果数组(“res”)、当前字符串的缓冲区(“buf”)和前一个字符的缓冲区。 然后,函数在初始值中的每个字符上进入一个循环。根据当前字符和解析状态,它会更新状态变量并将字符添加到缓冲区或结果数组中。 该函数使用“assert”函数来确保初始值的语法正确,如果遇到非法字符或序列,则会引发错误。

举例说明:如果输入的是 a: .ascii "hello","world" 则name为a,type为ascii,第3个参数会交给parseInitValue处理成包含”hello”、”world”的List。

变量中继状态处理除了不会新建一个DataSegVar与变量开始状态基本一致。

指令段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static List<String> expandMacros(List<String> asm, List<Integer> lineno) {
List<String> expandedAsm = new ArrayList<>(asm);
String[] macros = MacroExpansionRules.expansionRules.keySet().toArray(new String[0]);
int bias = 0;

for (int i = 0; i < asm.size(); i++) {
String v = asm.get(i);
String labelPreserve = "";
Pattern labelPattern = Pattern.compile("^(\\w+:)\\s*([\\w\\s$]+)$");
Matcher lableMatcher = labelPattern.matcher(v);
if (lableMatcher.matches()) {
labelPreserve = lableMatcher.group(1);
v = lableMatcher.group(2).trim();
}
for (String macro : macros) {
Pattern pattern = MacroExpansionRules.expansionRules.get(macro).pattern;
Matcher m = pattern.matcher(v);
if (m.matches()) {
String[] replacer = MacroExpansionRules.expansionRules.get(macro).replace(m);
replacer[0] = labelPreserve + " " + replacer[0];
expandedAsm.remove(i + bias);
expandedAsm.addAll(i + bias, Arrays.asList(replacer));

lineno.remove(i + bias);
lineno.addAll(i + bias, new ArrayList<>(Collections.nCopies(replacer.length, lineno.get(i + bias))));
bias += replacer.length - 1;
break;
}
}
}

return expandedAsm;
}

在开始指令段的处理之前需要对其进行宏指令扩展,如上图所示,expandMacros函数用于在汇编语言源代码中扩展宏。该函数接收两个参数:asm(汇编语言指令的数组)和lineno(对应于指令的行号数组)。

函数开始时,创建了asm数组的副本,并初始化了几个变量,包括macros(从expansionRules对象中获取的键的数组),以及bias(用于跟踪由于替换指令为扩展宏而引起的偏移)。

然后,函数遍历asm数组中的每个指令。对于每个指令,它检查是否匹配LabelPattern正则表达式。如果匹配,它会保留标签部分并修剪指令的其余部分。

接下来,函数检查指令是否匹配expansionRules对象中任何宏的模式。如果匹配,它获取匹配宏的替换器,将保留的标签添加到替换器中的第一条指令前,然后在asm数组中用扩展宏替换原始指令。它还更新lineno数组以反映宏扩展后的新行号,并更新bias以考虑数组长度的变化。

最后,函数返回所有宏都已扩展的asm数组。前面宏指令已经介绍过,这里调用replace函数,并传入正则匹配后的参数即可返回替换的指令。再将这些处理后的指令插入到原指令中并更新lineno。

处理宏指令后正式加入指令段处理,和数据段的处理相同,先判断是否存在格式问题,再初始化开始地址startAddr和下一个地址nextAddr。与之不同的是,需要对label进行预处理:

1
2
3
4
5
6
7
8
9
10
if (labelMatcher.matches()) {
labels.add(new TextSegLabel(labelMatcher.group(1), insLineno, Utils.getOffsetAddr(startAddr, (insLineno - 1) * Utils.sizeof("ins"))));
if (!labelMatcher.group(2).trim().isEmpty()) {
insLineno++;
}
instructions.add(labelMatcher.group(2));
} else {
insLineno++;
instructions.add(v);
}

遍历所有的指令,对于符合label正则的,将label的地址、label的行数、label的名字存入labels变量中。在进行这几部预处理后,指令段进入逐行转译阶段,逐行阶段由parseOneLine方法处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static Instruction parseOneLine(String asm, int lineno) {
// 处理助记符
Pattern pattern = Pattern.compile("^\\s*(\\w+)\\s*(.*)");
Matcher matcher = pattern.matcher(asm);
if (!matcher.matches()) {
System.out.print("\"没有找到指令助记符,在代码第 " + lineno);
}
String symbol = matcher.group(1);
// 检验助记符合法性
int instructionIndex = -1;
for (int i = 0; i < minisysInstructions.size(); i++) {
if (minisysInstructions.get(i).getSymbol().equals(symbol)) {
instructionIndex = i;
break;
}
}
// 单行汇编去空格
asm = Utils.serialString(matcher.group(2));
// pc移进
pc += Utils.sizeof("ins");
// 开始组装Instruction对象
Instruction res = Instruction.newInstance(minisysInstructions.get(instructionIndex));
res.setSrc(symbol+" "+asm);
for (InstructionComponent component : res.getComponents()) {
if (component.getVal().trim().isEmpty()) {
res.setComponent(component.getDesc(), component.toBinary(res.getInsPattern().matcher(asm)));
}
}

return res;
}

如上图所示,即为parseOneLine的具体实现,函数首先使用正则表达式从asm字符串中提取助记符(指令的符号名称)。接下来,函数通过在MinisysInstructions数组中查找其索引来检查助记符的有效性。

然后,函数使用serialString函数从asm字符串中删除所有空格,并将程序计数器(pc)增加一个指令的大小。

接着,函数开始组装Instruction对象。它根据在MinisysInstructions数组中找到的指令创建Instruction类的新实例。函数然后遍历指令的每个组件。对于每个不是指令二进制中的FIXED的组件(即需要填充的变量),使用setComponent方法将组件从toBinary中获得的值设置为其二进制表示。

最后,函数返回组装的Instruction对象。需要注意的是该语句,也是Instruction的核心。

1
component.toBinary(res.getInsPattern().matcher(asm))

该语句中的toBinary即为上面InstructionComponent中提到的toBinary方法,其功能是生成二进制表示。具体实现如下

1
2
3
4
5
6
public String toBinary(Matcher m) {
if(toBinary != null && m.matches())
return toBinary.apply(m);
else
return "";
}

在Java中,调用java.util.function.Function变量时需要使用apply方法,这一机制允许对存储的参数执行预先定义的操作。举例来说,在处理add指令中,需要进行如下操作:对25位到21位的二进制进行转换,可以通过m -> Register.regToBin(m.group(2))来实现(其中m表示正则匹配对象,通过m.group(2)获取第二个参数,并对其执行寄存器转二进制的操作)。

这种机制的优势在于,通过正则表达式可以灵活地获取指令的各个参数。通过调用apply方法,将正则匹配后的参数传递给函数变量,可以轻松实现二进制的转换。这使得指令的处理过程更加灵活、模块化,能够适应不同指令格式的需求。

Linker.java 链接器的实现

Minisys 体系使用哈佛结构,指令 MEM 有 64 KB,按字节编址。因此,其地址范围为 0x00000000 ~ 0x0000FFFF。指令 MEM 布局如下:

地址 作用
0x00000000 ~ 0x00000499 BIOS 区域。大小为 500 H = 1280 D Byte,最多存放 1280 / 4 = 320 条指令。
0x00000500 ~ 0x00005499 用户程序区域。大小为 5000 H = 20480 D Byte,最多存放 20480 / 4 = 5120 条指令。
0x00005500 ~ 0x0000EFFF 空。
0x0000F000 ~ 0x0000F499 中断处理程序入口。大小为 500 H = 1280 D Byte,最多存放 1280 / 4 = 320 条指令。
0x0000F500 ~ 0x0000FFFF 中断处理程序。大小为 B00 H = 2816 D Byte,最多存放 2816 / 4 = 704 条指令

通过前述计算,我们能够得知指令的最高限制和位置分布。在链接器中,通过countIns方法计算asm文件中的指令个数。如果计算得到的指令个数超过了最高限制,将触发错误处理。反之,若未超过限制,则通过添加nop(空指令)进行补充,以使指令数量达到相应的地址要求。实现大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// User App 0x00000500 ~ 0x00005499
int userASMInsCount = countIns(userASM);
assertLength(userASMInsCount, 5120, "用户程序段过长。");
int userNopPadding = 5120 - userASMInsCount;

....其他区域代码

// User Application
allProgram.append("# ====== User Application START ======\n");
allProgram.append("# User Application Length = ").append(userASMInsCount).append("\n");
allProgram.append(userASM).append("\n");
allProgram.append("# User Application Padding = ").append(userNopPadding).append("\n");
allProgram.append("nop\n".repeat(userNopPadding));
allProgram.append("# ====== User Application END ======\n");

Converter.java:Coe文件的生成

对于指令段,由textsegToCoe方法生成。该方法中主要实现如下:

1
2
3
4
5
6
7
8
for (Instruction ins : textSeg.getIns()) {
StringBuilder buf = new StringBuilder();
for (InstructionComponent comp : ins.getComponents()) {
buf.append(comp.getVal());
}
coe.append(Utils.binToHex(buf.toString(), false)).append(",\n");
lineno++;
}

该方法的主要操作就是将之前InstrumentComponent中toBinary方法得到的val进行合并生成二进制表示。

使用说明

  1. 请在jar文件路径下,创建bios的文件,将src\snippet中3个asm文件放进bios中

    • 如果没有asm文件,无法使用链接,既无法加上 “-l” 参数。
    • 可以使用自己的bios文件,但需要改成相应的名字。
    • src\snippet中3个文件分别用来:bios引导系统程序入口、interpret-entry中断程序的入口、interpret-handler中断程序处理 。
  2. jar的项目结构如下:

1
2
3
4
5
6
7
jar文件路径
│ miniasm-java.jar

└─bios
minisys-bios.asm
minisys-interrupt-entry.asm
minisys-interrupt-handler.asm
  1. 指令格式如下所示,其中 “-l” 为可选选项,表示是否链接。
1
$ java -jar minisys-java.jar <in_file> <out_dir> -l

<in_file> 表示输入的文件路径,<out_dir>表示输出文件路径,输出的文件如果没创建则会新建。如果输入的文件和jar在同一路径,则可以直接使用$ java -jar minisys-java.jar in.asm out -l

in.asm可替换自己的asm文件名字,输出的文件会保持在当前目录的out文件夹里面。也可以直接使用绝对路径。

测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.DATA 0x00001000
buf: .WORD 0x000000ff,0x55005500
buf2: .byte 1
.ascii "hello"
.TEXT 0x00003456
start: addi $t0, $zero, 0
lw $v0, 20($t0)
addi $t0, $t0, 4
lw $v1, 20($t0)
add $v0, $v0, $v1
addi $t0, $t0, 4
sw $v0, 20($t0)
j start
```

输入数据如上,输出的解析结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
textSeg:{
startAddr:13400, Instruction:{
addi $t0,$zero,0 Hex:20080000
lw $v0,20($t0) Hex:8d020014
addi $t0,$t0,4 Hex:21080004
lw $v1,20($t0) Hex:8d030014
add $v0,$v0,$v1 Hex:00431020
addi $t0,$t0,4 Hex:21080004
sw $v0,20($t0) Hex:ad020014
j start Hex:08000d16
},
Labels:{
start
}
}
dataSeg:{
startAddr:0x00001000 ,vars:{
name:buf addr:4096 {
type:word val:0x000000ff
type:word val:0x55005500
}
name:buf2 addr:4104 {
type:byte val:1
type:ascii val:hello
}
}
}

可以看到dataSeg里面存储了startAddr数据以及vars,vars中存储了具体的变量type以及val。

对于textSeg里面也存储了startAddr,对于Instruction也成功转译为二进制。


用Java实现汇编器+链接器
https://edsad122.github.io/blog/2024/01/11/用Java实现汇编器-链接器/
作者
Edasd
发布于
2024年1月11日
许可协议