1. 先看懂L6236E在说什么:链接器找不到"第一个出场"的代码段
用Keil MDK开发的朋友,十有八九都撞到过这个报错。当时我第一回见,是在一个接手项目的第二天——别人给的工程,一编译直接卡死在链接阶段,弹出一行红色的:
.\Objects\xxx.sct(7): error: L6236E: No section matches selector - no section to be FIRST/LAST.行号还不固定,有时候是第7行,有时候是第3行,但错误码永远是这个L6236E。很多人第一反应是去网上搜"怎么解决",但说实话,你如果不懂这行字背后的链接机制,搜来的答案大概率是"加个启动文件""检查一下芯片型号"这种知其然不知其所以然的回复,照着弄可能碰巧好了,换个场景又废。
先把这行报错拆开看。
在ARM Compiler的链接阶段,链接器会读取分散加载描述文件(就是.sct文件),这个文件里定义了ROM、RAM的分布区域,以及每个区域里应该放什么内容。以最常见的STM32工程为例,默认生成的xxx.sct大概长这样:
; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************************* LR_IROM1 0x08000000 0x00080000 { ; load region size_region ER_IROM1 0x08000000 0x00080000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) .ANY (+XO) } RW_IRAM1 0x20000000 0x00010000 { ; RW data .ANY (+RW +ZI) } }重点看这行:
*.o (RESET, +First)意思是:在某个目标文件(*.o)里,必须存在一个名为RESET的代码/数据段,并且它要被放在这个region的最前面(+First)。这个RESET段,就是启动文件里的中断向量表。处理器复位后第一条指令从0x08000000取到的是栈顶地址,紧接着取到的是复位向量地址,这决定了程序必须在这个位置放中断向量表。
现在你回头看那个报错:
No section matches selector - no section to be FIRST/LAST.
它的意思就是:链接器在整个工程的编译产物里,没找到任何一个满足条件、能被标记为FIRST或LAST的section。翻译成大白话——你工程里压根没有一个叫RESET的段,或者说没有任何一段代码被指定放到这个位置,所以链接器直接罢工了。
那问题就变成:为什么好好的工程会没有RESET段?下面几节我把实战里遇到过的原因一个一个捋,并且给出验证方法,避免你瞎猜。
2. 从"启动文件"入手排查:九成的L6236E都在这儿
2.1 启动文件到底去哪儿了
这是最常见的情况,没有之一。
Keil工程树里的源文件其实分几种状态:普通参与编译、被排除构建(Exclude from build)、以及压根不在工程里。很多人接手旧工程,或者自己在网上下了个Demo,会发现工程树里所有文件都在,唯独少了startup开头的那个文件。
STM32的启动文件命名通常是这样的:
startup_stm32f103xe.sstartup_stm32f407xx.sstartup_stm32f429xx.s- 或者老一些的
startup_stm32f10x_hd.s
这个文件就是产生RESET段的源头,它里面写了整个中断向量表,包括初始栈指针、复位处理器、各个中断的默认处理函数。没有它,编译器再努力也编不出RESET段。
排查方法很简单:
- 在Keil左侧Project窗口展开你的目标分组,找有没有
.s后缀的文件; - 找到了就先双击打开看一眼,里面有没有
AREA RESET, CODE, READONLY这行(不同芯片写法略有差异,但关键词是RESET); - 确认文件在,但编译还是报L6236E——右键该文件,看选项里
Options for File中Exclude from build是否被勾上了。勾了就取消,重新编译。
这里插一句:很多人在网上下的HAL库Demo,或者GitHub上拉的开源工程,作者用的是IAR或者STM32CubeIDE,文件夹里根本没放Keil的启动文件。这时候你不能只加个空文件,必须去ST官方固件库的CMSIS/Device/ST/STM32F1xx/Source/Templates/arm目录下找对应型号的启动文件加进工程。
2.2 芯片型号和启动文件不匹配
这种错误更隐蔽,编译不报错,但链接必炸。
我举个例子。假设你现在用的是STM32F103ZET6(512KB Flash),但工程里挂的启动文件是startup_stm32f103c8t6.s(64KB Flash的,多见于小容量芯片)。启动文件里不只会放向量表,还会定义堆栈大小、以及一些芯片特有的初始化方式。两者混用,在某些链接配置下不会直接报"型号不对",而是导致section属性异常,最后就推送出L6236E。
更典型的场景是从F1往F4迁移,或者在F407和F429之间切换。有人为了省事,换芯片后只改了Device型号,启动文件还是老的。F4系列的启动文件在向量表长度、SystemInit调用方式上和F1完全不同,这时候报的错五花八门,L6236E就是其中之一。
验证与处理:
- 点击魔术棒(Options for Target)→ Device选项卡,确认当前选择的芯片型号;
- 去ST固件库里找到对应型号的Keil启动文件,替换工程里原有的;
- 替换完务必把原来那个.s文件从工程里移除,别让两个启动文件同时存在。
工程里挂两个启动文件是个非常经典的低级错误,链接器有可能会去匹配其中任意一个的RESET段,结果两个都不是合法定义,照样报错。
2.3 C/C++选项卡里的宏定义被清空
这点很多老手都会忽略,但新手特别容易踩。
某些芯片的启动文件里,不仅仅有向量表,还包含一些条件编译的代码。比如有些F1系列的启动文件里会通过STM32F10X_HD这种宏来决定是否编译进大容量芯片特有的中断处理。
Cortex-M内核的芯片启动文件相对"干净",但很多厂商(比如GD32、华大、NXP的部分系列)的启动文件里,都会预编译宏相关的判断。当你在C/C++选项卡的Define框里把宏删了或者覆盖了,启动文件里的一部分段定义就不会被编译进去,RESET段缺失,链接阶段就冒出L6236E。
检查方法:
- 打开 Options for Target → C/C++ → Preprocessor Symbols → Define;
- 对照芯片型号,查看是否缺少必须的宏。以STM32F103系列为例,如果用标准外设库,至少要有
STM32F10X_HD或STM32F10X_MD这类区分容量的宏(具体看是Standard Peripheral Library还是HAL库,HAL库通常需要STM32F103xE这种宏); - 可以用一个已知能正常编译的工程作为参照,把Define里的内容补全。
提示:有些移植老工程的朋友不知道,Keil 5从某个版本起对启动文件的默认编译行为做了调整,个别旧工程需要手动勾选"Use default compiler version 5"或者加
--c99才能正常编过。但L6236E和编译器版本的关系没那么大,如果上述方法都不生效,再考虑这个方向。
3. 分散加载文件(.sct)被改坏或选错:链接器动手脚的元凶
第二种高频场景,是.sct文件本身出了问题。很多人对它理解不深,觉得就是个自动生成的东西,碰都不碰。但一旦你手动改过一次,后面就可能埋雷。
3.1 被选中执行的.sct和实际内容对不上
Keil里分散加载文件的位置在:
Options for Target → Linker → Use Memory Layout from Target Dialog
当你勾选Use Memory Layout from Target Dialog时,Keil会根据你在Target选项卡里填的ROM/RAM地址,自动生成.sct文件,你不需要关心它长什么样。
但如果你取消了这个勾选,下面那个Scatter File编辑框变为可编辑状态,你就可以指定一个自定义的.sct文件。问题就出在这儿——当你指定的.sct文件内容是针对其他芯片写的,或者是从旧工程拷过来忘了改地址,链接器就会拿着这个"错误地图"去寻找段。
举个真实场景:一个F103ZE的工程,Flash是512K(0x08000000–0x0807FFFF),但.sct文件还是从F103C8T6工程拷来的,只定义了64K(0x08000000–0x0800FFFF)。程序稍微大一点,链接器在ROM区后半部分找不到足够的空间放启动文件定义的段,就会报一堆乱七八糟的错误,其中一定包含L6236E。
另外还有一种情况是这个.sct文件虽然格式正确、地址也对,但缺少了启动段标记。比如说有人手动改过,把*.o (RESET, +First)这行误删了。那就会立刻触发L6236E——因为链接器要求region有一个FIRST段,但你的描述文件里根本没说用哪段来当FIRST。
排查方法:
- 打开 Options for Target → Linker;
- 检查是否勾选了
Use Memory Layout from Target Dialog; - 如果没勾,看
Scatter File指向的文件路径,用编辑器打开,对照你当前芯片的起始地址和大小; - 确认每一段region里是否包含类似的FIRST/LAST标记行,尤其是第一个加载域。
3.2 恢复默认.sct的操作
如果排查一番发现.sct文件里内容乱七八糟,最简单止损的办法是:
- 打开 Options for Target → Linker;
- 勾选
Use Memory Layout from Target Dialog; - 删除下面Scatter File编辑框里的自定义路径;
- 点OK,重新编译。
这样Keil会重新按照Target对话框里的IROM1/IRAM1配置自动生成一份干净的sct文件。绝大多数情况下,这个动作就能解决由于sct文件损坏导致的问题。
3.3 手动改.sct时的保命原则
如果你确实有需求要手动定制内存布局(比如在RAM里跑代码、或者把某个固定地址放Bootloader的跳转信息),那么修改.sct文件时务必保留每个region内部的FIRST/LAST声明。
一个可用的最小示例:
LR_IROM1 0x08000000 0x00100000 { ER_IROM1 0x08000000 0x00100000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) .ANY (+XO) } RW_IRAM1 0x20000000 0x00020000 { .ANY (+RW +ZI) } }如果你自定义了某个段放在固定地址,比如要用__attribute__((section("MY_DATA"))),可以在RW区加一行:
RW_IRAM1 0x20000000 0x00020000 { .ANY (+RW +ZI) *(.MY_DATA) }反过来,如果你的项目里确实没有启动文件(比如纯内存初始化、或者某个已经初始化好环境的场景),你需要把*.o (RESET, +First)删掉,同时把ER_IROM1里的First属性改成其他段名。但这是极少数场景,普通应用不要碰。
4. 进阶排查链路:从.map文件和链接日志找到真正的"断点"
如果前面几招都试过了还不行,就说明问题不像"少了启动文件"这么简单。这时候我建议你不要继续盲试,而是翻开链接器给出的详细日志和生成产物,进行逻辑推理。
4.1 打开链接器的详细输出
默认情况下,Keil的Build Output只显示错误和警告,很多细节被吞了。你可以通过设置让链接器输出更详细的信息:
Options for Target → Listing → 在"Linker Listing"区域勾选所有项目,尤其是"Memory Map" 和 "Cross Reference"
重新编译后,在工程目录的Listings文件夹下会生成一个.map文件,用记事本打开。
先看文件末尾的Image Symbol Table和Memory Map of the image,确认启动文件里的标号是否存在。正常情况下,你会看到类似:
Reset_Handler 0x08000000 Code SystemInit 0x080001b8 Code __Vectors 0x08000000 Data ; 注意某些编译器把向量表识别为Data如果连__Vectors、Reset_Handler都没有,基本可以确认RESET段没有被打进最终镜像。
4.2 用ARM编译器工具链做"尸检"
Keil安装目录下有一堆命令行工具,其中fromelf很实用,可以把最终ELF文件反汇编/导出详细信息。命令类似于:
fromelf --text -z -d --output=output.txt .\Objects\YourProject.axf-z是显示零初始化段信息,-d是反汇编代码段。查看生成的txt里有没有Reset_Handler和__Vectors,再看它们的地址是不是落在0x08000000附近。
如果没有生成.axf文件,说明链接失败,看不到产物。这时候可以考虑先注释掉.sct里的FIRST行,临时编一个不要求FIRST的镜像,看看能不能链接成功。如果这样能出.axf,就证明问题确实出在"没有可用的RESET段",而不是.sct文件其他地方的语法或地址问题。
4.3 查看Build Output窗口最顶部的警告
有些L6236E报错之前,其实编译过程已经给出过警告,只不过在长长的输出里被滚动淹没了。编译完后,从Build Output窗口的最顶上开始往下翻,留意类似这样的信息:
compiling startup_stm32f103xe.s... assembling startup_stm32f103xe.s...如果有assembling这行,说明启动文件被汇编器正常处理了。如果这一行都没出现,说明这个.s文件根本没进入编译流程——这时候就算你加再多的.sct配置也白搭。
我遇到过一种情况:工程里.s文件确实在,但我用的Arm Compiler 6编译器需要额外的汇编选项,结果启动文件被静默跳过。后来在.s文件的Options里把Assembler选项卡的Assemble Thumb Code和相关配置改成和默认工程一致,问题才解决。
4.4 从报错行号反推哪个region缺段
L6236E报错信息里括号中的数字,比如.sct(7),指的就是出错位置在第7行。你可以打开自己工程的链接文件,数一数是哪一行对应的region报错。
以我前面的示例sct为例:
第1行: ; ************************************************************* 第2行: ; *** Scatter-Loading Description File generated by uVision *** 第3行: ; ************************************************************* 第4行: 第5行: LR_IROM1 0x08000000 0x00080000 { 第6行: ER_IROM1 0x08000000 0x00080000 { 第7行: *.o (RESET, +First) 第8行: *(InRoot$$Sections) 第9行: .ANY (+RO) 第10行: .ANY (+XO) 第11行: } 第12行: RW_IRAM1 0x20000000 0x00010000 { ...报错说是第7行,对应就是*.o (RESET, +First)这一句。所以该行所要求的RESET段缺失。如果你的报错行号是其他行,比如指向LR_IROM1那一行,说明整个加载域都有问题,可能就是地址重叠或者非法。
这个方法能帮你快速确认:到底是所有region都没段匹配,还是只有某一个region缺段。这两种情况的处理逻辑不太一样——前者通常是启动文件缺失,后者往往是段选择器的写法问题。
5. 从CubeMX生成工程和旧工程迁移中的特殊雷区
5.1 STM32CubeMX生成的Keil工程为什么也会报L6236E
这几年用CubeMX建工程的人越来越多,按理说它生成的工程应该是开箱即用的。但你要是遇到过以下操作,照样会踩到L6236E:
- 生成工程后,手动把
startup_stm32xxxx.s文件从工程里删了,想自己写启动逻辑; - 用CubeMX切换了芯片型号,但
Project Manager → Project里的工具链设置没跟着变,或者旧启动文件没被覆盖; - 在
.ioc文件里改了Flash/RAM大小,但重新生成代码时勾选了"备份旧工程",结果新旧启动文件混杂; - 使用了自己添加的链接脚本,而CubeMX自动生成的sct被覆盖了。
第一种场景比较难办。如果你确定不要ST的启动文件,那么你得自己提供一个向量表段,并且保证它带有初始栈指针和复位向量。很多从裸机汇编开始写固件的老工程师会这么干,但对于绝大多数应用来说,直接用官方启动文件是成本最低、最稳妥的方案。
第二种场景的处理方法是:在CubeMX里重新选择正确的芯片,然后在Project Manager → Linker Settings里确认Heap/Stack大小没问题,最后重新生成代码,让CubeMX把启动文件一并刷新。
5.2 旧工程"换芯片"最完整的操作顺序
如果你手上有个F103C8T6的旧工程,要改成F103ZET6,千万别只改Device型号就完事。我的标准操作顺序如下,你可以直接照着做:
- 备份整个工程目录(这一步每次都要做,别省);
- Options for Target → Device,选择新芯片;
- Options for Target → Target,核对ROM/RAM地址和大小。F103C8T6的RAM是20KB(0x20000000,大小0x5000),Flash 64KB(0x08000000,大小0x10000);换成F103ZET6后,RAM是64KB(0x10000),Flash是512KB(0x80000);
- 从ST官方固件库或者另一个正常运行的F103ZET6工程里,复制
startup_stm32f103xe.s到工程目录,然后在Keil里删除旧启动文件、添加新启动文件; - 检查C/C++选项卡的Define,F103ZE对应
STM32F10X_HD(标准外设库)或STM32F103xE(HAL库); - 清理编译产物,点Rebuild(不是Build,是Rebuild)。
- 如果还报错,去检查Linker选项卡里是否勾着Use Memory Layout from Target Dialog,确认Scatter File路径没有指向旧芯片的sct。
这套流程走下来,L6236E基本不会再出现。
5.3 使用AC5和AC6编译器时的启动文件差异
Keil MDK从5.x版本开始,默认的编译器逐渐向Arm Compiler 6(基于Clang)迁移。AC6和AC5在处理汇编启动文件上有很多差异,这一点会被很多人忽略。
AC5使用的armcc/armasm,对.s汇编文件的语法比较宽松。而AC6使用的armclang,内置的汇编器对伪指令的支持不够全面,某些老启动文件在AC6下会汇编失败,或者产生不同的段属性。
如果你的工程是从AC5迁移到AC6后开始报L6236E,优先检查:
- 启动文件是否是比较老、为AC5编写的版本;
- ST官方针对AC6发布过新版本的启动文件(CMSIS pack里的通常没问题);
- 在Options for Target → Target → ARM Compiler里,尝试暂时切换回
Use default compiler version 5,如果能编译通过,说明就是启动文件对AC6兼容性的问题。
从Arm Compiler 5切到6之后,很多旧版启动文件会直接报汇编错误,不会走到L6236E这一步。但有一种情况是汇编只产生警告,没有报错,最后段的属性不对,才最终在链接阶段爆炸。
5.4 工程路径和特殊字符的隐患(这个方法很少有人提)
最后分享一个很多人不会第一时间想到的原因:工程所在路径太长,或者包含中文、空格、特殊符号。
当年我遇到过一个很邪门的L6236E,所有的检查和替换都做完了,问题就是复现不了。折腾了一下午,最后把整个工程文件夹从D:\工作文件\测试项目\基于STM32的温控系统_V2_最终版(1)\挪到D:\keil_projects\temp\,重新编译,一切正常。
原因大概是链接器在解析sct里通配符和路径时,对中文字符和多级嵌套目录的处理在某些编译器版本里有Bug。虽然这个bug不是每次必现,但一旦遇到,排查起来极其耗费时间。
所以建议你的工程存放路径尽量满足:
- 全英文;
- 不要有空格;
- 层级不要超过三四级;
- 不要以数字开头(在某些老版本里也会出问题);
- 更不要放在桌面这种权限有时受限的目录下。
6. 实操总结:最快的解决路径和一套顺手能用的验证方法
我把上面所有的排查逻辑压缩成一套可以直接照做的动作,建议你按顺序执行,不要跳步。
第一步:确认启动文件存在且参与编译
- 工程树里有没有
.s文件? - 右键
.s文件 → Options for File → 确认 Exclude from Build 未勾选。 - 打开
.s文件,搜索AREA和RESET关键字。
第二步:检查芯片型号和宏定义
- Options for Target → Device,确认芯片型号。
- Options for Target → C/C++,确认Define里有对应型号的宏。
- 如果不确定宏应该是什么,去正常能编译的工程抄一份,或者查ST官方模板。
第三步:重置分散加载文件
- Options for Target → Linker,勾选
Use Memory Layout from Target Dialog,清除自定义Scatter File路径。 - Options for Target → Target,确认IROM1起始地址0x08000000,大小按芯片Flash实际大小填;IRAM1起始0x20000000,大小按芯片实际RAM填。
- Rebuild整个工程。
第四步:查看详细链接输出定位
- Options for Target → Listing,勾选Linker Listing下的Memory Map、Cross Reference等。
- 重新编译后打开.map文件,搜索
Reset_Handler和__Vectors。 - 如果找不到,说明启动段的产生就有问题,回头检查汇编文件。
- 也可以用fromelf反汇编.axf文件验证段地址。
第五步:整路径、整编译器
- 工程路径改成纯英文短路径。
- 尝试在Target选项卡里切AC5/AC6,判断是否是编译器兼容性问题。
这套流程我前前后后帮同事处理了不下十次,耗时从几分钟到一下午都有。但其实九成以上的情况,走完第二步就能解决,第三步都很少需要用到。
再分享一个我自己的习惯:**每次新建工程时,先不写任何业务代码,只放一个main函数和一个printf重定向,确认能在这个工程模板里编译、下载、跑通串口,再往里加功能。**这样一旦后面报错,你能确定是新增代码导致的问题,而不是工程基础配置本来就有毛病。L6236E这类链接错误,说白了就是工程配置和实际代码对不上号,你得让工程底子干净,问题才好定位。