【问题标题】:How does the compiler differentiate indentically-named items编译器如何区分同名项目
【发布时间】:2021-05-15 20:49:48
【问题描述】:

在以下示例中:

int main(void) {
    int a=7;
    {
        int a=8;
    }
}

生成的程序集将是这样的(来自 Compiler Explorer),无需优化:

main:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    $7, -4(%rbp)   // outer scope: int a=7
        movl    $8, -8(%rbp)   // inner scope: int a=8 
        movl    $0, %eax
        popq    %rbp
        ret

如果有重复命名的变量,编译器如何知道变量在哪里?也就是说,在内部范围内时,内存地址为%rbp-8,而在外部范围内时,地址为%rbp-4

【问题讨论】:

  • "编译器是怎么知道的" 因为语言需要它。编译器如何实现这是一个不同的问题,并且没有普遍有效的答案,因为这是每个实现都可以以自己的方式做出的选择。
  • 一个示例实现是使用链表解决冲突的哈希表。
  • 或者编译器为每个作用域创建某种标签,并保留一个变量表,其键是对(identifier, scope-tag)
  • 进入一个新的作用域,编译器可以在堆栈中添加新的符号,在创建时发现同一层的冲突,在引用时发现所有层的冲突。
  • 这就是 gcc 提供-Wshadow 编译器选项的原因——以确保您捕获任何阴影变量并避免它们,或者确认它们不会导致未指定的程序值。 (大多数编译器提供类似的选项)

标签: c assembly scope compiler-construction


【解决方案1】:

有很多方法可以实现本地范围规则。这是一个简单的例子:

  • 编译器可以保留一个嵌套范围列表,每个范围都有自己的符号定义列表。
  • 这个列表最初只有一个全局范围的元素,
  • 在解析函数定义时,会在函数参数名称的作用域列表前添加一个新的作用域元素,并将每个参数名称与该作用域元素的标识符列表中的相应信息一起添加。
  • 对于每个新块,它会在范围列表前面添加一个新的范围元素。 for ( 在其第一个子句中也为定义引入了一个新范围。
  • 离开作用域后(在块的末尾),它会从作用域列表中弹出作用域元素。
  • 在解析声明或定义时,如果对应的符号已经在当前作用域的列表中,则为局部重定义,这是禁止的(extern前向声明除外)。否则,该符号将添加到范围列表中。
  • 当它在表达式中遇到一个符号时,它会在当前作用域的符号列表中查找它,并在作用域列表中查找每个连续的作用域,直到找到它。如果找不到符号,则它是未定义的,根据最新的 C 标准,这是一个错误。否则,符号信息将用于进一步的解析和代码生成。

对类型和对象名称执行上述步骤,为structunionenum 标签维护单独的符号列表。

在所有这些发生之前,在程序翻译的一个单独阶段中执行预处理。

【讨论】:

    【解决方案2】:

    C 编程语言有一些规范,例如n1570(或更新的)。该规范在 §6.2.1 中定义了标识符的范围

    所以任何 C 编译器都应该遵循该规范。

    C 编译器如何实现该规范需要一本好书来解释。我推荐Dragon book

    一些简单或复杂的 C 编译器是open source。查看 TinyCCnwccClangGCC 的源代码以了解它们是如何实现该规范的(它们有 symbol tables,但详细信息因每个编译器而异)。

    如果有重复命名的变量,编译器如何知道变量在哪里?

    它管理符号表,并在解析块时更新它们。通常,编译器会构建一些已编译源代码的abstract syntax tree,并且该树中表示变量的叶子会引用某个符号表。 GCC 编译器记录其Generic TreeGIMPLE 数据结构并提供dump options 以输出它们。您还可以将您的foo.c 编译为gcc -S -O -fverbose-asm foo.c 并查看发出的汇编代码foo.s


    最后,你的例子可以被认为是糟糕的编程风格。一些编码指南(如MISRA-CGNU coding standards)不允许或不鼓励这样做。你的code review 进程应该捕获这样的代码(在我看来,你的例子是一个非常不可读的代码)。

    我的感觉是单字母变量的作用域应该很小——最多十几行。

    我建议查看(寻求灵感)现有 free software 项目的 C 代码(如 GNU bash 或 @987654338 @)。已注意选择易于理解的名称。

    利用现代源代码编辑器,例如 GNU emacsvim。您可以将它们配置为通过几次键盘按键来键入长标识符(它们有auto-completion;并且像GNU readline 这样的一些输入库也提供了这一点)。由于您(或您的同事)将花费更多的时间阅读源代码而不是键入它,这样的努力(命名你的变量和标识符)是值得的时间。

    如果您使用GCC 作为编译器,请将其调用为gcc -Wall -Wextra -g 以获得大量警告和调试信息。您还可以使用Frama-CClang static analyzer 等静态源代码分析工具。

    对于现实生活中的软件项目(例如GTK),你会有一个指定编码约定的文档,你可以写一些GCC plugin检查大多数其中。另请参阅DECODER 项目。

    对于您的软件项目的某些部分,您可以使用 C 代码生成器,例如 SWIGGNU bison。在某些情况下,您将拥有自己的 C 代码生成器。然后一定要生成 long C 标识符,以减少名称冲突的可能性。

    一些code obfuscation 工具正在重命名大多数C 标识符。如果您交付的 C 源代码没有 cmets 并且生成的大多数标识符(如_0TwK4TkhEG),则生成的 C 代码可以在您的客户端站点上编译,并且实际上将保持不可读。从技术上讲,您可以编写一个代码混淆器,将可读的 C 代码转换为神秘的 C 代码。

    【讨论】:

    • 问题中的代码可以作为 minimal reproducible example 的本地影子另一个本地。显然,它的目的不止于此,因为它没有做任何其他事情。我认为您不需要将一半的​​答案花在这样一个事实上,即这种阴影通常是一个坏主意和糟糕的风格。值得一提的是它是 PS,编译器对此发出警告,但没有理由假设 OP 还不知道这一点。
    • 事实上,当质疑编译器的工作原理时,“糟糕的代码”可以做出好的思想实验。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-24
    相关资源
    最近更新 更多