“计算机是怎样一层层搭起来的?”系列 · 1/4
我们每天都在写 Python、C、Java 或 JavaScript,但处理器并不会“看懂”这些语言。它真正执行的是由指令集规定的一串机器指令。那么,从人类可读的代码到电路能够执行的动作,中间究竟发生了什么?
一、“计算机只认识 0 和 1”其实是一句方便理解的简化二、同样是一串比特,为什么有时是数字,有时又是指令?三、机器语言:CPU 真正执行的“程序”四、汇编语言:把难读的机器码换成人类能记住的名字五、高级语言真正解决的问题:不是“少写几个字符”六、编译器:把一种语言系统地变成另一种语言七、那 Python 为什么不用先生成一个 .exe 才能运行?八、Java 又在做什么?——故意多加一层九、既然最后都要落到机器指令,为什么还需要那么多编程语言?十、真正贯穿计算机世界的东西:抽象系列导航
一、“计算机只认识 0 和 1”其实是一句方便理解的简化
我们常说计算机只认识 0 和 1。
这句话抓住了核心,但并不严格。
CPU 并不是坐在那里“阅读”字符 0 和 1。最底层真正存在的是晶体管、电压状态、时钟信号和逻辑电路。工程上把两类稳定状态抽象成二进制的 0 与 1,于是复杂电路就可以建立在布尔逻辑之上。
所以更准确的链条是:
物理状态 → 逻辑 0/1 → 位模式 → 机器指令 → 更高层的软件抽象
二进制并不是计算机世界的终点,而是我们开始描述数字电路以后最方便的一层语言。
二、同样是一串比特,为什么有时是数字,有时又是指令?
例如下面这一串:
它本身没有唯一含义。
按照 ASCII 解释,它可以表示字符
A;按照某种整数格式解释,它可以表示一个数;放到某个处理器规定的位置,它又可能成为机器指令的一部分。所以计算机中一个非常重要的事实是:
比特本身不携带“天然意义”,意义来自解释规则和上下文。
程序和数据在存储器里最终都可以表现为比特。区别在于 CPU 此刻把哪些比特当作指令取出、按照哪一种指令集解释,以及程序又把其他数据按什么格式处理。
三、机器语言:CPU 真正执行的“程序”
每一种处理器体系都会定义自己的指令集架构,也就是 ISA。
它规定了诸如:
- 有哪些寄存器;
- 可以执行哪些指令;
- 指令如何编码;
- 如何读写内存;
- 如何寻址、跳转以及读写内存;
- 程序怎样跳转、比较和计算。
x86-64、ARM64、RISC-V 都是指令集架构的例子。
所谓机器语言,就是这些指令经过编码后的比特模式。
假设一个处理器拥有“把两个数相加”的指令,那么机器码中就必须有某种编码告诉 CPU:
这不是普通数据,而是一条加法指令;操作数在哪里;结果应该写到哪里。
CPU 的取指、译码、执行循环会不断重复这件事。
四、汇编语言:把难读的机器码换成人类能记住的名字
如果程序员只能直接写机器码,编程会极其痛苦。
于是人们给机器指令起了助记符。
例如一段汇编可能长这样:
这里的
mov、add 并不是 CPU 天生认识的英语单词。汇编器会把它们翻译成目标处理器真正执行的机器码。于是第一层重要抽象出现了:
汇编语言已经比直接写二进制友好多了,但它依然非常贴近具体硬件。
五、高级语言真正解决的问题:不是“少写几个字符”
C、C++、Rust、Java、Python 等语言把抽象继续向上推进。
程序员可以写:
而不必亲自决定:
- 数据具体放在哪个寄存器;
- 中间结果什么时候写回内存;
- 函数调用怎样保存现场;
- 条件判断对应哪一条跳转指令;
- 不同处理器的指令编码是什么。
高级语言真正带来的,是从机器细节中解放出来。
人类开始描述“我要做什么”,而把越来越多“具体怎样让处理器完成”交给编译器、运行时和操作系统。
六、编译器:把一种语言系统地变成另一种语言
以 C、C++、Rust 这类常见的提前编译语言为例,一段源代码通常不会一步“直接变成 0 和 1”。
中间可能经历:
编译器不仅是翻译器,还是一个非常复杂的优化系统。
例如同一个表达式,人类写出来的形式未必是处理器执行效率最高的形式。编译器可以重新安排指令、删除无用计算、内联函数、利用向量指令等。
七、那 Python 为什么不用先生成一个 .exe 才能运行?
这里最容易出现一个误解:
“编译型语言会变成机器码,解释型语言则一句一句直接阅读源代码。”
现实比这复杂得多。
以常见的 CPython 为例,Python 源代码通常会先被处理并编译成一种更适合虚拟机执行的字节码,然后由 Python 虚拟机解释执行。
所以链条更像:
其他语言和实现还可能加入 JIT(即时编译),在程序运行过程中把热点代码进一步编译成机器码。
因此,“编译”和“解释”并不是泾渭分明的两个世界,而是实现语言的不同策略。
八、Java 又在做什么?——故意多加一层
Java 的经典思路是先把源代码编译成 JVM 字节码:
这样程序面对的首先不是某一款真实 CPU,而是一台抽象出来的“Java 虚拟机”。
只要不同平台都实现兼容的 JVM,同一份字节码就能跨平台运行。
这就是“一次编写,到处运行”背后的核心思路之一:不是让所有硬件变得一样,而是在不同硬件之上人为建立一个共同的抽象层。
九、既然最后都要落到机器指令,为什么还需要那么多编程语言?
因为不同语言解决的问题不同。
C 强调可预测的底层控制;Python 强调开发效率与表达能力;Rust试图在接近系统编程性能的同时,把很多内存安全问题提前到编译阶段;Java 借助虚拟机与成熟运行时提供跨平台环境;JavaScript 则随着 Web 平台形成了自己的生态。
语言之间的差异不仅是语法。
它们还在选择:
- 哪些错误让编译器提前阻止;
- 哪些事情交给运行时处理;
- 程序员需要管理多少底层细节;
- 性能、可移植性、安全性和开发效率之间怎样取舍。
所以:
不同编程语言,本质上是在设计不同的人机分工方式。
十、真正贯穿计算机世界的东西:抽象
回头看这条链:
每增加一层,我们都在做同一件事:
把下面一层复杂的细节隐藏起来,为上面一层提供更容易使用的规则。
计算机并不是突然从 0 和 1 “跃迁”成 Python 的。
它是由许多层抽象一点点搭出来的。
而这也引出了下一篇的问题:当机器指令真正开始运行时,它究竟运行在什么硬件上?除了 CPU,一台完整计算机还需要哪些部分共同工作?
系列导航
第 1 篇(本文):计算机只认识 0 和 1,我们究竟是怎样让它看懂代码的?
下一篇:一台计算机究竟由什么组成?——从 CPU 到主板的完整硬件地图
- Author:阿尔法
- URL:https://yjy.hauchet.cn/article/from-binary-to-programming-languages
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts







