当前位置: 首页 > news >正文

为什么 Cortex-M3 需要向量表?向量表为什么必须放在地址 0 附近?

难度:★★
本文首发于我的嵌入式技术公众号「OneChan」,未经授权禁止转载。


上一篇文章我们聊到,Cortex-M3 上电后会自动从0x00000000取栈指针,从0x00000004取复位地址。这两个值加上后面一串地址,就是所谓的“向量表”。

可问题是——为什么非得用向量表?直接把中断服务函数的地址写死不行吗?

还有,为什么向量表默认必须放在地址 0 附近?我把它挪到 RAM 里行不行?

今天就把这两个问题彻底拆开揉碎。


一、没有向量表的世界是什么样子?

先说结论:没有向量表,CPU 照样能响应中断,但会非常笨拙。

很多老式 CPU(比如经典的 8051)就没有向量表。

8051 的中断机制是这样的:

  • 外部中断 0 发生后,硬件自动跳转到0x0003去执行。
  • 定时器 0 中断发生后,硬件自动跳转到0x000B去执行。
  • 串口中断发生后,硬件自动跳转到0x0023去执行。

每个中断的入口地址是硬件写死的,永远改不了。

这带来两个问题:

第一,中断函数必须精确放在那个地址上。
如果你的外部中断 0 处理程序超过 8 个字节(0x00030x000B只有 8 字节空间),它就会踩进定时器 0 的地盘。所以 8051 程序员必须在0x0003处放一条LJMP指令,跳转到真正的中断服务函数。每个中断入口都成了“跳板”。

第二,中断向量地址没法改。
如果你的程序想从 Bootloader 跳到应用程序,应用程序的中断表在哪?8051 没这概念,所有中断入口都是死的。所以做 OTA 升级时,要么让 Bootloader 转发所有中断,要么在应用层手动跳转,非常痛苦。

ARM Cortex-M3 的设计者显然受够了这一套。他们决定:让中断向量变成一个表,表里放的是函数地址,而不是跳转指令。表的起始地址可以改。

这就是向量表。


二、向量表的本质:一张“函数指针数组”

打开启动文件,你看到的向量表长这样:

__Vectors DCD __initial_sp ; 0x00:栈顶 DCD Reset_Handler ; 0x04:复位 DCD NMI_Handler ; 0x08:不可屏蔽中断 DCD HardFault_Handler ; 0x0C:硬件错误 DCD MemManage_Handler ; 0x10:内存管理错误 DCD BusFault_Handler ; 0x14:总线错误 DCD UsageFault_Handler ; 0x18:用法错误 ; ... 后面还有几十个 DCD SysTick_Handler ; 0x3C:系统滴答定时器 ; 外设中断从 0x40 开始 DCD WWDG_IRQHandler DCD PVD_IRQHandler ; ... 一直排下去

DCD是汇编伪指令,意思是“在这里分配一个 32 位空间,并把后面的值填进去”。

所以向量表的本质,就是一个32 位整数数组,放在内存的某个起始地址。数组的第 0 项是栈顶,第 1 项是复位入口,第 2 项是 NMI 入口,以此类推。

中断发生时,硬件做的事极其简单:

  1. 根据中断号n,算出向量地址 =向量表基址 + 4 × n
  2. 从这个地址读出一个 32 位值。
  3. 把这个值塞进 PC(程序计数器)。

就这么简单。

硬件不需要知道这个值是什么函数,不需要管它是不是跳转指令。它只负责读一个地址,然后跳过去。

这个设计的精妙之处在于:

  • 向量表里存的是地址,不是指令。所以中断服务函数可以放在任何地方,不需要像 8051 那样在固定地址放跳板。
  • 向量表本身也是一块普通内存。既然是内存,就能改。你能在运行时把某个中断向量换成另一个函数的地址,实现动态 Hook。
  • 向量表的起始地址存在一个叫VTOR(向量表偏移寄存器)的寄存器里。改了VTOR,整个向量表就可以搬到别处去。

三、为什么向量表“默认”必须在地址 0 附近?

这是整篇文章的核心问题。

Cortex-M3 的VTOR寄存器复位后的默认值是0x00000000

所以上电那一刻,CPU 会去0x00000000找向量表。

那问题来了:既然VTOR可以改,为什么 ARM 不把默认值设成其他地址,比如0x08000000(Flash 起始地址)?

答案藏在两个约束里:历史兼容性芯片启动的确定性

约束一:历史兼容性

ARM7TDMI(比如经典的 LPC2000 系列)没有VTOR寄存器。它的向量表硬绑定在0x00000000

0x00000000这个地址在很多芯片上不是 Flash——它可能是 Boot ROM,也可能映射到 RAM。为了让向量表放在 Flash 里,芯片厂家搞了一个“内存重映射”机制:上电时0x00000000映射到 Boot ROM;运行后通过写某个寄存器,把0x00000000重映射到 Flash。

到了 Cortex-M3,ARM 设计了VTOR,理论上可以一劳永逸。但为了让从 ARM7 迁移过来的工程师和芯片厂家不骂娘,默认值依然保留了0x00000000

约束二:芯片启动的确定性

设想一下,如果VTOR复位默认值是0x08000000

上电后 CPU 去0x08000000读栈顶,去0x08000004读复位入口。一切正常。

但如果芯片的 Flash 是空的呢?或者 Flash 控制器还没初始化完成呢?

CPU 会读到垃圾数据,然后跳到随机地址,芯片直接跑飞。更可怕的是,调试器可能都连不上,因为调试接口也依赖 CPU 正常工作。

而把默认值定在0x00000000,给了芯片厂家一个“安全兜底”的机会。

大多数 Cortex-M3 芯片在0x00000000处放的是一块Boot ROM(出厂固化的启动代码)。这块 ROM 里有一个精简的向量表,至少能保证:

  • 栈指针指向一个有效的 RAM 区域(通常是内部 SRAM)。
  • 复位入口指向 Boot ROM 里的初始化代码。

Boot ROM 代码跑起来后,会检查 Flash 是否为空、是否有有效程序。如果一切正常,它再把VTOR改成0x08000000,然后跳转到用户的Reset_Handler

这就是为什么你能通过串口或 USB 烧录一个空片子的原因——因为 Boot ROM 在0x00000000接住了复位,给了你烧录程序的机会。

如果默认VTOR直接指向 Flash,空片就变砖了。


四、那0x00000000到底映射到哪?

这里容易混淆,必须讲清楚。

物理上0x00000000这个地址不一定对应固定的存储介质。芯片厂家通过总线矩阵或重映射控制器,可以动态改变0x00000000指向谁。

常见的设计有:

芯片系列0x00000000上电映射运行后可重映射到
STM32F1Flash(0x08000000)或 Boot ROM,由 BOOT0/1 引脚决定固定,不可软件重映射
STM32F4由 BOOT 引脚决定,可选 Flash / 系统存储器 / SRAM固定
NXP LPC17xxBoot ROM软件可重映射到 Flash 或 RAM
TI LM3SFlash(0x00000000直接就是 Flash 首地址)固定

STM32 的设计比较特别:它没有传统的“内存重映射”机制,而是用地址别名的方式。当你把VTOR设为0x08000000时,中断发生时硬件直接从0x08000000读向量,0x00000000处的原始向量表就不再使用了。

但不管怎么实现,逻辑上都是:复位时 CPU 访问0x00000000,芯片保证这个地址上有有效的向量。


五、向量表里那个加 1 的秘密

上一篇文章提过一句:向量表里所有函数地址的LSB(最低位)必须是 1

为什么?

Cortex-M3 只支持 Thumb 指令集。而 ARM 体系规定:跳转到一个地址时,如果 LSB=0,CPU 会认为这是 ARM 指令,试图切换到 ARM 状态——但 Cortex-M3 根本没 ARM 状态,所以立刻 HardFault。

所以向量表里存的不是函数地址本身,而是函数地址 + 1

硬件取到这个值后,会自动把 LSB 清 0,得到真实的跳转地址,同时知道“这是在 Thumb 状态下执行”。

这个设计是 ARM 的历史包袱,但也是保证 Cortex-M3 只跑 Thumb 代码的一道硬件锁。


六、VTOR 重定位的实战意义

聊到这儿,VTOR的价值就出来了。

场景一:Bootloader + App

你的 Bootloader 在 Flash 开头(0x08000000),App 在后面的某个偏移(比如0x08008000)。

App 里必须有自己的向量表。所以 App 启动后第一件事就是:

SCB->VTOR=0x08008000;

这样中断发生后,硬件会去0x08008000找向量,而不是 Bootloader 的0x08000000

如果不改 VTOR,所有中断都会跑进 Bootloader 的中断服务函数,App 永远收不到中断。

场景二:RAM 中调试

有些调试场景下,你把程序直接下载到 RAM 里运行(比如0x20000000)。这时向量表也在 RAM 里。你必须:

SCB->VTOR=0x20000000;

否则 CPU 还是会去0x000000000x08000000找向量,而那里根本没有你的中断函数。

场景三:动态中断 Hook

在一些特殊场合(比如安全监控),你可以在运行时把某个中断向量替换成监控函数的地址,实现无侵入的拦截。这比用函数指针 Hook 更底层,也更隐蔽。


七、举一反三

问题 1:RISC-V 有没有向量表?

有类似的概念,但机制不同。

RISC-V 的向量表叫mtvec(Machine Trap-Vector Base Address)。它有两种模式:

  • 直接模式:所有中断和异常都跳转到同一个地址(mtvec的值)。软件自己去读mcause寄存器判断中断类型。
  • 向量模式:类似 ARM 的向量表,每个中断有独立的入口偏移。

但 RISC-V 的向量表没有规定必须放在地址 0。复位入口地址是硬件写死的(比如0x00001000),和向量表无关。

问题 2:为什么 Cortex-M3 的向量表大小是固定的?

不是固定的。VTOR只定义了基址,向量表的大小由芯片厂家决定。

理论上,VTOR可以指向任何 128 字节对齐的地址(因为向量表至少需要对齐到 32 项的边界)。实际芯片有多少个中断,向量表就多大。STM32F103 有 60 多个中断,向量表大小约 300 字节。超出部分读回来是 0,硬件不关心。

问题 3:为什么启动文件中.sct要给向量表加+FIRST

因为向量表必须是执行域的第一个东西。

如果链接器把其他代码或数据放在了向量表前面,VTOR指向的地址就不是真正的向量表了。CPU 会取到错误的值,直接跑飞。

+FIRST强制向量表放在最开头,保证0x08000000处就是__initial_sp


八、写在最后

回到最初的两个问题:

为什么 Cortex-M3 需要向量表?
因为它把中断入口从“固定跳转地址”变成了“可配置的函数指针数组”,让中断处理程序可以放在任何地方,也让动态重定位成为可能。

为什么向量表必须放在地址 0 附近?
因为VTOR复位默认值是0x00000000。这个设计既是历史兼容性的妥协,也是芯片启动安全性的兜底——在用户程序还没跑起来之前,Boot ROM 能在0x00000000接住复位,避免空片变砖。

理解了向量表,你就理解了 Cortex-M3 中断系统的骨架。

下次有人问你“中断是怎么找到服务函数的”,告诉他:硬件只是去一个数组里读了个函数指针,然后跳过去。就这么简单。


下一篇预告:《为什么启动文件通常用汇编而不是 C 写?》——在 C 环境建立之前,汇编是唯一能裸奔的语言。

关注「OneChan」,不错过后续 84 篇“为什么”。

http://www.jsqmd.com/news/686021/

相关文章:

  • 聊聊2026年华聊可不可以运作,深圳哪些社交软件性价比高? - 工业推荐榜
  • 前端资源加载管理
  • 用户故事管理化技术中的用户故事计划用户故事实施用户故事验证
  • 别再用暴力枚举了!PTA L1-006连续因子题,用数学优化把复杂度降下来
  • 宁波推荐工商注册公司服务费用大概多少钱 - myqiye
  • 别再只用timeNow了!CAPL时间函数全解析:从毫秒到纳秒,精准掌控你的CANoe测试时序
  • GPU实例选型指南:从推理到训练的全场景适配
  • 2026年靠谱的广州烘干机/离心烘干机/热风烘干机主流厂家对比评测 - 品牌宣传支持者
  • Spring Boot 多线程任务池管理技巧
  • 从Sensor到屏幕:深入浅出聊聊Camera 3A算法里的那些“坑”与优化实战
  • 英文论文AI率居高不下?实测6款降AI工具,教你写出地道“学术风”
  • 如何查看物化视图DDL_DBMS_METADATA.GET_DDL提取完整的视图与日志语句
  • 2026好用的持久净水炭,高性价比净水活性炭供应商推荐 - 工业推荐榜
  • ESP32开发环境Python依赖报错?别慌,这份保姆级排查指南帮你搞定(附ESP-IDF V4.2实战)
  • 别再乱用Instant和Duration了!用UE5 GAS的Gameplay Effect,完整构建你的角色Buff/Debuff系统
  • RWKV-7 (1.5B World)流式输出优化:WebSocket协议适配与前端渲染技巧
  • 3DMAX插件避坑指南:Geometry Projection几何投影安装后没反应?可能是你的‘标准基本体’没转换
  • 【Docker网络隔离终极指南】:20年运维专家亲授5种生产级隔离配置方案,99%的团队都用错了
  • Windows屏幕标注终极指南:免费开源工具ppInk的完整教程与实战应用
  • 嵌入式Linux开发踩坑记:TI AM62x平台SD卡初始化报错-110的完整修复流程
  • AI Agent 开发: 你需要知道的 9 个核心技术 -- 从 ReAct 到多 Agent 协作的技术全景
  • 2026年除重金属净水炭费用大揭秘,哪家收费合理 - myqiye
  • pidgenx.dll文件丢失找不到怎么办?免费下载方法分享
  • Phi-mini-MoE-instruct多语言效果:中→英→法→中回译保真度测试与语义一致性分析
  • CardEditor:3MB桌面软件如何让桌游卡牌制作效率提升300%?
  • 2026年评价高的广州塑料甩干机/不锈钢甩干机/离心甩干机公司选择指南 - 行业平台推荐
  • CCC数字钥匙NFC车主配对全流程解析:从准备到收尾的五个关键阶段
  • 3分钟搞定Windows任务栏美化:TranslucentTB终极透明化指南
  • Redis Sentinel 高可用架构
  • 从RPA到PlayWright:我用Java重写Boss直聘爬虫的完整心路与代码