【算法】远方来信,从数学表达式算法到汇编语法解释器

在繁华的都市中,小悦作为一名软件工程师,每天都在这座钢筋水泥的森林里忙碌。她的生活似乎被工作和各种琐碎的事情填满了,但在这个繁忙的生活中,她总能在工作之余找到一些小小的乐趣。

这天下班后,小悦收到了一封来自国外同学苏菲的email。邮件的内容让她的思绪一下子飘回了那个学习汇编语言的大学时代。

苏菲是一个非常聪明的女孩,她们俩在大学时期成为了要好的朋友。苏菲对编程有着浓厚的兴趣,而小悦则是对理论知识情有独钟。在大学最后一年的上机考试中,她们俩通过逆波兰表达式算法,合作完成了一个数学算式表达式的算法。

这个算式表达式算法是小悦在上机考试中实现的,它主要用于解决数学算式计算的问题。但现在回想起来,她觉得这个算法还有很多可以完善的地方,甚至可以在它基础上开发出自己的脚本语言解释器。于是,她决定利用业余时间来改进这个算法。

在众多实用的编程项目中,页面脚本解释器和SQL语言解释器是两个比较热门的选择。但是这两个项目的语法也相对复杂,因此小悦决定从更简单的汇编语言解释器开始研究。

小悦开始仔细研究汇编语言的语法和指令集,她想要用cSharp写一个自己的汇编语法解释器。她知道,实现一个汇编语法解释器并不是一件容易的事情,但她也坚信,只要自己不断努力和探索,一定能够成功。

在实现汇编语法解释器的过程中,小悦参考了大量的资料和文档,不断调整和完善自己的代码。她遇到了很多困难和挫折,但她都没有放弃。每当遇到问题时,她都会想起苏菲和那个算式表达式算法,这让她有了继续前进的动力。

经过一段时间的努力,小悦终于成功地写出了一个自己的汇编语法解释器。这个解释器能够解析汇编语言语法,并根据程序员输入的语法执行相应的操作。

小悦的闺蜜小欣看到她的成果后,打趣说:“你真是编程界的女侠啊!”小悦听后笑了笑,心中不禁想起了苏菲。她想:“如果苏菲还在国内,我们一定会有更多的合作机会。”

然而,生活总是充满了未知和变数。苏菲毕业后选择了留在国外工作和生活,而小悦则在国内继续着她的软件工程师生涯。虽然两人已经很少联系,但小悦一直珍藏着她们之间的友谊和那个上机考试中的算式表达式算法。

在小悦实现汇编语法解释器的过程中,她不仅提升了自己的编程能力,还进一步理解了算式表达式算法的原理和实现方式。她相信,这个经验将会成为她未来职业生涯中的一笔宝贵财富。

如今的小悦已经不再是那个单纯为了应付考试而编程的女孩了。她在编程领域有着自己的追求和梦想。她希望通过自己的努力和不断的学习,成为一个更加优秀的软件工程师,为这个数字化时代贡献自己的力量。


小悦需要实现的汇编解释器语法如下:

mov x, y - 将y(无论是整数还是寄存器的值)复制到寄存器(变量)x中。
inc x - 使寄存器x的内容增加1。
dec x - 使寄存器x的内容减少1。
add x, y - 将寄存器x的内容与y(无论是整数还是寄存器的值)相加,并将结果存储在x中(即register[x] += y)。
sub x, y - 从寄存器x中减去y(无论是整数还是寄存器的值),并将结果存储在x中(即register[x] -= y)。
mul x, y - 与乘法相同(即register[x] *= y)。
div x, y - 与整数除法相同(即register[x] /= y)。
label: - 定义函数标签位置,一般用于函数定位(标签 = 标识符 + 冒号,标识符是与任何其他命令不匹配的字符串)。跳转命令和调用针对程序中的这些标签位置。
jmp lbl - 跳转到自定义函数标签lbl。
cmp x, y - 比较x(无论是整数还是寄存器的值)和y(无论是整数还是寄存器的值)。结果用于条件跳转(jne,je,jge,jg,jle和jl)。
jne lbl - 如果上一个cmp命令的值不相等,则跳转到标签lbl。
je lbl - 如果上一个cmp命令的值相等,则跳转到标签lbl。
jge lbl - 如果在之前的cmp命令中x大于或等于y,则跳转到标签lbl。
jg lbl - 如果在之前的cmp命令中x大于y,则跳转到标签lbl。
jle lbl - 如果在之前的cmp命令中x小于或等于y,则跳转到标签lbl。
jl lbl - 如果在之前的cmp命令中x小于y,则跳转到标签lbl。
call lbl - 调用由lbl标识的子程序。当在子程序中找到ret时,指令指针应返回到此call命令之后的指令。
ret - 当在子程序中找到ret时,指令指针应返回到调用当前函数的指令。
msg '输出结果:  ',x - 这条指令存储程序的输出。它可能包含文本字符串(以单引号分隔)和寄存器。参数的数量不限,会因程序而异。
end - 这条指令表示程序正确结束,因此返回存储的输出(如果程序在没有此指令的情况下终止,它应返回默认输出:参见下文)。
; - comment注释指令在代码执行程序时不运行。

示例语法:

//代码定义
var program = @"
; My first program
mov a, 5
inc a
call function
msg '计算结果:(5+1)/2 = ', a ; output message
end

function:
div a, 2
ret
";
//执行代码
AssemblerInterpreter.Interpret(program);
//msg命令输出结果 
计算结果:(5+1)/2 = 3


算法实现1:

  1 public class AssemblerInterpreter
  2 {
  3      public static string Interpret(string input)
  4     {
  5       // 存储寄存器的字典
  6       var register = new Dictionary<string, int>();
  7       // 存储比较结果的字符串
  8       var compare  = "";
  9       // 存储标签的字典
 10       var label    = new Dictionary<string, int>();
 11       // 整数类型的栈,存储代码当前位置
 12       var stack    = new Stack<int>();
 13       // 存储消息的字符串构建器
 14       var messages = new StringBuilder();
 15       
 16       // 将输入的程序按行分割
 17       var program = input.Split("\n");
 18       
 19       // 遍历程序,找到标签并存储其位置
 20       for(var i = 0; i < program.Length; i++)
 21       {
 22         var parts = program[i].Split(":");
 23         if (parts.Length == 2) label.Add(parts[0], i);
 24       }
 25         
 26       // 遍历程序的每一行指令
 27       for (var i = 0; i < program.Length; i++)
 28       {
 29         // 将当前行的指令解析为操作符和操作数
 30         var token = TokensFrom(program[i]);
 31         if (token.Length == 0) continue; // 跳过空行
 32         
 33         // 根据操作符执行相应的操作
 34         if (token[0] == "mov")
 35         {
 36           // 将操作数存储到寄存器中
 37           var r = token[1];
 38           var val = ValueOf(token[2]);
 39           if (register.ContainsKey(r)) register[r] = val;
 40           else register.Add(r, val);
 41         }
 42         else if (token[0] == "inc") register[token[1]++;
 43         else if (token[0] == "dec") register[token[1]--;
 44         else if (token[0] == "add") register[token[1]] += ValueOf(token[2]);
 45         else if (token[0] == "sub") register[token[1]] -= ValueOf(token[2]);
 46         else if (token[0] == "mul") register[token[1]] *= ValueOf(token[2]);
 47         else if (token[0] == "div") register[token[1]] /= ValueOf(token[2]);
 48         else if (token[0] == "msg")
 49         {
 50           // 将消息内容添加到消息字符串构建器中
 51           var args = ParseMsg(program[i])
 52             .Select(s => s.First() == '\''
 53                       ? s.Length >= 2 ? s[1..^1] : s
 54                       : ValueOf(s).ToString());
 55           messages.Append(string.Concat(args));
 56         }
 57         else if (token[0] == "call")
 58         {
 59           // 将当前位置压入栈中,并跳转到指定标签位置
 60           stack.Push(i);
 61           i = label[token[1]];
 62         }
 63         else if (token[0] == "ret") i = stack.Pop(); // 从栈中弹出位置并跳转
 64         else if (token[0] == "cmp")
 65         {
 66           // 比较两个值并存储比较结果
 67           var x = ValueOf(token[1]);
 68           var y = ValueOf(token[2]);
 69           
 70           if (x == y) compare = "=";
 71           else if (x > y) compare = ">";
 72           else if (x < y) compare = "<";
 73           else compare = "!=";
 74         }
 75         // 根据比较结果进行条件跳转
 76         else if (token[0] == "jmp") i = label[token[1]];
 77         else if (token[0] == "jne")
 78         {
 79           if (compare != "=") i = label[token[1]];
 80         }
 81         else if (token[0] == "je")
 82         {
 83           if (compare == "=") i = label[token[1]];
 84         }
 85         else if (token[0] == "jge")
 86         {
 87           if (compare == ">" || compare == "=") i = label[token[1]];
 88         }
 89         else if (token[0] == "jg")
 90         {
 91           if (compare == ">") i = label[token[1]];
 92         }
 93         else if (token[0] == "jle")
 94         {
 95           if (compare == "<" || compare == "=") i = label[token[1]];
 96         }
 97         else if (token[0] == "jl")
 98         {
 99           if (compare == "<") i = label[token[1]];
100         }
101         else if (token[0] == "end") return messages.ToString();
102       }
103 
104       return null;
105 
106       // 从输入的行中提取标记并返回标记数组
107       string[] TokensFrom(string line) =>
108         Regex.Split(RemoveComment(line), "[ ,]+")  // 使用正则表达式分割去除注释后的行,并去除空格
109             .Select(s => s.Trim())  // 去除每个标记的首尾空格
110             .Where(s => !string.IsNullOrEmpty(s))  // 去除空字符串
111             .ToArray();  // 转换为数组
112 
113         // 解析消息内容并返回消息参数数组
114         string[] ParseMsg(string line)
115         {
116             var args = Regex.Replace(line, @"^\s*msg\s*", "");  // 去除消息指令并获取消息内容
117             var result = new List<string>();  // 创建存储结果的列表
118 
119             var token = new StringBuilder();  // 创建 StringBuilder 存储当前标记
120             var inQuote = false;  // 标记是否在引号内
121             foreach (var c in args)  // 遍历消息内容的每个字符
122             {
123                 if (c == '\'' && inQuote)  // 如果是引号且在引号内
124                 {
125                     inQuote = false;  // 标记离开引号状态
126                     token.Append(c);  // 将引号添加到标记中
127                 }
128                 else if (c == '\'' && !inQuote)  // 如果是引号且不在引号内
129                 {
130                     inQuote = true;  // 标记进入引号状态
131                     token.Append(c);  // 将引号添加到标记中
132                 }
133                 else if (c == ',' && !inQuote)  // 如果是逗号且不在引号内
134                 {
135                     result.Add(token.ToString().Trim());  // 将当前标记去除首尾空格后添加到结果列表
136                     token.Clear();  // 清空当前标记
137                 }
138                 else if (c == ' ' && !inQuote) continue;  // 如果是空格且不在引号内,则继续下一次循环
139                 else if (c == ';' && !inQuote) break;  // 如果是分号且不在引号内,则结束循环
140                 else token.Append(c);  // 其他情况将字符添加到当前标记中
141             }
142             if (token.Length > 0) result.Add(token.ToString().Trim());  // 将最后一个标记去除首尾空格后添加到结果列表
143             return result.ToArray();  // 转换为数组并返回
144         }
145 
146         // 去除行中的注释并返回处理后的行
147         string RemoveComment(string line) => Regex.Replace(line, ";.*", "");  // 使用正则表达式去除分号及其后的内容
148 
149         // 获取变量的值,如果是整数则直接返回,否则从寄存器中获取
150         int ValueOf(string x) => int.TryParse(x, out var r) ? r : register[x];
151     }
152 }

这段代码实现了一个基于指令集的虚拟机执行算法解释器。它解释并定义了一系列基本的汇编指令,包括mov、inc、dec、add、sub、mul、div、msg、call、ret、cmp、jmp、jne、je、jge、jg、jle、jl和end。这些指令可以操作寄存器、栈、标签和消息,并且可以进行条件跳转和比较操作。

这个算法是一个汇编解释器,它模拟了虚拟机的功能。在算法中,通过使用字典存储寄存器的值、比较结果的字符串、标签的位置等信息,以及使用栈存储代码当前位置,来模拟虚拟机的处理器、内存、寄存器等功能。算法遍历输入的程序,解析每一行指令并执行相应的操作,如将操作数存储到寄存器中、进行加减乘除运算、比较两个值等。同时,算法还实现了条件跳转和消息输出等功能,以模拟虚拟机的指令集。通过这种方式,算法实现了对输入程序的解释和执行,从而实现了对虚拟机的模拟。这样的虚拟机模拟可以使程序在不同的平台上运行,提高了程序的灵活性和可移植性。

虚拟机程序AssemblerInterpreter.Interpret作为一个.net宿主程序,接收并执行这些外部输入的汇编语言代码。

该算法首先接收一个汇编语法表达式的字符串作为输入,然后通过解析字符串,逐个获取操作数和操作符。在解析过程中,算法会根据操作符的类型和操作数的值,进行相应的计算操作。这些操作包括将指令位置压入栈中、从栈中弹出指令在寄存器中的位置并进行计算等。

算法的核心部分是遍历语法表达式并执行相应的操作。在这个过程中,算法会逐个解析字符串中的字符,并根据解析的结果执行相应的计算步骤。一旦完成计算,算法会将最终的结果存储在栈顶,而栈顶的元素就是最终的指令计算结果。

注1:else if (token[0] == "ret") i = stack.Pop(); // 从栈中弹出位置并跳转

这行代码是解释器中处理ret指令的部分。当解释器遇到ret指令时,它需要从调用栈中弹出一个位置并跳转到该位置。

让我们来解释一下这行代码的逻辑:

  1. 首先,解释器解析到ret指令,并将其作为一个操作符token
  2. 接着,解释器检查token[0]是否等于"ret",即检查标记的第一个元素是否是"ret"。这是为了确保当前指令是ret指令。
  3. 如果token[0]等于"ret",则执行i = stack.Pop(),这意味着从调用栈stack中弹出一个位置,并将其赋值给程序计数器i。这样程序计数器将跳转到之前调用指令的下一条指令位置,实现了ret指令的跳转功能。

注2:在代码47行,操作符分支处理中,可以根据实际情况扩展新的赋值函数

else if (token[0] == "div") register[token[1]] /= ValueOf(token[2]);
else if (token[0] == "expression") ... //在这里可扩展其他语法功能,除了加减乘除,还可以添加逆波兰表达式算法进行变量赋值或其他自定义函数处理等


基于指令集的虚拟机执行算法的历史故事可以追溯到计算机科学和软件工程的早期发展阶段,它经历了多个关键的里程碑和发展趋势:

1. 早期计算机:在早期的计算机系统中,程序员需要直接编写机器码指令,这些指令直接操作计算机的硬件。这种方式对于程序员来说非常繁琐和复杂,因此人们开始寻找更高级的抽象方式来编写程序。这个时期,一些先驱者提出了汇编语言,它提供了一种更接近硬件的编程方式,使得程序员可以更容易地编写机器码指令。
2. 汇编语言:为了简化程序员编写机器码指令的工作,汇编语言被引入。汇编语言是一种低级的编程语言,它使用助记符(mnemonics)来代替机器码指令,并提供了一些符号标签来简化跳转和调用等操作。这使得程序员可以更加高效地编写程序,减少了出错的可能性。
    高级语言:1957年:IBM的约翰·巴科斯(John Backus)创建全世界第一套高级语言:Fortran(formula translate)。Fortran的跨平台限制主要是由于其与特定的操作系统和硬件架构的紧密相关性,如windows/linux,以及不同编译器和平台的差异和限制。如果需要在不同的操作系统或硬件平台上运行Fortran程序,可能需要重新编译或修改程序,以确保它能够在目标平台上正确运行。

3. 虚拟机:为了实现跨平台的程序执行,虚拟机概念被引入。虚拟机是一个软件实体,它模拟了一台计算机,包括处理器、内存、寄存器等,并提供了一组指令集。程序员可以使用这个指令集来编写程序,并在虚拟机上执行,如JVM,而不需要关心底层硬件和操作系统。虚拟机的引入使得程序可以在不同的平台上运行,提高了程序的灵活性和可移植性。
4. 基于指令集的虚拟机执行算法:虚拟机中的核心部分是基于指令集的虚拟机执行算法。它负责解释和执行程序中的指令,包括数据操作、代码跳转、函数调用等。执行算法通常包括指令解析、寄存器操作、内存访问、跳转控制等步骤。这个算法的实现除了完成基本指令需求,还需要考虑性能、安全性和稳定性等方面的问题。
5. 发展趋势:随着计算机科学和软件工程的发展,基于指令集的虚拟机执行算法得到了不断的改进和优化。例如,引入了即时编译(JIT)技术、增强了指令集、优化了执行引擎等,使得虚拟机执行算法在性能和功能上得到了显著提升。同时,随着云计算和移动互联网的普及,基于指令集的虚拟机执行算法也在这些领域得到了广泛的应用和发展。


通过测试用例(5+1)/2,我们将执行AssemblerInterpreter.Interpret(program)来运行这段程序,并期望msg命令输出"计算结果:(5+1)/2 = 3"。

程序的执行步骤如下:

  1. 首先,AssemblerInterpreter.Interpret(program)将解释器应用于给定的程序字符串program
  2. 解释器将逐行解析程序字符串,并执行相应的操作。在解释器中,会调用TokensFrom函数来解析每一行的标记。
  3. 对于每个标记,解释器将执行相应的操作,比如mov指令会移动一个值5到一个寄存器,inc指令会增加寄存器中的值,即(5+1),call指令会调用一个函数function,即(5+1)/2,msg指令会输出常量消息和a的值3。
  4. 在执行msg指令时,解释器会调用ParseMsg函数来解析消息内容,并输出最终的消息结果。

在这个测试用例中,当msg指令被执行时,ParseMsg函数会解析消息内容"'计算结果:(5+1)/2 = ', a",并输出"计算结果:(5+1)/2 = 3"作为最终结果。

因此,通过执行AssemblerInterpreter.Interpret(program),程序将按预期输出"计算结果:(5+1)/2 = 3"。


算法实现2:

  1 public class AssemblerInterpreter
  2 {
  3     public static string Interpret(string input)
  4     {
  5         Regex trimmer = new Regex(@"\s\s+");
  6         string[] unparsedInstructions = input.Split("\n");
  7         Dictionary<string, int> registers = new Dictionary<string, int>();
  8         Dictionary<string, int> labels = new Dictionary<string, int>();
  9         IInstruction[] instructions = new IInstruction[unparsedInstructions.Length];
 10         int endOfProgram = 0;
 11         //cast to instructions
 12         for(int i = 0; i < unparsedInstructions.Length;  i++)
 13         {
 14             string cleanString = unparsedInstructions[i].TrimStart();
 15             cleanString = trimmer.Replace(cleanString, " ");
 16             string[] instructionParts = cleanString.Split(" ");
 17             switch(instructionParts[0]){
 18                 case ";":
 19                     instructions[i] = new Idle();
 20                     break;
 21                 case "msg":
 22                     instructions[i] = new Msg(cleanString.Substring(4));
 23                     break;
 24                 case "call":
 25                     instructions[i] = new Call(instructionParts[1]);
 26                     break;
 27                 case "mov":
 28                     instructions[i] = new Mov(instructionParts[1].Remove(1), new Arguement(instructionParts[2]));
 29                     break;
 30                 case "inc":
 31                     instructions[i] = new Inc(instructionParts[1]);
 32                     break;
 33                 case "dec":
 34                     instructions[i] = new Dec(instructionParts[1]);
 35                     break;
 36                 case "add":
 37                     instructions[i] = new Add(instructionParts[1].Remove(1), new Arguement(instructionParts[2]));
 38                     break;
 39                 case "sub":
 40                     instructions[i] = new Sub(instructionParts[1].Remove(1), new Arguement(instructionParts[2]));
 41                     break;
 42                 case "div":
 43                     instructions[i] = new Div(instructionParts[1].Remove(1), new Arguement(instructionParts[2]));
 44                     break;
 45                 case "mul":
 46                     instructions[i] = new Mul(instructionParts[1].Remove(1), new Arguement(instructionParts[2]));
 47                     break;
 48                 case "cmp":
 49                     instructions[i] = new Cmp(new Arguement(instructionParts[1].Remove(1)), new Arguement(instructionParts[2]));
 50                     break;
 51                 case "jmp":
 52                     instructions[i] = new Jmp(instructionParts[1]);
 53                     break;
 54                 case "jne":
 55                     instructions[i] = new Jne(instructionParts[1]);
 56                     break;
 57                 case "je":
 58                     instructions[i] = new Je(instructionParts[1]);
 59                     break;
 60                 case "jge":
 61                     instructions[i] = new Jge(instructionParts[1]);
 62                     break;
 63                 case "jg":
 64                     instructions[i] = new Jg(instructionParts[1]);
 65                     break;
 66                 case "jle":
 67                     instructions[i] = new Jle(instructionParts[1]);
 68                     break;
 69                 case "jl":
 70                     instructions[i] = new Jl(instructionParts[1]);
 71                     break;
 72                 case "ret":
 73                     instructions[i] = new Ret();
 74                     break;
 75                 case "end":
 76                     endOfProgram = i;
 77                     break;
 78                 default:
 79                     if(instructionParts[0].Contains(":") && instructionParts[0].IndexOf(":") == instructionParts[0].Length - 1)
 80                     {
 81                         labels.Add(instructionParts[0].Remove(instructionParts[0].Length - 1) , i);
 82                     }
 83                     instructions[i] = new Idle();
 84                     break;
 85             }
 86         }
 87         return RunProgram(registers, labels, instructions, endOfProgram);
 88     }
 89 
 90     public static string RunProgram(Dictionary<string, int> registers, Dictionary<string, int> labels, IInstruction[] instructions,int endOfprogram)
 91     {
 92         bool ended = false;
 93         int memoryPointer = 0;
 94         string output = string.Empty;
 95         string ans = string.Empty;
 96         Stack<int> callStack = new Stack<int>();
 97         int comparison = 0;
 98         while (memoryPointer < instructions.Length)
 99         {
100             if(endOfprogram == memoryPointer)
101             {
102                 ended = true;
103                 break;
104             }
105             memoryPointer = instructions[memoryPointer].Execute(registers, labels, callStack, memoryPointer, ref comparison, out output);
106             ans = $"{ans}{output}";
107         }
108         if(ended)
109         {
110             return ans;
111         }
112         else
113         {
114             return null;
115         }
116     }
117 
118     class Arguement
119     {
120         int _number;
121         string _reg;
122 
123         public Arguement(string value)
124         {
125             if (!int.TryParse(value, out _number))
126             {
127                 _reg = value;
128             }
129             else
130             {
131                 _reg = string.Empty;
132             }
133         }
134 
135         public int GetArguementValue(Dictionary<string, int> registers)
136         {
137             int value;
138             return registers.TryGetValue(_reg, out value) ? value : _number;
139         }
140     }
141 
142     public interface IInstruction
143     {
144         int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output);
145     }
146 
147     class Idle : IInstruction
148     {
149         public Idle() { }
150 
151         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
152         {
153             output = string.Empty;
154             return pointer + 1;
155         }
156     }
157 
158     class Msg : IInstruction
159     {
160         string _msg;
161         public Msg(string msgAndRegs)
162         {
163             _msg = msgAndRegs;
164         }
165 
166         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
167         {
168             string[] parts = _msg.Split("'");
169             
170 
171             bool insideQoute = false;
172             int QouteStart = 0;
173             Queue<Tuple<string, string>> message = new Queue<Tuple<string, string>>();
174             for(int i = 0; i < _msg.Length; i++)
175             {
176                 string character = _msg[i].ToString();
177                 if(!insideQoute && character.Equals(";"))
178                 {
179                     break;
180                 }
181                 if(character.Equals("'"))
182                 {
183                     if(insideQoute)
184                     {
185                         message.Enqueue(new Tuple<string, string>("message", _msg.Substring(QouteStart + 1, i - QouteStart - 1)));
186                         insideQoute = false;
187                     }
188                     else
189                     {
190                         insideQoute = true;
191                         QouteStart = i;
192                     }
193                 }
194                 else if(!insideQoute)
195                 {
196                     if (!character.Equals(" ") && !character.Equals(","))
197                     {
198                         message.Enqueue(new Tuple<string, string>("reg", character.ToString()));
199                     }
200                 }
201             }
202             output = string.Empty;
203             while(message.Count > 0)
204             {
205                 Tuple<string, string> temp = message.Dequeue();
206                 if (temp.Item1.Equals("message"))
207                 {
208                     output = $"{output}{temp.Item2}";
209                 }
210                 else
211                 {
212                     output = $"{output}{registers[temp.Item2]}";
213                 }
214             }
215             return pointer + 1;
216         }
217     }
218 
219     class Call : IInstruction
220     {
221         string _label;
222         public Call(string label)
223         {
224             _label = label;
225         }
226 
227         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
228         {
229             output = string.Empty;
230             callStack.Push(pointer);
231             return labels[_label] + 1;
232         }
233     }
234 
235     class Mov : IInstruction
236     {
237         string _reg;
238         Arguement _arg;
239         public Mov(string reg, Arguement arg){
240             _reg = reg;
241             _arg = arg;
242         }
243 
244         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
245         {
246             output = string.Empty;
247             registers[_reg] = _arg.GetArguementValue(registers);
248             return pointer + 1;
249         }
250     }
251 
252     class Inc : IInstruction
253     {
254         string _reg;
255         public Inc(string reg)
256         {
257             _reg = reg;
258         }
259 
260         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
261         {
262             output = string.Empty;
263             registers[_reg]++;
264             return pointer + 1;
265         }
266     }
267 
268     class Dec : IInstruction
269     {
270         string _reg;
271         public Dec(string reg)
272         {
273             _reg = reg;
274         }
275 
276         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
277         {
278             output = string.Empty;
279             registers[_reg]--;
280             return pointer + 1;
281         }
282     }
283 
284     class Add : IInstruction
285     {
286         string _reg;
287         Arguement _arg;
288         public Add(string reg, Arguement arg)
289         {
290             _reg = reg;
291             _arg = arg;
292         }
293 
294         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
295         {
296             output = string.Empty;
297             registers[_reg] += _arg.GetArguementValue(registers);
298             return pointer + 1;
299         }
300     }
301 
302     class Sub : IInstruction
303     {
304         string _reg;
305         Arguement _arg;
306         public Sub(string reg, Arguement arg)
307         {
308             _reg = reg;
309             _arg = arg;
310         }
311 
312         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
313         {
314             output = string.Empty;
315             registers[_reg] -= _arg.GetArguementValue(registers);
316             return pointer + 1;
317         }
318     }
319 
320     class Mul : IInstruction
321     {
322         string _reg;
323         Arguement _arg;
324         public Mul(string reg, Arguement arg)
325         {
326             _reg = reg;
327             _arg = arg;
328         }
329 
330         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
331         {
332             output = string.Empty;
333             registers[_reg] *= _arg.GetArguementValue(registers);
334             return pointer + 1;
335         }
336     }
337 
338     class Div : IInstruction
339     {
340         string _reg;
341         Arguement _arg;
342         public Div(string reg, Arguement arg)
343         {
344             _reg = reg;
345             _arg = arg;
346         }
347 
348         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
349         {
350             output = string.Empty;
351             registers[_reg] /= _arg.GetArguementValue(registers);
352             return pointer + 1;
353         }
354     }
355 
356     class Ret : IInstruction
357     {
358         public Ret() { }
359 
360         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
361         {
362             output = string.Empty;
363             return callStack.Pop() + 1;
364         }
365     }
366 
367     class Cmp : IInstruction
368     {
369         Arguement _leftArg;
370         Arguement _rightArg;
371         public Cmp(Arguement leftArg, Arguement rightArg)
372         {
373             _leftArg = leftArg;
374             _rightArg = rightArg;
375         }
376 
377         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
378         {
379             output = string.Empty;
380             comparison = _leftArg.GetArguementValue(registers) - _rightArg.GetArguementValue(registers);
381             return pointer + 1;
382         }
383     }
384 
385     class Jmp : IInstruction
386     {
387         string _label;
388 
389         public Jmp(string label)
390         {
391             _label = label;
392         }
393 
394         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
395         {
396             output = string.Empty;
397             return labels[_label] + 1;
398         }
399     }
400 
401     class Jne : IInstruction
402     {
403         string _label;
404 
405         public Jne(string label)
406         {
407             _label = label;
408         }
409 
410         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
411         {
412             output = string.Empty;
413             if(comparison != 0)
414             {
415                 return labels[_label] + 1;
416             }
417             return pointer + 1;
418         }
419     }
420 
421     class Je : IInstruction
422     {
423         string _label;
424 
425         public Je(string label)
426         {
427             _label = label;
428         }
429 
430         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
431         {
432             output = string.Empty;
433             if (comparison == 0)
434             {
435                 return labels[_label] + 1;
436             }
437             return pointer + 1;
438         }
439     }
440 
441     class Jge : IInstruction
442     {
443         string _label;
444 
445         public Jge(string label)
446         {
447             _label = label;
448         }
449 
450         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
451         {
452             output = string.Empty;
453             if (comparison >= 0)
454             {
455                 return labels[_label] + 1;
456             }
457             return pointer + 1;
458         }
459     }
460 
461     class Jg : IInstruction
462     {
463         string _label;
464 
465         public Jg(string label)
466         {
467             _label = label;
468         }
469 
470         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
471         {
472             output = string.Empty;
473             if (comparison > 0)
474             {
475                 return labels[_label] + 1;
476             }
477             return pointer + 1;
478         }
479     }
480 
481     class Jle : IInstruction
482     {
483         string _label;
484 
485         public Jle(string label)
486         {
487             _label = label;
488         }
489 
490         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
491         {
492             output = string.Empty;
493             if (comparison <= 0)
494             {
495                 return labels[_label] + 1;
496             }
497             return pointer + 1;
498         }
499     }
500 
501     class Jl : IInstruction
502     {
503         string _label;
504 
505         public Jl(string label)
506         {
507             _label = label;
508         }
509 
510         public int Execute(Dictionary<string, int> registers, Dictionary<string, int> labels, Stack<int> callStack, int pointer, ref int comparison, out string output)
511         {
512             output = string.Empty;
513             if (comparison < 0)
514             {
515                 return labels[_label] + 1;
516             }
517             return pointer + 1;
518         }
519     }
520 }

算法2与算法1的实现效果是一样的:

优点:
1. 使用了正则表达式来清理和解析输入,使得输入处理更加灵活和健壮。
2. 使用了字典来存储寄存器和标签,提高了程序的执行效率。
3. 使用了接口和多态来执行指令,使得代码更加模块化和可扩展。
4. 使用了堆栈来跟踪调用和返回地址,使得程序执行流程更加清晰。

缺点:
1. 代码相对较长,可能导致可读性较差。

总体来说,算法2相比算法1更加灵活和健壮,但也可能更加复杂。在处理复杂的指令集和程序逻辑时可能更加适用。


测试用例:

  1 using NUnit.Framework;
  2 using System;
  3 using System.Collections.Generic;
  4 using System.Linq;
  5 using System.Text;
  6 using System.Text.RegularExpressions;
  7 
  8 [TestFixture]
  9 public class AssemblerInterpreterTest
 10 {
 11 
 12     #region Basic tests
 13     [Test]
 14     public void TestBasic()
 15     {
 16         for (int i = 0; i < expected.Length; i++)
 17         {
 18             Assert.AreEqual(expected[i], AssemblerInterpreter.Interpret(displayProg(progs[i])));
 19         }
 20     }
 21 
 22     private static string[] progs = {
 23             "\n; My first program\nmov  a, 5\ninc  a\ncall function\nmsg  '(5+1)/2 = ', a    ; output message\nend\n\nfunction:\n    div  a, 2\n    ret\n",
 24             "\nmov   a, 5\nmov   b, a\nmov   c, a\ncall  proc_fact\ncall  print\nend\n\nproc_fact:\n    dec   b\n    mul   c, b\n    cmp   b, 1\n    jne   proc_fact\n    ret\n\nprint:\n    msg   a, '! = ', c ; output text\n    ret\n",
 25             "\nmov   a, 8            ; value\nmov   b, 0            ; next\nmov   c, 0            ; counter\nmov   d, 0            ; first\nmov   e, 1            ; second\ncall  proc_fib\ncall  print\nend\n\nproc_fib:\n    cmp   c, 2\n    jl    func_0\n    mov   b, d\n    add   b, e\n    mov   d, e\n    mov   e, b\n    inc   c\n    cmp   c, a\n    jle   proc_fib\n    ret\n\nfunc_0:\n    mov   b, c\n    inc   c\n    jmp   proc_fib\n\nprint:\n    msg   'Term ', a, ' of Fibonacci series is: ', b        ; output text\n    ret\n",
 26             "\nmov   a, 11           ; value1\nmov   b, 3            ; value2\ncall  mod_func\nmsg   'mod(', a, ', ', b, ') = ', d        ; output\nend\n\n; Mod function\nmod_func:\n    mov   c, a        ; temp1\n    div   c, b\n    mul   c, b\n    mov   d, a        ; temp2\n    sub   d, c\n    ret\n",
 27             "\nmov   a, 81         ; value1\nmov   b, 153        ; value2\ncall  init\ncall  proc_gcd\ncall  print\nend\n\nproc_gcd:\n    cmp   c, d\n    jne   loop\n    ret\n\nloop:\n    cmp   c, d\n    jg    a_bigger\n    jmp   b_bigger\n\na_bigger:\n    sub   c, d\n    jmp   proc_gcd\n\nb_bigger:\n    sub   d, c\n    jmp   proc_gcd\n\ninit:\n    cmp   a, 0\n    jl    a_abs\n    cmp   b, 0\n    jl    b_abs\n    mov   c, a            ; temp1\n    mov   d, b            ; temp2\n    ret\n\na_abs:\n    mul   a, -1\n    jmp   init\n\nb_abs:\n    mul   b, -1\n    jmp   init\n\nprint:\n    msg   'gcd(', a, ', ', b, ') = ', c\n    ret\n",
 28             "\ncall  func1\ncall  print\nend\n\nfunc1:\n    call  func2\n    ret\n\nfunc2:\n    ret\n\nprint:\n    msg 'This program should return null'\n",
 29             "\nmov   a, 2            ; value1\nmov   b, 10           ; value2\nmov   c, a            ; temp1\nmov   d, b            ; temp2\ncall  proc_func\ncall  print\nend\n\nproc_func:\n    cmp   d, 1\n    je    continue\n    mul   c, a\n    dec   d\n    call  proc_func\n\ncontinue:\n    ret\n\nprint:\n    msg a, '^', b, ' = ', c\n    ret\n"};
 30 
 31     private static string[] expected = {"(5+1)/2 = 3",
 32                                         "5! = 120",
 33                                         "Term 8 of Fibonacci series is: 21",
 34                                         "mod(11, 3) = 2",
 35                                         "gcd(81, 153) = 9",
 36                                         null,
 37                                         "2^10 = 1024"};
 38     #endregion
 39 
 40     #region Random tests
 41     [Test]
 42     public void RandomTests()
 43     {
 44         for (int i = 0; i < 1024; i++)
 45         {
 46             string randProg = GetRandomProg();
 47             string expected = InternalAssembler.Interpret(randProg),
 48                    actual = AssemblerInterpreter.Interpret(randProg);
 49 
 50             if (expected != null && !expected.Equals(actual)
 51              || expected == null && actual != null)
 52                 displayProg(randProg);
 53 
 54             Assert.AreEqual(expected, actual);
 55         }
 56     }
 57 
 58 
 59     private static string[] VARS = { "a", "b", "d", "t", "h", "k", "s", "m", "n", "g", "q", "e", "c", "o", "i", "u" };
 60     private static string[] JUMPS = { "jne", "je", "jge", "jg", "jle", "jl" };
 61     private static string[] OPERATIONS = { "add", "div", "sub", "mul" };
 62     private static string BASE_PROG = "\nmov {0}, {3}   ; instruction mov {0}, {3}\nmov {1}, {4}   ; instruction mov {1}, {4}\ncall func\nmsg 'Random result: ', {2}\nend\n\nfunc:\n  cmp {0}, {1}\n  {5} exit\n  mov {2}, {0}\n  {6} {2}, {1}\n  ret\n; Do nothing\nexit:\n  msg 'Do nothing'\n";
 63 
 64     private Random rand = new Random();
 65 
 66     private string GetRandomProg()
 67     {
 68         ISet<string> s = new HashSet<string>();
 69         while (s.Count != 3) s.Add(VARS[rand.Next(VARS.Length)]);
 70         List<string> vars = new List<string>(s);
 71         return string.Format(BASE_PROG, vars[0],
 72                                         vars[1],
 73                                         vars[2],
 74                                         "" + 1 + rand.Next(15),
 75                                         "" + 1 + rand.Next(15),
 76                                         JUMPS[rand.Next(JUMPS.Length)],
 77                                         OPERATIONS[rand.Next(OPERATIONS.Length)]);
 78     }
 79 
 80     private static class InternalAssembler
 81     {
 82         private static Regex TOKENIZER = new Regex(";.*|(?<cmd>('.*?'|-?\\w+))[:,]?\\s*");
 83 
 84         private static IDictionary<string, Func<int, int, bool>> CMP_FUNCS = new Dictionary<string, Func<int, int, bool>>();
 85         private static Dictionary<string, Func<int, int, int>> MATH_BI_FUNCS = new Dictionary<string, Func<int, int, int>>(),
 86                                                                MATH_MONO_FUNCS = new Dictionary<string, Func<int, int, int>>();
 87         private static ISet<string> JUMPS_CMD = new HashSet<string>(CMP_FUNCS.Keys);
 88         private static ISet<string> ALL_CMDS = new HashSet<string>(JUMPS_CMD);
 89 
 90         static InternalAssembler()
 91         {
 92             CMP_FUNCS.Add("jmp", (x, y) => true);
 93             CMP_FUNCS.Add("jne", (x, y) => x != y);
 94             CMP_FUNCS.Add("je", (x, y) => x == y);
 95             CMP_FUNCS.Add("jge", (x, y) => x >= y);
 96             CMP_FUNCS.Add("jg", (x, y) => x > y);
 97             CMP_FUNCS.Add("jle", (x, y) => x <= y);
 98             CMP_FUNCS.Add("jl", (x, y) => x < y);
 99 
100             MATH_BI_FUNCS.Add("add", (x, y) => x + y);
101             MATH_BI_FUNCS.Add("sub", (x, y) => x - y);
102             MATH_BI_FUNCS.Add("mul", (x, y) => x * y);
103             MATH_BI_FUNCS.Add("div", (x, y) => x / y);
104 
105             MATH_MONO_FUNCS.Add("inc", MATH_BI_FUNCS["add"]);
106             MATH_MONO_FUNCS.Add("dec", MATH_BI_FUNCS["sub"]);
107 
108             JUMPS_CMD.Add("call");
109             ALL_CMDS.UnionWith(new[] { "ret", "end", "mov", "cmp", "msg" });
110             ALL_CMDS.UnionWith(MATH_BI_FUNCS.Keys);
111             ALL_CMDS.UnionWith(MATH_MONO_FUNCS.Keys);
112         }
113 
114         private static int pointer;
115         private static Dictionary<string, int> registers, jumpsLbl;
116         private static Dictionary<string, bool> cmpDct;
117         private static StringBuilder output;
118         private static Stack<int> callStackP;
119         private static List<List<string>> instructions;
120 
121         public static string Interpret(string input)
122         {
123             pointer = 0;
124             registers = new Dictionary<string, int>();
125             cmpDct = new Dictionary<string, bool>();
126             output = new StringBuilder();
127             callStackP = new Stack<int>();
128 
129             TokenizeProgram(input);
130             SeekJumpLabels();
131             UpdateCmp("0", "0");
132 
133             while (0 <= pointer && pointer < instructions.Count)
134             {
135                 string cmd = instructions[pointer][0];
136 
137                 if (CMP_FUNCS.ContainsKey(cmd)) pointer = MoveTo(cmd, Label());
138                 else if (MATH_BI_FUNCS.ContainsKey(cmd)) UpdateRegs(MATH_BI_FUNCS[cmd], x(), y());
139                 else if (MATH_MONO_FUNCS.ContainsKey(cmd)) UpdateRegs(MATH_MONO_FUNCS[cmd], x(), "1");
140                 else if (cmd.Equals("mov")) registers[x()] = IsNum(y()) ? int.Parse(y()) : (registers.ContainsKey(y()) ? registers[y()] : 0);
141                 else if (cmd.Equals("cmp")) UpdateCmp(x(), y());
142                 else if (cmd.Equals("call")) { callStackP.Push(pointer); pointer = MoveTo("jmp", Label()); }
143                 else if (cmd.Equals("ret")) pointer = callStackP.Pop();
144                 else if (cmd.Equals("msg")) output.Append(FormatMessage(instructions[pointer].GetRange(1, instructions[pointer].Count - 1)));
145                 else if (cmd.Equals("end")) return output.ToString();
146 
147                 pointer++;
148             }
149             return null;
150         }
151 
152 
153         private static string Label() { return x(); }
154         private static string x() { return instructions[pointer][1]; }
155         private static string y() { return instructions[pointer][2]; }
156         private static bool IsNum(string s) { return Regex.IsMatch(s, "^-?\\d+$"); }
157         private static int MoveTo(string cmd, string label) { return cmpDct[cmd] ? jumpsLbl[label] : pointer; }
158 
159         private static void TokenizeProgram(string input)
160         {
161             instructions = new List<List<string>>();
162 
163             int last = -1;
164             foreach (string line in input.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries))
165             {
166                 last++;
167 
168                 Match mTok = TOKENIZER.Match(line);
169                 instructions.Add(new List<string>());
170 
171                 while (mTok.Success)
172                 {
173                     string tok = mTok.Groups["cmd"].Value;
174                     if (tok != null && tok.Length != 0)
175                         instructions[last].Add(mTok.Groups["cmd"].Value);
176 
177                     mTok = mTok.NextMatch();
178                 }
179 
180                 if (instructions[last].Count == 0)
181                 {
182                     instructions.RemoveAt(last);
183                     last--;
184                 }
185             }
186         }
187         
188         private static void SeekJumpLabels()
189         {
190             jumpsLbl = new Dictionary<string, int>();
191             int i = -1;
192             foreach (List<string> cmd in instructions)
193             {
194                 i++;
195                 if (!ALL_CMDS.Contains(cmd[0])) jumpsLbl[cmd[0]] = i;
196             }
197         }
198         
199         private static void UpdateCmp(string xS, string yS)
200         {
201             foreach (string f in CMP_FUNCS.Keys)
202             {
203                 int x;
204                 if (!registers.TryGetValue(xS, out x))
205                     x = 0;
206 
207                 int yop;
208                 if (!registers.TryGetValue(yS, out yop))
209                     yop = 0;
210 
211                 int y = IsNum(yS) ? int.Parse(yS) : yop;
212                 cmpDct[f] = CMP_FUNCS[f](x, y);
213             }
214         }
215 
216         private static void UpdateRegs(Func<int, int, int> op, string x, string y)
217         {
218             int yop;
219             if (!registers.TryGetValue(y, out yop))
220                 yop = 0;
221 
222             registers[x] = op(registers[x], IsNum(y) ? int.Parse(y) : yop);
223         }
224 
225         private static string FormatMessage(List<string> lst)
226         {
227             return string.Join("", lst.Select(s => registers.ContainsKey(s) ? registers[s].ToString() : (IsNum(s) ? s : s.Substring(1, s.Length - 2))));
228         }
229     }
230 
231 
232     #endregion
233 
234     private string displayProg(string p)
235     {
236         Console.WriteLine("\n----------------------\n");
237         Console.WriteLine(p);
238         return p;
239     }
240 }

测试用例中的随机测试算法RandomTests()是通过生成随机的程序代码来测试解释器的正确性。具体来说,它生成了一个基础的程序代码模板 `BASE_PROG`,并在其中随机选择了三个寄存器变量,一个操作符和一个跳转条件来随机生成一个新的程序代码。生成的程序代码包含了一些基本的指令,如mov、call、msg、cmp、ret等,以及随机选择的操作符和跳转条件。生成的程序代码会被传递给两个解释器(InternalAssembler和AssemblerInterpreter)进行解释执行,然后比较它们的输出结果是否一致。

具体来说,生成程序代码的过程如下:
1. 从预定义的变量数组 `VARS` 中随机选择三个不同的寄存器变量。
2. 从预定义的操作符数组 `OPERATIONS` 中随机选择一个操作符。
3. 从预定义的跳转条件数组 `JUMPS` 中随机选择一个跳转条件。
4. 将随机选择的寄存器变量、操作符和跳转条件填入基础程序代码模板 `BASE_PROG` 中,生成一个新的程序代码。

生成的程序代码会包含随机选择的寄存器变量、操作符和跳转条件,以及一些固定的指令,如mov、call、msg、cmp、ret等。生成的程序代码会被传递给两个解释器进行解释执行,并比较它们的输出结果是否一致。如果两个解释器的输出结果不一致,会输出生成的程序代码,以便进行调试和分析。


RamdomTests()随机测试,以确保两种不同的解释器(InternalAssembler 和 AssemblerInterpreter)期望对于相同的汇编语言程序能够产生相同的结果。

  1. 测试用例:在这段代码中,通过一个循环(for (int i = 0; i < 1024; i++)),生成了1024个随机的汇编程序(randProg)。每一个这样的程序都被用来同时测试两个解释器:InternalAssembler 和 AssemblerInterpreter
  2. 期望值与实际值的对比:对于每一个随机生成的程序,都同时得到了两个结果:一个是测试类中定义的解释器InternalAssembler的解释结果(expected),另一个是AssemblerInterpreter的解释结果(actual)。然后,这两个结果被对比。
  3. 检查结果:如果两个解释器的结果不同(expected != actual),那么会打印出这个有问题的程序(displayProg(randProg))。然后,会使用断言(Assert.AreEqual(expected, actual))来确认两个结果是否一致。如果两个结果一致,那么测试通过;否则,测试失败。

对比两个解释器结果的测试方法的必要性在于:

  • 确保正确性:通过对比两个解释器的结果,可以确认它们是否都能正确地解释汇编程序。如果有任何不一致,那么可以立即发现并调查原因,从而改进解释器的准确性。
  • 性能比较:虽然此代码没有明确地比较执行时间,但通过同时运行两个解释器并比较结果,可以在某种程度上间接地比较它们的性能。如果一个解释器总是比另一个快,那么可以考虑优化它。
  • 一致性:即使两个解释器的结果是正确的,也可能会因为实现的不同而产生微小的差异。通过对比这些差异,可以确保它们在实现上的一致性,避免潜在的混淆和错误。

总的来说,这种对比是一种有效的测试策略,可以确保解释器的正确性和一致性,同时还可以在某种程度上评估它们的性能。 

posted @ 2023-12-04 08:46  lanedm  阅读(247)  评论(0编辑  收藏  举报