⚙️ 第八章 从源文件到可执行文件
📄 一句话概括

本机代码看着是一堆字节,按 CPU 的规则读出来就是一条条指令;指令是变长的,只能从前往后读。

📝 本节和前面几节的关系

8.1 说清了 CPU 只认本机代码,这一节看看它到底是什么样子。

一、字节与指令

本机代码就是字节。但同一个字节,按 CPU 的规则读出来就是一条指令。 比如 B8 05 00 00 00 读作把 5 放进 eax 寄存器,C3 读作返回。 所以【指令】不是天上掉下来的东西——它只是对某些字节的约定读法。 这也解释了 8.1 那句话:换一种 CPU,约定变了,同一串字节的意思就全变了。

二、指令是变长的

注意上面两条指令:一条占 5 个字节,一条只占 1 个字节。 这就是为什么 CPU 只能从前往后一条条读,不能随便跳到中间。 因为跳到中间时,CPU 不知道自己是从指令的开头读起,还是从半截读起。 所以【程序的入口地址】必须是一个指令边界——这一点在后面的汇编章节会体现得很明显。

三、反汇编:把字节读回给人看

既然规则是公开的,那就一定能把字节读回成人能看懂的形式。 做这件事的工具叫反汇编器——它把字节按 CPU 的规则翻译成指令助记符。 顺带一提:反汇编出来的代码没有变量名、没有注释,逻辑却一览无余。 这也是为什么关键代码光靠【编译成二进制】是保护不住的——要保护得用加密、混淆这类额外手段。

四、Python 的字节码是同一回事

Python 也有字节码,只是它的读者是解释器而不是 CPU。 把一段三行源码编出来,就能看到一条条具名指令:RESUME、LOAD_CONST、STORE_NAME…… 同样的道理:规则公开,就能反汇编。 所以 8.1 的那张对照表可以再加一行:源码给人看、字节码给解释器看、本机代码给 CPU 看。

五、动手 Demo:看一看本机代码的内容

本机代码看起来是一堆字节,但只要按 CPU 的规则去读,它就是一条条指令。 用 Python 的字节码对照着看同一个道理。

import dis
src = "x = 1" + chr(10)
src += "y = x + 2" + chr(10)
src += "print(y)" + chr(10)
code = compile(src, "<demo>", "exec")
for ins in list(dis.get_instructions(code))[:8]:
    print(ins.opname, ins.argval)

实跑输出:

--------------------------------------------------------------------
看一看本机代码的内容
--------------------------------------------------------------------

本机代码看起来是一堆字节,**但只要按 CPU 的规则去读,它就是一条条指令。**

  字节(十六进制)     按 x86 读出来     意思
  ------------------------------------------------------------
  B8 05 00 00 00       mov eax, 5        把 5 放进 eax 寄存器
  83 C0 03             add eax, 3        eax 再加 3
  89 C3                mov ebx, eax      把 eax 复制到 ebx
  C3                   ret               返回

  **注意最后一条只占 1 个字节,第一条占 5 个字节——**
  所以指令是变长的,**CPU 只能从前往后一条条往下读,不能随便跳进中间。**

  用 Python 的字节码对照着看同一个道理——
  下面这段也是【字节】,也要按规则反汇编才能读:

    RESUME                 0
    LOAD_CONST             1
    STORE_NAME             x
    LOAD_NAME              x
    LOAD_CONST             2
    BINARY_OP              0
    STORE_NAME             y
    LOAD_NAME              print

  区别只在【谁来读】:**Python 的字节码由解释器读,本机代码由 CPU 读。**
  这也解释了为什么可以用反汇编工具看到程序的逻辑——**只要规则公开,字节就能被读懂。**

指令是变长的(本机代码里一条占 1 字节、另一条占 5 字节),所以只能从头往后读。 规则公开,字节就能被读懂——这也是为什么二进制并不等于【看不懂】。

30 秒实验

  1. 查一查 x86 里 ret 和 nop 各占几个字节。
  2. 想一想:为什么在二进制里随便找一个位置开始读,多半会读出乱码。

常见误区

  1. 以为二进制就【没法看懂】。有规则就能反汇编。
  2. 以为指令都是等长的。变长意味着只能顺序往下读。
自测三问(点开对答案) 1. 本机代码和指令是什么关系? —— 本机代码是字节,指令是按 CPU 的规则对某些字节的约定读法。 2. 为什么 CPU 只能顺序读取指令? —— 指令是变长的,跳到中间可能落在一条指令的半截上。 3. 反汇编器是做什么的? —— 把本机代码的字节按 CPU 规则翻译成人类能读的指令。

相关笔记