编译器实现之旅——第十六章 代码装载、链接器、全局变量与main函数
在上一章的旅程中,我们已经实现了函数调用的代码生成器分派函数,但在上一章的末尾,我们留下了三个问题:
- 我们需要为全局变量压栈
- main函数需要在程序启动时被自动调用
- 我们需要实现一个链接器,以将所有的CALL伪指令转变为一条真正的CALL指令
所以,在这一章的旅程中,我们就将解决这三个遗留问题,为代码生成器的漫长旅途画上圆满的句号。
1. 全局变量
我们要解决的第一个问题是为全局变量压栈。首先,让我们来看看栈内存结构图中,与全局变量相关的部分:
+-------+-----+-----+-----+-----+-----+-----+ ...
| 索引值 | 0 | 1 | 2 | 3 | 4 | 5 | ...
+-------+-----+-----+-----+-----+-----+-----+ ...
| 值 | ? | 2 | ? | ? | ? | ? | ...
+-------+-----+-----+-----+-----+-----+-----+ ...
^ ^ ^ ^ ^ ^
| | | | | |
a b b[0] b[1] b[2] c
事实上,全局变量压栈的实现思路和上一章中局部变量压栈的实现思路是基本一致的,但全局变量压栈的实现要比局部变量压栈的实现简单得多,这主要归功于以下几点:
- 在符号表中,__GLOBAL__键所存储的信息就是所有全局变量的信息,不需要进行类似于"将形参与局部变量分离"这样的操作
- 全局变量也不需要进行类似于"倒序压栈"这样的操作,符号表中的变量编号就是栈中这个变量的索引值
- 全局变量中的数组的第一个元素在栈中的索引值是编译期已知的:一定是符号表中的变量编号加1,并不需要借助相关的计算指令
有了上述结论作为铺垫,就让我们来看看全局变量压栈的实现吧。请看:
vector<pair<__Instruction, string>> __CodeGenerator::__generateGlobalVariableCode() const
{
vector<pair<__Instruction, string>> codeList;
for (auto &[_, infoPair]: __symbolTable.at("__GLOBAL__"))
{
// Array
if (infoPair.second)
{
// Calc the array start address (variable number + 1)
codeList.emplace_back(__Instruction::__LDC, to_string(infoPair.first + 1));
}
// Push the array start address
// (Or only a meaningless int for global scalar memeory)
codeList.emplace_back(__Instruction::__PUSH, "");
// Push array content (by array size times)
for (int _ = 0; _ < infoPair.second; _++)
{
codeList.emplace_back(__Instruction::__PUSH, "");
}
}
return codeList;
}
上述代码中,我们遍历符号表中__GLOBAL__键所对应的信息;如果当前变量的数组长度为0,则我们直接生成一条PUSH指令即可;否则,如果当前变量的数组长度不为0,则我们就将"符号表中的变量编号 + 1"装载入AX中,再执行PUSH,以将数组的第一个元素在栈中的索引值压栈;并继续压栈数组长度次。
2. main函数
本节中,我们将要为main函数的自动调用做准备。显然,main函数也是一个函数,所以调用main函数的思路与调用普通函数的思路是基本一致的,但调用main函数的实现要比调用普通函数的实现简单的多,这主要归功于以下几点:
- main函数一定没有实参,故不需要进行实参压栈;此外,也就不需要"将形参与局部变量分离"这样的操作了
- 调用main函数后,虚拟机将直接退出,故不需要进行退栈
也就是说,调用main函数的实现完全就是调用普通函数的实现的删减版,我们只需要将调用普通函数的实现删减至以下这几个操作即可:
- 将局部变量倒序压栈
- 追加一条"CALL main"伪指令
将__generateCallCode函数的实现照搬过来,然后按照上文讨论的那样进行删减,我们就得到了调用main函数的实现。请看:
vector<pair<__Instruction, string>> __CodeGenerator::__generateMainPrepareCode() const
{
/*
The "main" function is a special function, so the following code is
similar with the function: __generateCallCode
*/
vector<pair<__Instruction, string>> codeList;
vector<pair<string, pair<int, int>>> pairList(__symbolTable.at("main").size());
// ..., Local2, Local1, Local0
for (auto &mapPair: __symbolTable.at("main"))
{
pairList[pairList.size() - mapPair.second.first - 1] = mapPair;
}
// The "main" function has definitely no params
for (auto &[_, infoPair]: pairList)
{
if (infoPair.second)
{
for (int _ = 0; _ < infoPair.second; _++)
{
codeList.emplace_back(__Instruction::__PUSH, "");
}
codeList.emplace_back(__Instruction::__ADDR, to_string(infoPair.second));
codeList.emplace_back(__Instruction::__PUSH, "");
}
else
{
codeList.emplace_back(__Instruction::__PUSH, "");
}
}
// Call the "main" function automatically
codeList.emplace_back(__Instruction::__CALL, "main");
return codeList;
}
将我们刚刚实现的__generateMainPrepareCode函数与上一节实现的__generateGlobalVariableCode函数合并在一起,我们就得到了一个用于生成"全局代码"的函数:
vector<pair<__Instruction, string>> __CodeGenerator::__generateGlobalCode() const
{
auto codeList = __generateGlobalVariableCode();
auto mainPrepareCodeList = __generateMainPrepareCode();
codeList.insert(codeList.end(), mainPrepareCodeList.begin(), mainPrepareCodeList.end());
return codeList;
}
main函数确实调用起来了,但是细心的读者可能已经发现了:上文中"调用main函数后,虚拟机将直接退出"从何而来?联想到虚拟机的实现中,我们也并没有讨论任何和"main函数"有关的话题呀。也许你已经猜到接下来的故事了:如果我们将main函数生成的代码强行排布在代码生成器生成的代码列表的最后一部分,那么当虚拟机执行完最后一条main函数的代码后,其就会自动退出了。没错,我们正要这么做。请接着往下看。
3. 代码装载
不难发现,虽然还有部分遗留问题没有解决,但我们所有的代码生成器分派函数,以及用于生成"全局代码"的函数均已实现,也就是说,我们已经可以为抽象语法树中的每个函数声明节点,以及一个虚拟的"__GLOBAL__"节点生成代码了。在生成代码的同时我们不能忘记:在每个函数的末尾,也就是这个函数执行完毕的时候,我们都需要追加一条RET指令,以使得IP重新回到调用点,唯一的例外是main函数,其不需要这条指令。
首先,我们可以将每个函数的函数名及其生成的代码组织为一个哈希表,为后续操作做准备。请看:
unordered_map<string, vector<pair<__Instruction, string>>> __CodeGenerator::__createCodeMap() const
{
unordered_map<string, vector<pair<__Instruction, string>>> codeMap
{
{"__GLOBAL__", __generateGlobalCode()},
};
/*
__TokenType::__Program
|
|---- __Decl
|
|---- [__Decl]
.
.
.
*/
for (auto declPtr: __root->__subList)
{
/*
__VarDecl | __FuncDecl
*/
if (declPtr->__tokenType == __TokenType::__FuncDecl)
{
/*
__TokenType::__FuncDecl
|
|---- __Type
|
|---- __TokenType::__Id
|
|---- __ParamList | nullptr
|
|---- __LocalDecl
|
|---- __StmtList
*/
auto curFuncName = declPtr->__subList[1]->__tokenStr;
auto codeList = __generateStmtListCode(declPtr->__subList[4], curFuncName);
if (curFuncName != "main")
{
/*
The instruction "RET" perform multiple actions:
1. IP = SS.POP()
Now the SS is like:
... Local5 Local4 Local3 Param2 Param1 Param0 OldBP
2. BP = SS.POP()
Now the SS is like:
... Local5 Local4 Local3 Param2 Param1 Param0
So we still need several "POP" to pop all variables.
(See the function: __generateCallCode)
*/
codeList.emplace_back(__Instruction::__RET, "");
}
codeMap[curFuncName] = codeList;
}
}
return codeMap;
}
得到这个代码哈希表后,我们就得到了一个重要信息:每个函数所生成的代码分别有多少条。此时,如果我们还知道每个函数在CS中的排列顺序,我们就可以计算出每个函数的第一条指令在CS中的索引值了。而有了这个索引值,我们也就能够将所有的CALL伪指令转变为真正的CALL指令了。事实上,当我们生成哈希表时,每个函数在CS中的排列顺序就已经确定了。但我们不能忘记两个特例:
- 显然,"全局代码"应该出现在CS的开头
- main函数必须排在最后。正如上文中已经讨论过的那样:这么做的目的是为了让虚拟机在执行完main函数的最后一条指令后自动退出
也就是说,CS中指令的排列应该如下所示:
"全局代码"的第一条指令
"全局代码"的第二条指令
"全局代码"的第三条指令
...
"全局代码"的最后一条指令(一定是"CALL main")
函数A的第一条指令
函数A的第二条指令
函数A的第三条指令
...
函数A的最后一条指令
函数B的第一条指令
函数B的第二条指令
函数B的第三条指令
...
函数B的最后一条指令
...
main函数的第一条指令
main函数的第二条指令
main函数的第三条指令
...
main函数的最后一条指令
由此,我们可以在满足上述两个特例的前提下,遍历代码哈希表,并同时做两件事:
- 将代码哈希表中的各个代码列表合并到一个代码列表中
- 计算每个函数的第一条指令在CS中的索引值
请看:
pair<vector<pair<__Instruction, string>>, unordered_map<string, int>> __CodeGenerator::__mergeCodeMap(
const unordered_map<string, vector<pair<__Instruction, string>>> &codeMap)
{
vector<pair<__Instruction, string>> codeList;
// funcJmpMap: Function name => Function start IP
unordered_map<string, int> funcJmpMap;
// Global code must be the first part
int jmpNum = codeMap.at("__GLOBAL__").size();
codeList.insert(codeList.end(), codeMap.at("__GLOBAL__").begin(), codeMap.at("__GLOBAL__").end());
// Other functions
for (auto &[funcName, subCodeList]: codeMap)
{
if (funcName != "__GLOBAL__" && funcName != "main")
{
codeList.insert(codeList.end(), subCodeList.begin(), subCodeList.end());
funcJmpMap[funcName] = jmpNum;
jmpNum += subCodeList.size();
}
}
// The "main" function must be the last function
codeList.insert(codeList.end(), codeMap.at("main").begin(), codeMap.at("main").end());
funcJmpMap["main"] = jmpNum;
return {codeList, funcJmpMap};
}
正如上文中所讨论的那样,__mergeCodeMap以一种特殊的顺序遍历codeMap,在遍历的过程中,其同时做了两件事:
- 将当前遍历到的子代码列表合并至codeList
- 通过累加jmpNum的方式,计算每个函数的第一条指令在CS中的索引值,并以函数名作为键,将此索引值存放在funcJmpMap中
至此,我们就完成了代码装载。
4. 链接器
接下来,我们开始实现链接器。链接器的功能很简单:遍历所有生成的指令,找到并转变其中的每一条CALL伪指令至一条真正的CALL指令。说到这里,也许你已经十分明确了:我们已经有能力确定任意一条指令在CS中的索引值,这当然就包括所有的CALL指令;我们也已经得到了每个函数的第一条指令在CS中的索引值;现在,我们只需要用被调用的函数(即CALL伪指令的参数)的第一条指令在CS中的索引值,减去当前CALL伪指令在CS中的索引值,就是真正的CALL指令需要跳转的位置了。请看:
void __CodeGenerator::__translateCall(vector<pair<__Instruction, string>> &codeList, const unordered_map<string, int> &funcJmpMap)
{
// A virtual "IP"
for (int IP = 0; IP < (int)codeList.size(); IP++)
{
if (codeList[IP].first == __Instruction::__CALL)
{
codeList[IP].second = to_string(funcJmpMap.at(codeList[IP].second) - IP);
}
}
}
上述代码中,我们创建了一个虚拟的IP,以跟踪每一条指令在CS中的索引值。在遍历codeList的过程中,如果我们发现当前指令是一条CALL伪指令,我们就使用CALL伪指令后接的函数名,在funcJmpMap中查到这个函数的第一条指令在CS中的索引值,并将其与IP相减,就得到了真正的CALL指令需要的参数。
5. 将它们合并在一起
经历了漫长的旅途,我们终于为代码生成器的最终实现铺平了一切道路。现在,我们要做的便是将它们合并在一起。请看:
vector<pair<__Instruction, string>> __CodeGenerator::__generateCode() const
{
auto codeMap = __createCodeMap();
auto [codeList, funcJmpMap] = __mergeCodeMap(codeMap);
__translateCall(codeList, funcJmpMap);
return codeList;
}
至此,代码生成器,乃至整个CMM编译器的实现,就都已经全部完成了。而我们的旅程,也即将到达终点...
请看下一章:《编译器实现之旅——第十七章 终章》。
编译器实现之旅——第十六章 代码装载、链接器、全局变量与main函数的更多相关文章
- 20190902 On Java8 第十六章 代码校验
第十六章 代码校验 你永远不能保证你的代码是正确的,你只能证明它是错的. 测试 测试覆盖率的幻觉 测试覆盖率,同样也称为代码覆盖率,度量代码的测试百分比.百分比越高,测试的覆盖率越大. 当分析一个未知 ...
- 【C++】《C++ Primer 》第十六章
第十六章 模板与泛型编程 面向对象编程和泛型编程都能处理在编写程序时不知道类型的情况. OOP能处理类型在程序允许之前都未知的情况. 泛型编程在编译时就可以获知类型. 一.定义模板 模板:模板是泛型编 ...
- 《Linux命令行与shell脚本编程大全》 第十六章 学习笔记
第十六章:创建函数 基本的脚本函数 创建函数 1.用function关键字,后面跟函数名 function name { commands } 2.函数名后面跟空圆括号,标明正在定义一个函数 name ...
- Gradle 1.12 翻译——第十六章. 使用文件
有关其它已翻译的章节请关注Github上的项目:https://github.com/msdx/gradledoc/tree/1.12,或訪问:http://gradledoc.qiniudn.com ...
- 第十六章——处理锁、阻塞和死锁(3)——使用SQLServer Profiler侦测死锁
原文:第十六章--处理锁.阻塞和死锁(3)--使用SQLServer Profiler侦测死锁 前言: 作为DBA,可能经常会遇到有同事或者客户反映经常发生死锁,影响了系统的使用.此时,你需要尽快侦测 ...
- CSS3秘笈复习:十三章&十四章&十五章&十六章&十七章
第十三章 1.在使用浮动时,源代码的顺序非常重要.浮动元素的HTML必须处在要包围它的元素的HTML之前. 2.清楚浮动: (1).在外围div的底部添加一个清除元素:clear属性可以防止元素包围浮 ...
- JAVA之旅(十六)——String类,String常用方法,获取,判断,转换,替换,切割,子串,大小写转换,去除空格,比较
JAVA之旅(十六)--String类,String常用方法,获取,判断,转换,替换,切割,子串,大小写转换,去除空格,比较 过节耽误了几天,我们继续JAVA之旅 一.String概述 String时 ...
- Gradle 1.12用户指南翻译——第二十六章. War 插件
其他章节的翻译请参见: http://blog.csdn.net/column/details/gradle-translation.html 翻译项目请关注Github上的地址: https://g ...
- Gradle 1.12用户指南翻译——第三十六章. Sonar Runner 插件
本文由CSDN博客万一博主翻译,其他章节的翻译请参见: http://blog.csdn.net/column/details/gradle-translation.html 翻译项目请关注Githu ...
- 《HTTP 权威指南》笔记:第十六章&第十七章 国际化、内容协商与转码
<HTTP 权威指南>笔记:第十六章 国际化 客户端通过在请求报文中的 Accept-Language 首部和 Accept-Charset 首部来告知服务器:“我理解这些语言.”服务器通 ...
随机推荐
- “国产双系统”出炉,RK3568J非对称AMP:Linux+RTOS/裸机
"非对称AMP"双系统是什么 AMP(Asymmetric Multi-Processing),即非对称多处理架构."非对称AMP"双系统是指多个核心相对独立运 ...
- MSSQL慢查询查询与统计
查询MSSQL慢查询: SELECT TOP 20 TEXT AS 'SQL Statement',last_execution_time AS 'Last Execution Time' ,(tot ...
- ELK日志缺失问题排查-Logstash消费过慢问题
1. 背景 另外一个推荐系统的推荐请求追踪日志,通过ELK收集,方便遇到问题时,可以通过唯一标识sid来复现推荐过程 在一次上线之后,发现日志大量缺失,缺失率达90%,确认是由上线引起的,但因为当时没 ...
- const isProduction = process.env.NODE_ENV === 'production'; 作用
一. process 要理解 process.env.NODE_ENV 就必须要了解 process,process 是 node 的全局变量,并且 process 有 env 这个属性, 但是没有 ...
- GitHub 创始人资助的开源浏览器「GitHub 热点速览」
你是否注意到,现在主流的浏览器如 Chrome.Edge.Brave 和 Opera 都采用了谷歌的 Chromium 引擎?同时,谷歌每年不惜花费数十亿美元,确保其搜索引擎在 Safari 中的默认 ...
- 新一代云原生日志架构 - Loggie的设计与实践
Loggie萌芽于网易严选业务的实际需求,成长于严选与数帆的长期共建,持续发展于网易数帆与网易传媒.中国工商银行的紧密协作.广泛的生态,使得项目能够基于业务需求不断完善.成熟.目前已经开源:https ...
- WSS SSL HTTPS之间的关系
ssl: secure socket layer 安全套接层,简单来说是一种加密技术,通过它可以在通信的双方上建立一个安全的通信链路,因此数据交互的双方可以安全地通信,而不用担心数据被窃取:wss: ...
- BS架构和CS架构应用
概述 B/S结构即浏览器和服务器结构.它是随着Internet技术的兴起,对C/S结构的一种变化或者改进的结构.在这种结构下,用户工作界面是通过WWW浏览器来实现,极少部分事务逻辑在前端(Br ...
- <script> 和 <script setup> 的一些主要差别
<script setup> 是 Vue 3 中的新特性,它是一种简化和更具声明性的语法,用于编写组件的逻辑部分.相比之下,<script> 是 Vue 2 中常用的编写组件逻 ...
- MySQL之DQL
*****DQL -- 数据查询语言 查询不会修改数据库表记录! 一. 基本查询 1. 字段(列)控制 1) 查询所有列 SELECT * FROM 表名; SELECT * FROM emp ...