Add the notion of the expression statement
直接翻译过来是“添加表达式概念语句”,没搞明白他写这个commit的作用是什么。
知识点
从他的代码修改看是为了在codegen中移除遍历Node
时每次调用gen
后再生成附加的pop
指令
一.parse部分修改
添加了一个ND_EXPR_STMT
节点类型用于在构造纯表达式语句。
1
2
3
4
5
6
static Node *stmt(void) {
//...
Node *node = new_unary(ND_EXPR_STMT, expr());
expect(";");
return node;
}
二.codegen部分修改
codegen部分除了上面提到的移除遍历Node
时每次调用gen
后再生成附加的pop
指令,其次是gen按照ND_EXPR_STMT
节点类型生成相应的指令。
1
2
3
4
5
6
7
8
switch (node->kind) {
//..
case ND_EXPR_STMT:
gen(node->lhs);
printf(" add rsp, 8\n");
return;
//...
}
这里ND_EXPR_STMT
节点为什么生成的是一条 add rsp, 8
指令。这需要搞明白在x86中 push、pop实际是cpu提供的简化指令。
push x
等价于
1
2
3
4
5
6
sub rsp,8
mov rsp,x
;这跟RISC-V中有点像
; addi sp, sp, -8
; sd x, 0(sp)
pop x
等价于
1
2
3
4
5
mov x,rsp
add rsp,8
;这跟RISC-V中有点像
; addi sp, sp, 8
; ld x,0(sp)
ND_EXPR_STMT节点生成代码的目的
再回来看看一个语句(stmt)定义:除了可能是return
语句就或者是纯表达式expr
。而return
语句生成的指令直接弹出返回值并退出
1
2
3
4
5
6
7
switch (node->kind) {
case ND_RETURN:
gen(node->lhs);
printf(" pop rax\n");
printf(" ret\n");
return;
}
除了ND_NUM
节点(这次修改如果输入测试数据是一个纯数字生成的代码就是错误的了)其他ND_ADD
、ND_SUB
、ND_MUL
、ND_DIV
等的计算结果都是存储到rax
并且在gen
函数的结尾为了方便和下一个节点计算采用栈操作(push rax
)。所以ND_EXPR_STMT
节点生成的指令add rsp, 8
说白了就是平衡栈,目的是为了抵消gen
函数中生成的最后一条push rax
。
为什么需要平衡栈呢?
先来了解下
rip/eip
rip/eip
指令指针寄存器(ip=instruction pointer),如它的名字作用也很明显保存着cpu要执行的下一条指令的内存地址.再来看看
call
指令做了什么调用
call
指令实际等价于1 2
push eip jmp x_fun_addr
最后来回忆下前一节
ret
指令做了什么ret会将返回地址弹出堆栈,并跳转到该地址。
可见这里弹出返回地址实际就是
call
指令压入的eip
.
至此解释了为什么要平衡栈。之所以这么绕是因为他的codegen至今为止还是比较简单,生成的main函数的汇编代码缺少了典型的函数汇编代码模版。比如一个函数的汇编代码通常模版是:
1
2
3
4
5
6
7
push rbp
mov rbp, rsp
sub rsp,x*8 ;局部变量空间。这两句可以用enter指令替代,但enter比较慢少有编译器用
;...
mov rsp,rbp ;恢复原来的栈.这句和下一句intel提供了leave指令
pop rbp
ret
从这个汇编模版可见:无论一个函数里面有多少本地变量,无论里面你怎么捣腾堆栈(rsp),只要在退出时恢复rsp就不会影响后续代码的执行。后续作者应该还是要对ND_EXPR_STMT
节点生成的代码做修改。