本文是对开源小项目c4进行学习的笔记,也可以视为对其实现的内存追踪,为的是能够更加深刻的了解程序实现的具体步骤。本项目的亮点在于仅仅通过4个函数就在内存中模拟出了对程序逐字逐句的解析并转换为伪汇编代码,最后在对伪汇编进行解析,输出程序执行结果;当然简洁性的代价就是该编译器仅支持特定语义的解析,并且刚才提到了编译和执行过程都是在内存中去模拟的,并不会生成特定阶段的目标文件比如:
.i
,.s
,.out
文件,但是便于我们去阅读和学习,从中我们可以看出编译器最复杂的地方可能在于语义分析的逻辑部分了。
基本结构与函数的理解
- 全局变量
1 | char *p, *lp, // 指向source area的指针,读入的代码会以文本信息的形式读入在该区域 |
- 枚举类型
1 | // 类型、关键字、操作码标识 |
- 函数
1 | /** |
准备阶段
为了便于调试我利用c4.c
建立一个Xcode工程,并且在main
函数中给argc``argv
进行了赋值:
1 | int main(int argc, char **argv) |
在此我们以编译一下代码段为例子进行解析:
1 |
|
接下来就是为各个区域申请内存空间:
1 | int main(int argc, char **argv) |
申请完成之后内存布局是这样的:

在申请完symbol area,text area,data area,stack area这四片区域之后,程序将支持的关键字和库函数转换为特定结构写入到symbol所对应的内存区域中,方便之后解析的时候使用,对应代码:
1 | int main(int argc, char **argv) |
执行完成后symbol area内存布局情况如图:

其中Hash段和Name段是通过关键字或函数名称字符串运算得到到,拿关键字char
举例,源码这里存在一些疏漏,我们会在最后说明:
1 | Hash字段值为:(c+h+a+r)*147*64+strlen(char) |
语义解析阶段
接下来就要进行语义的解析并在text area生成响应的伪汇编代码:
1 | int main(int argc, char **argv) |
再此过程中,会为曾将解析过的函数、函数的参数以及函数作用于内声明的变量,在symbol area内生成相应的结构来对他们进行标识和记录,最后在对它们的特定字段进行清理,如图:

在text area生成的伪汇编代码后的内存布局情况如图:

汇编执行阶段
最后就是对text area生成的汇编代码进行解析和执行,代码如下:
1 | int main(int argc, char **argv) |
这个阶段利用pc读取text area生成的伪汇编代码,然后利用指向stack area的指针sp模拟执行,示意图如下:

纠错
查找支持的关键字或库函数过程中,在
next()
函数以下代码段中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20void next()
{
...
else if ((tk >= 'a' && tk <= 'z') || (tk >= 'A' && tk <= 'Z') || tk == '_') {
pp = p - 1;
while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_')
tk = tk * 147 + *p++;
tk = (tk << 6) + (p - pp);
id = sym;
while (id[Tk]) {
if (tk == id[Hash] && !memcmp((char *)id[Name], pp, p - pp)) { tk = id[Tk]; return; }
id = id + Idsz;
}
id[Name] = (int)pp;
id[Hash] = tk;
tk = id[Tk] = Id;
return;
}
...
}
1 |
|
// 这条语句在编译的时候编译器会报错
tk == id[Hash] && !memcmp((char *)id[Name], pp, p - pp)
// 正确的写法
tk == id[Hash] && !memcmp((char *)(id+Name), pp, p - pp)
1 | 当我们对Name进行存储的时候是将字符串指针强转为int型去存储的,即: |
id[Name] = (int)pp;
1 |
|
id[Name] = *(int *)pp;
1 |
|
// 如果比较的字符串长度小于4个字节,比较长度就取该长度;
// 如果比较的字符串长度大于4个字节,比较长度就取4个字节长度进行比较,因为在存储的之后我们只记录了字符串的前4个字节;
int cmp_len = (p - pp) > 4 ? 4 : (p - pp);
if (tk == id[Hash] && !memcmp((char *)(id+Name), pp, cmp_len))
{
tk = id[Tk];
return;
}
1 |
|
id[Val] = (int)(e + 1);
1 |
|