【问题标题】:Design Pattern For Making An Assembler制作汇编器的设计模式
【发布时间】:2011-04-07 19:49:27
【问题描述】:

我正在制作一个 8051 汇编器。

在一切之前都是一个标记器,它读取下一个标记、设置错误标志、识别 EOF 等。
然后是编译器的主循环,它读取下一个标记并检查有效的助记符:

mnemonic= NextToken();
if (mnemonic.Error)
{
    //throw some error
}
else if (mnemonic.Text == "ADD")
{
    ...
}
else if (mnemonic.Text == "ADDC")
{
    ...
}

它继续几个案例。比这更糟糕的是每个案例中的代码,它检查有效参数然后将其转换为编译代码。现在它看起来像这样:

if (mnemonic.Text == "MOV")
{
    arg1 = NextToken();
    if (arg1.Error) { /* throw error */ break; }
    arg2 = NextToken();
    if (arg2.Error) { /* throw error */ break; }

    if (arg1.Text == "A")
    {
        if (arg2.Text == "B")
            output << 0x1234; //Example compiled code
        else if (arg2.Text == "@B")
            output << 0x5678; //Example compiled code
        else
            /* throw "Invalid parameters" */
    }
    else if (arg1.Text == "B")
    {
        if (arg2.Text == "A")
            output << 0x9ABC; //Example compiled code
        else if (arg2.Text == "@A")
            output << 0x0DEF; //Example compiled code
        else
            /* throw "Invalid parameters" */
    }
}

对于每个助记符,我必须检查有效参数,然后创建正确的编译代码。在每种情况下检查每个助记符重复的有效参数的代码非常相似。

那么有没有改进此代码的设计模式?
或者只是一种更简单的实现方式?

编辑:感谢他,我接受了 plinth 的回答。不过,如果您对此有想法,我将很乐意学习它们。谢谢大家。

【问题讨论】:

    标签: c++ design-patterns assembly compiler-construction


    【解决方案1】:

    多年来,我编写了许多汇编程序来进行手动解析,坦率地说,使用语法语言和解析器生成器可能会更好。

    原因如下 - 典型的装配线可能如下所示:

    [label:] [instruction|directive][newline]
    

    一个指令将是:

    plain-mnemonic|mnemonic-withargs
    

    一个指令将是:

    plain-directive|directive-withargs
    

    等等

    使用像Gold 这样的解析器生成器,您应该能够在几个小时内完成 8051 的语法。这种手动解析的优点是您将能够在汇编代码中包含足够复杂的表达式,例如:

    .define kMagicNumber 0xdeadbeef
    CMPA #(2 * kMagicNumber + 1)
    

    这可以是一个真正的手工完成。

    如果您想手动完成,请制作一个包含所有助记符的表格,其中还包括它们支持的各种允许的寻址模式以及每种寻址模式,每个变体将占用的字节数以及操作码它。像这样的:

    enum {
        Implied = 1, Direct = 2, Extended = 4, Indexed = 8 // etc
    } AddressingMode; 
    
    /* for a 4 char mnemonic, this struct will be 5 bytes.  A typical small processor
     * has on the order of 100 instructions, making this table come in at ~500 bytes when all
     * is said and done.
     * The time to binary search that will be, worst case 8 compares on the mnemonic.
     * I claim that I/O will take way more time than look up.
     * You will also need a table and/or a routine that given a mnemonic and addressing mode
     * will give you the actual opcode.
     */
    
    struct InstructionInfo {
        char Mnemonic[4];
        char AddessingMode;
    }
    
    /* order them by mnemonic */
    static InstructionInfo instrs[] = {
        { {'A', 'D', 'D', '\0'}, Direct|Extended|Indexed },
        { {'A', 'D', 'D', 'A'}, Direct|Extended|Indexed },
        { {'S', 'U', 'B', '\0'}, Direct|Extended|Indexed },
        { {'S', 'U', 'B', 'A'}, Direct|Extended|Indexed }
    }; /* etc */
    
    static int nInstrs = sizeof(instrs)/sizeof(InstrcutionInfo);
    
    InstructionInfo *GetInstruction(char *mnemonic) {
       /* binary search for mnemonic */
    }
    
    int InstructionSize(AddressingMode mode)
    {
        switch (mode) {
        case Inplied: return 1;
        / * etc */
        }
     }
    

    然后您将获得每条指令的列表,其中又包含所有寻址模式的列表。

    所以你的解析器变成了这样:

    char *line = ReadLine();
    int nextStart = 0;
    int labelLen;
    char *label = GetLabel(line, &labelLen, nextStart, &nextStart); // may be empty
    int mnemonicLen;
    char *mnemonic = GetMnemonic(line, &mnemonicLen, nextStart, &nextStart); // may be empty
    if (IsOpcode(mnemonic, mnemonicLen)) {
        AddressingModeInfo info = GetAddressingModeInfo(line, nextStart, &nextStart);
        if (IsValidInstruction(mnemonic, info)) {
            GenerateCode(mnemonic, info);
        }
        else throw new BadInstructionException(mnemonic, info);
    }
    else if (IsDirective()) { /* etc. */ }
    

    【讨论】:

    • 我的内存大约是 200 KiB,我很高兴你用这个替换了之前的面向对象代码!现在是一个后续(我知道......)问题:您以前使用Instruction8051 { public string Mnemonic { get; set; } public List&lt;InstructionInfo&gt; Info { get; set; } } 命令模式的方式是?有什么原因/情况我更喜欢命令模式而不是查找表?
    • 命令模式?不 - 他们俩都将围绕查找表进行设计。早期的 C# 代码只是利用了该语言对泛型集合和自动属性的支持。
    【解决方案2】:

    是的。大多数汇编程序使用描述指令的数据表:助记符、操作码、操作数形式等。

    我建议查看as 的源代码。 不过我很难找到它。 看here。 (感谢侯赛因。)

    【讨论】:

    • @wallyk:我已经想到了一个查找表,其中包含助记符、每个助记符的参数数量、它们的类型等。但是速度和内存对我来说很重要,因为它将在内存小且处理器速度慢的机器上运行。所以我认为这将是我最后的选择。
    • @Hossein:查找表可能不会比拥有多个您当前拥有的案例代码副本更多的内存。至于性能,您可能不会注意到任何差异。您的代码已经在使用字符串比较和函数调用,所以它不像您正在优化一个紧密的内部循环。记住 80/20 规则。
    • 表驱动方法经过大量尝试,适用于小内存、慢处理器实现。考虑在您的方法中设置每个比较需要多少代码。在表格驱动的方法中,只有一位代码可以进行比较,例如助记符。它在一个不比一系列文字比较慢的循环内。两者都试一试:我相信您会发现这张桌子在大多数方面都更胜一筹。
    • 如果速度和内存是您关心的问题,您绝对应该使用表格。 Apple II 中最快的组装程序之一,总速度约为 8K,在 1MHz 机器上以每分钟数千行的速度运行。你的内存限制是什么?如果您无法在 1K 以下描述整个指令集及其寻址模式,我会感到惊讶。
    • “智能数据结构和愚蠢的代码比其他方式工作得更好。” -- 大教堂和集市 (en.wikipedia.org/wiki/The_Cathedral_and_the_Bazaar)
    【解决方案3】:

    我认为您应该研究访问者模式。它可能不会使您的代码更简单,但会减少耦合并提高可重用性。 SableCC 是一个用于构建广泛使用它的编译器的 java 框架。

    【讨论】:

    • 你确定吗?我看到访客主要用于树(如 AST 中的 T)。与大型语言的编译器相反,您并不完全需要/在汇编器中拥有一棵树。
    • 不,但是您可以拥有一个 Mnemonic 类,该类可以通过访问者模式定义其输出代码。您循环助记符,将访问者传递给他们,他们返回自己的输出。我从来没有设计过汇编器,所以这只是一个猜测,呵呵。
    【解决方案4】:

    当我使用 Microcode 仿真器工具时,我将所有内容都转换为 Instruction 类的后代。来自Instruction 的是类别类,例如Arithmetic_Instruction 和Branch_Instruction。我使用 factory 模式来创建实例。

    最好的办法可能是掌握汇编语言语法规范。编写一个 lexer 以转换为标记(**请不要使用 if-elseif-else 阶梯)。然后根据语义,发出代码。

    很久以前,汇编程序至少要经过两次:第一次解析常量并形成骨架代码(包括符号表)。第二遍是生成更具体或绝对的值。

    你最近读过龙书吗?

    【讨论】:

    • 龙之书也讨论汇编器吗?我仍在尝试找到它的可下载版本或获得翻译版本,但已经成功。
    【解决方案5】:

    你看过“Command Dispatcher”模式吗?

    http://en.wikipedia.org/wiki/Command_pattern

    一般的想法是创建一个处理每条指令(命令)的对象,并创建一个将每条指令映射到处理程序类的查找表。每个命令类都有一个通用接口(例如 Command.Execute( *args )),它肯定会给你一个比当前庞大的 switch 语句更干净/更灵活的设计。

    【讨论】:

    • 由于性能问题,我宁愿远离“太多”的面向对象。
    猜你喜欢
    • 2013-02-15
    • 2023-03-08
    • 2018-09-29
    • 1970-01-01
    • 2023-03-17
    • 1970-01-01
    • 2017-08-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多