☰
Keil MDK L6236E报错排查:分散加载文件与启动文件匹配
2026/11/3 13:55:58 网站建设 项目流程

“代码编译一晚上没动静,点一下链接一秒就翻车”,这大概是每个常年混迹在 Keil MDK 环境里的嵌入式开发者都经历过的瞬间。屏幕下方的 Build Output 窗口刷出一行红字:L6236E: No section matches selector - no section to be FIRST/LAST,旁边还带着一个 .sct 文件名和行号。你明明什么都没改,只是换了个启动文件、动了一下 Target 里的容量配置,或者从同事那里拷了一份分散加载文件过来,链接器就开始罢工了。这篇内容就是围绕这个报错展开的完整排查思路:从链接器到底在抱怨什么,到分散加载文件(.sct)和启动文件之间的对应关系,再到用工具把“真实现场”挖出来,最后给出一套可以照着抄的修复流程。它适合刚上手 MDK、第一次见到这个报错的新人,也适合被历史工程折磨过的老手——因为 L6236E 的成因往往不在报错那一行,而藏在工程配置的角落里。

1. L6236E 到底在报什么:把链接器的话翻译成人话

1.1 报错原文的四层含义

先把原始信息完整贴出来,通常是这个形式:

.\Objects\project.sct(7): error: L6236E: No section matches selector - no section to be FIRST/LAST.

这行字信息量其实很大,逐段拆开看:

  • .\Objects\project.sct(7):链接器用的是Objects目录下的project.sct,问题出在第 7 行。这一行绝大多数情况下就是*.o (RESET, +First)或者类似的+First/+Last约束语句。注意,行号是定位锚点,但真正的原因往往不在这份文件的写法上,而在于工程里压根没有能被打上这个标记的段。
  • error:这是错误级别,不是警告。armlink 在遇到+First/+Last约束无法满足时会直接终止链接,不生成 axf/hex。
  • No section matches selector:选择器(selector)没有匹配到任何段(section)。选择器指的是*.o、(RESET)、.ANY这一类表达式,段指的是目标文件里真实存在的、有名字和属性的代码/数据块。
  • no section to be FIRST/LAST:具体来说,是带位置约束的那条规则空了。链接器需要找到至少一个满足条件的段来放到执行区域的最前面,一个都没找到,于是报错。

所以这句话翻译过来就是:“你让我把某个段放在最前面,可我在所有参与链接的目标文件里,找不到符合条件的段。”问题既可能出在“文件里没有这个段”,也可能出在“选择器写得不对,明明有段但选择器匹配不上”,这两类要分开查。

1.2 分散加载的执行区域与选择器

要彻底理解这个报错,得先搞清楚 ARM 链接器的分散加载模型。一个典型的自动生成的 .sct 长这样:

LR_IROM1 0x08000000 0x00040000 { ; load region size_region ER_IROM1 0x08000000 0x00040000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) .ANY (+XO) } RW_IRAM1 0x20000000 0x00010000 { ; RW data .ANY (+RW +ZI) } }

这里面有两层容器:外层是加载区域(load region),描述程序烧录到 Flash 里的布局;内层是执行区域(execution region),描述运行时这些内容被放到哪里。对于绝大多数 Cortex-M 芯片,加载地址和执行地址是一致的,所以看起来像重复,但概念上必须分清。

每个执行区域内部是一串选择器规则,从上到下依次吸附目标文件里的段。规则有三种典型写法:按模块名(startup_xxx.o)、按段名((RESET))、按属性(+RO、+RW、+ZI、+XO)。.ANY是通配符,意思是“剩下的都给我”。+First和+Last是位置约束,它要求链接器在这个执行区域的头部或尾部留出位置,并且必须有段被放进去。

关键点在于:+First/+Last是强约束。(InRoot$$Sections)这种没有位置约束的规则,哪怕一个段都没匹配上,顶多只是让__main里的库函数初始化出问题,链接器本身不会立刻报错;但+First一旦落空,就是硬错误。这也解释了为什么 L6236E 几乎总是伴随着+First出现。

1.3 为什么偏偏是 +First 最容易踩坑

从工程实践看,(RESET, +First)这条规则承担的任务非常特殊:它要把启动文件里的向量表段固定放在 Flash 的最开头,因为 Cortex-M 内核复位后从地址 0x00000000(映射到 Flash 起始)取栈顶指针,从 0x00000004 取复位向量。这个位置不能有任何偏移,所以必须用+First强约束。

正因为它的地位特殊,一旦工程里的“向量表段”消失了、改名了、被排除了,链接器就没有任何退路。相比之下,.ANY (+RO)这种规则即使在某个配置下匹配不到东西,也只是让区域变小而已。所以你可以理解为:+First是整份 .sct 里最“脆”的一根弦,工程结构稍有风吹草动,先断的就是它。

还有一个容易忽略的细节:+First和+Last只能用在执行区域里,不能写在加载区域层级。有人手写 .sct 时把*(RESET, +First)写到了LR_IROM1 {的下一层但忘了包一层ER_,链接器同样会给出让人摸不着头脑的报错。写自定义 .sct 时,花括号的层级一定要数清楚。

2. 四条主因:先按概率排个序

2.1 启动文件缺位:最常见也最隐蔽

在大量实际案例里,L6236E 的第一嫌疑人是启动文件没有真正参与链接。所谓“参与链接”,不是说它躺在工程目录里就行,而是要同时满足三个条件:被加入工程(Project 树里能看到)、没有被勾选 “Exclude from Build”、并且编译产物确实生成到了Objects目录下。

第二和第三个条件特别容易被忽略。有些人从别的工程拷贝了一份startup_xxx.s到本地目录,然后在 Project 窗口里“Add Existing Files”,结果文件被加到了某个不参与当前 Target 的分组里,或者加进去之后右键属性里被设成了排除编译,Build 时没有任何报错,.o文件没生成,链接阶段直接崩。

还有一种更隐蔽的情况:工程里同时存在两份启动文件,一份是旧芯片的,一份是新芯片的,两份都被加进了工程。此时两个.o里都有RESET段,链接器会把它们堆在一起,通常先报L6200E: Symbol __initial_sp multiply defined之类的重复定义错误。但如果两份启动文件的段名不一样(比如一份叫RESET,一份叫RESET_SEG),就可能出现“看起来有启动文件,但+First匹配不到”的诡异现象。

判断方法很直接:Build 完成后去Objects目录看有没有startup_xxx.o。没有就是没编译进去,先解决这个再说。

2.2 段名对不上:大小写和拼写

如果.o文件确实生成了,下一步就是确认它里面到底有没有叫RESET的段。标准 CMSIS 启动文件的写法是:

AREA RESET, DATA, READONLY EXPORT __Vectors EXPORT __Vectors_End EXPORT __Vectors_Size __Vectors DCD __initial_sp DCD Reset_Handler

AREA后面的第一个参数就是段名。如果这个位置被改成了RESET_VECTORS、RESET_SEG、VECTOR,而 .sct 里还写着(RESET, +First),链接器当然找不到。armlink 的段名匹配是大小写敏感的,Reset、reset、RESET在它眼里是三个不同的段。这一点在 Linux 环境待久了的开发者那里尤其容易翻车,因为 GNU 工具链的一些配置对大小写没那么敏感,习惯迁移过来就容易忽略。

还有一种情况是启动文件根本不是标准 CMSIS 结构。比如某些厂商在自己的启动文件里把向量表直接定义成了AREA |.isr_vector|, DATA, READONLY,段名带竖线且是.isr_vector。这种文件在 Keil 里能不能用,取决于你怎么配 .sct。如果直接套用自动生成的 .sct,必然报 L6236E,因为自动生成的模板里写死的是(RESET, +First)。

排查方式:打开启动文件,搜索AREA,把所有段的定义列出来,和 .sct 里的选择器一条条对。这个动作看着笨,但十次里有六次能直接定位问题。

2.3 分散加载文件与芯片容量不匹配

第三类原因和 .sct 里的地址、长度参数有关。Target 对话框里 IROM1 和 IRAM1 的起始地址、容量,会直接决定自动生成的 .sct 内容。常见的错误配置包括:

配置项错误示例后果
IROM1 Start0x08000000正确,但写成0x00000000区域性偏移,烧录后无法启动
IROM1 Size芯片 256KB,填成0x00080000(512KB)链接器不会报错,但越界访问
IROM1 Size芯片 256KB,填成0x0004000(少一个 0,只有 16KB)代码放不下,报L6406E: No space in execution regions
IRAM1 Size芯片 48KB,填成0x00010000(64KB)运行时栈溢出,现象随机

这里要给出换算方法,因为很多人卡在十六进制上。1KB = 1024 字节 = 0x400,所以:

  • 64KB = 64 × 0x400 = 0x10000
  • 128KB = 0x20000
  • 256KB = 0x40000
  • 48KB = 48 × 0x400 = 0xC000
  • 20KB = 0x5000

快速心算的技巧是:把 KB 数乘 1024 得到十进制字节数,再转十六进制;或者记住 256KB = 0x40000 这个基准向下推。填错容量不一定直接触发 L6236E,但如果把 IROM1 的 Size 填成 0 或者勾掉了 Use Memory Layout 又留了个空文件,链接器就找不到ER_IROM1区域,报错会以 L6236E 或 L6220E 的形式跳出来。

2.4 工程配置残留与路径错位

最后一大类是配置残留。典型场景是工程从别处拷来,或者从一个芯片型号切到另一个型号,Target 里的分散加载文件路径还指向原来的绝对路径。表现是:你打开 Options for Target → Linker,看到 Scatter File 框里写着一个D:\旧工程\Objects\xxx.sct,本机上这个路径根本不存在。

这种情况下,链接器可能报L6002U: Could not open file,但也可能因为路径被解析到了一个陈旧的、内容不完整的 .sct 上,进而报 L6236E。尤其是当Use Memory Layout from Target Dialog没勾选,而 Scatter File 框又不是全自动生成时,最容易出这类问题。

判断方法:在 Linker 页面看清楚到底勾的是哪一个。勾了自动布局,就没法手改 .sct(改了会被下次 Build 覆盖);没勾,就必须确保 Scatter File 框指向的是一份真实存在且内容正确的文件。两者是互斥的,切换时一定要同步清理旧文件,不然就会在Objects目录里留一堆“僵尸 .sct”。

3. 取证:让链接器自己说清楚哪里空了

3.1 从 .map 文件倒推段表

光靠猜不行,得让工具给出证据。第一条路径是map 文件,但要注意:链接失败时通常不生成完整 map,所以更实用的做法是先看上一次成功链接的 map,或者临时把报错那条规则的+First删掉,让它先链接过去再生成 map。

在 Options for Target → Listing 页面,勾上 “Linker Listing” 下的 “Memory Map”,编译后就能在Objects目录里拿到.map文件。打开后重点看两段:

  • Image Symbol Table里的Local Symbols,能看到每个.o贡献了哪些符号,__Vectors在哪个段里;
  • Memory Map of the image里的Execution Region ER_IROM1,能看到该区域里每个段来自哪个目标文件、大小多少。

如果ER_IROM1的第一项不是startup_xxx.o的RESET段,而是某个库文件,就说明+First没生效或者被别的东西顶掉了。这个信息比报错本身有用得多。

一个实操心得:map 文件里段的排列顺序就是最终烧录顺序,+First生效时,向量表段必然排在最前面,其基地址等于执行区域的起始地址。如果你看到向量表段不在第一位,基本可以断定链接脚本里那条规则没起作用,或者被后面的规则覆盖了。

3.2 用 fromelf 看目标文件的段清单

第二条路径是直接检查目标文件本身。Keil 安装目录下自带fromelf.exe,通常位于Keil_v5\ARM\ARMCLANG\bin或Keil_v5\ARM\ARMCC\bin下。用它能把.o文件里的段表原样打出来:

fromelf --text -v --output=startup_sections.txt ".\Objects\startup_xxx.o"

输出里会有一段 “Section Table”,列出每个段的编号、名字、大小、类型。你要找的是名字为RESET、类型为SHT_PROGBITS、属性带ALLOC的那一条。找到就说明段存在,问题在选择器写法或工程配置;找不到就说明启动文件有问题,或者根本链接的不是这个.o。

也可以在命令行里直接过滤,Linux/macOS 下:

fromelf --text -v Objects/startup_xxx.o | grep -A2 -i "section"

Windows 的 CMD 里用findstr:

fromelf --text -v Objects\startup_xxx.o | findstr /i "section"

这个动作的价值在于排除法。你先确认.o里有RESET段,再回头检查.sct里的写法,范围一下就缩小了。我见过不少人反复改.sct的文字,结果真正的原因是Objects目录里的启动文件是三天前的旧版本,段名早就改过了。

3.3 打开 --verbose 观察选择器匹配过程

第三条路径略微“重”一些,但信息最全:给链接器加--verbose参数,让它把匹配过程打出来。在 Options for Target → Linker → Misc controls 里填:

--verbose

重新编译,Build Output 里会多出大量链接细节,包括每个执行区域被创建、每个选择器被处理的过程。虽然数量大,但你可以用关键词定位:搜RESET,看链接器是否提到过这个段名,有没有类似Pattern (*.o(RESET)) matched 0 sections的记录。这种信息几乎是直接的答案。

这个手段的使用场景是“配置看起来都对,就是不生效”。比如你确认了.o里有RESET,.sct里写的也是(RESET, +First),但依然报错。这时候 verbose 输出往往会暴露真正的原因,比如链接器读的是另一份.sct,或者你改的.sct在Objects目录里被下次 Build 覆盖了。

注意:--verbose会让 Build Output 变得很长,排查完记得把它从 Misc controls 里删掉,否则日常编译会被大量日志淹没。

4. 一次完整的实操修复过程复盘

4.1 现场:报错信息与工程配置

这里用一个模拟项目来说明完整流程。项目代号X-Demo,一颗 Cortex-M3 内核、256KB Flash、48KB RAM 的器件,工程从另一台机器整体拷贝过来,Target 名字还是同事定义的Target 1。

现象:编译所有源文件都通过,链接阶段报

.\Objects\X-Demo.sct(7): error: L6236E: No section matches selector - no section to be FIRST/LAST.

第一步不是改文件,而是打开 Options for Target → Linker,看当前配置。发现 “Use Memory Layout from Target Dialog” 是勾选状态,Scatter File 框灰显,指向.\Objects\X-Demo.sct。这说明用的是自动生成的分散加载文件,内容由 Target 页面里的 IROM/IRAM 参数决定。

再切到 Target 页面,看到 IROM1 的 Start 是0x08000000,Size 是0x00040000(256KB),IRAM1 是0x20000000、0x0000C000(48KB)。这两个值是对的,和芯片手册一致。也就是说,自动生成的 .sct 本身没有问题,ER_IROM1区域存在,(RESET, +First)那行也正常。

那问题就只能出在“没有段能匹配”这一侧了。范围一下缩小到启动文件。

4.2 排查路径与每一步的判据

第二步,打开Objects目录,找startup_*.o。结果是没有找到。目录里只有各个.c文件对应的.o,没有启动文件的编译产物。这是决定性的证据。

第三步,回到 Project 窗口展开所有分组,逐个查看,发现启动文件确实存在于一个名为Startup的分组里,但该分组的文件夹图标是灰的——右键属性里 “Include in Target Build” 没勾上。这就是根因:文件在工程里,但被排除在构建之外。

这里有个细节值得说:Keil 对“排除编译”的文件不会给出任何编译期提示,Build 阶段既没有警告也没有错误,只有在链接阶段才以 L6236E 的形式暴露。所以看到这个报错,第一个动作就该是检查Objects目录里有没有启动文件的.o。

第四步,勾上 “Include in Target Build”,重新 Build。这一次启动文件编译通过了,.o出现了,但报错没有消失,还是同一行。说明还有第二层问题。

第五步,用 fromelf 检查新生成的.o:

fromelf --text -v --output=sec.txt ".\Objects\startup_xxx.o"

打开sec.txt,在 Section Table 里找到这一行:

# Name Type Addr Off Size ES Lk Inf Al 2 RESET PROGBITS 00000000 000034 00000130 00 0 0 2

段名是RESET,没问题。但往下翻,发现这个段的名字后面标的属性里没有ALLOC之外的异常。也就是说段本身是真的。

第六步,回头看.sct。既然.o里有RESET,.sct里写的是*.o (RESET, +First),为什么匹配不上?这时候打开Objects\X-Demo.sct通读全文,发现问题了:文件里写的是

LR_IROM1 0x08000000 0x00040000 { ER_IROM1 0x08000000 0x00040000 { startup_xxx.o (RESET, +First) ...

选择器写成了具体模块名 + 段名的组合。理论上这是合法的,但前提是模块名精确匹配。而工程里的启动文件实际叫startup_xxx_hd.s,编译出来是startup_xxx_hd.o,名字多了一个_hd后缀。.sct里写的是简化名,链接器当然匹配不上。

到这里根因就清楚了:这份.sct是从另一个工程手工拷贝过来的自定义文件,不是自动生成的。因为如果勾了 “Use Memory Layout from Target Dialog”,链接器生成的模板里一定是*.o (RESET, +First),用通配符而不是具体模块名。这也解释了为什么前面看到 Scatter File 框是灰的——其实它指向的是一份被手工覆盖过内容的文件,路径虽然一样,内容早就不是自动生成的版本了。

4.3 修复后的文件对照与验证方法

修复方式有两种,按场景选:

第一种,如果工程本来就不需要精细控制内存布局,直接勾上 “Use Memory Layout from Target Dialog”,让 Keil 重新生成.sct。操作后要手动删除Objects目录下那份旧文件,再全量 Rebuild,否则可能读到缓存。

第二种,如果确实需要自定义布局(比如要留一段 bootloader 区、要把某个函数放到 RAM 里跑),就保留自定义.sct,但把选择器改成通配形式:

LR_IROM1 0x08000000 0x00040000 { ER_IROM1 0x08000000 0x00040000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x0000C000 { .ANY (+RW +ZI) } }

注意这里*.o用了通配符,无论启动文件叫什么名字都能匹配到。同时把 RAM 大小改成0xC000对应 48KB,和芯片一致。如果项目里还有额外的段要指定位置(比如把某个.o放到固定地址),再加独立规则,但一定放在.ANY之前,因为.ANY会吸走所有剩下的。

验证方法分三步,缺一不可:

第一步看 map 文件。Rebuild 后打开.map,在Memory Map of the image里找Execution Region ER_IROM1,第一行应该是:

Base Addr Size Type Attr Idx E Section Name Object 0x08000000 0x00000130 Data RO 3 RESET startup_xxx_hd.o

基地址等于0x08000000,且是列表第一项,说明+First生效。

第二步看 hex 头部。用任意十六进制查看工具打开生成的.hex或.bin,前 4 个字节应该是栈顶指针初始值(通常指向 RAM 末尾,如0x2000C000附近的合法值),紧接着 4 个字节是复位向量地址。如果这两项是0xFFFFFFFF或者明显不对,说明向量表没放对位置。

第三步上电验证。烧录后确认程序能正常跳转到main,SystemInit里的时钟配置生效。这一步能兜住很多“链接过了但运行不对”的隐性问题。

提醒:如果你改过芯片型号或者调整过 IROM/IRAM 参数,务必执行一次Project → Clean Targets再全量编译。Keil 的增量编译对.sct变更的感知并不总是可靠,残留的旧.o会让人误判修复是否生效。

5. 常见问题速查表与几条硬经验

5.1 速查表

下面这张表把实际排查中高频出现的现象与对策整理在一起,遇到报错时按行对照,能省掉不少来回试的时间。

现象高度可疑原因验证手段处理
报错指向 .sct 第 7 行,提示 no section to be FIRST启动文件未参与链接查Objects目录有无startup_*.o加入工程并勾选 Include in Target Build
.o存在但依然报错段名大小写/拼写不一致fromelf -v看 Section Table统一改成RESET或调整 .sct 选择器
换芯片型号后开始报错启动文件与芯片容量不匹配核对 Target 里 IROM/IRAM 参数换对应容量的启动文件,重算 Size
自定义 .sct 后失效选择器写死具体模块名检查是否有xxx.o (RESET, +First)改成*.o (RESET, +First)
改完 .sct 无效果文件被自动生成覆盖看 Linker 页是否勾了自动布局关掉自动布局或直接删旧文件
同时报 L6200E 重复定义工程里有多个启动文件搜工程树中所有startup_*.s只保留一个,其余排除
报错消失但程序跑飞向量表放错位置查 hex 前 8 字节确认+First真生效、基地址正确

5.2 几条踩坑心得

第一,不要迷信“我什么都没改”。L6236E 的触发往往不是一次显式修改,而是某次拷贝、某次切换 Target、某次 Clean 之后的副作用。养成习惯:每次动过 Linker 页面或 Target 参数,都做一次全量 Rebuild,不要依赖增量。

第二,看报错的行号,但别只看行号。行号指向的是症状,不是病因。第 7 行写着+First,问题可能在第 300 行之外的工程配置里。我的习惯是先确认三件事:.o有没有、段有没有、选择器对不对,按这个顺序排查,绝大多数情况三轮之内能收敛。

第三,手写.sct时优先用通配符。除非你确实需要把某个特定模块钉在某个地址上,否则选择器一律写*.o (段名, +First)。写死模块名的做法看似精确,实际上是把文件名当成了接口,一改文件名就断,维护成本极高。

第四,向量表段名不要随便改。有些开发者为了“看起来规范”,把启动文件里的AREA RESET改成AREA VECTOR_TABLE,然后忘了同步改.sct,直接触发 L6236E。正确的顺序应该是先改.sct再改启动文件,或者干脆两个都不改——RESET这个名字已经成了事实标准,没有改的必要。

第五,保留一份能工作的备份。我自己的习惯是:每当一个工程的链接配置调通,就把.sct文件和 Target 页面的截图一起存进工程的doc目录。下次换芯片、升级工具链之后出现类似报错,直接拿这份备份对照,比从零排查快得多。

第六,正确看待自动生成的.sct。勾选 “Use Memory Layout from Target Dialog” 之后,Objects目录下的.sct是链接器生成的中间产物,不是源文件。手动修改它只在当次 Build 生效,下次 Build 会被覆盖。如果确实需要定制布局,应该在工程目录下新建一份独立的.sct,在 Linker 页面取消自动勾选并显式指向它,这样才不会被工具链“悄悄擦掉”。

最后再分享一个我在实际项目里验证过的小技巧:当你不确定是.sct的问题还是启动文件的问题时,可以临时把.sct里那条+First删掉,只留*.o (RESET),然后重新链接。如果链接能过、map 里RESET段也出现了,说明段本身没问题,问题在选择器写法或位置约束;如果链接仍然报“找不到段”,那就百分之百是启动文件这一侧的事。这个“去掉约束做对照”的二分法,比对着两份文件逐字比对要快得多,尤其是在工程规模大、文件多的时候。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询