本节摘要:Thumb-2 是 ARM32 世界的"双轨制":16 位指令管代码密度,32 位指令管功能齐全,两者在同一段代码里自由混排。本节用同一个函数在 ARM 与 Thumb-2 两种编码下的尺寸对照开始,讲到状态切换指令
bx的低比特玄机与.thumb_func标记的必要性,是一份可以直接照做混编实验的实录。
前两节拆的都是定长四字节的 AArch64。但嵌入式世界的很大一部分至今跑在 32 位 ARM 上,那里存在一套不同的安排:T32(俗称 Thumb-2)把两字节与四字节指令混在一起用。这一节不是怀旧——真实单片机固件、boot 代码、旧 BSP 的启动文件里 T32 是主流,读懂它是嵌入式方向的必修课。我们照本书的惯例做实验:先跑出数据,再拆原理。
Thumb 诞生的动机在 2.1 节提过一句:上世纪九十年代的片上 ROM 以 KB 计,指令每省两字节,成本就实打实降一分。纯 16 位的第一代 Thumb 指令少、功能残(访问不了太多寄存器组合),ARM 公司后来在 ARMv6T2 引入 Thumb-2:以 16 位指令为主干、按需插入 32 位指令补全功能。结果是一套"按需伸缩"的编码——常用操作两字节搞定,复杂操作才动用四字节。A64 的世界里没有这个烦恼(一律四字节),所以这节的经验只适用于 A32 与 T32 双态的处理器。
实验对象还是累加函数,这次用 ARM32 工具链编两份,一份 ARM 态一份 Thumb 态:
arm-linux-gnueabihf-gcc -marm -O2 -c sum.c -o sum_arm.o arm-linux-gnueabihf-gcc -mthumb -O2 -c sum.c -o sum_thumb.o arm-linux-gnueabihf-size sum_arm.o sum_thumb.o # 产物尺寸对比:text 段 thumb 版通常缩小两成上下,循环越短差距越明显
反汇编对比核心循环,差异比尺寸更直观:
ARM 态(A32,一律四字节): 3c: e1a01003 mov r1, r3 40: e5b32004 ldr r3, [r3, #4]! 44: e0811003 add r1, r1, r3 48: e2522001 subs r2, r2, #1 Thumb-2 态(T32,两字节与四字节混排): 34: 1c5b adds r3, r3, #1 // 两字节:常用短指令 36: 6859 ldr r1, [r3, #4] // 两字节 38: 4419 add r1, r1, r3 // 两字节 3a: 3a01 subs r2, r2, #1 // 两字节 3c: d1fa bne 34 // 两字节:短距离跳转
注意 Thumb 版地址列的步进:多数行差 2,个别行差 4——这就是混排的指纹。解读:省出来的尺寸来自两处,一是高频短指令压到 16 位,二是循环体短了之后条件跳转可以近距离表达。变式:把函数里加一行用高编号寄存器的复杂寻址,再看 Thumb 版出现四字节指令的位置——T32 的分配规律是"常用与简单归 16 位,复杂与罕见归 32 位",编译器自动选择,写汇编的人也可以显式选择。

两套编码怎么在运行中切换?答案藏在 bx(分支并交换)与 blx 指令的目标地址最低位里:最低位是 1,切进 Thumb 态;是 0,切进 ARM 态。地址最低位不参与真实寻址(指令至少两字节对齐),被征用为"模式旗标"。这带来一个著名的坑:从 C 里调用一个 Thumb 函数,函数指针的最低位必须是 1,否则处理器按 ARM 态译码 Thumb 代码,轻则行为错乱重则非法指令异常。汇编手写时用 .thumb_func 标记函数,链接器会替你把符号地址补上旗标:
.syntax unified .thumb // 本文件按 Thumb 态汇编 .global sum .type sum, %function sum: .thumb_func // 声明:这是 Thumb 函数,符号带模式旗标 movs r2, #0 // 注意 movs:T32 里很多运算默认刷标志 .loop: ldr r3, [r0], #4 adds r2, r2, r3 subs r1, r1, #1 bne .loop mov r0, r2 bx lr // 返回:按 lr 最低位决定回到哪种态
三处细节连着前文:.thumb_func 是 3.2 节"伪指令"精神的延伸——标记给工具链看,旗标给处理器看;bx lr 的返回同时完成"降级回调用方的态",ARM 态与 Thumb 态函数互调因此是透明的;T32 的 movs、adds 默认刷标志,与 A64"必须显式加 s 才刷标志"的克制风格不同,移植代码时条件标志的时序差异要专门核对。
第一代 Thumb(T16 时代)没有条件后缀可用,ARM 用一个特殊构造补位:IT 块——ITTEE EQ 开头的一行声明"接下来四条指令分别按等于条件执行、反条件执行、反条件执行、无条件执行"。它把 A32 的"条条可条件化"压缩成了一种可读性很差的包装。ARMv8 的 AArch32 态还兼容 IT 块,A64 则彻底删除——读现代 A64 反汇编时看不到 it 指令,看到 it 就知道脚下是 32 位世界。
真实固件里做一次观察更直观:找一块 ARM32 开发板的官方固件(或用工具链编一段最小裸机程序),objdump 打开后数一数:入口附近的中断向量表是四字节步进还是跳转桩、函数序言用 push 还是 stm、地址列步进 2 与 4 的比例。多数商用固件 Thumb 占比在九成以上——结论很务实:嵌入式方向的面试与日常,Thumb 读得比 ARM 态还熟才够用。
问:混编工程里怎么决定某个文件用哪种态? 答:按"密度敏感度"分。体积大头(业务逻辑)编 Thumb,性能关键小段(中断里的极短处理、位操作热点)单独放 ARM 态文件并在链接时显式安置;切换点尽量少,每次 bx 跨态都有流水线重定向的小代价。编译驱动工程用 #pragma 与编译开关按文件控制,纯手工工程用 3.3 节正文里的 .thumb 指令按文件声明。
bx 与 blx 是双向门。.thumb_func 必写:漏写会导致函数指针缺旗标,跨态调用直接翻车。指令与编码两章到此收官。下一章进入数据搬运的主战场:内存访问机制与寻址模式,那里藏着 ARM 汇编最漂亮的语法设计。