这个L6236E报错,只要搞过Keil MDK开发的朋友,多多少少应该都撞见过。它长得比较吓人,又是.sct文件,又是error级别,但实际处理起来,绝大多数情况都是几分钟的事。这篇文章我会把这个错误的来龙去脉、真正触发的原因、一步步排查的方法,以及几个不太容易想到的冷门坑都捋一遍。不管你用的是AC5还是AC6编译器,不管你是新手还是老手,按文中的顺序走一遍,基本都能解决。
1. L6236E这个错到底在报什么
1.1 报错原文逐字段拆解
先看这行完整的错误信息:
.\Objects\xxx.sct(7): error: L6236E: No section matches selector - no section to be FIRST/LAST.把这行拆开看,其实信息量很大:
.\Objects\xxx.sct(7):说明问题出在链接阶段的分散加载文件里,具体是第7行。注意,这里的路径是工程编译中间产物里的.sct文件,不是你源文件目录下的工程文件。L6236E:这是ARM链接器armlink生成的错误编号,L开头表示Linker,6236是个错误码。No section matches selector:没有任何输入段(section)能匹配上这个选择器。no section to be FIRST/LAST:因为没有匹配的section,所以链接器无法把这个段放到执行区的起始或末尾位置。
翻译成人话就是:链接器在打"包"的时候,.sct文件里规定某个执行区(Execution Region)的第一个位置(FIRST)或最后一个位置(LAST)必须是某个指定的东西,但链接器找遍了所有编译产物,发现根本没有这个东西可选。于是它罢工了。
1.2 .sct文件在链接时干了什么
.sct全称是Scatter File,中文叫分散加载描述文件。它在单片机工程里的作用,简单来说就是告诉链接器:"你的代码、数据、堆栈,分别应该放到存储器的哪些位置。"
一个典型的Cortex-M工程里,.sct文件长下面这个样子:
; 12345678.sct LR_IROM1 0x08000000 0x00080000 { ER_IROM1 0x08000000 0x00080000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00010000 { .ANY (+RW +ZI) } }注意看第一行执行区ER_IROM1里的*.o (RESET, +First),它的意思就是:把任意目标文件里名为RESET的section,放到这块存储区的最前面。为什么要把RESET放最前面?因为STM32这类芯片上电后,从0x08000000开始读取栈顶地址,紧接着读取Reset_Handler的入口地址。这块区域是向量表,必须从Flash起始地址开始放,顺序一乱,程序启动就飞了。
所以,.sct里每个执行区选的"首"和"尾"都是被硬件启动要求或特定设计逻辑锁定的,缺了它,链接器不知道该怎么排布内存布局,只能报错。
2. 会触发L6236E的常见情况
2.1 启动文件缺席或没进工程
这是最最常见的原因,没有之一。启动文件(Startup)是负责定义堆栈、中断向量表、启动代码的核心汇编文件,通常叫startup_stm32f10x_hd.s之类。
如果新建工程的时候忘了把启动文件加进去,或者加了之后不小心删掉了,又或者文件显示灰色没有参与编译,那么RESET向量表对应的section就压根不存在。此时.sct里*.o (RESET, +First)自然匹配不到任何东西,L6236E就冒出来了。
判断方法很简单,打开工程左侧Project栏,展开Application/User相关分组,看Startup文件是否在列,并且文件名是否正常显示。如果文件被排除编译,图标上会有一个类似减号或禁止的标记。
2.2 AC5与AC6编译器版本不匹配
这个问题在近几年的工程迁移中尤其突出。Keil MDK从5.x版本开始默认集成了AC6(基于armclang),同时保留AC5(基于armcc)。
老工程大多数沿用AC5对应的启动文件,语法是基于ARM汇编风格的,比如:
AREA RESET, DATA, READONLY而AC6要求启动文件必须是GNU风格的ARM汇编,对于Cortex-M系列,代码区别其实不是特别大,但指令伪指令的写法、宏定义的处理、节名的声明方式都有差异。同一种启动文件在AC5和AC6下的报错逻辑不同,有时AV6会直接报语法错误,但有些时候编译能过,链接阶段却出现L6236E,因为编译器处理sections的方式不一致。
2.3 分散加载描述与section名对不上
有些工程师喜欢自己改.sct文件,比如把代码区拆成两部分,一部分放IROM1,一部分放IROM2,然后在每个执行区里都写了xxx.o (SECTIONNAME, +First)。
如果这么写了,但对应的目标文件或者section名拼写错了,比如把STARTUP写成了START,或者把RESET写成了RESERT,链接器一样匹配不到。要特别留意.sct文件里大写小写的问题,ARM链接器对符号名的大小写敏感,reset和RESET不是同一个东西。
2.4 条件编译让向量表凭空消失
这种相对隐蔽。启动文件里有时会这样写:
IF :LNOT::DEF:__STARTUP_CONFIG IMPORT SystemInit IMPORT __main ENDIF IMPORT |Image$$ARM_LIB_STACK$$ZI$$Limit|加上ST公司的宏定义、芯片型号未定义或定义错误的时候,某些vector entry被条件编译活生生跳过了,导致最终保留的section结构不完整。还有一种情况是所有DCD指令被编译成了空的向量表,链接器虽然找到了RESET section,但是KEYWORDS列表不匹配,链接时依旧报FIRST/LAST错误。
3. 按这个顺序排查,基本都能解决
3.1 第一步:确认启动文件是否真的参与编译
这个步骤其实最容易被忽略,因为只要之前工程能用,很少有人怀疑启动文件不见了。实际上,我遇到过好几次这样的情况:工程从A电脑拷贝到B电脑之后,启动文件路径失效,显示的是黄色感叹号,但工程还能打开,编译和链接时不报"文件找不到",而是直接当成不存在。原因在于MDK对缺失文件会跳过,但不会主动提示,链接阶段就表现为找不到section。
操作建议:
- 右键点击工程名,选择Manage Project Items。
- 找到放置启动文件的分组,看文件名前有没有异常标记。
- 如果文件丢了,直接从安装目录拷贝一个对应型号的启动文件进来。
- 右键启动文件,确认勾选了Include in Target Build。
如果启动文件状态正常,却还是报错,再看下一步。
3.2 第二步:核对编译器版本与启动文件版本
点击魔术棒(Options for Target),进入Target页面,看右侧ARM Compiler下拉框选的是什么版本。
- 如果选的是
Use default compiler version 5,对应AC5。 - 如果选的是
Use default compiler version 6,对应AC6。 - 有的工程还选了
Version 5.06 update 7 (build 960)这种具体版本号。
知道编译器版本以后,再看启动文件是哪个版本。MDK安装目录下,不同编译器版本对应的启动文件目录不一样。如果工程里的启动文件是AC5时代的老文件,而你切到了AC6,就会有兼容性问题。
有个快速验证方法:把工程里的启动文件替换成当前MDK安装目录下对应芯片厂商、对应型号的startup_xxx.s文件。比如ST官方库里的Device目录下,一般同时有ARM和GCC两个版本的启动文件,AC6用ARMGCC兼容的风格也没问题。
这里有一个非常具体的点,AC6下打开老工程会报L6236E,原因往往出在RESET区域的定义上。AC5的汇编器会默认帮你在AREA声明里补全属性,而AC6不会。如果一个启动文件开头只写了:
PRESERVE8 THUMB AREA RESET, DATA, READONLY在AC5下编译完全没问题,但AC6下可能仍然编译通过,却在链接时报FIRST/LAST错误。解决办法不是改.sct,而是把启动文件源码里那段AREA RESET的定义改成完整形式:
AREA RESET, DATA, READONLY或者使用更符合GNU规范的:
.syntax unified .section .isr_vector,"a",%progbits很多人不熟悉,直接从ST官方新固件包里拿最新的startup文件替换即可,ST已经适配了AC6。
3.3 第三步:恢复官方默认.sct再增量修改
这是个特别实用的排错手段。如果你自己动过.sct文件,又不太确定改得对不对,最快的方式是恢复默认配置,让Keil自己生成分散加载描述.
操作路径:
- 打开Options for Target -> Linker页面。
- 取消勾选
Use Memory Layout from Target Dialog。 - 此时你可以看到当前的.sct内容,先备份一下,然后清空写回默认配置。
- 重新勾选
Use Memory Layout from Target Dialog,点击OK。
恢复默认后,系统会根据Target页面里的ROM/RAM地址范围自动生成一份.sct,这份文件理论上不会出现FIRST/LAST错误。如果恢复默认后编译还报错,那问题一定出在工程文件本身,而不是你的分散加载配置。
如果恢复默认后错误消失,说明你的原始.sct文件写错了。此时再增量修改,每次只改一小部分,编译一次,就能很快定位到是哪条规则被链接器拒绝。
3.4 第四步:用map文件和fromelf验证section是否生成
这一招很少在基础教程里被提到,但排查这类问题非常管用。
在编译完成后,进入Objects或者Listings目录,找到.map文件。这是链接器生成的存储器映像文件,里面记录了所有被列入链接的section,以及每个section的起始地址、大小和所属的目标文件。
打开.map文件,搜索RESET,看能不能找到类似这样的行:
RESET 0x08000000 Section 384 startup_stm32f103xe.o如果找到了,说明RESET section在链接阶段是存在的,报错点可能在LAST部分,比如某个指定为+Last的段没有被生成。
如果没找到,说明这个section从入口Catalog阶段就被丢弃了。很多使用AC6的环境里,链接器默认会回收未引用的section,这意味着即使编译出RESET,如果代码里没有引用到其中的符号,链接器也会把它丢弃。你可以通过加入--keep参数强制保留,或者仔细检查.sct里的匹配规则。
具体做法:
在Linker页面往下找Misc Controls输入框,加入:
--keep=startup_stm32f10x_hd.o(RESET)保存重新编译。注意,这个写法在不同编译器版本下略有差异,AC6里也可以这样写:
--keep=*.o(RESET)另外可以用fromelf命令行工具直接查看elf文件里的section。在命令行下进入Objects目录,执行:
fromelf --text -z -d output.axf-z参数可以输出section布局,能很方便看到哪些section真实存在,哪些被丢弃了。这招比翻map文件更直观,因为它是直接读取最终elf的输出结果。
4. 冷门但真实存在的深坑
4.1 自定义.sct里出现"空执行区"
这个情况是我在一次多段Flash映射的工程里踩到的。当时把Flash切成了几段,不同段放不同功能,.sct里写了多个执行区。其中一个执行区的代码是由条件编译控制的,某个板卡配置下,这整块代码都不会编译进来。
结果就是执行区定义里没有任何有效section能放进去,而我在这个空执行区里还写了+First和+Last,链接器直接报L6236E。
解决办法是把FIRST/LAST从空执行区里移走,或者在链接选项里添加允许空区的参数。ARM链接器其实有一个指令EMPTY,专门给这种可能为空的执行区预留位置:
ER_IROM2 0x08040000 0x00010000 { .ANY (+RO) }本质上,如果一个执行区里没有实际内容,最好的做法是不给这个区加任何约束,让它能够被链接器忽略。
4.2 +Last与.ANY的配合问题
另一种情况和优先级有关。如果你在.sct里写了类似:
ER_IROM1 0x08000000 0x00080000 { *.o (RESET, +First) .ANY (+RO) *(.rodata*) }然后期待.rodata段被放到Last位置,写法上需要明确加上+Last,否则它的位置完全由链接器根据内存填充逻辑决定。更重要的是,如果.ANY匹配到的段已经在物理上占据了这个LAST地址,而你又额外指定了别的段放最后,就会产生L6236E。
正确写法是把+Last直接加在指定的section上:
ER_IROM1 0x08000000 0x00080000 { *.o (RESET, +First) .ANY (+RO) *(LastRegion, +Last) }但前提是这个LastRegionsection确实存在于某个文件中。如果它不存在,错误照旧。所以用+Last时,一定要确认对应的section确实存在,并且没有被垃圾回收。
4.3 老工程从C51迁移到MDK-ARM时的遗留问题
还有个大坑来自51单片机转ARM工程的开发者。C51工程里习惯把所有代码段都用code关键字定义,而MDK-ARM工程里RAM和ROM的分工更明确。有些从C51复制过来的代码,在启动文件里定义了自定义的CODE段或者IDATA段,然后下意识地在.sct里增加了对应执行区,结果模块之间引用关系混乱,出现了L6236E。
如果是从STC、N76这类51单片机转过来的,建议干脆把启动文件换掉,不要沿用任何旧式工程模板,直接新建立一个标准MDK-ARM工程,再手动添加源文件,能省去大量莫名其妙的兼容性问题。
5. 把历史上撞见过的同类报错汇总一下
5.1 不同变体错误信息的速查对照
L6236E有很多变体,核心不变,报错位置和selector内容却各不一样。我整理了一份对照表,方便以后遇到类似问题可以直接判断方向。
| 错误信息片段 | 真正的含义 | 排查方向 |
|---|---|---|
No section matches selector - no section to be FIRST/LAST | 指定为FIRST或LAST的section不存在 | 启动文件缺失、section名拼错、函数被裁剪 |
No section matches selector - no section matches startup.o(RESET) | 明确指定了startup.o的RESET段但没匹配到 | 启动文件没有参与编译或目标文件名不同 |
No section matches selector - no section matches * (CheckButton) | 自定义段CheckButton没有生成 | 条件编译未开启、源文件漏添加 |
No section matches selector - no section matches main.o(+RO) | 找不到main目标文件的只读段 | main文件没加入工程或编译类型不对 |
Execution region ... has no space | 执行区空间耗尽,和L6236E伴生出现 | Flash内存不足,或体积超过了目标芯片容量 |
这个表的核心思想是:信息里出现的selector越具体,越容易定位。什么都没写只说FIRST/LAST的,反而是最模糊的,优先考虑启动文件层面。
5.2 解决之后务必做的三项检查
我见过一些朋友,报错解决了但不注意细节,后面很快又踩到别的坑。解决L6236E之后,我建议至少做三次确认:
第一,检查生成的.axf文件里RESET vector的地址是否符合预期。用fromelf或者调试器查看复位向量所在位置,Cortex-M3/4/7系列的向量表首地址应该是0x08000000,对应字节是栈顶地址。如果栈顶地址明显不对,说明启动文件里的堆栈定义也有问题。
第二,清空一次编译中间文件,重新全量编译。Keil的增量编译偶尔会因为Objects目录存在旧文件而出问题。Clean Target以后重新Rebuild,可以排除中间文件干扰,尤其适合刚还完编译器或工程模板的情况。
第三,检查启动文件中SystemInit函数的调用路径。如果SystemInit没被正确导入,虽然不会直接触发L6236E,但可能引发其他启动异常。启动文件底部的__main和SystemInit引用是ARM嵌入式程序启动的重要链条,最好确保它们都正常。
写在最后
我也踩过很多次L6236E,印象最深的是有一次折腾了一下午,查了启动文件、编译器设置、分散加载,都找不到原因,最后发现只是自己在加源文件的时候不小心把MALLOC.C的文件名拼错了个字母,导致某个全局数组所在section没编译出来。
所以现在的习惯是:报错之后先不急着修改,按"启动文件参与情况 -> 编译器版本匹配 -> .sct恢复默认 -> 用map文件验证"这个顺序走一遍。这样看起来慢,实际是最快找到问题的路径。如果遇到不确定的地方,最重要的就是区分清楚,这个section到底有没有被编译出来。只要它存在,解决L6236E就只是链接规则的问题;它要是压根不存在,你再怎么写选择器,链接器也不可能凭空帮你造出来。