计算机原理101: 程序是如何运行的
《计算机底层的秘密》读书笔记 + GPT问答整理
引子
#include <stdio.h>
int main() {
printf("Hello, world!\n");
return 0;
}
你用刚学的C语言写了一段hello word, 编译, 运行, 成功在屏幕print了出来。这背后都隐藏了哪些细节呢? 让我们通过一个个的问题来探索。
1.这段代码是如何被CPU识别的?
CPU只能识别0、1这样的机器码, 所以我们先用编译器把上边的代码编译成机器码
C源码 → 预处理 → 编译 → 汇编 → 链接 → 可执行文件 → 加载进内存 → CPU执行
阶段 | 描述 | 工具 | 处理后的文件 |
---|---|---|---|
源码 | C代码 | .c 文件 | .c |
预处理 | 宏展开、引入头文件 | gcc -E | .i |
编译 | 翻译成汇编代码 | gcc -S | .s |
汇编 变成机器码 | (目标文件) | gcc -c | .o |
链接 | 连接库、生成可执行文件 | gcc | .exe |
执行 | 操作系统加载、CPU执行 | ./hello | .exe |
2.汇编成机器码后是如何被CPU执行的?
机器码(指令)被加载到内存后, CPU根据程序计数器来从内存获取对应的指令执行。
3.CPU内部常见的寄存器及其功能有哪些?
1. 通用寄存器(General Purpose Registers)
用于临时保存数据、操作数、中间结果。 x86(32位)示例:
- EAX:累加器(加法、乘法等默认使用)
- EBX:基址寄存器
- ECX:计数器(如循环次数)
- EDX:数据寄存器 64位版本:如 RAX, RBX, RCX, RDX RISC 架构(如 RISC-V、MIPS):通常有 16~32 个通用寄存器(如 x0 ~ x31)
2. 专用寄存器(Special Purpose Registers)
名称 | 功能说明 |
---|---|
程序计数器(PC) | 存储下一条将被执行的指令地址(也叫 Instruction Pointer ,如 x86 中的 EIP/RIP ) |
指令寄存器(IR) | 存储当前正在执行的指令 |
栈指针(SP) | 指向当前栈顶的位置 |
基址指针(BP) | 用于栈帧定位 |
状态寄存器/标志寄存器(FLAGS 或 PSW) | 储存运算结果的状态标志(如零标志 ZF、进位标志 CF、溢出标志 OF) |
4.CPU能执行的操作有哪些?
一、数据传输指令(Data Transfer)
用于在寄存器、内存、I/O 设备之间移动数据。
- MOV:数据在寄存器和内存之间传送
- LOAD / STORE:从内存读取或写入数据
- PUSH / POP:数据入栈 / 出栈
- IN / OUT:和I/O设备交换数据
二、算术运算指令(Arithmetic Operations)
执行基本的数值运算。
- ADD:加法
- SUB:减法
- MUL / DIV:乘法 / 除法
- INC / DEC:自增 / 自减
三、逻辑运算指令(Logical Operations)
执行位级或逻辑运算。
- AND, OR, XOR, NOT
- SHL / SHR:逻辑左移 / 右移
- CMP:比较两个数
四、控制转移指令(Control Flow)
用于改变程序的执行顺序。
- JMP:无条件跳转
- JE / JNE / JG / JL / JZ / JNZ:条件跳转(例如判断相等、不等、大于、小于、是否为零等)
- CALL / RET:调用子程序 / 返回
- LOOP:循环指令(部分架构提供)
五、处理器控制指令(Processor Control)
控制CPU状态、标志、权限等。
- HLT:停机
- NOP:空操作,占用时钟但不做任何事
- INT:中断请求
- CLI / STI:关闭 / 开启中断
5.CPU 如何处理多个程序?
CPU执行多个程序时, 需要在不同程序间切换, 切换的时候还需要保存Context(程序计数器, 各个寄存器的值等等)
操作系统解决了这个问题, 并创建了进程的地址空间来保存Context。
当你启动一个进程(如运行一个 .exe 文件),操作系统会为这个进程分配: ▶️ 一整块虚拟地址空间,包含以下区域:
区域 | 说明 |
---|---|
栈区(Stack) | 函数调用、参数、局部变量 |
堆区(Heap) | 存储动态分配的内存(如 malloc ) |
数据区(Data) | 存储全局变量、静态变量 |
代码区(Text) | 存储程序的指令(如 main() ) |
文件描述符表 | 打开的文件、Socket、设备等 |
内存映射段(MMap) | 映射的库文件(如 libc )等 |
6.操作系统和其它程序有什么区别, 以及操作系统是如何启动的呢?
比较项 | 操作系统(OS) | 普通程序(应用程序) |
---|---|---|
权限级别 | 拥有最高权限(内核态 / Ring 0) | 权限受限(用户态 / Ring 3) |
运行位置 | 常驻内存核心区域(内核空间) | 动态加载,运行于用户空间 |
功能 | 管理硬件资源、调度程序、提供接口 | 执行特定业务逻辑,如 Word、浏览器 |
代码触发机制 | 主动接收中断或系统调用 | 被操作系统调度后执行 |
操作系统控制一切的执行和访问权限。普通程序不能直接访问硬件,都要通过操作系统提供的系统调用接口(例如 read()、malloc()、send())进行.
调度流程
- CPU开机后最先运行的是操作系统内核代码(内核线程 / 中断服务程序)。
- 操作系统维护一个就绪队列(ready queue),里面是待执行的用户程序。
- 定时器发出中断(如每10ms),操作系统中断当前执行,进入内核态。
- 内核保存当前进程状态(寄存器、PC、栈等),切换到另一个进程(加载另一个程序的上下文),恢复执行。
- 所有用户程序看起来像是“独占CPU”,其实都被操作系统轮流调度。
操作系统是一个进程吗?
操作系统本身不是一个普通意义上的“进程”,而是一组运行在“内核态(kernel mode)”的代码。
操作系统不是一个单独的进程
- 操作系统是系统运行的 核心软件,包括内核、驱动、调度器、文件系统、内存管理等模块。
- 它运行在 内核态(Ring 0),拥有对 CPU、内存、硬件的完全控制权。
- 它不是用户态程序,也不是一个“能被调度的进程”。
操作系统是“怎么运行”的?
🔹 操作系统的核心组件(如 Linux Kernel、Windows NT 内核):
- 在系统启动时由引导加载程序加载进内存
- 运行后一直常驻内存(代码 + 数据)
- 通过 中断、系统调用、定时器 机制响应用户程序的请求
- 不参与进程调度,而是“调度别人”
7.CPU是如何把计算结果写回内存和硬盘的?
CPU如何把结果写回“内存”?
▶️ 场景:某程序执行 a = b + c;
- CPU 从内存中读取变量 b 和 c 到寄存器
- 在 ALU(算术逻辑单元) 中执行加法
- 计算结果保存在通用寄存器中(如 RAX)
- 通过 MOV 指令把寄存器内容写回内存某地址(即变量 a 所在的内存)
CPU如何把结果写回“硬盘”?
❗重点:CPU 不能直接访问硬盘! 写入硬盘的过程必须通过操作系统 + I/O控制器 + 缓冲机制来完成,过程如下: ▶️ 示例:程序执行 fwrite(data, size, 1, file);
- CPU调用操作系统提供的系统调用(如 write() 或 fwrite())
- 操作系统内核将数据从内存复制到内核缓冲区
- 操作系统通过磁盘驱动程序向磁盘控制器(如 SATA/NVMe)发送写指令
- 磁盘控制器将数据写入磁盘特定扇区(通过 DMA 或中断方式)
- 完成后通知CPU写操作完成(通过中断)
中间过程使用了:
- 文件系统(如 NTFS/ext4)决定写到哪
- 磁盘缓存/缓冲区 暂存数据
- DMA(Direct Memory Access) 减少CPU负担
- 系统调用(syscall) 跨越用户态 → 内核态
8. Python的程序是如何运行起来的, 和C#, C, C++, Go, Java的区别是什么?
Python 本质上是解释执行的语言(非原生编译),会将 .py 编译为 .pyc(字节码),由解释器执行。
语言 | 编译/解释 | 运行方式 | 中间步骤/虚拟机 | 性能 |
---|---|---|---|---|
Python (CPython) | 解释为主 | .py → .pyc → PVM执行 |
✅ Python虚拟机(PVM) | ❌ 慢(受GIL限制) |
C | 编译 | .c → .exe(机器码) → 直接运行 |
❌ 无虚拟机 | ✅ 原生快 |
C++ | 编译 | .cpp → .exe → 直接运行 |
❌ 无虚拟机 | ✅ 原生快 |
Java | 编译+解释 | .java → .class(字节码) → JVM解释/编译执行 |
✅ JVM(JIT+GC) | ✅ 快(JIT优化) |
C# | 编译+解释 | .cs → IL(中间语言) → CLR执行 |
✅ .NET CLR(JIT) | ✅ 快(靠近Java) |
Go | 编译 | .go → 可执行文件 → 直接运行 |
❌ 无虚拟机 | ✅ 快,部署简单 |