编译1
Posted megachen
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编译1相关的知识,希望对你有一定的参考价值。
4. 编译
- 创建compiler目录, 也就是我们C语言项目的compiler模块, 该模块与使用该脚本语言的用户是最亲近的了, 因为编译就是一个桥梁, 将用户输入的文本转换为内部调用, 在VM内部, 我们通过Value统一操作对象, ObjHeader实现多态等功能, 对于Value和ObjHeader, 如果知道是什么类型的就使用具体以Obj开头的对象, 如果不知道则使用Value, 但是这些都不是用户直接操作的, 用户需要通过compiler这个模块将一些自己的输入, 如var f = 10中的f转为一个Value, 转换就需要在compiler模块中定义一些struct辅助实现
- compileUnit处理的是一个代码集合, 如一个模块, 模块中又会有一个class, 构成了嵌套结构, 注意一点, 在compiler模块中的数据结构体中需要考虑到嵌套, 所以引入了scopeDepth和enclosing开头的指向外层的指针
- 在compiler目录下创建compiler.h, 因为不在object模块中, 所有都不是对象, 没有ObjHeader
- 因为Value是与后端交互(通过指令)的, 而compiler模块与用户交互的, 所有该模块基本上就没有Value的使用
compiler结构体"们"
- compiler的核心业务就是生成指令然后交给VM执行, 在生成指令的时候, 同时我们也要存储数据, 最常见的数据就是符号表了, 有常量表, 局部变量表, 显然易见, 单纯有指令没有数据是不行的
- 下面是.h文件中结构的定义
/* 定义一堆与编译有关的数据结构, 这些基本是与用户很接近 */
// 这些自己规定
#define MAX_ID_LEN ...
#define MAX_METHOD_NAME_LEN ...
#define MAX_SIGN_LEN ...
#define MAX_ARGS ...
#define MAX_LOCAL_VARS ...
#define MAX_UPVALUES ...
#define MAX_FILED_NUM ...
// 注意: 要与object模块中的obj_fn.h文件中定义的ObjUpvalue区别开来
typedef struct upvalue {
// 是否应用了外层函数的局部变量
bool is_enclosing_local_var;
// 如果应用了, 则应用的是外层栈中的哪一个
uint32_t index;
} Upvalue;
// 在模块中用不到:-)
typedef struct LocalVar {
const char *name;
uint32_t length;
int scope_depth; // 作用域的深度(这应该很好理解)
bool is_upvalue; // 是否是对于内层函数的闭包
} LocalVar;
typedef enum {
SignatureConstruct,
SignatureMethod,
SignatureSetter,
SignatureGetter,
SignatureSubScriptGetter,
SignatureSubScriptSetter
} SignatureType;
typedef struct Signature {
SignatureType type;
const char *name;
uint32_t length;
uint32_t arg_num;
} Signature;
typedef struct Loop{
int cond_start_index;
int body_start_index;
int exit_index_index;
int scope_depth_index;
struct Loop *enclosing_loop; // 外层循环
} Loop;
typedef struct {
ObjString *name;
SymbolTable fields;
bool is_static; // 当前编译的是否是静态方法
IntBuffer instantmethods; // 实例方法索引
IntBuffer staticmethod; // 静态方法索引
Signature *signature; // 当前编译类中, 编译的是哪个方法签名
} ClassBookKeep;
typedef struct compileUnit CompileUnit;
// compiler提供的接口, 用于定义模块变量, 包括class的定义, 模块全局变量的定义
int defineModuleVar(VM *vm, ObjModule *objModule, const char *name, uint32_t length, Value value);
- 在compiler中定义编译模块的核心数据结构, compileUnit
struct compileUnit{
ObjFunc *obj_func;
// 这里的localVars存储的不仅仅指当前与的, 还有子域的, 所以128个是有一点小哈, 但是无所谓了
LocalVar localVars[MAX_VARS];
uint32_t localVarNum; // 指向最新的可用的LocalVar, 和ObjThread中的Frame使用是一样的
Upvalue upvalues[MAX_UPVALUES]; // 保存upvalue
ClassBookKeep *classBK; // 可以编译的是类
uint32_t max_slot_size
Loop *loop; // 可能会有循环
int scope_depth; // 考虑到了嵌套
struct compileUnit *enclosingCompileUnit; // 考虑到了嵌套, 指向外层编译单元
Parser *cur_parser; // 为什么, 加入import一个模块则需要创建一个新的parser, 所以需要cur_parser指定到底是哪一个parser
}
// 在调用init函数初始化compileUnit结构体的时候, 默认应该是一个对于模块的compileUnit, 所以在init函数初始化的逻辑应该是这样的, ObjFn需要在init中new一个
// !!!!!!!!!!!!!注意, CompileUnit针对的是module, function和method等有业务逻辑的, class的定义不是, 因为它只能声明有哪些属性和那些方法, 所有我们需要考虑isMethod
void initCompileUnit(CompileUnit *cu, CompileUnit *enclosing, Parser *parser, bool isMethod) {
cu->obj_func = newObjFunc();
cu->classBK = NULL;
cu->loop = NULL;
// vm切换parser
parser->cur_cu = cu;
cu->cur_parser = parser;
// 表示当前初始化的CU就是一个模块编译单元, 它没有外层的CU了
if (enclosing == NULL) {
cu->scope_depth = -1;
cu->enclosingCompileUnit = NULL;
} else {
// 如果是方法, 则要考虑到this对象
if (cu->isMethod) {
cu->localVars[0].name = "this";
cu->localVars[0].length = 4;
} else {
// 为了和isMethod==true的情况一致
cu->localVars[0].name = NULL;
cu->localVars[0].length = 0;
}
cu->localVarNum = 1;
cu->scope_depth = 0;
cu->localVars[0].is_upvalue = false;
}
}
// 定义一个更加底层的函数, compileProgram函数专门用于编译CompileUnit, 所有需要一个CompileUnit指针参数
static void compileProgram(CompileUnit *cu);
// compiler模块还需要定义写入指令的函数
// 我们规定操作码占用一个字节, 操作数可以是1个字节也可能是2个字节, 这要看具体的操作码, 操作码的定义可以参考Python和Intel CPU
// 写入一个字节则返回index, 写入两个字节则不用返回
static uint32_t writeOneByte(VM *vm, CompileUnit *cu, code);
static void writeTwoBytes(VM *vm, CompileUnit, code);
...
- 实现defineModuleVar函数, 这里只提一下思路
我们知道在ObjModule中有moduleVarName和moduleVarValue, 它维护这name->value的映射, 我们在定义模块变量(注意是模块变量)的时候, 需要先判断是否模块已经存在在moduleVarName中, 如果没有存在则将其添加到moduleVarName中, 对应的Value也添加到moduleVarValue中, 在编译阶段我们传入的Value因为为NULL类型的; 如果已经存在了, 我们还需要判断moduleVarValue中的那个name对应的value的类型是不是NUM类型, 如果是则表示之前没有声明就应用了, 这在C/C++中是错误, 但是在Java和Python中是可以弥补的, 他们支持先应用, 接着在后面的位置查找声明, 我们也和Java和Python一样, 在第一种情况中, 我们没有考虑到一个变量引用与声明的关系, 所以就算是前向引用了我们也直接加入到了moduleVarName和moduleVarValue中, 在后者我们也是为了解决第一种情况的偷懒, 在调用defineModuleVar的时候, 如果是前向应用则传入的Value是NUM类型的, num是行号, 在第二种情况下, 如果判断是Value的type是NUM则表示之前前向引用了, 在这里我们继续为其赋新的Value值。当所有的变量都添加到moduelVarName和moduleVarValue中之后, 我们好在在编译一遍, 判断是否还有Value的类型为NUM的, 如果有则表示前向应用了, 但是没有声明, 接着报错就可以了
compiler的核心功能实现
- 怕忘了在这里写一下, 在生成指令的时候, 一定要将要指令要操作的东西存起来, 接着通过index或者其他方式在VM执行的通过index之类的可以访问到
- 在编译中我们要解决语法分析与语义分析, 采用TDOP套路
- 下面是compiler.c文件中的函数实现, 通过下面的函数了解编译原理
// 根据TDOP, 为所有的运算符定义权重Binding Power, 与优先级差不多
// 优先级从低到高
typedef enum {
BP_NONE,
BP_LOWEST,
BP_ASSIGN,
BP_CONDITION,
BP_LOGIC_OR,
BP_LOGIC_AND,
BP_EQUAL,
BP_IS,
BP_CMP,
BP_BIT_OR,
BP_BIT_AND,
BP_BIT_SHIFT,
BP_RANGE,
BP_TERM,
BP_FACTOR,
BP_UNARY,
BP_CALL,
BP_HIGHTEST
} BindingPower;
// 根据TDOP, 需要定义一个结构体, 该结构体为符号结构体, 需要有如下属性
typedef struct {
const char *id; // 符号名, 如+, -, /
BindingPower lbp; // 对左操作数的吸引力
// 下面的函数
// 除非这个符号既可以是前置又可以是中置, 否则nud和led其中一个为NULL
// 在SymbolBindRule对象中调用nud或者led方法在该方法的最后一定要调用函数生成指令
DenotationFn nud; // 如果该符号不关心左操作数则调用nud, 如常量, 字面量, 前置符号
DenotationFn led; // 如果该符号关心左操作数则调用led, 如*, /, %等
MethodSignatureFn methodSign;// 如果符号在一个类中, 则该符号被视为一个方法, 根据Signature生成方法签名
} SymbolBindRule;
// TDOP核心函数expression, 讨论来啦, Mark了, 此时curToken一定是一个操作符
/*
大致思路:
1. 获取当前curToken
2. 获取下一个nextToken
3. 调用curToken.nud方法, left接受返回值
4. 在while条件中比较nextToken.lbp与参数rbp
5. t = nextToken
6. 调用getNextToken获取nextNextToken
7. left = t.led(left)
8. 在函数的最后返回left
*/
static void expression(CompileUnit *cu, BindPower rbp) {
DenotationFn nud = Rules[cu->cur_parser->curToken.type].nud;
getNextToken(cu->parser);
nud(cu);
while (rbp < Rules[cu->cur_parser->curToken.type].lbp) {
DenotationFn led = Rules[cu->cur_parser->curToken.type].led;
getNextToken(cu->Parser);
led(cu);
}
}
// 现在定义SymbolBindRule中的led与nud的逻辑
// 中置运算符nud为NULL, led需要定义, 因为中置运算符关心左操作数, 我们命名为infixOperator, 这里的中置运算符灵感来源于C++的运算符重载, 我们在该脚本语言中将所有的中置运算符都当做函数调用, 如1+1 -> 1.+(1)
static void infixOperator(CompileUnit *cu) {
// 获取preToken对应的SymbolBindRule
// 将其lbp作为rbp, 因为中置运算符中lbp==rbp
// 紧接着调用expression(cu, rbp)构成递归
// 本来到这里就已经完了的, 但是我们这是面向对象的语言, 类中的方法也是中置运算符, 所以需要在调用了expression之后生成方法标签, 在生成方法标签函数中***将方法签名生成对应的指令***
SymbolBindRule *rule = Rules[cu->cur_parser->preToken.type];
BindPower rbp = rule->lbp;
expression(cu, rbp);
// ***参数为1, 因为1+2 -> 1.+(2), +方法参数就是2一个***
Signature *sign = {MethodType, , 参数为1};
emitCallSignature(...);
}
// 现在为前置运算符定义nud函数, 它的表现形式与led一样, 和C++的运算符重载一样, 如!3->3.!, 只不过没有参数罢了
/*
获取preToken对应的SymbolBindRule
调用expression
生成方法签名指令, 但是又因为其没有参数, 所以方法签名就是方法名
*/
static void unaryOperator(CompileUnit *cu) {
SymbolBindRule *rule = Rules[cu->cur_parser->preToken.type];
// BP_UNARY是前置运算符的BP
expression(cu, BP_UNARY);
// 生成调用方法的指令, 在emitCall函数中生成指令, 其实按理说也可以使用上面个提到的emitCallSignature函数的, 只是现在这个unaryOperator有一点特别, 它的方法名就是方法签名
emitCall(...);
}
// 字面量和常量的nud为literal, 直接将生成常量的指令
static void literal(CompileUnit *cu, Value constant) {
emitLoadConstant(cu, constant);
}
// ====================================================
// 刚才提到了emitCall和emitCallSignature函数, 现在使用伪代码实现以下
static void emitCallSinagure() {
method_sign_string = sign2string
if method_sign_string not in vm->allMethodNames:
add
// need argNum to choose callX(call0?, call1? and so on)
writeOpCodeOperand(method_sign_string, argNum);
// 注意点, 现在还没有完呢, 考虑到继承, 这里还有写入一个空的slot, 将来绑定到基类上
writeOperand();
}
static void emitCall() {
// 思路和emitCallSignature一样就是不用sign2string了
}
// 关于sign2string参考
// 将Signature转换为字符串, 也就是方法签名, 参数使用_占位
static uint32_t sign2String(Signature *sign, char *buf) {
uint32_t pos = 0;
// 复制方法名xxx
memcpy(buf + pos, sign->name, sign->length);
pos += sign->length;
switch(sign->type) {
// xxx, 无参数, 上面的memcpy已经完成
case SIGN_GETTER:
break;
// xxx=(_), 和C++中的操作符重载非常的类似
case SIGN_SETTER:
buf[pos++] = ‘=‘;
buf[pos++] = ‘(‘;
buf[pos++] = ‘_‘;
buf[pos++] = ‘)‘;
break;
// xxx(_, ...)
case SIGN_CONSTRUCT:
case SIGN_METHOD: {
buf[pos++] = ‘(‘;
uint32_t idx = 0;
while (idx < sign->argNum) {
buf[pos++] = ‘_‘;
buf[pos++] = ‘,‘;
++idx;
}
if (idx == 0) {
buf[pos++] = ‘)‘;
} else {
buf[pos - 1] = ‘)‘;
}
}
break;
// xxx[_,...]
case SIGN_SUBSCRIPT:{
buf[pos++] = ‘[‘;
uint32_t idx = 0;
while (idx < sign->argNum) {
buf[pos++] = ‘_‘;
buf[pos++] = ‘,‘;
++idx;
}
if (idx == 0) {
buf[pos++] = ‘]‘;
} else {
buf[pos - 1] = ‘]‘;
}
}
break;
case SIGN_SUBSCRIPT_SETTER: {
buf[pos++] == ‘[‘;
uint32_t idx = 0;
while (idx < sign->argNum - 1) {
buf[pos++] = ‘_‘;
buf[pos++] = ‘,‘;
++idx;
}
if (idx == 0) {
buf[pos++] = ‘]‘;
} else {
buf[pos - 1] = ‘]‘;
}
buf[pos++] = ‘=‘;
buf[pos++] = ‘(‘;
buf[pos++] = ‘_‘;
buf[pos++] = ‘)‘;
}
break;
}
buf[pos] = ‘ ‘;
return pos;
}
// 定义emitLoadConstant函数用来加载常量的指令
static void emitLoadConstant() {
// 既然要加载常量, 那就应该先把源码中解析到的常量保存起来, 返回一个index, 这样再生成指令, 该指令表示加载常量, 参数就是常量在常量表中的index
index = addConstant()
write(constant, index);
}
- 关于各个符号的nud与led
1. 字面量: nud函数literal将id添加到表中, 生成loadConstant指令
2. 前置运算符: nud函数为单元函数(unaryOperator), 在该函数中会生成调用方法的指令
3. 中置运算符: led函数为infixOperator, 该函数中生成调用方法签名的指令
- 接口程序
ObjFn *compileModule(VM *vm, ObjModule *objModule, const char *sourceCode) {
// 注意, 一个模块一个parser, 所以这里需要创建一个的新的parser
Parser parser;
parser.parent = vm->cur_parser;
vm->cur_parser = parser;
// 初始化parser
// 要编译就要有CompileUnit, 让cu来编译程序
CompileUnit cu;
initCompileUnit(&cu);
if (!matchNextToken(parser, TOKEN_EOF)) {
compileProgram(cu); // 在compile
}
}
以上是关于编译1的主要内容,如果未能解决你的问题,请参考以下文章
Android 逆向Android 逆向通用工具开发 ( Android 平台运行的 cmd 程序类型 | Android 平台运行的 cmd 程序编译选项 | 编译 cmd 可执行程序 )(代码片段
错误记录Android Studio 编译报错 ( Could not determine java version from ‘11.0.8‘. | Android Studio 降级 )(代码片段
错误记录Android Studio 编译报错 ( Could not determine java version from ‘11.0.8‘. | Android Studio 降级 )(代码片段