关于llvm kaleidoscope: 记一次Debug血泪之路

简而言之,慎(bu)用(yong)全局变量!                                

这次debug基本上花了我一周的时间,我基本上是晚上9点30下自习回然后调试到11点30,如此反复一周直到今天周五终于解决了,,以前都听说前辈们 说尽量不要使用全局变量,我只当个笑话顺而过,今天我可能走了前辈们的老路,我实在忍不住要告诫各位请慎用全局变量,如果不当笑话对待这点那这篇文章目的就达到了,后面可以省略了。

以下是可以被省略的正文。上学期到这学期始我林林总总写过几个编译器前端,有lexyacc自底向上自动生成的也有手写词法分析自顶向下的递归下降分析,但是还从来没做过后端,一来是感觉自己差点火候二来也太懒感觉量大繁琐。这学期开学偶然在知乎听说llvm有成熟的代码生成优化以及到到目标机器的代码生成,想来自己看了那么多theory还从来没有实践过真正的编译器,说不遗憾肯定是假的,然后我翻了一遍llvm documentation发现有个极简编译器kaleidoscope的demo,于是准备撸起袖子实现一遍,官方demo是linux下的,我用visual studio 2015实现了一遍,然后稍微把它面向对象了一番。问题就在这里的“稍微”,我只把词法分析语法分析用面向对象进行表示,其余部分用demo里的静态/全局变量/函数。直到LLVM IR代码生成都是熟悉的味道熟悉的套路,但是到了chapter4添加了一个优化器和JIT解释器就遇到了九天神坑,首先一大堆LINK ERRORs,好在都在接受范围内,编译了一大堆依赖项后编译通过了。当我输入一个表达式"1+2"就出现了nullptr异常,然后我从startup开始很自然的进入parser.parserDriver();

int main() {
	llvm::InitializeNativeTarget();
	llvm::InitializeNativeTargetAsmPrinter();
	llvm::InitializeNativeTargetAsmParser();

	kaleidoscope::Parser parser;
 
	fprintf(stderr, ">> ");
	parser.getNextToken();

	theJIT = llvm::make_unique<llvm::orc::KaleidoscopeJIT>();
	initializeModuleAndPassManager();
	
	parser.parserDriver();
	return 0;
}

进入parserDriver后大概长这个样子

/// top ::= definition | external | expression | ';'
void kaleidoscope::Parser::parserDriver() {
	while (true) {
		fprintf(stderr, ">> ");
		switch (currentToken) {
		case Token::TokenEOF:
			return;
		case ';': // ignore top-level semicolons.
			getNextToken();
			break;
		case Token::TokenDef:
			handleFunctionDefinition();
			break;
		case Token::TokenExtern:
			handleExtern();
			break;
		default:
			handleTopLevelExpression();
			break;
		}
	}
}

这里由于我输入的是"1+2"在kaleidoscope的定义里面应该属于toplevelexpression,继续跟踪handleTopLevelExpression(),

void kaleidoscope::Parser::handleTopLevelExpression() {
	if (auto funcAST = parseTopLevelExpr()) {
		if (funcAST->codegen()) {

			//omit these codes...
		}
	}
	else {
		getNextToken();
	}
}

我也不清楚到底是解析表达式错误还是代码生成,先进parserTopLevelExpr下了断点看了一下函数正常返回,排除解析错误那接下来就是代码生成

llvm::Function * FunctionAST::codegen() {
	auto & p = *(this->funcProto);
	funcPrototypeMap[this->funcProto->getFunctionName()] = std::move(this->funcProto);
	llvm::Function * theFunction = getSpecifiedFunction(p.getFunctionName());
        //omit...
}

逐语句跑了一遍发现是第三行引发的异常,断点显式getFunctionName没有问题,那肯定就是函数问题了,继续跟踪getSpecifiedFunction

static llvm::Function * getSpecifiedFunction(std::string name) {
	// First, see if the function has already been added to the current module.
	if (auto *F = theModule->getFunction(name))
		return F;

	// If not, check whether we can codegen the declaration from some existing
	// prototype.
	auto FI = funcPrototypeMap.find(name);
	if (FI != funcPrototypeMap.end())
		return FI->second->codegen();

	// If no existing prototype exists, return null.
	return nullptr;
}

逐语句发现是对llvm::Module::getFunction的问题,getFunction在module的符号表查询指定的函数如果不存在就返回null

Function *Module::getFunction(StringRef Name) const {
  return dyn_cast_or_null<Function>(getNamedValue(Name));
}

继续getNamedValue()

GlobalValue *Module::getNamedValue(StringRef Name) const {
  return cast_or_null<GlobalValue>(getValueSymbolTable().lookup(Name));
}

问题就在这里了,异常显示的this is nullptr也就是说getValueSymbolTable返回的是nullptr理所当然后面的lookup成员函数调用出错,进入getValueSymbolTable

  /// Get the symbol table of global variable and function identifiers
  const ValueSymbolTable &getValueSymbolTable() const { return *ValSymTab; }

这是Module类的一个函数,返回private  ValueSymbolTable *ValSymTab;而ValSymTab在module初始化的时候会分配内存

Module::Module(StringRef MID, LLVMContext &C)
    : Context(C), Materializer(), ModuleID(MID), SourceFileName(MID), DL("") {
  ValSymTab = new ValueSymbolTable();
  NamedMDSymTab = new StringMap<NamedMDNode *>();
  Context.addModule(this);
}

但是这里ValSymTab指向的内存却是没有分配。我想应该是堆不够的问题,我相信我的电脑,没有为什么,然后剩下的可能就是theModule变量出现了问题。纵观整个解决方案,用到static std::unique_ptr<llvm::Module> theModule;也就initializeModuleAndPassManager(),getSpecifiedFunction(std::string name),我 先是构跟踪n了itializeModuleAndPassManager(),回到main,断点显式theJIT没有问题(这里多说一句,在windows下KaleidoscopeJIT里面的if (auto Sym = CompileLayer.findSymbolIn(H, Name, true))应该改成false否则会出现问题,这也是个坑),进入initializeModuleAndPassManager()

int main() {
	llvm::InitializeNativeTarget();
	llvm::InitializeNativeTargetAsmPrinter();
	llvm::InitializeNativeTargetAsmParser();

	kaleidoscope::Parser parser;
 
	fprintf(stderr, ">> ");
	parser.getNextToken();

	theJIT = llvm::make_unique<llvm::orc::KaleidoscopeJIT>();
	initializeModuleAndPassManager();
	
	parser.parserDriver();
	return 0;
}

断点显示变量都没问题了,都正常被赋值了,wtf?难道我之前跟踪错了?诡异的事情发生了,我顺手给theModule加了个监控,虽然在这个initializeModuleAndPassManager里面theModule没有任何问题,但是进入parser.parserDriver后突然监控显示变量就出现问题了,我真是一脸懵逼,parser.parserDriver()根本没有对theModule的操作啊,为什么无缘无故变量的值会变,我都不知道看了多少遍源码,终于发现AST.h里面的theModule是按照官方demo的写法是static变量,我隐约记得全局static变量只能在文件内使用,而我在codegen的文件内直接引用了它,虽然不明白为什么会过编译但所幸发现了问题,去掉static后LINKERROR报错显示这几个变量重定义,因为多次include .h文件变量会多次定义,最后放到.cpp编译通过输入"1+2"显示"evaluate to 3.00000"莫名感动。

回想起这惨痛的debug经历深感惭愧,感觉写多了业务逻辑代码脑子里好像少了一种思考的东西,忘记了很多基础,遇到问题就无脑baidu google,稍微解决不了就换库换包, 一直顺风顺水没怎么自己努力解决过问题,没想过有些东西换上千百次都不会变。痛定思痛,文以记之。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏ACM小冰成长之路

51Nod-1837-砝码称重

ACM模版 描述 ? 题解 根据题目中的样例解释,我们完全可以大胆的猜测,次数至多不超过两次,所以一共可能是 0、1、20、1、2 次,00 次很容易想就是 n...

2319
来自专栏深度学习自然语言处理

【收藏】这些Python代码技巧,你肯定还不知道

人们还经常把 Python 笑称为「可执行伪码(executable pseudocode)」。但是,当你可以编写这样的代码时,很难去反驳这种言论:

843
来自专栏牛客网

51信用卡前端凉面

1800
来自专栏听雨堂

数据库范式

关系数据库设计之时是要遵守一定的规则的。尤其是数据库设计范式 现简单介绍1NF(第一范式),2NF(第二范式),3NF(第三范式)和BCNF,另有第四范式和第五...

2006
来自专栏牛客网

面经总结

面试记录 头条 - 一面 - 自我介绍 - 连续子数组的最大和 - 二叉树任意两个节点之间路径的最大长度 - 二叉树的深度 - 一面上个周只记得这么多了 - 二...

4037
来自专栏进击的程序猿

The Clean Architecture in PHP 读书笔记(二)

设计模式是对软件中通用问题的总结,有了设计模式,方便我们进行交流,譬如一说MVC,我们就知道是怎么回事了,不然我们必须巴拉巴拉一大堆话去描述,不易于传播、交流,...

824
来自专栏码匠的流水账

java降低竞争锁的一些方法

本文介绍一下提升并发可伸缩性的一些方式:减少锁的持有时间,降低锁的粒度,锁分段、避免热点域以及采用非独占的锁或非阻塞锁来代替独占锁。

1211
来自专栏码农分享

3.1、苏宁百万级商品爬取 思路讲解 商品爬取

https://ds.suning.cn/ds/generalForTile/000000010044087492_,000000000688241235_,0...

3032
来自专栏Kirito的技术分享

JAVA 拾遗--JPA 二三事

记得前几个月,spring4all 社区刚搞过一次技术话题讨论:如何对 JPA 或者 MyBatis 进行技术选型?传送门:http://www.spring4...

42510
来自专栏我是攻城师

Java高级软件工程师面试考纲

2964

扫码关注云+社区

领取腾讯云代金券