在上一章的旅程中,我们已经实现了函数调用的代码生成器分派函数,但在上一章的末尾,我们留下了三个问题:

  1. 我们需要为全局变量压栈
  2. main函数需要在程序启动时被自动调用
  3. 我们需要实现一个链接器,以将所有的CALL伪指令转变为一条真正的CALL指令

所以,在这一章的旅程中,我们就将解决这三个遗留问题,为代码生成器的漫长旅途画上圆满的句号。

1. 全局变量

我们要解决的第一个问题是为全局变量压栈。首先,让我们来看看栈内存结构图中,与全局变量相关的部分:

+-------+-----+-----+-----+-----+-----+-----+  ...
| 索引值 | 0 | 1 | 2 | 3 | 4 | 5 | ...
+-------+-----+-----+-----+-----+-----+-----+ ...
| 值 | ? | 2 | ? | ? | ? | ? | ...
+-------+-----+-----+-----+-----+-----+-----+ ...
^ ^ ^ ^ ^ ^
| | | | | |
a b b[0] b[1] b[2] c

事实上,全局变量压栈的实现思路和上一章中局部变量压栈的实现思路是基本一致的,但全局变量压栈的实现要比局部变量压栈的实现简单得多,这主要归功于以下几点:

  1. 在符号表中,__GLOBAL__键所存储的信息就是所有全局变量的信息,不需要进行类似于"将形参与局部变量分离"这样的操作
  2. 全局变量也不需要进行类似于"倒序压栈"这样的操作,符号表中的变量编号就是栈中这个变量的索引值
  3. 全局变量中的数组的第一个元素在栈中的索引值是编译期已知的:一定是符号表中的变量编号加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函数的实现要比调用普通函数的实现简单的多,这主要归功于以下几点:

  1. main函数一定没有实参,故不需要进行实参压栈;此外,也就不需要"将形参与局部变量分离"这样的操作了
  2. 调用main函数后,虚拟机将直接退出,故不需要进行退栈

也就是说,调用main函数的实现完全就是调用普通函数的实现的删减版,我们只需要将调用普通函数的实现删减至以下这几个操作即可:

  1. 将局部变量倒序压栈
  2. 追加一条"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中的排列顺序就已经确定了。但我们不能忘记两个特例:

  1. 显然,"全局代码"应该出现在CS的开头
  2. main函数必须排在最后。正如上文中已经讨论过的那样:这么做的目的是为了让虚拟机在执行完main函数的最后一条指令后自动退出

也就是说,CS中指令的排列应该如下所示:

"全局代码"的第一条指令
"全局代码"的第二条指令
"全局代码"的第三条指令
...
"全局代码"的最后一条指令(一定是"CALL main")
函数A的第一条指令
函数A的第二条指令
函数A的第三条指令
...
函数A的最后一条指令
函数B的第一条指令
函数B的第二条指令
函数B的第三条指令
...
函数B的最后一条指令
...
main函数的第一条指令
main函数的第二条指令
main函数的第三条指令
...
main函数的最后一条指令

由此,我们可以在满足上述两个特例的前提下,遍历代码哈希表,并同时做两件事:

  1. 将代码哈希表中的各个代码列表合并到一个代码列表中
  2. 计算每个函数的第一条指令在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,在遍历的过程中,其同时做了两件事:

  1. 将当前遍历到的子代码列表合并至codeList
  2. 通过累加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函数的更多相关文章

  1. 20190902 On Java8 第十六章 代码校验

    第十六章 代码校验 你永远不能保证你的代码是正确的,你只能证明它是错的. 测试 测试覆盖率的幻觉 测试覆盖率,同样也称为代码覆盖率,度量代码的测试百分比.百分比越高,测试的覆盖率越大. 当分析一个未知 ...

  2. 【C++】《C++ Primer 》第十六章

    第十六章 模板与泛型编程 面向对象编程和泛型编程都能处理在编写程序时不知道类型的情况. OOP能处理类型在程序允许之前都未知的情况. 泛型编程在编译时就可以获知类型. 一.定义模板 模板:模板是泛型编 ...

  3. 《Linux命令行与shell脚本编程大全》 第十六章 学习笔记

    第十六章:创建函数 基本的脚本函数 创建函数 1.用function关键字,后面跟函数名 function name { commands } 2.函数名后面跟空圆括号,标明正在定义一个函数 name ...

  4. Gradle 1.12 翻译——第十六章. 使用文件

    有关其它已翻译的章节请关注Github上的项目:https://github.com/msdx/gradledoc/tree/1.12,或訪问:http://gradledoc.qiniudn.com ...

  5. 第十六章——处理锁、阻塞和死锁(3)——使用SQLServer Profiler侦测死锁

    原文:第十六章--处理锁.阻塞和死锁(3)--使用SQLServer Profiler侦测死锁 前言: 作为DBA,可能经常会遇到有同事或者客户反映经常发生死锁,影响了系统的使用.此时,你需要尽快侦测 ...

  6. CSS3秘笈复习:十三章&十四章&十五章&十六章&十七章

    第十三章 1.在使用浮动时,源代码的顺序非常重要.浮动元素的HTML必须处在要包围它的元素的HTML之前. 2.清楚浮动: (1).在外围div的底部添加一个清除元素:clear属性可以防止元素包围浮 ...

  7. JAVA之旅(十六)——String类,String常用方法,获取,判断,转换,替换,切割,子串,大小写转换,去除空格,比较

    JAVA之旅(十六)--String类,String常用方法,获取,判断,转换,替换,切割,子串,大小写转换,去除空格,比较 过节耽误了几天,我们继续JAVA之旅 一.String概述 String时 ...

  8. Gradle 1.12用户指南翻译——第二十六章. War 插件

    其他章节的翻译请参见: http://blog.csdn.net/column/details/gradle-translation.html 翻译项目请关注Github上的地址: https://g ...

  9. Gradle 1.12用户指南翻译——第三十六章. Sonar Runner 插件

    本文由CSDN博客万一博主翻译,其他章节的翻译请参见: http://blog.csdn.net/column/details/gradle-translation.html 翻译项目请关注Githu ...

  10. 《HTTP 权威指南》笔记:第十六章&第十七章 国际化、内容协商与转码

    <HTTP 权威指南>笔记:第十六章 国际化 客户端通过在请求报文中的 Accept-Language 首部和 Accept-Charset 首部来告知服务器:“我理解这些语言.”服务器通 ...

随机推荐

  1. 虽然说自己是个废物,但年纪轻轻就有了120w

  2. Nginx负载配置

    目录 Nginx 负载均衡笔记 1. 概述 1.1 Nginx 简介 1.2 负载均衡概述 2. 四层负载均衡(传输层) 2.1 工作原理 2.2 特点 2.3 优缺点 优点 缺点 2.4 示例场景 ...

  3. MySQL_数据库命名规范及约定

    操作规范 如无说明,建表时一律采用innodb引擎: 如无说明,数据库表编码集(utf8,utf8_bin)ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf ...

  4. Vue3 之 reactive、ref、toRef、toRefs 使用与区别,源码分析详细注释

    目录 reactive.ref.toRef.toRefs 使用与区别 reactive ref 作用及用法 toRef 作用及用法 toRefs 作用及用法 ref,toRef,toRefs 源码实现 ...

  5. oeasy教您玩转vim - 1 - # 存活下来 🥊

    存活下来 更新 apt 源,升级 vim vim 是什么 vim 是类 unix 系统上的一个文本编辑神器,在 Linux 系统环境中也被许多程序员使用,书写程序和文档. 我们本次课程将围绕 Vim ...

  6. 靶机练习: hacksudo---Thor

    靶机:hacksudo---Thor 准备工作 靶机地址: http://download.vulnhub.com/hacksudo/hacksudo---Thor.zip MD5 校验:d12168 ...

  7. STL 算法 <algorithm>,

    STL 算法部分主要由头文件 <algorithm>,<numeric>,<functional > 组成.要使用 STL 中的算法函数必须包含头文件 < a ...

  8. 写写Redis十大类型GEO(地理空间)的常用命令

    又是一个用于处理sql性能的类型,这个类型的核心思想就是把三维的地球变成二维的坐标,再将二维的坐标换成一维的点块,最后将一维的点块转化成base32编码 类型就是zset, 在shell的启动命令后加 ...

  9. 写写Redis十大类型hyperloglog(基数统计)的常用命令

    hyperloglog处理问题的关键所在和bitmap差不多,都是为了减少对sql的写操作,提高性能,用于基数统计的算法.基数就是一种数据集,用于收集去重后内容的数量.会有0.81%的误差 hyper ...

  10. 基于Drone实现CI/CD【0到1架构系列】

    CI/CD是持续性集交和持续性部署,简单来讲就是自动化构建和自动化部署.目前有很多集成方案,也有很多组装方案,只要能实现自动化构建出制品,再自动部署到生产环境就行. 目前很多源代码都集成了CI/CD功 ...