跳到正文
Hexarchh 的博客
返回

直到我开始写 NEMU

编辑此页

以前上计算机组成原理的时候,我一直觉得”取指、译码、执行”这些词离实际编程很远。

书上会写:

CPU 从 PC 指向的地址取出指令,经过译码后执行,再更新 PC。

看起来很简单。

直到我开始写 NEMU。

我第一次看到这样一段代码:

static void exec_once(Decode *s, vaddr_t pc) {
  s->pc = pc;
  s->snpc = pc;
  isa_exec_once(s);
  cpu.pc = s->dnpc;
}

刚开始看的时候,我其实完全不知道这几行代码为什么就能”运行一个程序”。

pc 是什么?

snpcdnpc 又是什么?

isa_exec_once() 到底干了什么?

那条 addi a0, a0, 1 到底是在什么时候变成二进制的?


后来我才慢慢意识到:

我们平时写下的一行 C 代码,在机器眼里早就已经完全消失了。

比如:

a = b + 1;

编译器可能把它变成:

addi a0, a1, 1

汇编器再把这条指令编码成一个 32 位整数。

最终 CPU 看到的东西,可能只是:

00000000000101011000010100010011

NEMU 所做的事情,就是假装自己是一颗 CPU。

它从内存中把这 32 位取出来,然后根据 opcode、funct3、funct7 等字段判断:

“哦,这是一条 addi。”

然后找到源寄存器,提取立即数,做一次加法,再把结果写入目标寄存器。

最后更新 PC。

就这样,一条指令结束。

然后下一条。

下一条。

再下一条。

直到程序结束。


这件事情最让我震撼的地方其实不是 NEMU 本身。

而是我第一次发现:

计算机并没有我以前想象中那么神秘。

一个复杂的软件栈,如果不断往下剥:

Python

→ C/C++

→ 汇编

→ ISA

→ 寄存器和内存

→ 数字逻辑

最后依然是一些非常简单的规则。

复杂性只是被一层一层包装起来了。


以前学编程的时候,我经常会有一种感觉:

“这个函数为什么能这么调用?”

“操作系统为什么能启动程序?”

“调试器为什么能看到寄存器?”

“函数调用为什么会自动返回?”

很多时候我的答案都是:

因为系统就是这么设计的。

现在我越来越不喜欢这个答案。

比如函数为什么可以返回?

因为 jal 会保存返回地址。

返回地址通常保存在 ra

函数结束时执行类似:

jalr x0, 0(ra)

PC 又回到了调用者。

所谓的:

return;

底下其实发生的是这些事情。


我现在越来越喜欢这种学习方式:

不是记住”它能工作”。

而是继续往下问:

为什么?

如果不知道,就再往下一层。

直到找到一个我能够完全理解的机制为止。

也许这也是我越来越喜欢操作系统、体系结构和 AI Infra 的原因。

这些方向都在研究同一个问题:

那些我们平时认为”理所当然”的能力,到底是怎么被构建出来的?

而我想做的事情,就是把这些黑盒,一个一个拆开。


编辑此页
分享这篇文章:

上一篇
你好,世界:这个博客开张了
下一篇
我想做的是 Systems for AI