很早就想写一篇关于符号表的学习小结,可是迟迟不能下笔。其一是因为符号表在编译器的设计中占有举足轻重的地位【我们在学习编译原理的时候更多的是注重 principles ,而没有关心一个编译器的实现,所以符号表讲解的也比较少】,编译阶段的每 “ 遍 ” 都会和符号表打交道,本人只做过一个 Mini C 的编译器的前端部分,感觉功底不够;其二是因为我想在原来 C 语言的基础上,增加 C++ 语言符号表的一些知识,对于 C++ 的符号表至今为止我还没有在 Internet 上找到相关的专门描述,有一本书《 C++ 编程艺术》【英文《 The Art of C++ 》,侯先生将其翻译为《实战 C++ 》】中的第九章作者给出一个 Mini C++ 的例子,但是你会发现这个例子被称为 Mini C 更为合适,为什么这么说?因为在 Mini C++ 中我们不能看到它是怎么处理访问控制符,不能看到多态,不能看到继承,也不能看到虚函数等。啊,这些正是 C++ 区别于 C 的部分它都没有体现出来,为什么还叫做 Mini C++ 呢?当然如果你不了解任何一个编译器的实现方式这个例子还是值得你去实验一下,如果你没有这本书也找不到这本书的电子版,可以发邮件给我。
如果文中有任何错误的地方,拜托你一定要指点我,谢谢了!
符号表存储的内容有哪些?从编译器来看, 符号表与编译的各个阶段都有交互,符号表的内容也会在编译器的不同阶段包含不同的内容【一般来讲,在词法分析,语法分析阶段编译器都是填充符号表,在语义分析阶段更多得操作是从符号表中查询数据,当然还有删除符号表的内容】。一般来讲,符号表有内存地址和函数 / 变量的对应关系,编译时节点的各种属性(类型,作用域,分配空间大小,(函数)的参数类型)等。对符号表的具体使用方法每个编译器都不同。
目标文件中的符号表用来输出函数 / 变量符号信息,供连接时给其他模块引用。这种符号表中主要包含函数 / 变量的名称和地址对应关系,其中的地址一般是位置无关码。
【在此在推荐一本好书《 Linker & Loader 》,目前在网上还能找到中科大的一位博士翻译的中文版本,这本书比较详细的介绍了连接器和加载器的工作原理】
我们可以想象在编译器中符号表的管理使用可以分为三步:收集符号属性、根据 BNF 范式进行语法的合法性检查、在生成目标代码阶段使用。
一、 收集符号属性
编译程序扫描说明部分收集有关标识符的属性,并在符号表中建立符号的相应属性信息。例如,编译程序分析到下述两个说明语句
int iVar;;
double fArray[2];
则在符号表中收集到关于符号 iVar 的属性是一个整型变量,关于符号 fArray 的属性是具有 2 个 double 元素的一维数组。这只是一个简单说明,我们想看一下符号表具体的数据结构。但目前我还没有找到一个非常完整的描述。那么先来看一下 Java 的 Class 文件中找到一些类似存储符号表信息的方法。
借鉴 Java 的 Class 文件结构
下面的 ClassFile 结构 Class 文件的 C 语言描述【以下内容可参考《 Java 虚拟机规范 》第四章或者网上文章 《 解读 Java Class 文件格式 》 】:
struct ClassFile
{
u4 magic; // 识别 Class 文件格式,具体值为 0xCAFEBABE ,
u2 minor_version; // Class 文件格式副版本号,
u2 major_version; // Class 文件格式主版本号,
u2 constant_pool_count; // 常数表项个数,
cp_info **constant_pool; // 常数表,又称变长符号表,
u2 access_flags; // Class 的声明中使用的修饰符掩码,
u2 this_class; // 常数表索引,索引内保存类名或接口名,
u2 super_class; // 常数表索引,索引内保存父类名,
u2 interfaces_count; // 超接口个数,
u2 *interfaces; // 常数表索引,各超接口名称,
u2 fields_count; // 类的域个数,
field_info **fields; // 域数据,包括属性名称索引,域修饰符掩码等,
u2 methods_count; // 方法个数,
method_info **methods; // 方法数据,包括方法名称索引,方法修饰符掩码等,
u2 attributes_count; // 类附加属性个数,
attribute_info **attributes; // 类附加属性数据,包括源文件名等。
};
其中 u2 为 unsigned short , u4 为 unsigned long :
cp_info **constant_pool 是常量表的指针数组,指针数组个数为 constant_pool_count ,结构体 cp_info 如下:
struct cp_info
{
u1 tag; // 常数表数据类型
u1 *info; // 常数表数据
};
常数表数据类型 Tag 定义如下:
#define CONSTANT_Class 7
#define CONSTANT_Fieldref 9
#define CONSTANT_Methodref 10
#define CONSTANT_InterfaceMethodref 11
#define CONSTANT_String 8
#define CONSTANT_Integer 3
#define CONSTANT_Float 4
#define CONSTANT_Long 5
#define CONSTANT_Double 6
#define CONSTANT_NameAndType 12
#define CONSTANT_Utf8 1
每种类型对应一个结构体保存该类型数据,例如 CONSTANT_Class 的 info 指针指向的数据类型应为 CONSTANT_Class_info
struct CONSTANT_Class_info
{
u1 tag;
u2 name_index;
};
CONSTANT_Utf8 的 info 指针指向的数据类型应为 CONSTANT_Utf8_info
struct CONSTANT_Utf8_info
{
u1 tag;
u2 length;
u1 *bytes;
};
Tag 和 info 的详细说明参考《 Java 虚拟机规范》第四章 4.4 节。
access_flags 为类修饰符掩码,域与方法都有各自的修饰符掩码。
#define ACC_PUBLIC 0x0001
#define ACC_PRIVATE 0x0002
#define ACC_PROTECTED 0x0004
#define ACC_STATIC 0x0008
#define ACC_FINAL 0x0010
#define ACC_SYNCHRONIZED 0x0020
#define ACC_SUPER 0x0020
#define ACC_VOLATILE 0x0040
#define ACC_TRANSIENT 0x0080
#define ACC_NATIVE 0x0100
#define ACC_INTERFACE 0x0200
#define ACC_ABSTRACT 0x0400
#define ACC_STRICT 0x0800
field_info **fields 是类域数据的指针数组,指针数组个数为 fields_count ,结构体 field_info 定义如下:
struct field_info
{
u2 access_flags; // 域修饰符掩码
u2 name_index; // 域名在常数表内的索引
u2 descriptor_index; // 域的描述符,其值是常数表内的索引
u2 attributes_count; // 域的属性个数
attribute_info **attributes; // 域的属性数据,即域的值
};
例如一个域定义如下:
private final static byte UNSET=127;
则该域的修饰符掩码值为: ACC_PRIVATE | ACC_STATIC | ACC_FINAL=0x001A 。常数表内 name_index 索引内保存数据为 UNSET ,常数表内 descriptor_index 索引内保存的数据为 B ( B 表示 byte, 其他类型参考《 Java 虚拟机规范》第四章 4.3.2 节)。 attributes_count 的值为 1 ,其中 attributes 是指针数组。指针数组个数为 attributes_count ,在此为 1 , attribute_info 结构体如下:
struct attribute_info
{
u2 attribute_name_index; // 常数表内索引
u4 attribute_length; // 属性长度
u1 *info; // 根据属性类型不同而值不同
};
attribute_info 可以转换 (cast) 为多种类型 ConstantValue_attribute , Exceptions_attribute , LineNumberTable_attribute , LocalVariableTable_attribute , Code_attribute 等。
因为域的属性只有一种: ConstantValue_attribute ,因此此结构体转换为
struct ConstantValue_attribute
{
u2 attribute_name_index; // 常数表内索引
u4 attribute_length; // 属性长度值,永远为 2
u2 constantvalue_index; // 常数表内索引,保存域的值
// 在此例中,常数表内保存的值为 127
};
method_info **methods 是方法数据的指针数组,指针数组个数为 methods_count ,结构体 method_info 定义如下:
struct method_info
{
u2 access_flags; // 方法修饰符掩码
u2 name_index; // 方法名在常数表内的索引
u2 descriptor_index; // 方法描述符,其值是常数表内的索引
u2 attributes_count; // 方法的属性个数
attribute_info **attributes; // 方法的属性数据,
// 保存方法实现的 Bytecode 和异常处理
};
例如一个方法定义如下:
public static boolean canAccessSystemClipboard()
{
...
}
则 access_flags 的值为 ACC_PUBLIC | ACC_STATIC =0x0009 ,常数表内 name_index 索引内保存数据为 canAccessSystemClipboard ,常数表内 descriptor_index 索引内保存数据为 ()Z ; ( 括号表示方法参数, Z 表示返回值为布尔型,详细说明参照《 Java 虚拟机规范》第四章 4.3.2 节 ) 。 attribute_info **attributes 是方法的属性指针数组,个数为 attributes_count ,数组内保存的是常数表索引, info 为 Code_attribute 或 Exceptions_attribute 。
ClassFile 结构体中的 attribute_info **attributes 是附加属性数组指针,个数为 attributes_count 。
struct SourceFile_attribute
{
u2 attribute_name_index; // 常数表内索引
u4 attribute_length; // 属性长度值,永远为 2
u2 sourcefile_index; // 常数表内索引, info 保存源文件名
};
C++ 的符号表的特别之处
因为 Java 的 Class 文件是一个中间文件,在 Class 文件中保存的信息是很多的,而且和 C++ 编译器也是不完全相同,那么上面的结构中那些是我们在 C++ 编译器生成的符号表中也必须有的?我想上面的 field_info 、 method_info 、 namespace 和 const 等在 C++ 的符号表也是需要的,但是这些内容也不完全相同。
那我们先看以下几个常见的问题,加深 C++ 编译器的理解
« 符号表如何存储静态变量(包括全局变量和局部变量)和类非静态成员变量有哪些区别?
对于静态变量, C++ 编译器的处理方法和 Java 解释器处理的方法有类似的地方。那就是访问控制符 public/private/protected 和 static 一样都会写入符号表;对于非静态成员变量符号表存储的什么内容?有没有这些访问控制符?类的非静态成员在类中的表示是通过偏移量来访问,只有和对象邦定以后才能找到其真实的地址,那么在编译后的目标文件中没有为成员分配地址,访问控制符也就没有写入到目标文件。【注 1: 为什么访问控制符是在编译期间处理,而不放在运行期间处理?如果要放在运行期间处理必须完成新的连接器和加载器来保证,另外重要的是运行效率的问题】【注 2: 参考 CSDN 上一篇文章,一个类的成员变量修改了访问控制符,在另外一个文件被引用,是否必须编译修改的文件才能连接成功?《 今天面试碰到的一个以前没有想过的问题(顺便给一点分出去) 》】。
« 符号表如何处理 const 、 volatile 变量?
C++ 编译器把 Const 对象放在了符号表之中, C 语言一般是放在只读数据区。【为什么 C++ 编译器这么做?我想一个原因就是减少一些存储操作次数】。对于 volatile 变量我们应该怎么考虑?声明的 voliate 告诉编译器不能优化它呀,我们只能在符号表中增加标识位来告诉编译器自己不优化 volatile 变量。
« 符号表怎么处理虚函数、虚继承、多继承的情况?
其实对于这些处理在 Lippman 的《 Inside the C++ Object Model 》中有详细的介绍。也许你知道那些应该放在符号表中,那些是不能的。例如对于局部静态对象 static class obj; 的定义那些不需要放在符号表中?【注:局部静态变量只要求在函数执行时初始化一次】,为了保证 Obj 满足要求,需要附加标志变量,这个附加变量能放在符号表中吗?不能,原因很简单,它需要在运行时控制对象的构造和析构。
一个多继承,有虚函数的例子:
struct __mptr
{
int delta; // 多继承下第二个对象要调整的偏移量
int index; // 是否为虚函数的标志
union {
ptrtofunc faddr;
int v_offset;
};
};
这个数据应该放在哪里?程序的目标代码中还是符号表中?显然这个数据应该放在符号表之中。当然对于 C++ 编译器的不通现实,数据结构可能是不同的,但是我们知道了那些数据应该放在符号表之中啦。
C++ 编译器中对于异常是如何完成的?看看《深入探索C++对象模型》,然后再从汇编上分析,在符号表上的处理并没有很大的技巧。但是对于模板的处理,符号表就 要起很大的作用啦。目前大多数C++编译器为了减少模板带来的代码扩张基本上都会增加一次编译,准确的获取要实例化的模板对象,可惜的对此内容知之甚少, 不敢乱讲。
也许你觉得我扯了大半天,也没有告诉你一个符号表的准确结构。真的很抱歉,我也没有设计过 C++ 编译器,当然没有办法给出一个完整的结构,我主要是想探索一下怎么设计一个 C++ 编译器使用的符号表。【对于符号表的管理目前常用的是 Hash table ,可以参考相关文档设计 Hash 表以及解决冲突的办法】
二、 根据 BNF 范式进行语法的合法性检查
根据文法,可以识别代码。在此处的语法检查主要指的符号表的检查,是编译器出错处理的一部分。例如我们重复定义变量,其根本原因就是变量信息进入符号表时发现已经存在了数据项【当然这个数据项不是上面所说的 Hash 冲突】。
是否有符号表的语义检查?从我看来是没有的,语义是什么?语义指的是代码实际代表的意义,符号表仅仅是符号,没有实际的语义。
三、 在生成目标代码阶段使用
我想看过编译原理的都经常看到这样的函数 lookup(id) ,指的是在符号表中查找 id 对象。在生成目标代码的时候就是要根据程序的语义,不断地查找符号表,然后生成目标代码。
每个符号变量在目标代码生成时需要确定其在存储分配的位置(主要是相对位置)。语言程序中的符号变量由它被定义的存储类别(如在 C 、 FORTRAN 语言中)或被定义的位置(如分程序结构的位置)来确定。首先要确定其被分配的区域。例如,在 C++ 中常见的段( sections )有 .bss , .data , .text , .rdata , .init , .fini 等【可参考《 Linker & Loader 》或者自己通过反汇编查看,如通过 GCC 的 Objdump - D ExeFile 或者 Visual Studio 中的 dumpbin 就可以看到可执行文件中所有的段】。其次是根据变量出现的次序,(一般来说)决定该变量在某个区中所处的具体位置,这通常使用在该区域中相对区头的相对位置确定。而有关区域的标志及相对位置都是作为该变量的语义信息被收集在该变量的符号表属性中。
四、 后记
如果在以后有时间有机会的话能够阅读的 GCC/G++ 的源代码(不知道未来的 N 多年中真的是否有这样的机会),我一定重写这篇文章。但是现在,如果你发现我的错误,一定通知我。如果你也有一些涉及到编译器的问题( C and/or C++ Compiler ),我也非常乐意和你一起探讨。
在此恳请指点文中的错误。
为了使大家能够看到我所参考的文章,我把他们都重新的放在一起,不仅仅是一个连接。希望没有侵犯原作者的版权吆。
参考的文章:
【1】 符号表, http://www.cppblog.com/pengkuny/archive/2006/12/18/16581.html
【2】 Mini Java 编译器, http://blog.csdn.net/sandy_xu/category/106115.aspx
【3】 今天面试碰到的一个以前没有想过的问题(顺便给一点分出去), http://topic.csdn.net/t/20030227/18/1474390.html
【4】 Const 的思考, http://www.openitpower.com/wenzhang/115/12008_1.html
【5】 解读 Java Class 文件格式 , http://blog.csdn.net/tyrone1979/archive/2006/07/23/964560.aspx
推荐读物:
【1】 编译原理。个人手头有好几本,每本都有他的特点。
【2】 Linker & Loader ,网上可以找到中英文版本。
【3】 深入探索 C++ 对象模型,网上中英文版本都可以找到。
转自 http://blog.csdn.net/abortexit/archive/2007/04/24/1583306.aspx