问题描述
编译之后的目标代码中是不是有一个作用域的标签来记录的?要推荐阅读材料的话,对这个问题我会推荐lcc书
《A Retargetable C Compiler》的第3章:这本书透彻的讲解了一个简单但完整的C语言编译器lcc 3.x的设计与实现,而第3章整章就是用于讲解符号表管理的。
符号表的使用贯穿lcc,一直到最后的代码生成;第16、17、18章分别讲述了对MIPS、SPARC和x86的代码生成,可以看到符号表在代码生成过程中的作用——将局部变量映射到寄存器或着调用栈上,将全局变量映射到全局地址上,等等。
在代码生成后,符号表就消失了,生成出来的代码里就不再带有局部变量的符号信息,对局部变量的“作用域”概念也就随之消失。
不过值得留意的是,虽然执行最终生成的代码并不需要对局部变量的符号表信息,但涉及链接(无论是静态还是动态链接)的地方还是会用到符号表。在目标文件里的函数层面符号表信息通常会以“导入表”(import table)和“导出表”(export table)的方式记录自己依赖于什么外部提供的实现,而自己又暴露出了哪些实现给外部使用。对于可重定位的目标文件(relocatable object file),还会有一个“重定位表”(relocation table)来记录代码中包含的需要修正的地址的偏移量。
另外,调试符号信息也是一种可选的、源自符号表的信息。这种信息对程序执行本身没什么用,但是对调试器是至关紧要的——有了它才可以在调试的时候映射回源程序的行号、变量。所以调试符号信息可能包含局部变量的符号信息(包括变量名、类型、作用域等都有可能包含在内)。
这样就完美解答了题主的问题。
lcc 4.x的代码在Github上:
drh/lcc · GitHub讲编译原理的书多半都会介绍符号表的概念,不过既然题主问的是C语言的变量作用域的实现,那我还是首推实际介绍C语言编译器的实现的书 ^_^
====================================================
然后是我自己码的回答。
题主的问题:
编译之后的目标代码中是不是有一个作用域的标签来记录的?
常见情况:不是。
- 只考虑代码执行的需求的话,C的变量作用域通常只是编译器前端里的概念,如果不考虑输出调试符号信息的需求的话,变量作用域在编译器后端是不需要的。
- 如果考虑上生成的目标代码要支持源码级调试,那么行号、变量名及其作用域等信息都得记录下来。结果就是类似DWARF、PDB这样的专门存调试符号信息的格式;很重要所以得再说一次,这些信息并不是执行代码所必须的。
简单说,编译器可以分为前端和后端两部分。
- 前端负责:词法分析 -> 语法分析 -> 语义分析 -> 中间代码生成
- 后端负责:平台无关优化 -> 指令选择 -> 平台相关优化 -> 指令调度 -> 寄存器分配 -> 目标代码生成
做优化比较多的编译器还可以从后端分出一块“中端”(middle-end)来,专门负责做优化,而让“后端”只负责做目标代码生成(可能包括寄存器分配)。
在编译器前端里,从开始一直到语义分析的阶段,会有一种叫做“符号表”(symbol table)的数据结构贯穿于编译过程中。符号表是实现变量作用域的关键——常见做法是用hashtable来实现 (变量名 -> 变量信息) 的映射关系,并且通过嵌套的符号表来实现作用域。
看个例子,一个用Java实现的C的简化版C-flat语言的编译器cbc里的作用域实现:
cbc/LocalScope.java at master · aamine/cbc · GitHub- 它用Map<String, DefinedVariable>来记录本作用域的变量名映射关系;
- 它用parent链来构成嵌套的作用域。变量名的搜索总是从当前作用域开始,找不到再到上一层作用域搜索,直到到达顶层作用域为止。
再看个简单的例子,巨紧凑的迷你C编译器c4里也有符号表:
c4/c4.c at master · EarlGray/c4 · GitHub其中的int *sym就是符号表。不过c4不支持嵌套作用域,变量只支持函数级别(local)和全局级别(global)的作用域,所以处理起来相当简单:
- 局部变量的处理:见 if (d[Class] == Loc)
- 全局变量的处理:见 else if (d[Class] == Glo)
- 还有一些特殊处理的命名空间,例如函数见 else if (d[Class] == Fun)
- …等等。
关于c4的更详细的分析,请参考另一个回答:
有哪些关于c4 - C in four function 编译器的文章? - RednaxelaFX 的回答编译器的前后端不一定要共用同一份符号表。
简单的编译器可能会选择让前后端共享同一个符号表,因为后端生成的代码的模式可能跟语言层面的概念仍然有紧密联系(例如说源码中的局部变量概念可能一直持续到了代码生成阶段);上面举的lcc、cbc、c4都是如此。
而更复杂的编译器中,后端处于优化的需求可能不会维护与前端一一对应的“变量”概念,例如说源码层面的一个变量的多次定义(赋值)可能会在后端里被拆分为多个变量,或者说后端可能根本不需要符号表,这样的话跟前端共享符号表的意义就不大了。
另一方面,如果一个编译器后端是想独立出来作为一个库,允许多种前端插在上面搭配使用的话,为了解耦也会选择不与前端共享符号表,例如Clang(前端)的符号表就不会持久到LLVM(后端)里。
如果后端根本不需要符号表的话,那作用域信息到后端就已经消失了——反正前端已经在语义分析检查过代码的正确性,而变量的定义与使用不可能超过其可见范围——作用域——所以其实后端在知道变量的定义与使用的情况下也就不需要额外的符号表信息了。
这方面请参考另一个问题的回答:
如果变量在后面的代码中不再被引用, 在生存期内, 它的寄存器可以被编译器挪为他用吗? - RednaxelaFX 的回答最开头也提到过,虽然只是为了生成可执行代码并不需要在目标代码里带有符号表,但如果要支持调试的话这种信息可能还是需要的。基本上这就是要求编译器后端要有机制能跟踪优化后的变量跟源码层面的变量的对应关系(不过此时不一定是一一对应了,可能有多个后端变量对应一个源码层面的变量),并且将该信息输出到编译生成的产物里(不一定在目标代码里,而可能在附带的外部数据结构,例如前面提到的DWARF格式或PDB格式的文件)。