从晶体管到main():CPU到底怎么执行0和1?
从MOS管开关到五级流水线,从上电复位到跳转main(),拆解CPU执行二进制指令的完整链路。
一、0和1的物理真相:晶体管开关
CPU看到的不是0和1,是电压。NMOS管栅极加高电平(如3.3V)导通,低电平(0V)截止;PMOS管相反。一个NMOS+一个PMOS组成CMOS反相器——输入1输出0,输入0输出1,这就是非门。
用晶体管串并联,搭出与门、或门、异或门。异或门+与门组合成半加器,两个半加器+或门组成全加器,全加器级联就是加法器——ALU的核心。所有运算,归根结底是晶体管开关的通断组合。
| 层级 | 构成 | 功能 |
|---|---|---|
| 晶体管 | NMOS + PMOS | 开关通断,表示0/1 |
| 逻辑门 | 非门、与门、或门、异或门 | 基本逻辑运算 |
| 加法器 | 全加器级联 | 二进制加法 |
| ALU | 加法器+逻辑运算单元 | 算术和逻辑运算 |
| CPU | ALU+控制器+寄存器+缓存 | 完整指令执行 |
二、指令周期:五级流水线
经典RISC处理器(如ARM Cortex-M、RISC-V)把一条指令的执行拆成五个阶段:
1. 取指(IF — Instruction Fetch)
PC寄存器保存下一条指令的地址。CPU把PC值送上地址总线,从存储器读出指令,存入IR(指令寄存器)。随后PC自动递增,指向下一条指令。ARM Thumb指令集下PC加2,ARM指令集下加4,RISC-V 32位加4。
2. 译码(ID — Instruction Decode)
IR中的二进制码送入译码器。以RISC-V的R型指令为例,32位被切分为:
[31:25] funct7 | [24:20] rs2 | [19:15] rs1 | [14:12] funct3 | [11:7] rd | [6:0] opcode
译码器根据opcode判断指令类型(ADD/LW/SW/BEQ...),从寄存器组读出源操作数,生成控制信号。
3. 执行(EX — Execute)
控制单元根据译码结果调度:
- 算术逻辑指令 → ALU执行运算
- 访存指令 → ALU计算内存地址
- 分支指令 → ALU比较+计算跳转目标
4. 访存(MEM — Memory Access)
仅Load/Store指令需要此阶段:
- Load:从内存读数据到寄存器
- Store:把寄存器数据写入内存
运算类指令跳过此阶段。
5. 写回(WB — Write Back)
ALU运算结果或内存读取的数据写回目标寄存器,同时更新状态标志位(N/Z/C/V)。
流水线的意义:当第1条指令在执行阶段时,第2条在译码,第3条在取指——三条指令重叠执行,吞吐量提升近5倍。
三、从上电到main():ARM Cortex-M启动全流程
以STM32(Cortex-M3/M4)为例,按下复位键后硬件自动完成以下操作:
第1步:读取向量表
Cortex-M复位后,CPU从地址0x00000000读取4字节写入MSP(主栈指针),再从0x00000004读取4字节作为Reset_Handler的入口地址写入PC。
注意:STM32的Flash物理起始地址是
0x08000000,芯片通过地址别名机制将0x00000000映射到Flash起始位置。Cortex-M7还支持通过VTOR寄存器重定位向量表。
向量表前几项:
| 偏移 | 内容 | 说明 |
|---|---|---|
| 0x00 | 初始MSP值 | 栈顶地址,必须8字节对齐 |
| 0x04 | Reset_Handler地址 | 最低位必须为1(Thumb模式) |
| 0x08 | NMI_Handler | 不可屏蔽中断 |
| 0x0C | HardFault_Handler | 硬件故障 |
第2步:Reset_Handler执行初始化
Reset_Handler是启动文件(如startup_stm32f103.s)中定义的汇编函数,依次完成:
- 复制.data段——有初值的全局变量从Flash搬到RAM
- 清零.bss段——未初始化的全局变量在RAM中填0
- 调用SystemInit()——配置系统时钟(HSE/HSI/PLL)
- 调用__libc_init_array()——C++构造函数
- 跳转main()
// 典型Reset_Handler伪代码(参考STM32 HAL库启动文件)
void Reset_Handler(void) {
// 1. .data段:Flash → RAM
uint32_t *src = &_sidata; // Flash中的数据起始
uint32_t *dst = &_sdata; // RAM中的.data起始
while (dst < &_edata) *dst++ = *src++;
// 2. .bss段清零
dst = &_sbss;
while (dst < &_ebss) *dst++ = 0;
// 3. 系统初始化
SystemInit();
__libc_init_array();
// 4. 跳转用户代码
main();
while(1); // main不应返回
}
四、指令执行实例:ADD指令的五级流水
以ARM指令ADD R0, R1, R2(R0 = R1 + R2)为例,假设R1=5, R2=3:
| 阶段 | 操作 | 数据流 |
|---|---|---|
| IF | PC指向指令地址,从Flash读取32位指令码 | 0xE0810002 → IR |
| ID | 译码识别为ADD,读取R1和R2 | R1=5, R2=3 → ALU输入 |
| EX | ALU执行加法 | 5 + 3 = 8 |
| MEM | 无内存操作,跳过 | — |
| WB | 结果8写入R0,更新标志位 | R0=8, Z=0, C=0 |
五、常见问题
Q:为什么.data段要从Flash复制到RAM?
A:.data段的全局变量在运行时会被修改,Flash只读,必须复制到RAM才能写入。.bss段只需清零,初始值在Flash中不占空间。
Q:为什么向量表里存的是地址而不是跳转指令?
A:Cortex-M的向量表存的是中断服务程序的入口地址,不是指令。CPU硬件自动把该地址加载到PC,比执行跳转指令少一个时钟周期,中断响应更快。
Q:程序能直接在Flash上运行吗?
A:能。这叫XIP(Execute In Place),Cortex-M支持直接从Flash取指。但Flash读取速度比RAM慢(典型70ns vs 10ns),且变量必须在RAM中修改,所以.data/.bss段必须搬到RAM。
六、动手验证:GitHub上的CPU模拟器项目
想亲手实现一个CPU?这些开源项目值得参考:
| 项目 | 架构 | 语言 | 特点 |
|---|---|---|---|
| RISCV-Simulator | RISC-V RV64I | C++ | 五级流水线+分支预测 |
| exactstep | RISC-V/MIPS/ARM-v6m | C++ | 多架构指令精确模拟 |
| 6502-emulator | MOS 6502 | C++ | 代码可读性优先,适合入门 |
| sixty502 | MOS 6502 | Go | 周期精确,含反汇编器 |
这些模拟器的核心都是同一个循环:cpu::step() → 取指 → 译码 → 执行 → 写回,和真实CPU的工作方式一一对应。
七、总结
CPU执行程序的本质链路:晶体管开关→逻辑门→加法器→ALU→五级流水线。从上电复位开始,CPU通过向量表找到栈指针和Reset_Handler地址,完成.data/.bss段初始化后跳转main()。理解这条链路,调试启动失败、HardFault、中断不响应等问题才有方向。