拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CLion编译STM32报错.ARM.extab?链接脚本与C++异常处理全解析

CLion编译STM32报错.ARM.extab?链接脚本与C++异常处理全解析

最近用CLion编译STM32Cube初始化工程时,我被一个链接报错卡了很久。报错信息是non constant or forward reference address expression for section .ARM.extab。第一眼看到这个错误,我下意识以为是CLion的Toolchain配置出了问题,或者CMake哪里没配对,结果围着IDE来回折腾了半天,问题纹丝不动。后来才意识到,这个报错跟CLion半点关系都没有,根子在链接脚本和C++异常处理上。

如果你也用CLion做STM32开发,遇到过同样的报错,或者对.ARM.extab这个段感到陌生,这篇文章应该能帮上忙。我会从报错现场讲起,把这个错误涉及的原理拆开讲清楚,然后给出两条实际可行的解决路径,最后附上我排查过程中踩过的坑和一份速查表。

1. 报错场景复现:CLion里到底发生了什么

1.1 我的编译环境与复现步骤

先说下环境,方便你对照。我用的CLion版本是2023.2,工具链是arm-none-eabi-gcc10.3.1,配合STM32CubeMX6.9初始化工程,目标芯片是STM32F103C8T6。CubeMX生成工程的时候,我为了在代码里写C++类,把工程语言切到了C++,生成之后直接用CLion打开CMakeLists.txt开始编译。

编译流程走到链接阶段时,CLion的Build窗口刷出一段报错:

[ 50%] Linking CXX executable firmware.elf /opt/gcc-arm-none-eabi/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld: non constant or forward reference address expression for section .ARM.extab collect2: error: ld returned 1 exit status make[3]: *** [CMakeFiles/firmware.elf.dir/build.make:63: firmware.elf] Error 1 make[2]: *** [CMakeFiles/firmware.elf.dir/all:94: firmware.elf] Error 2

注意看关键信息:编译器没有报错,是链接器ld抛出的异常。这意味着源文件编译都通过了,问题出在最后一步把各个目标文件拼成可执行文件的时候。搞清楚这一点很重要,因为很多人看到CLion报错就以为IDE设置有问题,实际上IDE只是把链接器的错误信息原样展示出来了。

1.2 为什么说这个报错不能全怪CLion

CLion本身只是一个集成开发环境,真正干活的是外部的工具链。对于STM32 Cube工程来说,CLion调用的是CMake + arm-none-eabi-gcc + arm-none-eabi-ld。

当时我为了确认是不是CLion的锅,直接在项目根目录下打开终端,手动执行了cmake --build .,结果抛出了和CLion窗口里一模一样的错误。这个验证动作直接排除了CLion自身配置的因素,把问题锁定到了链接器脚本和工具链的交互上。

很多人在这一步会反复检查CLion的Toolchain设置、CMake选项、环境变量路径,这些折腾其实没什么意义。嵌入式编译流程里,如果碰到的是链接阶段的报错,首先怀疑的应该是链接脚本(.ld文件)、目标文件段分布、内存区域配置这三个方向,IDE只是背锅的。

2. .ARM.extab到底是什么,凭什么影响我的链接

2.1 异常展开表与C++代码的关系

.ARM.extab是ARM架构里专门存放异常展开表的段。看到“异常”两个字,你应该已经猜到了,这跟C++的try/catch机制有关。

当C++代码中使用异常处理时,编译器会在每个可能抛异常的函数里埋入额外的元数据。一旦运行时真的抛出异常,CPU需要从当前函数逐层回溯调用栈,找到对应的catch处理器。ARM体系的处理器要完成这个回溯,就得靠两张表:

  • .ARM.exidx:索引表,记录了每个函数对应的展开信息入口。
  • .ARM.extab:实际的展开表,描述了函数栈帧的布局、如何恢复调用者的PC和SP等细节。

链接器最终要把这两张表放到某个固定的内存区域(通常是FLASH只读区),并且要确保程序运行时能通过__exidx_start和__exidx_end这两个符号找到表的边界。在STM32CubeMX生成的标准链接脚本里,已经有对应的处理:

.ARM.extab : { . = ALIGN(4); *(.ARM.extab* .gnu.linkonce.armextab.*) . = ALIGN(4); } >FLASH .ARM : { . = ALIGN(4); __exidx_start = .; *(.ARM.exidx*) __exidx_end = .; . = ALIGN(4); } >FLASH

关键问题在于:如果链接脚本里这段定义缺失、顺序被改动,或者通配符写得不全,链接器就无法为.ARM.extab计算出合法的地址,自然就报出non constant or forward reference address expression。

2.2 链接脚本怎么给段分配地址

链接脚本可以理解为一张“段落地图”。编译完的目标文件里包含很多段,比如.text(代码段)、.rodata(只读数据段)、.data、.bss,链接器需要知道每个段放到内存地址的哪个位置。

STM32CubeMX生成的.ld文件里,MEMORY命令规定了芯片的物理内存布局,SECTIONS命令则描述了每个输出段放在哪个内存区域。例如:

MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 64K }

如果.ARM.extab这个输出段在SECTIONS里没有被正确映射到FLASH区域,或者所在的区域定义本身就有问题,链接器在尝试为它确定VMA(虚拟地址,也就是运行时地址)时就会失败。

2.3 “non constant or forward reference address expression”这句话的实质

这句话翻译过来是:“无法为.ARM.extab段计算恒定的地址,或者表达式中包含前向引用。”

我用一个生活化的类比来解释。链接器就像搬家公司的调度员,段就是一件件待搬运的家具,内存区域就是房间。调度员要提前给每件家具安排一个摆放位置,但如果他手里只有一张不完整的清单,或者清单上写着“沙发放X位置,而X位置的说明要等桌子确定了才有”,那调度员当然会罢工。

具体到.ARM.extab,常见触发原因有这么几种:

  • 链接脚本里.ARM.extab段的起始地址ALIGN(4)依赖的当前位置.还没确定,或者依赖了后面才定义的符号。
  • .ARM.exidx段里没有定义__exidx_start和__exidx_end,而工具链在计算.ARM.extab地址时需要引用这两个符号。
  • 链接脚本里某个内存区域或符号名称拼写错误,导致地址表达式无法求值。
  • 工程里用了较新版本的GCC编译器,但链接脚本是旧工程拷贝过来的,语法或段命名有差异。

理解了根因,后面给方案就有方向了。

3. 方案一:把链接脚本改对,.ARM.extab就能安分落地

3.1 先打开链接脚本,检查关键段落

链接脚本一般位于工程根目录,后缀是.ld,在CMakeLists.txt里可以通过搜索-T参数找到。比如我工程里的CMakeLists中有这样一行:

target_link_options(${PROJECT_NAME}.elf PRIVATE -T${CMAKE_CURRENT_SOURCE_DIR}/STM32F103C8Tx_FLASH.ld )

打开这个.ld文件,用编辑器搜索.ARM.extab和.ARM.exidx。正常情况下应该能看到我前面贴的那两段。如果完全搜不到,那问题基本就定位了——脚本里压根没有给异常展开表安排位置。

如果搜到了但仍然报错,就要仔细检查细节。我见过最典型的一种错误写法是:

.ARM.extab : { *(.ARM.extab) } >FLASH

这里的*(.ARM.extab)少了末尾的通配符星号。当GCC编译C++代码时,生成的段名可能是.ARM.extab.text.main这种带后缀的子段,精确匹配*(.ARM.extab)根本匹配不到,展开表数据就被排除在输出段之外,最终一样导致链接失败。

正确的写法是:

.ARM.extab : { . = ALIGN(4); *(.ARM.extab* .gnu.linkonce.armextab.*) . = ALIGN(4); } >FLASH

注意*(.ARM.extab*)最后的星号*,它才能把.ARM.extab开头的所有变体都囊括进来。

3.2 三种常见的错误写法与修正方式

我梳理一下几种我遇到过或查资料确认过的错误情况。

情况一:.ARM.extab和.ARM.exidx段落整个被删掉了。有些人在精简脚本时觉得这些段可有可无,直接删掉。解决方法最简单,把标准脚本模板里的两段补回去。位置一般放在.text和.rodata之后、.data之前。

情况二:__exidx_start和__exidx_end未定义。工具链在链接时会有隐式引用,如果脚本里.ARM.exidx段没有定义这两个符号,链接器可能报前向引用错误。确保按标准写法补上:

.ARM : { . = ALIGN(4); __exidx_start = .; *(.ARM.exidx*) __exidx_end = .; . = ALIGN(4); } >FLASH

情况三:脚本把段放到未定义的内存区域。比如有人写了>ROM,但MEMORY中定义的区域叫FLASH,链接器不知道ROM是什么,地址表达式无法求值。检查一下区域名称是否跟MEMORY里的定义完全一致。

3.3 改完之后记得重新加载工程并清理

改完链接脚本后,有几个容易被忽略的步骤:

  • 在CLion里右键点击CMakeLists.txt,选择Reload CMake Project,让CMake重新解析链接脚本路径。
  • 执行一次Build > Clean,然后重新Build。这里多说一句:如果只改脚本不清理,有些旧的构建缓存文件会让链接器仍然使用旧的段布局信息,报错可能原样复现。

我第一回改完脚本,编译居然还是报同样的错,当时差点怀疑是脚本没生效。后来发现是build目录里的firmware.elf和.map文件还是旧的,清理重建之后才恢复正常。

4. 方案二:嵌入式端本来就很少用C++异常,关掉它更省心

4.1 哪些情况下可以放心关掉异常

在我的实际开发经验里,STM32这类资源紧张的MCU上,裸机程序或RTOS任务里用到C++异常的场景其实非常少。异常机制本身需要额外内存存放展开表,还会增加代码体积,对于Flash和RAM都有限制的嵌入式设备来说,有时候弊大于利。

如果你的工程满足以下条件,直接关闭C++异常是性价比最高的选择:

  • 代码中没有使用try/catch,或者即使有,也只用了一些简单逻辑。
  • 没有使用依赖异常机制的第三方C++库。
  • 对代码体积有强烈要求,希望在编译阶段就砍掉异常展开表相关的开销。

关掉异常之后,.ARM.extab段根本不会被生成,链接器自然也就不会因为它的地址问题而报错了。

4.2 在CMakeLists.txt里怎么配置

在CLion工程中,找到CMakeLists.txt,找到项目对应的target,在编译选项中添加-fno-exceptions和-fno-rtti。C++的RTTI(运行时类型识别)也和异常一样会引入额外元数据,嵌入式环境中通常也不需要。

比较全局的做法是这样:

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions -fno-rtti")

如果只想针对当前目标生效,可以在add_executable之后添加:

target_compile_options(${PROJECT_NAME}.elf PRIVATE -fno-exceptions -fno-rtti)

这里的${PROJECT_NAME}.elf要和CMakeLists里add_executable定义的目标名保持一致。STM32CubeMX生成的CMakeLists中,目标名通常是项目名.elf这种形式。

修改完成后重载CMake工程,重新编译,这个链接报错一般就会消失。因为编译器不再为C++代码生成异常展开相关的段,.ARM.extab和.ARM.exidx自然就没有内容需要链接器放置了。

4.3 关掉异常会带来哪些连带影响

这里必须说清楚,关掉异常不是没有代价的。

最直接的影响是,如果代码里用了try/catch,编译会直接报错。标准库里的某些接口(比如std::vector::at()、std::map::at())在越界或找不到元素时会抛异常,异常被关闭后,这些接口的行为会变成调用std::terminate,程序直接死掉。所以在关闭异常之前,建议先全局搜索一下代码里有没有try和catch关键字。

另外,关闭RTTI后,dynamic_cast和typeid这些功能也不能用了。如果工程中大量使用多态和类型判断,这个改动的影响面会比想象中大。我个人的建议是:如果工程里根本没用多态,关RTTI无所谓;如果用了多态但没用到dynamic_cast,也可以关;什么时候要慎重呢,就是你依赖第三方库的时候,第三方库可能强制要求开启RTTI。

如果你只是想让这个报错消失,同时工程里也没用到异常机制,直接用方案二就行。如果你确实需要异常处理,那就老老实实回头改链接脚本,也就是方案一。

5. 排查这个报错时踩过的坑和速查表

5.1 别急着在CLion里折腾,排查顺序有讲究

这次排查下来,我最想分享的经验是:碰到这种链接错误,排查顺序真的很重要。

第一步,先在命令行里手动跑一次CMake构建,确认报错与IDE无关。这一步花不了两分钟,却能排除掉大量变量。第二步,检查链接脚本本身,看.ARM.extab和.ARM.exidx段是否完整、是否写进了正确区域。第三步,检查CMakeLists里的链接选项,确认-T参数指向的脚本确实是当前生效的那个文件。第四步,清理构建目录重新编译。

我当时就是顺序反了,先在CLion设置里翻来覆去地改Toolchain路径,改CMake选项,不仅浪费时间,还让问题看起来更玄乎。其实回归本质,把命令行的构建输出当作权威信息,排查效率会高很多。

5.2 三个容易忽视的坑

第一个坑:链接脚本来自旧工程。如果把以前的老工程脚本直接拷过来用,可能缺少新工具链需要的段定义。现在的CubeMX版本生成的脚本已经比较完善,但老版本或网友分享的脚本就不一定了。

第二个坑:build目录缓存。修改链接脚本后,如果不清除旧的构建缓存,链接产物可能不会更新,导致报错复现。这个在前面提过,这里再强调一次,因为它实在太容易坑人了。

第三个坑:CubeMX重新生成代码会覆盖链接脚本。这个是最隐蔽的。CubeMX每次点"Generate Code"都会重新生成.ld文件,你手工改的内容会全部消失。如果不想被覆盖,建议把简历脚本另存为一个名字,比如叫my_linker.ld,然后在CMakeLists里把-T参数指向它。这样CubeMX重新生成时就不会动了。

5.3 问题排查速查表

我把这次排查过程中用到的一些判断点和处理方式整理成了表格,方便你对照查找:

报错现象或判断点可能原因处理方式
链接时报non constant or forward reference address expression for section .ARM.extab链接脚本中缺失.ARM.extab/.ARM.exidx段定义补全标准段定义,参考3.1节
脚本中只有*(.ARM.extab),没有星号通配符编译器生成的是带后缀的子段,精确匹配未覆盖改为*(.ARM.extab*)
脚本中.ARM.exidx段没有__exidx_start和__exidx_end符号工具链需要这两个符号定位展开表边界按标准脚本补全
CMake构建手动执行也报错与CLion无关,问题在链接脚本或CMake链接选项重点检查-T参数指向的脚本
CLion清理重建后仍报同样的错构建目录缓存残留旧段布局信息手动删除build目录后重新构建
CubeMX重新生成代码后问题重现手工修改的链接脚本被CubeMX覆盖改用自定义链接脚本名,在CMake中指定
工程用不到C++异常,仅需尽快编译通过.ARM.extab展开表产生是异常机制导致加-fno-exceptions -fno-rtti编译选项
工程确实需要异常处理关闭异常不可行修复链接脚本,保留展开表段定义

这张表基本覆盖了我能想到的所有情况。实际排查时,从第一行开始往下走,每排除一项就重新编译一次,很快就能确定根因。

说实话,这个报错本身的技术难度不算高,但它撞上了嵌入式开发里一个容易忽略的盲区:链接脚本。我们平时关注的是怎么写代码、怎么调外设,很少有人意识到这些看不见的段承载了多少运行时机制。这次解决之后,我再也不随便精简链接脚本里的段定义了。宁可多留一些标准片段,也不要为了“看起来简洁”给后面挖坑。

返回列表