仓库源文

.. Kenneth Lee 版权所有 2022

:Authors: Kenneth Lee :Version: 0.1 :Date: 2022-05-22 :Status: Draft

设计语言和编码语言的区别


最近评审一个设计的时候写了一个意见,写好后觉得自己形容得挺好的,这里把整个概念 扩展一下。

原始的意见大致是这样的:

| XX,你这写的不是设计,这叫写代码。“pc+=2",这是写代码,“pc向后跳过一条指令的 | 长度”,这是写设计。前者是和机器说话,后者是和人说话。我们写设计是避免我们和机 | 器说话的时候,说的意思不是我们意图上希望达成的目的。所以,设计不是吧和机器说 | 的话用设计文档重新说一遍,而是要你首先从人的角度想明白整个问题,避免给机器说 | 的不是人的本来意图。

我用一个反向工程的例子来说明这个问题。前段时间,我要给GDB增加一个平台支持,所以我 分析了一下它初始化寄存器的的方案,我一开始写出来是顺着代码的逻辑来总结的,这个版本 我写成了这样:

| 算法背景:gdb中,每种不同的平台用一个gdbarch结构表示,其中tdesc_data表示它 | 的寄存器描述。gdbarch需要根据平台的类型和它调试的目标系统共同决定 | 实际被调试的系统的寄存器结构是什么样的。这个寄存器结构,就叫 | tdesc_data。 | 算法目的:生成针对gdbarch的tdesc_data | 算法原理: | 1. 生成一个空的tdesc_data | 2. 算法输入的early_data寄存器全部加入tdesc_data | 3. 遍历被调试目标提供的tdesc的寄存器定义: | 1. 全部寄存器加入一个Hash | 2. 和early_data一样的从Hash中删除 | 3. 剩下在Hash中的寄存器全部调用unknown_cb询问建议的寄存器id,移到 | tdesc_data中 | 4. 把Hash中剩下顺序编号,也加入到tdesc_data中 | 5. 删除Hash

这个其实我已经进行一定限度的抽象了,如果你对比一下原始的tdesc_use_registers()函 数,那里包含更多的细节。但其实这个仍是机器语言,这个语言背后透露的意图,我们其实 不那么清楚。所以,我做了一个变化(这里只保留原理部分):

| 算法原理: | 1. 用early_data初始化一个新的tdesc_data | 2. 遍历被调试目标提供的tdesc的寄存器定义: | 1. 和early_data一样的忽略 | 2. 回调函数能回答的先编号 | 4. 剩下的也都编号到列表的最后面

你看,这里我们大体已经知道这个意图了,让我把它彻底翻译成“人话”:

| 总结起来:就是你这个平台一定会有的寄存器,你都定义在early_data中,连上一个目标以后 | 如果目标里面有额外的寄存器,就先补充一些你认识的(你通过回调函数说你认识),剩下的 | 给你一个随意编号的id,也加入到列表中。

你看,最后这个,就是我们设计希望表达的东西。写设计其实我们首先是写的最后一个部 分呢,然后根据需要写中间的部分,但我们不写代码就擅长表达的部分。否则我不如直接 写代码。代码还能被CPU校验,而人脑并不擅长校验代码,你把代码写到设计中折磨自己的 大脑干什么呢?

不少工程师都理解不了我这里说的这个概念,他们甚至跟我分辨说,PC=PC+2这个表达更精 准。其实这确实是对的,PC=PC+2这句话确实很精准,但它不肯定是不是你的意图啊。你的 意图从来都不是未来把一个程序地址加个2啊,你的大脑是要打算把程序指针移到下一条指 令上,当你这样思考的时候,你就会多出一个校验逻辑来:在我要写作的地方,是否所有 的指令都是16位的?我应该如何加才能让指令移动到下一条指令?本指令有没有可能是跳 转?所以下一条指令是否需要从跳转目标计算出来?这些,才是人的意图。你把这个地方 定义为“精准”,它是精准说要PC+=2了,问题你的目标到底达成了吗?

保证意图和最终编码一致,才是我们进行意图建模的原因,也就是我们写设计的原因。

前段时间还有一个事情,有人写一个CPU模拟程序的时候,模拟到一个Transaction Memory 的行为。我评审到他的代码的时候,我就问,“你怎么保证a,b,c的提交是一次性的呢?”, 这位工程师告诉我:“我在会话开始的时候设置了一个标记,如果有这个标记,就会执行a, 然后我会读入内存,然后执行b,最后执行c……”,然后我问:“这样做a, b, c的提交是一次性, 不会被打断了吗?”,他说:“我不知道,我们几个的代码是这样写的。”

你看,他说的非常精准,代码先如何,再如何,在如何如何,他都说了,没有任何错误, 但我们的意图得以贯彻了吗?如果意图不能被贯彻,那我们写的代码是干什么用的呢?

这是很多新手工程师的通病,他们刚学会接受机器的语言,能让机器语言说得有条有理的 就很高兴了,都忘了自己要干什么了。这个阶段是自然规律,但一直停在这个阶段就不是 了。

这个问题其实最终是个建模能力的问题。我再举一个案例来说明这一点。

昨天我们设计了一个支持CPU系统调用的行为,有人写了一个系统调用返回的指令方案。我 要求写个设计,有工程师就写了一个指令定义,支持系统调用正常返回(返回到系统调用 下一条指令上)和重启返回(就是返回到系统调用本身的指令上)。我就问:为什么需要 重启返回呢?

他说:Linux就是这样的啊。

我就说,Linux是这样,又不是不能改。如果不能改,你也不应该设计新的指令,你本来就 在创新,要改变Linux,所以,这里关键是Linux这样做的意图是什么,你只有知道这个意 图,才会摸到Linux真正的限制,摸到什么是必须的,什么是不能改的。

于是他又进行了一个分析,这次他发现Linux在系统调用挂住用户程序后,遇到中断有两种 可能:

  1. 产生一个信号处理
  2. 从系统调用返回

如果是前者,信号处理返回以后,用户程序的上下文已经改变,只能让用户态回到系统调 用的位置上,让用户程序再来一次。

然后我质疑说:如果是这样,为什么不是信号处理回到内核,然后内核接着挂着原来的上 下文继续完成后续的工作,直到系统调用可以正常返回呢?

这个问题昨天发生的,新的分析还没有完成,我也不在这里讨论这个分析的结果。我想用 这个例子给读者说明:其实我们做设计的目的是为写代码找原因,而不是重复我们要写的 代码。

我们设计一条系统调用返回指令,不是为了设计这条指令,而是我们写操作系统的时候需 要解决用户程序和操作系统内核通讯的问题。我们的系统调用返回怎么做,不依靠系统调 用怎么做本身,而依靠我们用户程序和操作系统有什么通讯的意图。我们要决定我们这个 意图的真实语义,所以我们才写的设计文档。那些把代码在设计文档上重复一次的行为, 只是一个设计的样子,并不是设计。