程序控制与执行模型本质

✅ 一句话本质

程序执行 = 指令在内存中按顺序执行 + 跳转

CPU 执行流程

  1. 取指令:从内存中获取下一条要执行的指令。

  2. 执行:对获取的指令进行解码和执行

  3. 更新PC:更新程序计数器(Program Counter)指向下一条指令地址

顺序执行机制

核心机制

程序按指令地址连续递增的方式一行一行执行,由PC寄存器控制执行顺序。

面试追问链

Q1:程序是怎么一行一行执行的?

A1:

  • 通过PC寄存器自动递增实现指令的顺序执行。

  • 每条指令执行后,PC指向下一条指令地址。

Q2:CPU 如何知道下一条指令?

A2:

  • PC寄存器中存储的就是下一条指令的内存地址。

Q3:函数调用为什么能返回?

A3:

  • 调用函数前会保存返回地址,函数执行完后跳转回该地址。

高分要点

  • PC寄存器:程序计数器,存储下一条指令地址。

  • 指令地址连续递增:顺序执行的基础机制。

  • 自动更新:每执行一条指令,PC自动指向下一条。

分支控制(if/switch)

分支本质

条件成立时跳转到指定指令地址执行。

if 语句实现

if(a > b) 本质实现:

  1. 执行比较指令(CMP)

  2. 根据比较结果执行条件跳转(JZ/JNZ等)

面试追问:

Q1:if是怎么实现的?

A1:

  • 通过条件跳转指令实现。

  • CPU 本身不直接支持if,而是通过条件跳转指令实现。

switch 优化机制

Q1:switch 和 if-else 区别?

A1:

  • switch:使用跳转表(jump table)实现 O(1) 查找

  • if-else:顺序比较,最坏情况O(n)

Q2:switch 为什么快?

A2:

  • 连续 case:使用 jump table,直接索引

  • 离散 case:退化为if链

高分要点:

  • jump table:跳转表,实现常数时间复杂度

  • 优化条件:case 值连续时才能使用 jump table

  • 退化机制:case 离散时自动转化为 if-else 链

退化机制(for/while)

退化机制

条件判断 + 跳转回循环开始位置

for vs while

本质相同点:

  • 都编译为条件判断 + 跳转指令

  • 都可能产生死循环。

  • 都可能被编译器优化

死循环问题

while(1);

面试追问:

Q1:会被优化掉吗?

A1:

  • 如果循环无副作用,可能被优化掉

  • volatile 关键字:防止编译器优化

高分要点

  • 无副作用循环可能被编译器优化删除

  • 使用 volatile 变量可防止优化

  • 嵌入式开发中常用 volatile 防止优化

函数调用机制

函数调用过程

  1. 参数压栈:将参数按调用约定压入栈

  2. 保存返回地址:将下一条指令地址压栈

  3. 跳转到函数:跳转到函数入口地址

  4. 执行函数体:执行函数内部指令

  5. 恢复现场:恢复调用前的寄存器状态

  6. 返回调用点:跳转回 调用点 继续执行

栈帧结构

栈帧本质结构:

| 高地址 | 
| 参数   | 
| 返回地址| 
| 保存寄存器| 
| 局部变量| 
| 低地址 |

核心要点:

  • 创建时机:函数调用时创建。

  • 销毁时机:函数返回时销毁。

  • 隔离作用:每个函数有独立的栈帧空间。

参数传递机制

  • 只有值传递:参数传递本质上都是值传递

  • 指针传递:传递的是地址值

Q1:为什么可以修改外部变量?

A1:

  • 因为传递的变量地址(指针),通过地址间接修改

Q2:数组参数是什么?

A2:

  • 数组参数退化为指针传递

高分要点:

  • 指针是值传递,但值是地址

  • 数组参数实际传递的是数组首地址

  • 结构体参数可能直接传递整个结构体

返回地址机制

函数返回原理:

  • 存储位置:栈或LR寄存器(链接寄存器)

  • 恢复机制:函数返回时从栈中弹出返回地址

  • 安全风险:返回地址被覆盖会导致程序崩溃

栈溢出攻击:

  • 覆盖返回地址可改变程序执行流

  • 缓冲区溢出是常见攻击方式

递归机制

递归本质

函数不断调用自身,每次调用都创建新的栈帧

核心问题

Q1:递归和循环区别?

A1:

  • 递归:函数调用自身,有函数调用开销

  • 循环:通过跳转指令实现,无函数调用开销

Q2:递归为什么会爆栈?

A2:

  • 每次递归调用都创建新的栈帧

  • 栈空间有限,递归深度过大耗尽栈空间

Q3:尾递归优化?

A3:

  • 编译器可将尾递归优化为循环

  • 消除函数调用开销,避免栈溢出

控制流坑点

未定义行为(UB)

经典例子

int i = 1;
i = i++;  // 未定义行为

面试追问:

Q1:为什么是未定义行为?

A1:

  • 同一变量在单个序列点内被多次修改

  • 编译器可任意处理,结果不可预测

短路求值

逻辑运算符特性:

a && b  // a为假时,b不执行
a || b  // a为真时,b不执行

为什么要短路?

  • 性能优化:避免不必要的计算

  • 安全保护:防止空指针访问等错误

  • 逻辑正确:保证逻辑表达式正确性

switch穿透(fall-through)

特性

switch(x) {     
    case 1:
    case 2:        
         // 共享代码块         
    break; 
}

面试要点:

  • 设计意图:允许多个case共享同一段代码

  • 注意事项:忘记 break 会导致意外穿透

  • 现代语言:部分语言要求显示标注穿透意图。