做嵌入式开发的朋友,肯定都见过这行红字:
.\Objects\xxx.sct(7): error: L6236E: No section matches selector - no section to be FIRST/LAST.说实话,我第一次看到这个错误的时候,第一反应是“我是不是内存超了?”因为L开头、又带.sct,看着很像分散加载文件哪块写坏了。后来翻了好多资料、试了不少蠢办法,才搞明白它真正的含义:链接器在分散加载文件里指定了某个输入段要放到执行域的 FIRST 或 LAST 位置,结果整个工程里根本找不到能匹配的段。简单讲,就是链接器想要一个“开头/结尾段”,但你没给它,或者给的名字对不上。
这个错误在 Keil MDK 环境下特别容易遇到,尤其是手改过.sct文件、换过芯片型号、从 AC5 切到 AC6,或者启动文件被不小心排除出编译的时候。这篇文章我就把自己踩过的坑、排查思路和处理方法完整写出来,希望能帮你在十分钟内定位问题,而不是像我当年那样在工程配置里瞎点半天。
1. 先把L6236E这条报错拆开看
1.1 错误信息里的三个关键信息
很多新手看到报错会急着去百度整句复制,但这条错误其实已经把所有线索都写在脸上了。我一般会把报错拆成三部分来看:
.\Objects\xxx.sct(7):指出错的分散加载文件路径和具体行号。这里(7)指的是.sct文件里的第 7 行,不是你的 C 代码第 7 行。优先看这一行写了什么。error: L6236E:ARM 链接器 armlink 的错误编号。看到L6xxx就知道是链接阶段的布局错误,不是语法错误,也不是编译错误。No section matches selector - no section to be FIRST/LAST:这句话是核心,翻译成人话就是——“我按你的选择器去工程里找输入段,结果一个都没匹配上,导致这个执行域的起始段或结束段是空的”。
所以,当你在 Keil 的 Build Output 窗口看到这行报错时,第一步不是去改 C 代码,也不是把整个工程 clean 掉重编,而是先找到那个.sct文件,打开第 7 行。绝大多数情况下,问题就集中在那一行以及它背后的启动文件上。
1.2 .sct文件到底在管什么
.sct,全称 Scatter Loading Description File(分散加载描述文件),是 ARM 链接器用来描述代码和数据在内存里怎么摆放的文件。Keil 默认会帮你自动生成一个,但也会在 Option for Target → Linker 选项卡里给你一个手动编辑的入口。
一个典型的 Keil 自动生成.sct长这样:
; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************************* LR_IROM1 0x08000000 0x00010000 { ER_IROM1 0x08000000 0x00010000 { startup_stm32f103xb.o (RESET, +First) * (InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00004000 { .ANY (+RW +ZI) } }在 MDK 自动生成的这个版本里,第 7 行往往就是startup_xxx.o (RESET, +First)这种写法。它的意思是:把startup_xxx.o这个目标文件里的RESET段放到ER_IROM1这个执行域的起始位置。这个位置通常是 Flash 的最低地址,也就是 CPU 复位后取向量表的地方。
如果这一行里的startup_xxx.o文件不存在、没有参与链接,或者RESET这个段名在文件里根本不存在,链接器就会立刻翻脸,抛出一句L6236E。
1.3 FIRST与LAST是怎么匹配的
“匹配”这两个字是理解这个错误的关键。分散加载文件里有两种特殊的段属性:
+First:把指定输入段放到执行域的最前面。+Last:把指定输入段放到执行域的最后面。
链接器要完成这个动作,需要你在括号里写清楚“段的来源”和“段的名字”,也就是选择器。比如startup_stm32f103xb.o (RESET, +First)中的startup_stm32f103xb.o是目标文件名,RESET是输入段名,+First是摆放方式。
ARM 链接器的选择器匹配规则是“活着找得到才罢休”,只要目标文件里没有这个段,或者目标文件压根没参与链接,它就会报L6236E。这里的selector指的就是括号里那段“目标和段的描述”,所以这条错误直译过来就是“没有任何段能匹配这个选择器,所以我不知道该把谁放到 FIRST/LAST”。
1.4 别和L6220E、L6238E搞混了
在排查L6236E的过程中,我见过不少人把链接错误混为一谈。这里顺手做个简单区分:
| 错误编号 | 典型含义 | 常见的误判 |
|---|---|---|
| L6236E | 找不到匹配的输入段作为 FIRST/LAST | 以为是内存溢出 |
| L6220E | 执行域大小超过地址范围限制 | 内存放不下 |
| L6238E | 执行域内没有任何输入段 | 段全被优化掉了 |
| L6242E | 加载域地址或执行域地址重叠/非法 | 地址配置错误 |
简单来说,L6220E才是真正的内存不够用,L6236E是“指定要放的段不存在”。如果你看到No section matches selector,就别去调整 IROM1 的大小了,根源百分之九十在启动文件和.sct的匹配关系上。
2. 标准排查流程:从第7行到根因
2.1 第一步:让Keil帮你定位到.sct那一行
Keil 其实已经帮你做了第一步定位。在 Build Output 窗口里,双击错误信息那一行,uVision 通常会自动打开对应的.sct文件,并把光标跳到出错的那一行。
我建议你把这一行完整截图或者复制出来,对照下面几个问题逐一排查:
- 这行里出现的目标文件名和你工程树里实际的启动文件是否一致?
- 括号里的段名(如
RESET、STACK、.isr_vector)在启动文件里是否真实存在? - 这个
.sct是 Keil 自动生成的,还是你从别的工程里拷贝过来的?
这里有个容易踩的坑:.sct文件的行号会因为文件开头的注释行而变化。有些人第 7 行是startup.o (RESET, +First),有些人第 7 行是*(.isr_vector, +First),还有人第 7 行是some_custom.o (MY_SECTION, +Last)。这不重要,重要的是你要找到“带 FIRST/LAST 的那一行”。
2.2 第二步:检查启动文件是否真的参与编译
启动文件(startup_*.s)是L6236E的头号关联方。很多情况下,错误原因就是启动文件被“排除出编译”了,或者工程里压根没有添加启动文件。
在 Keil 的工程树里,如果某个源文件被勾选了 “Exclude from build”,它的图标会变灰,文件名前会有个表示不参与编译的状态。但这种状态很容易被忽略,尤其当你从别人那边拷来工程时,可能对方误排除了某个文件,传到你这儿就成了一个“隐形的雷”。
确认方法很简单:
- 在工程树里展开启动文件所在的分组。
- 右键启动文件,选择 “Options for File ...”,打开文件级配置窗口。
- 查看 “Exclude from build” 复选框是否被勾选。
- 如果被勾选了,取消它,重新编译。
还有一种情况是启动文件根本没加进工程。比如你新建了一个 STM32 工程,忘了在 Device 启动向导里添加startup_stm32f103xb.s,或者手动新建工程时跳过了启动文件。这种情况下,编译也能通过一部分,但到了链接阶段就会因为找不到RESET段而报L6236E。
2.3 第三步:核对section名称是否一致
如果启动文件确实参与了编译,那就要看段名是否一致。ARM 汇编启动文件里定义一个段的方式通常是:
AREA RESET, CODE, READONLY有些启动文件会这样写:
AREA STACK, NOINIT, READWRITE还有用 GCC 风格或 ARMClang 风格写法的:
.section .isr_vector,"a",%progbits如果你在.sct里写的是*(RESET, +First),但启动文件里定义的实际段名是.isr_vector,那链接器当然会找不到。这个“名称不一致”的问题在 AC5 切到 AC6 时尤其常见,后面我会专门展开讲。
2.4 第四步:用map文件验证段是否存在
想确认某个段到底有没有被生成出来,最可靠的办法是打开生成的 map 文件,搜索段名。map 文件通常位于Objects\目录下,和工程同名,后缀是.map,比如demo.map。
用文本编辑器打开 map 文件,直接搜索.isr_vector或RESET,看能不能找到对应的行。如果能找到,说明段确实生成了,那么问题多半出在.sct的选择器写法上,比如目标文件名写错、大小写不对、路径带了奇怪的前缀。如果 map 文件里根本没有这个段,那说明段在更早的编译/优化阶段就被吞掉了,或者源文件里压根没定义。
这里有个小技巧,Keil 的 map 文件默认会列出所有输入段和它们所属的目标文件,比如:
0x08000000 0x100 Startup/startup_stm32f103xb.o(RESET)看到这一行,你就知道startup_stm32f103xb.o里的RESET段确实存在,而且占用了0x100字节。如果搜不到,那就回第二步和第三步继续查。
2.5 第五步:查编译优化和段保留
还有一个容易被忽视的环节:优化选项。链接器默认会做未使用段回收(section GC),如果一个段没有任何地方引用它,也没有通过--keep或者__attribute__((used))显式保留,它就可能被优化掉。
不过对于启动文件里的RESET段来说,通常不会因为“没有引用”被删掉,因为它会被链接器当作入口相关的输入段保留。但如果你自己定义了自定义段,比如放在指定 Flash 地址的版本信息、校验值,并试图在.sct里把它设为+First或+Last,就很容易被优化器吞掉。
这个时候,建议你从两个方向处理:
- 在 C 代码里给段定义加上
__attribute__((used)),明确告诉编译器“这个变量虽然没直接引用,但必须留下来”。 - 在 Linker 选项卡的 Misc controls 里加上
--keep=文件名(段名),强制链接器保留。
3. 六种实际场景与对应解法
3.1 场景一:启动文件被“Exclude from build”
这是我遇到最多的一种,尤其是接手别人工程时。某个同事为了验证临时现象,右键把startup.s排除出编译,后来忘了恢复,工程交到你手上,一编译就是L6236E。
这个场景的特征非常明显:.sct第 7 行写着startup_xxx.o (RESET, +First),但 Build Output 里你找不到启动文件被编译的命令行,map 文件里也搜不到RESET段。
解决办法很简单:右键工程树里的启动文件,打开文件选项,取消勾选 “Exclude from build”。重新编译后,错误基本就会消失。如果你打开文件选项时发现启动文件并没有被排除,但也没参与链接,那就直接检查这个文件是不是放在了一个没有被包含进构建的分组里。
3.2 场景二:换芯片型号后启动文件名/段名对不上
换芯片型号是另一个高频触发点。比如你把一个 STM32F103VET6 的工程改成 STM32F407VET6,或者从 F1 系列换成 GD32、AT32 等兼容芯片,但工程树里的启动文件还是旧的。
Keil 有个习惯:自动生成的.sct会引用当前 Device 名称对应的启动文件。假设你换成 STM32F407,MDK 自动生成的.sct里可能出现startup_stm32f407xx.o (RESET, +First),但你工程里实际添加的还是startup_stm32f10x_hd.s,两者对不上,链接器自然找不到匹配的 RESET 段。
处理办法是:
- 打开 Manage Run-Time Environment,或者用 Pack Installer 找到当前芯片对应的启动文件。
- 删除工程树里旧的启动文件。
- 添加正确的启动文件,比如
startup_stm32f407xx.s。 - 如果你是自己手动写的
.sct,记得同步修改目标文件名和段名。
这个场景我建议你把坑的根源记下来:换芯片不是只改一个 Device 型号就完事,启动文件和分散加载文件必须同步换。
3.3 场景三:AC5切AC6后RESET变成.isr_vector
Keil MDK 从 5.x 开始逐渐用 ARMClang(AC6)替代 ARMCC(AC5),很多人升级完编译器版本后发现原本好端端的工程突然报L6236E,原因就出在段命名差异上。
AC5 时代的启动文件,向量表通常放在一个叫RESET的 AREA 里,比如:
AREA RESET, CODE, READONLY EXPORT __Vectors __Vectors DCD __initial_sp DCD Reset_HandlerAC6 时代,很多启动文件用了 GCC 风格或重新整理过的写法,向量表段名变成了.isr_vector:
.section .isr_vector,"a",%progbits .globl __Vectors __Vectors: .long __initial_sp .long Reset_Handler如果.sct里还保留着*(RESET, +First),但启动文件用的是.isr_vector,链接器就会报No section matches selector。
解决方式很直接,看你的启动文件到底定义了哪个段名,然后把.sct里对应的 selector 改成匹配的名字。以 AC6 常见写法为例,第 7 行可以改成:
* (.isr_vector, +First)如果你想让工程对 AC5 和 AC6 都更稳健一些,可以在.sct里同时写两行,让链接器自己挑能匹配的那个:
* (RESET, +First) * (.isr_vector, +First)不过两行+First同时出现时,链接器会按顺序处理,精确匹配的第一个段才会放在真正的最前面,后面那个如果存在则紧随其后。这种写法虽然能兜底,但我不建议你长期这么做,搞清楚启动文件真实段名才是正道。
3.4 场景四:自定义FIRST/LAST段被条件编译屏蔽
有时候,报错的位置不在启动文件,而在你自定义的段上。比如你在 C 代码里写了这样一个段,想把它放到 Flash 某地址的末尾,做固件版本或校验和:
__attribute__((section(".version"))) const uint32_t version[] = { 0x20250101, 0x01, 0x00 };然后在.sct里写了:
* (.version, +Last)如果编译时某个条件宏没定义,导致这段代码整个被#ifdef吃掉,.version段根本不会生成,链接器自然找不到匹配的输入段,同样会抛L6236E。
排查这类问题时,我一般会先搜索一下这个段名在整个工程里出现过几次,把定义位置找出来,仔细看它的外层条件编译宏是什么。也可以用“临时注释掉所有条件编译”的方式验证,如果注释后错误消失,那就说明问题就出在宏定义上。
这种自定义+Last段在 bootloader、App 升级、固件校验等场景中特别常用,而且一旦报错很容易让人摸不着头脑。我建议在自定义段周围尽量不要写复杂的条件编译,或者至少保证默认分支也能生成一个空数组占位。
3.5 场景五:编译器/链接器把启动段当垃圾回收了
这个场景相对少见,但确实存在。ARMClang 的优化等级比较高时,如果一个段没有被任何符号引用,也没有被标成used,编译器可能根本不给它生成输出段。
典型的例子是:你在 C 文件里用__attribute__((section("MY_BOOT")))定义了一个函数,这个函数没有被任何地方调用,也没有显式地址引用,只是想通过.sct把它放到某个固定位置。结果编译优化后,MY_BOOT段直接消失了,链接器在.sct里找不到它,于是报L6236E。
解决办法有两条:
- 在函数或变量定义处加
__attribute__((used)),比如:
__attribute__((used, section(".my_boot"))) void boot_custom_func(void) { // ... }- 或者在 Linker 选项卡的 Misc controls 里增加
--keep指令:
--keep=main.o(.my_boot)这样链接器就会把该段保留下来。要注意--keep的写法里段名要和你 C 代码中的 section 名严格一致,否则一样匹配不上。
3.6 场景六:选择器语法本身写错了
最后一种情况确实有些不想承认,但.sct的语法是相当严谨的,一个空格、一个逗号错了都会导致匹配失败。
比如:
startup_stm32f103xb.o (RESET, + First):+First中间多了空格,会报语法错误或匹配不上。*(RESET +First):少了逗号,链接器会理解成别的意思。Startup_Stm32f103xb.o (RESET, +First):文件名大小写写错了。* (RESET, First):少了加号。
如果你想确认是不是语法问题,最快的办法是删掉这行里后半部分,只保留一个简单的*(RESET)试试,看错误是否变化。如果变成找不到目标文件之类的其他错误,说明前面那部分其实也有问题。
还有一种比较隐蔽的写法问题:在.sct文件里用了#include或预处理宏,结果预处理后的某一行变成了空内容或空选择器。Keil 对.sct的预处理支持有限,我建议尽量少用,避免给自己挖坑。
4. 直接能上手的4套处理方案
4.1 方案一:让MDK按Target配置重新生成.sct
如果你的工程本来没有手动维护.sct的需求,那我强烈推荐这个方案:让 Keil 自动生成分散加载文件,别自己手改。
操作步骤如下:
- 打开 Option for Target → Linker 选项卡。
- 取消勾选 “Use Memory Layout from Target Dialog” 前面的复选框,如果之前勾着的话。这里要注意,Keil 的逻辑是:勾选这个复选框,代表使用 Target 页面里 IROM1/IRAM1 的地址和大小配置自动生成
.sct;取消勾选,则允许你手动编辑下方的 Scatter File 路径和内容。 - 如果
.sct是自动生成的,你只需在 Target 选项卡里确认 IROM1、IRAM1 的起始地址和大小正确。 - 重新编译。
实际操作中,只要 Target 地址配置正确,自动生成的.sct一般不会出问题。很多报L6236E的工程,就是因为有人取消了自动生成,又手动改乱了.sct。把“手动维护”恢复成“自动生成”,往往是最快最稳的解法。
4.2 方案二:手动修正.sct中的FIRST行
如果你确实需要手动维护.sct,或者项目要求固定内存布局,那就认认真真把 FIRST/LAST 行改对。
首先要做的,是打开启动文件,确认向量表段的真实名字。这里我拿最常见的startup_stm32f103xb.s举例,它的开头通常是:
AREA RESET, CODE, READONLY那么.sct对应的行就应该是:
startup_stm32f103xb.o (RESET, +First)如果你用的是 AC6 风格启动文件,段名是.isr_vector,就要写成:
* (.isr_vector, +First)这里有个选择器书写的细节:直接写startup_xxx.o比写*更精确,因为*会匹配所有目标文件的同名段。如果工程里有两个文件都定义了RESET段,*会把两个段都拉到前面,顺序可能不符合预期。不过,当 bootloader 和 App 工程并存时,用*通配符做兼容更省心。
修改完后重新编译,如果错误消失,就说明 selector 已经匹配上了。如果还是报错,回到第 2 节的排查流程,继续查目标文件是否参与编译。
4.3 方案三:用__attribute__((used))和--keep把段保住
前面说过,优化器可能把明明定义了的段当成垃圾回收掉。这种情况光改.sct没用,得从“段生成”这一步就保住它。
在 C 代码中,给自定义段加上used属性,是一个很实用的习惯。比如保存固件版本号的段:
__attribute__((used, section(".version"))) const uint32_t app_version[] __attribute__((aligned(4))) = { 0x01020304, };加了used之后,编译器就会把这个段保留到目标文件里,即使它没有任何引用。链接器再通过.sct匹配时,就能找到它了。
如果你维护的是汇编启动文件,也可以用类似思路,比如AREA定义后加KEEP指令:
AREA RESET, CODE, READONLY KEEP RESETKEEP是 ARM 汇编里的段保留指令,能让链接器在处理该段时不做无用段回收。不过,标准 MDK 生成的启动文件通常不会被优化掉,用KEEP多一些保险也没坏处。
4.4 方案四:临时去掉FIRST做验证
如果上面所有方案都来不及细查,你又急着看后续代码能不能跑,可以先把.sct里的+First或+Last去掉,只保留匹配段:
* (RESET)这样链接器能顺利生成固件,但它不会强制把RESET段放到执行域最前面,导致中断向量表不在 Flash 起始地址。如果你只是验证某个逻辑、不烧录运行,这个临时方案是能救急的;但只要你需要在真实芯片上跑,就必须把 FIRST 恢复回来,否则系统上电后直接从错误地址启动,轻则跑飞,重则进 HardFault。
所以我一般只在一种情况下用它:想确认这个错误是不是只跟“位置摆放”有关,还是真的连段都没有。去掉+First后如果不再报错,说明段本身存在,问题多半出在+First的匹配规则上;如果去掉+First后依然报No section matches selector,那说明段压根没生成,还得回头查启动文件和优化设置。
4.5 方案该怎么选
遇到L6236E,我的选择顺序通常是:
| 情况 | 选择的方案 |
|---|---|
| 工程没特殊内存布局要求,纯开发调试 | 方案一,自动生成 |
| 工程需要固定内存布局,且改动不大 | 方案二,修正 FIRST/LAST 行 |
| 自定义段被优化器吃掉 | 方案三,加 used / --keep |
| 临时验证,不烧录运行 | 方案四,去掉 FIRST |
这里没有“哪个方案绝对好”,关键看你项目对内存布局有没有硬性要求。比如 bootloader + App 升级方案,App 的向量表要重定位到固定偏移,手动.sct几乎是必须的,那你就不能偷懒,一定要掌握方案二。
5. 后续几个建议:别再被同款错误逼疯
5.1 改动启动文件后,先看map再往下走
启动文件是L6236E的重灾区。我现在的习惯是:只要动过启动文件、编译器版本、芯片型号,就先打开 map 文件搜一下RESET或.isr_vector,确认段存在、地址正确,再继续写业务代码。
map 文件还有一个作用:定位向量表起始地址。右键点击工程名,打开 Objects 目录下的.map文件,搜索__Vectors或Reset_Handler,如果向量表正好位于 Flash 起始地址,说明分散加载的布局是正确。
5.2 从旧工程复制.sct时要小心
很多团队习惯把旧工程整个拷贝过来改芯片型号,xxx.sct也一起复用了。这种操作很容易踩坑,因为.sct里写死的目标文件名、地址范围和大小,跟新芯片的启动文件、Flash/RAM 参数可能完全不匹配。
碰到L6236E时,先想想这个.sct是不是“亲生”的。如果你是从同事那边拷贝的,或者从淘宝上买来的模板工程,那它跟当前工程的启动文件十有八九对不上。这时候按照方案一,重新用 Target 配置生成一份新的.sct,往往比在旧文件上修修改改更干净。
5.3 版本管理里把.sct当作一等公民
分散加载文件虽然只有几行,但它直接决定了固件的内存布局,非常重要。我见过不少 Git 仓库只提交了源代码,.sct文件没有提交,或者提交了但路径不对,导致别人 clone 下来后一编译就报L6236E。
建议在工程里把.sct文件的路径固定下来,不要放到临时目录或忽略目录。如果你用的是自动生成模式,则要确保 Target 页面的 IROM1/IRAM1 地址、大小配置对每个开发者一致。否则你本地能过,同事一编译就是各种L6xxx错误。
我在实际项目中还习惯把.sct文件的开头注释里写明适用芯片型号和启动文件名称。比如:
; *** Scatter-Loading Description File for STM32F407VG *** ; *** startup file: startup_stm32f407xx.s ***这样过了几个月再回来看工程,也能快速判断.sct和启动文件是不是配套的。踩过几次L6236E的坑之后,你会发现这个习惯能省掉很多莫名其妙的排查时间。
最后顺手分享一个我自己的小技巧:当你在.sct第 7 行看到(RESET, +First)但不确定启动文件里是不是叫 RESET 时,不要光靠肉眼找,直接在源文件里搜AREA或.section,十秒钟就能确认真实段名。这个错误本质上就是“名字对不上”,把它当成一场找名字的游戏,心态会轻松很多。