本机代码看着是一堆字节,按 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 秒实验
- 查一查 x86 里 ret 和 nop 各占几个字节。
- 想一想:为什么在二进制里随便找一个位置开始读,多半会读出乱码。
常见误区
- 以为二进制就【没法看懂】。有规则就能反汇编。
- 以为指令都是等长的。变长意味着只能顺序往下读。
自测三问(点开对答案)
1. 本机代码和指令是什么关系? —— 本机代码是字节,指令是按 CPU 的规则对某些字节的约定读法。 2. 为什么 CPU 只能顺序读取指令? —— 指令是变长的,跳到中间可能落在一条指令的半截上。 3. 反汇编器是做什么的? —— 把本机代码的字节按 CPU 规则翻译成人类能读的指令。- ☐ 下一节(8.3 编译器负责翻译源代码)
相关笔记
- 计算机只能执行本机代码 — CPU 只认这一种东西
- 编译器负责翻译源代码 — 谁来生成这些字节
- CPU的处理其实很简单 — 指令的种类其实很少
- ⌨️ 编程原理 · 索引 — 把编程全貌理一遍
- ⚙️ 程序是怎样跑起来的 · 索引