计算机原理101:程序是如何运行的

Posted by FridayLi on February 26, 2025

计算机原理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())进行.

调度流程

  1. CPU开机后最先运行的是操作系统内核代码(内核线程 / 中断服务程序)。
  2. 操作系统维护一个就绪队列(ready queue),里面是待执行的用户程序。
  3. 定时器发出中断(如每10ms),操作系统中断当前执行,进入内核态。
  4. 内核保存当前进程状态(寄存器、PC、栈等),切换到另一个进程(加载另一个程序的上下文),恢复执行。
  5. 所有用户程序看起来像是“独占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 → 可执行文件 → 直接运行 ❌ 无虚拟机 ✅ 快,部署简单