📄 一句话概括
最早的编程是手写 0 和 1:一次 5+3 要 72 位,写程序的人记的是字节含义,不是意图。
📝 本节和前面几节的关系
3.1 说了必然性,这一节从最原始的一步开始还原——看程序员最早在对付什么。
一、程序曾经是字节
算一次 5 + 3,在 x86 上长这样:
B8 05 00 00 00——把 5 放进 eax;83 C0 03——eax 加 3;C3——返回。
实测:3 条指令、9 个字节=72 位。
这 72 位里没有一个字叫【加】,只有【哪个字节做什么】。
二、难在记,不在想
现在的编程难在想清楚要做什么;那时候难在记住怎么做。
83 C0 是加法、89 C3 是搬移——这些全靠背,背错一个字节就是另一条指令。
而且错得不明显:它可能还是一条合法指令,只是干的不是你想的事。
这跟 2.6 讲的企鹅是同一类麻烦:不报错,逻辑错了。
三、地址要自己算
跳转指令里写的是偏移量,不是标签。
E9 F6 FF FF FF 里的 F6 FF FF FF 是【往回跳 10 个字节】——这个数得手算。
中间插一条指令,后面所有跳转的偏移全变。这就是最原始的维护噩梦。
四、这一步留下了什么
留下了两个至今没变的观念:程序就是数据、指令有固定长度和格式。 也留下了一个至今没消失的岗位:反汇编——把字节还原成指令(第 10 章会真的做一次)。 而程序员真正想要的,从第一天起就是【写意图,不写字节】——这就是 3.3 要迈的第一步。
五、动手 Demo:最初使用机器语言编写程序
把 5 + 3 的机器码摊开,数一数它有多少位。
再跟几种写法的字符数比一比。
code = ["B8 05 00 00 00", # mov eax, 5
"83 C0 03", # add eax, 3
"C3"] # ret
nbytes = sum(len(c.split()) for c in code)
print("指令数", len(code), "字节数", nbytes, "位数", nbytes * 8)
实跑输出:
--------------------------------------------------------------------
最初使用机器语言编写程序
--------------------------------------------------------------------
最原始的程序,是**手写一串 0 和 1**。后来的十六进制只是为了少写几个字。
算一次 `5 + 3`,在 x86 上长这样:
B8 05 00 00 00 mov eax, 5
83 C0 03 add eax, 3
C3 ret
实测:**3 条指令、9 个字节=72 位。**
换个角度看这个数字:
写法 写 5+3 要几个字符
-------------------------------------
纯二进制 72 个 0/1
十六进制 27 个(含空格)
汇编助记符 约 40 个
高级语言 7 个(total = a + b)
**注意:机器码里没有【加】这个概念,只有【哪个字节做什么】。**
写程序的人必须记住 `83 C0` 是加法、还要自己算跳转地址——
**一个地址算错,程序就跑到别处去了,而且不报错。**
这就是 3.2 说的:**最初的编程,难的是记,不是想。**
实测:3 条指令、9 个字节=72 位——这 72 位里没有一个字叫【加】。
写程序的人必须记住 83 C0 是加法,还要自己算跳转偏移;难在记,不在想。
30 秒实验
- 查一查
89 C3和83 C0分别是什么指令,感受一下【背字节】的滋味。 - 想一想:如果跳转偏移算错了,程序会怎样——是报错还是跑飞。
常见误区
- 以为机器码只是【难写】。它真正的问题是错了不报错。
- 以为十六进制是另一种语言。它只是二进制的省字写法。
自测三问(点开对答案)
1. `5 + 3` 的机器码有几个字节? —— 9 个字节(B8 05 00 00 00/83 C0 03/C3),合 72 位。 2. 手写机器码最难的地方是什么? —— 难在记:每个字节干什么全靠背,背错一个字节可能就是另一条合法但错误的指令。 3. 跳转指令里写的是什么? —— 偏移量,不是标签;中间插一条指令,后面所有跳转偏移都要重算。- ☐ 下一节(3.3 编程语言的第一步是汇编语言)
相关笔记
- 编程语言的第一步是汇编语言 — 下一步抽象
- OOP的出现具有必然性 — 这条链的起点
- 🔵 计算机基础 · 索引 — 二进制与机器指令
- 🧩 面向对象是怎样工作的 · 索引