您的位置:首页 > 其它

Gcc 1.31 考古(二) 简单声明的解析

2014-01-27 00:00 169 查看
"走近gcc" 一文下载地址: http://bbs.chinaunix.net/thread-4096950-1-1.html

我摘录了一小段如下:

一般声明 (学习走近 gcc)
int a; int a[10]; int *a;

本文遵守简化原则. gcc 很大, 如果面面俱到的论述, 就会分散您的注意力, 所以只挑

那些有用的.

文中有时会出现生硬的言辞, 如 "在我们关心的范围内宏 AAA 为空". 按理说这话不该

出现, 因为技术应该严谨, 但如果严谨的话.... 结果是大篇幅论述枝节. 所以本着简化

原则, 简单而生硬的了结.

由于我们现在的知识还不够, 所以我们先补补基础知识.

基础知识(1) --- tree

基础知识(2) --- gcc 的词法分析

所以这些基础知识可以到上文中去学习, 我就可以少写一些了. 当然该文所针对的是 gcc 4.5,
不过仍然有些概念在考古 gcc 1.31 的时候有用...

=== 然后是我们从语法文件开始了: 一个最简单的声明概览.

在 C 语言手册, 第4章给出的 C 语言声明的形式:

C 声明:

declaration: declaration-specifier declarator

声明: 声明说明符 声明器.

例子 "int a", "int" 是声明描述符, "a" 是声明器.

specifier: 说明符, declartion-specifier: 声明说明符, 有时我也称之为声明描述符.

declarator: 声明器, 给出要声明的对象和另一部分类型信息.

例子: "int *a", 声明说明符是 "int", 声明器是 "*a".

例子: "int a[10]", 声明说明符是 "int", 声明器是 "a[10]".

例子: "int *a, b", 声明说明符是 "int", 声明器是 "*a" 和 "b". 注意不是 "*b"

(由于声明十分重要, 所以不得不多用一些例子学习了)

我们的语法学习方法, 从一个例子开始, 列出其解析时将走过的产生式, 然后详细解释

每个非终结符及其产生式的含义.

使用的方法:

1. 自顶向下列出产生式(仅包括与此例子数据相关的产生式), 编号以方便指代.

2. 自底向上地进行归约, 在过程中计算每个左侧的非终结符的语法值, 一般该值是右侧

的关联代码块计算出来的. 如果未给出该代码块, 则缺省为 $$=$1.

3. 可以在产生式添加一些调试打印的语句, 如 print("$1 is: ", $1, ", $2 is: ", $2) 等.

格式: 左边非终结符 -> 右边符号(终结符, 或非终结符)列表, 圆括号() 中是该符号的语法

值的编号. 花括号 {} 表示该位置有关联代码, {$5} 表示该代码块的语法值编号, 也用于

指代该代码块. 对于有多个产生式的非终结符, 一般选择简单的一个.

1. 数据声明/定义: (选择简单的一个)

datadef -> typed_declspecs($1) setspecs($2) initdecls($3) ';'

2. 产生式 1.$1 (选择一个)

typed_declspecs ->typespec($1) reserved_declspecs($2) {$3}

3. 产生式 2.$1

typespec -> TYPESPEC

4. 产生式 2.$2 (选择最简单的空的那个)

reserved_declspecs -> /*empty*/ {$1}

5. 产生式 1.$2

setspecs -> /*empty*/ {$1}

6. 产生式 1.$3 (选择最简的)

initdecls -> initdcl($1) {$2}

7. 产生式 6.$1 (选择简单的)

initdcl -> declarator($1) maybeasm($2) {$3}

maybeasm -> /*empty*/ 略.

8. 产生式 7.$1 (按例子选择一个)

declarator ->notype_declarator($1) {$2}

9. 产生式 8.$1 (选择最简单的)

notype_declarator -> IDENTIFIER($1)

====

可思考/回答的问题:

1. 为什么有这么多产生式? -- 因为 C 语法较复杂.

2. 为什么指定非终结符选择某个变体, 而不是别的变体? -- 按照例子选择的.

3. 有没有什么好办法研究产生式?

====

下面自底向上地一步一步归约.

例子: "int a ;"

这个例子有三个 token, 分别是 "int", "a", ';'. 它们从 lexer(词法器)解析中返回的分别是

"int" -- 返回 TYPESPEC, "a" -- 返回 IDENTIFIER, ';' -- 分号(该字符值就是其词法值).

当读入 "int", "a" 时, (可设置调试标志 yydebug=true 状态, 查看归约的具体步骤).

-- 首先归约产生式 3: --

typespec -> TYPESPEC($1)

"int" 是关键字, 词法器返回为 TYPESPEC, 则按照此产生式归约, 此产生式没有关联代码块,

因而按照缺省, 执行为 $$=$1.

其中 $1 的值按照 "c-parse.y" 文件中的声明 (大约在 line 132):

%type <ttype> TYPESPEC ...

表示终结符 TYPESPEC 的类型是 ttype, 在 %union yylval {} 中 ttype 被定义为 tree ttype.

TODO: 解释 %union, <ttype>

终结符 TYPESPEC 的语法值是来自于词法器, "int" 关键字对应的是数组 ridpointers[RID_INT]

的值, 这个值是在初始化阶段构造的一个 tree 节点(tree node). 这个节点值是一个 tree_code 为 TYPE_DECL

的树节点, 表示这是一个类型声明, 具体这个例子而言, 就是 "int" 类型. 我们将它

写作 tree:(type_decl int) 甚至就是关键字 int 以方便指代.

TODO: 解释 ridpointers[], RID_INT

TODO: 解释 初始化期间建立的 ridpointers[] "int" tree 节点的值及其含义.

TODO: 解释 tree_code, tree_node, type_decl.

产生式执行缺省动作, $$=$1, 因而非终结符 typespec 在归约之后, 值为 tree:(type_decl int),

(这个节点相当于 C 语义 typedef int int)

-- 归约产生式 4: --

reserved_declspecs -> /*empty*/ {$1}

此产生式右部为空, 关联动作为:

$$ = null.

这样, 非终结符 reserved_declspecs 的值为 null.

-- 归约产生式 2: --

typed_declspecs ->typespec($1) reserved_declspecs($2) {$3}

在归约产生式 3 中, 我们知道非终结符 typespec 即在此产生式中 $1 的值为 tree:(type_decl int),

而 reserved_declspecs $2 的值为 null, 关联代码块 $3 为代码为:

$$ = tree_cons(null, $1, $2)

值是一个 tree_node, 实际的类型是 tree_list, 简写为 ((type_decl int)), 继续简化写为 (int).

函数 tree_cons(), 从名字看, 和 Lisp 的 cons 函数相似, 用于构造一个 list. 详细的以后有机会叙述.

-- 归约产生式 5 --

setspecs -> /*empty*/ {$1}

产生这个归约的原因是 setspecs 出现在归约非终结符 initdecls 之前, setspecs 不消耗任何

终结符或非终结符(为空), 但是执行一个语义动作 $1:

current_declspecs = null;

这里设置了全局变量 current_declspecs 为 null, 该值在后面归约产生式7 的时候使用.

然后是对终结符 "a" 的归约, "a" 在词法器中被返回为终结符 IDENTIFIER, 语法值为 tree_node,

增强的类型为 tree_identifier.

问题: 解释 tree_node, tree_list, tree_identifier. (后面解释)

-- 归约产生式 9: --

notype_declarator -> IDENTIFIER($1)

IDENTIFIER 标识符的值为 tree_identifier (一种 tree_node), 我们将其简写为 (identifier_node x), 如果

不会混淆的话, 可进一步简写为 x.

根据缺省语法动作 $$=$1, 则非终结符 notype_declarator 的值为 (identifier_node x).

-- 归约产生式 8: --

declarator ->notype_declarator($1)

根据缺省语法动作 $$=$1, 非终结符 declarator 的值为 (identifier_node x).

-- 归约产生式 7: --

initdcl -> declarator($1) maybeasm($2) {$3}

根据归约产生式8, 知道 declarator $1 的值为 (identifier_node x).

这里非终结符 maybeasm 处理 gcc asm 扩展语法, 我们暂时忽略, 并且认为 maybeasm 的语法值

即 $2=null.

关联语法动作 $3 伪代码为:

1. decl = start_decl($1, current_declspecs, null)

2. finish_decl(decl, null, $2=null)

这里执行了连续两个重要的函数: start_decl(), finish_decl(), 这两个函数的组合实现对

标识符 (identifier_node x) 的声明, 声明被创建为 tree_decl(tree_node 的一种) 并返回到变量

decl 中. 这个 decl 可以写作 (var_decl (identifier_node x) (type_decl int)), 进一步可简化写为

(var_decl int x), 再进一步不混淆的话简化写为 int x, int 指出变量 x 的类型.

这样, 非终结符 initdcl 的值就是 tree_node:(var_decl x int). 可简写为 int x, 以及 x.

在 start_decl(), finish_decl() 函数中, 建立的变量声明 (var_decl) 被存储在当前词法域中, 在

后续访问这个标识符("x")时就能查找到它绑定(binding)到一个变量的声明: var_decl.

-- 归约产生式 6: --

initdecls -> initdcl($1)

没有关联的语法动作. 缺省执行 $$=$1. (在这个例子中此语法值未使用)

现在非终结符 typed_declspecs, setspecs, initdecls 都已经归约了, 当遇到 ';' 终结符的时候:

-- 归约产生式 1: --

1. datadef -> typed_declspecs($1) setspecs($2) initdecls($3) ';'

这样就达到了本次例子 "int a;" 的结束: 归约为一个数据定义 datadef. 其语法值没有使用.

实际在归约产生式7 的时候, 已经完成了变量的声明的语义部分的工作了.

====

基本知识:

tree_node: 树节点. 在 parse 阶段建构的抽象语法树(abstract syntax tree, AST)的节点类型.

方法:

1. 使用面向对象的概念描述 tree_node 的结构, 层次,继承关系.

2. 适当抽象, 忽略一些实现细节, 用伪代码访问节点的槽(slot).

3. 使用/发明一些书写方法, 能够用类似 lisp 的方式书写 tree_node 类型,值,

这样可以简化问题. 画图太麻烦, 没时间画很多图.

每个 tree_node 拥有一个 code (节点代码), 类型为 enum tree_code, 根据此 code

确定此节点的类型.

声明有一个联合 tree_node, 指向该联合的指针类型为 tree.

typedef union tree_node *tree;

struct tree_common { // 结构(类) tree_node

int uid; // 唯一标识, 自动生成, 不能改变.

tree chain; // 引用到另一个 tree_node, 一般用于构成链.

tree type; // 一般是该节点值/声明的类型对象.

enum tree_code code; // 树节点编码(种类).

bit_flags xxx_attr; // 一组位标志. 以后详细说明.

}

以 OO 观念看, 结构 tree_common 作为所有其它树节点结构的基类.

在 gcc 的实现中, 使用一组宏来访问任意种类的树节点的 tree_common 结构中的槽.

槽(slot) 指结构 (如 tree_common) 中的字段. 使用槽 (slot) 这个词, 也是因为在 gcc

代码的注释中, 也称这些字段为 slot, 同 lisp 语言中称对象的字段的名称一致.

联合 union tree_node 的声明如下:

union tree_node {

tree_common common;

tree_identifier identifier; // 后面马上谈到.

其它各类 tree_node 在此 union 中情况类似...

}

先举一个这种宏的例子, 稍后会遇到更多:

#define TREE_UID(node) ((node)->common.uid)

这里 node 是 union tree_node 的指针, 因此 (node)->common.uid 就能访问到 uid 字段.

标识符树节点结构, 该结构 OO 上看, 从 tree_common 结构派生.

struct tree_identifier extends tree_common {

string pointer; // 此标识符的字符串, 如 "abc"

int length; // 此标识符字符串的长度. 略.

tree global_value, local_value, label_value, implicit_value; // 略, 以后遇到再叙述.

}

或者另一种写法:

struct tree_identifier {

tree_common common;

string pointer; // 此标识符的字符串.

// 其它字段略.

}

假设(由词法器)读入一个标识符 "foobar", 则在编译器的符号表(symtab)中插入一个新的

tree_identifier 的对象引用. 该对象的槽 pointer 指向字符串 "foobar", length 为标识符

长度, code 为枚举 enum tree_code 的值 IDENTIFIER_NODE(意为标识符节点).

我们借鉴/使用一个文本记法: (identifier_node foobar) 来表示这种节点. 如果不混淆的

情况下, 我们直接用 foobar 这个名字表示即可.

访问 tree_identifier 中的字段, 也是通过几个宏进行的, 如:

#define IDENTIFIER_POINTER(node) ((node)->identifier.pointer)

为了方便表述语义, 我们不使用这么复杂的宏来访问节点的槽, 而是使用伪代码, 例如:

node.uid -- 表示访问 node 的 uid 这个槽(字段).

node.identifier.pointer -- 表示访问 tree_identifier 类型的 node 的 pointer 这个槽.

如果没有混淆, 就用更简单的写法: node.pointer 或 node.iden 来表示了. (总之是简化再简化)

同理, 用 node.code, node.type, node.chain 等... 访问其它槽.

结构 struct tree_int_cst extends tree_common 表示整数常量. 但我们忽略细节, 直接使用

node.int_const 来表示这个常量整数的值, 而不考虑 gcc 实现中实际的方式. (它使用两个

int32 的数 来软件实现 int64 类型的整数, 我们现在暂略去此一实现细节)

一个 tree_int_cst 的节点我们使用文本记法: (int_const 123), 123 表示此常量的值, 如果

没有歧义的情况下, 一般直接简写为 123.

结构 struct tree_list extends tree_common {

tree purpose; // (可选)这个节点的值的说明, 用途, 目的等. 很多时候为 null.

tree value; // 这个节点的值.

}

这个结构非常类似于 Lisp 的 list, list 的 car 对应这个 value 字段, cdr 对应 tree_common

中的 chain 字段. 在 gcc 中, 经常使用此结构将一组对象构成一个列表. 我们说它像 lisp

是因为程序中使用此结构的很多函数, 都有类似与 lisp 的对应函数的名字... 合理推测作者一定

是深受 lisp 影响的.

我们用 (list value next-chain-value ...) 类似于 lisp 中的记法来书写出一个 list:

例子: 设有一个列表, 里面有 tree_node 节点 (list (int_const 123) (identifier_node x) (identifier_node y))

等, 则我们还可以简写为 (list 123 x y), 或者按照 lisp 更简单的写法: '(123 x y)

例如非终结符 typed_declspecs 的语法值就是这种 tree_list 的节点, 对于如下声明:

"static const unsigned int x;"

则归约到的非终结符 typed_declspecs 的值是 '(static const unsigned int).

结构 tree_decl 用于表示一个声明, 可以是一个变量声明, 其 code=VAR_DECL; 或一个

类型声明, 其 code=TYPE_DECL, 或参数, 函数, 等多种声明. (以后遇到再研究).

struct tree_decl extends tree_common {

string filename; // 所在文件, 估计一般用于调试信息.

int linenum; // 所在文件行.

tree name; // 这个声明的名字, 指向 tree_identifier 节点.

... 其它很多 slot 暂略.

}

对一个 code=VAR_DECL 的变量声明节点来说, 最主要的(当前关心的)属性槽是变量的

名字 -- name, 和变量的类型 -- type. 例如 "int x" 这样的声明, 就产生一个

name=(identifier_node x), type=(type_decl int) 这样的 tree_decl 节点.

我们将其写做 (tree_decl type=(type_decl int) name=(identifier_node x)), 按照

我们自然的简化倾向, 以及这是一个特定的 var_decl 类型的 tree_decl, 所以把它简写作

(var_decl int x), 然后更简写做 x.

在 gcc 1.31 的这个版本, 所有声明都使用一个 tree_decl 结构存放的, 所以有点各种

字段混淆一起. 我看后面版本的 gcc 根据 code 区分了多种 tree_decl 结构, 这样似乎有专门

的 var_decl 结构了, 就不至于混淆字段了.

对几种 tree_node 先简单说明一点, 以大致能理解 parse 所生成的非终结符的语法值

就可以了.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  gcc 源码学习