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 为什么说这个报错不能全怪CLionCLion本身只是一个集成开发环境真正干活的是外部的工具链。对于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编译选项工程确实需要异常处理关闭异常不可行修复链接脚本保留展开表段定义这张表基本覆盖了我能想到的所有情况。实际排查时从第一行开始往下走每排除一项就重新编译一次很快就能确定根因。说实话这个报错本身的技术难度不算高但它撞上了嵌入式开发里一个容易忽略的盲区链接脚本。我们平时关注的是怎么写代码、怎么调外设很少有人意识到这些看不见的段承载了多少运行时机制。这次解决之后我再也不随便精简链接脚本里的段定义了。宁可多留一些标准片段也不要为了“看起来简洁”给后面挖坑。
返回列表