十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ESP-IDF 链接器脚本生成机制(Linker Script Generation)完全指南:从 `.lf` 片段到自定义内存布局

ESP-IDF 链接器脚本生成机制(Linker Script Generation)完全指南:从 `.lf` 片段到自定义内存布局 ESP-IDF 链接器脚本生成机制Linker Script Generation完全指南从.lf片段到自定义内存布局【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf导读在 ESP-IDF 中代码与数据默认被放置到固定位置可执行代码与只读数据在 Flash 中可写数据在 RAM 中。但在实际开发中我们经常需要打破这种默认布局——例如把性能关键代码搬进 IRAM、把深睡眠唤醒存根或 ULP 协处理器代码放进 RTC 内存。链接器脚本生成Linker Script Generation机制正是 ESP-IDF 提供的、在组件级别声明这种放置意图的方案组件通过.lf链接器片段文件描述自己的放置规则构建系统在编译时收集、解析并处理这些信息最终动态生成链接脚本用于链接应用。读完本文你将掌握在 ESP-IDF 中创建链接器片段文件、在 CMake 中注册片段、以对象/符号/归档三级粒度指定放置位置、编写基于 Kconfig 的条件放置规则并理解其底层实现原理片段类型、Scheme、链接脚本模板与标记语法。1. 为什么需要链接器脚本生成机制ESP-IDF 芯片拥有多个内存区域可参考 memory-layout 相关文档代码和数据可以放置到不同区域中。默认情况下代码和只读数据rodata放在 Flash可写数据data/bss放在 RAM某些特殊数据放在 RTC 内存。但以下场景要求改变默认放置性能关键代码需要放入 RAMIRAM/DRAM以提升执行速度可执行代码放入 IRAM以便在 Cache 被禁用时依然可以运行如 panic handlerRTC 内存中的代码用于深睡眠唤醒存根wake stub在支持该特性的芯片上RTC 内存中的代码供 ULP 协处理器使用在支持 ULP 的芯片上。链接器脚本生成机制使得这些放置需求可以在组件级声明组件说明希望如何放置自己的符号symbol、目标文件object或整个归档archive。构建期间ESP-IDF 收集各组件提交的信息进行解析与处理将生成的放置规则用于链接应用。核心工作流组件提供 .lf 片段 → 构建系统收集解析 → 动态生成链接脚本 → 链接器按脚本放置代码/数据。2. 快速开始将代码/数据放入 RAM 与 RTC 内存本节演示 ESP-IDF 开箱即用的放置方案noflash与rtc假定存在如下组件components └── my_component ├── CMakeLists.txt ├── Kconfig ├── src/ │ ├── my_src1.c │ ├── my_src2.c │ └── my_src3.c └── my_linker_fragment_file.lf前提条件组件my_component在构建时归档为库libmy_component.a三个源文件my_src1.c、my_src2.c、my_src3.c分别编译为my_src1.o、my_src2.o、my_src3.omy_src1.o中定义了函数my_function1my_src2.o中定义了函数my_function2组件的 Kconfig 中有布尔型配置PERFORMANCE_MODEy/n和整型配置PERFORMANCE_LEVEL取值范围 0-3。2.1 创建并注册链接器片段文件链接器片段文件就是一个以.lf为扩展名的纯文本文件将期望的放置规则写在其中。创建后需要告知构建系统在组件CMakeLists.txt的idf_component_register调用中指定LDFRAGMENTS参数。其值可以是绝对路径也可以是从组件目录到片段文件的相对路径# 文件路径相对于 CMakeLists.txt idf_component_register(... LDFRAGMENTS path/to/linker_fragment_file.lf path/to/another_linker_fragment_file.lf ... )ESP-IDF 自身大量使用该机制。例如 esp_system/CMakeLists.txt 中注册了linker.lf与app.lf两个片段文件freertos/CMakeLists.txt 则根据 FreeRTOS 的配置SMP/单核等动态拼接linker_common.lf、linker_smp.lf、linker.lf列表后通过LDFRAGMENTS传入。这印证了LDFRAGMENTS是组件与链接器脚本生成器之间的标准接口。2.2 放置粒度放置可以在以下粒度层面指定目标文件级.obj/.o文件符号级函数/变量归档级.a文件放置整个目标文件假设my_src1.o整体是性能关键代码需要放入 RAMmy_src2.o整体包含深睡眠唤醒所需符号需要放入 RTC 内存。在片段文件中写[mapping:my_component] archive: libmy_component.a entries: my_src1 (noflash) # 将 my_src1 的所有代码/只读数据放入 IRAM/DRAM my_src2 (rtc) # 将 my_src2 的所有代码/数据/只读数据放入 RTC 快速内存/RTC 慢速内存未被指定的my_src3.o将使用默认放置详见下文 [default] 放置 一节。放置单个符号如果my_src1.o中只有my_function1是性能关键my_src2.o中只有my_function2需要在芯片从深睡眠唤醒后执行[mapping:my_component] archive: libmy_component.a entries: my_src1:my_function1 (noflash) my_src2:my_function2 (rtc)my_src1.o与my_src2.o中的其余函数以及整个my_src3.o使用默认放置。放置数据与此类似只需把函数名换成变量名my_src1:my_variable (noflash)警告符号粒度放置存在局限性为保证正确放置更推荐将相关代码/数据组织到独立源文件中然后使用对象粒度放置。放置整个归档将整个组件归档放入 RAM[mapping:my_component] archive: libmy_component.a entries: * (noflash)将整个组件放入 RTC 内存[mapping:my_component] archive: libmy_component.a entries: * (rtc)2.3 配置相关的条件放置只有当某条件成立时才需要特殊放置。例如当CONFIG_PERFORMANCE_MODE y时才将整个库放入 RAM[mapping:my_component] archive: libmy_component.a entries: if PERFORMANCE_MODE y: * (noflash) else: * (default)更复杂的场景CONFIG_PERFORMANCE_LEVEL 1时仅my_src1.o入 RAM 2时my_src1.o与my_src2.o 3时归档内所有对象入 RAM以上均不成立时整个库放入 RTC 内存[mapping:my_component] archive: libmy_component.a entries: if PERFORMANCE_LEVEL 1: my_src1 (noflash) elif PERFORMANCE_LEVEL 2: my_src1 (noflash) my_src2 (noflash) elif PERFORMANCE_LEVEL 3: my_src1 (noflash) my_src2 (noflash) my_src3 (noflash) else: * (rtc)条件判断支持嵌套下面的写法与上面等价[mapping:my_component] archive: libmy_component.a entries: if PERFORMANCE_LEVEL 3 PERFORMANCE_LEVEL 0: if PERFORMANCE_LEVEL 1: object1 (noflash) if PERFORMANCE_LEVEL 2: object2 (noflash) if PERFORMANCE_LEVEL 3: object2 (noflash) else: * (rtc)注意上述等价写法中的object1/object2为文档示例的原始命名实际应使用my_src1/my_src2。2.4 default 放置此前一直用默认放置指代未指定rtc/noflash规则时的回退方案。需要强调noflash与rtc并非普通关键字而是被称为scheme方案的片段实体。与它们类似还存在一个defaultscheme定义默认放置规则——代码/常量放 Flash、变量放 RAM 等。提示ESP-IDF 中使用链接器脚本生成机制的典型示例可参考 freertos/CMakeLists.txt 及其配套的 linker_common.lf。FreeRTOS 通过它把对象文件放入指令 RAM 以获得性能提升。3. 链接器脚本生成机制的内部原理链接是 C/C 源文件变成可执行文件的最后一步由工具链的链接器执行链接器接受指定代码/数据放置方式的链接脚本。在链接器脚本生成机制下过程并无不同唯一区别是传给链接器的链接脚本是动态生成的其来源是(1) 收集到的链接器片段文件与 (2) 链接器脚本模板。实现工具实现该机制的工具位于 tools/ldgen入口为ldgen.py其中包含解析片段、生成规则的完整 Python 实现与测试用例。3.1 链接器片段文件快速开始中提到片段文件是以.lf为扩展名、包含期望放置规则的纯文本文件——这是简化描述。片段文件实际包含的是片段fragment片段是携带信息的实体这些信息组合起来形成放置规则告诉链接器将目标文件的各个 section 放置到输出二进制的何处。共有三种片段类型sections、scheme与mapping。语法三种片段类型共享同一语法[type:name] key: value key: value value value ...type片段类型可取sections、scheme或mappingname片段名称对指定片段类型而言必须唯一key, value片段内容每种片段类型支持不同的 key 与不同的取值语法对于sections与scheme唯一支持的 key 是entries对于mapping同时支持archive与entries。注意若遇到多个同类型同名的片段将抛出异常。注意片段名称与 key 的合法字符仅为字母数字与下划线。条件检查条件检查使链接器脚本生成具备配置感知能力。根据涉及配置值的表达式是否为真决定使用 key 的某一组取值。求值使用 kconfiglib 包的eval_string遵循其要求的语法与限制。支持的运算符比较LessThan、LessThanOrEqualTo、MoreThan、MoreThanOrEqualTo、Equal、!NotEqual逻辑||Or、And、!Negation分组()Parenthesis条件检查的行为与常规语言中的if...elseif/elif...else块一致。条件既可用于key 值也可用于整个片段。下面两个片段等价# key 的值取决于配置 [type:name] key_1: if CONDITION y: value_1 else: value_2 key_2: if CONDITION y: value_a else: value_b# 整个片段定义取决于配置 if CONDITION y: [type:name] key_1: value_1 key_2: value_a else: [type:name] key_1: value_2 key_2: value_b注释片段文件中的注释以#开头处理时被忽略用于提供说明与文档。3.2 片段类型详解Sections段Sections 片段定义 GCC 编译器生成的目标文件 section 列表。可以是默认 section如.text、.data也可以是通过__attribute__关键字定义的用户自定义 section。可选的后缀表示包含该 section 以及以它开头的所有 section如.text表示.text与.text.*。这是比同时显式列出两者更推荐的做法。[sections:name] entries: .section .section ...示例# 非推荐写法 [sections:text] entries: .text .text.* .literal .literal.* # 推荐写法与上面等价 [sections:text] entries: .text # 表示 .text 与 .text.* .literal # 表示 .literal 与 .literal.*Scheme方案Scheme 片段定义某个 sections 片段被分配到哪个target目标。[scheme:name] entries: sections - target sections - target ...示例[scheme:noflash] entries: text - iram0_text # 名为 text 的 sections 片段中的条目将进入 iram0_text rodata - dram0_data # 名为 rodata 的 sections 片段中的条目将进入 dram0_data特殊的 default scheme存在名为default的特殊 scheme其特殊之处在于会从它的条目生成兜底放置规则catch-all placement rules。例如若其条目之一是text - flash_text则生成针对目标flash_text的放置规则*(.literal .literal.* .text .text.*)这些兜底规则实际上充当那些未指定 mapping的实体的回退规则。defaultscheme 定义于 esp_system/app.lf快速开始中引用的内置 schemenoflash、rtc等也定义在该文件中。下面摘录app.lf中noflash与rtc的定义与文档示例完全一致[scheme:rtc] entries: text - rtc_text data - rtc_data rodata - rtc_data bss - rtc_bss common - rtc_bss [scheme:noflash] entries: text - iram0_text rodata - dram0_data此外app.lf还定义了noflash_data仅 rodata → dram0_data、noflash_text仅 text → iram0_text、spm等方案并通过[mapping:default] archive: * entries: * (default)把所有未显式映射的归档实体收归default方案。app.lf中的defaultscheme 还演示了如何结合配置条件当APP_BUILD_USE_FLASH_SECTIONS y时text - flash_text、rodata - flash_rodata否则回退到iram0_text与dram0_data当ESP_ALLOW_BSS_SEG_EXTERNAL_MEMORY y时extram_bss - extern_ram否则进入dram0_bss。Mapping映射Mapping 片段定义可映射实体目标文件、函数名、变量名、归档使用哪个 scheme 片段[mapping:name] archive: archive # 输出归档文件名按构建结果即 libxxx.a entries: object:symbol (scheme) # 符号粒度 object (scheme) # 对象粒度 * (scheme) # 归档粒度三种放置粒度symbol同时指定目标文件名与符号名符号名可以是函数名或变量名object仅指定目标文件名archive指定*是所有归档下目标文件的简写。理解条目的含义以对象粒度放置为例展开后为object (scheme)再把 scheme 片段从其条目定义展开object (sections - target, sections - target, ...)继续展开 sections 片段object (.section, # 给定该目标文件 .section, # 将其此处列出的 section 放到 ... - target, # 该 target .section, .section, # 这些 section 同样处理 ... - target, ...) # 依此类推示例[mapping:map] archive: libfreertos.a entries: * (noflash)除了实体与 scheme条目中还可以指定 flags标志详见下一节。3.3 Mapping 条目中的 Flags标志支持以下标志为参数名[]为可选1.ALIGN(alignment[, pre, post])按alignment指定的量对齐放置。在映射片段条目生成的输入 section 描述之前和/或之后取决于是否指定pre、post或两者都指定生成. ALIGN(alignment)若pre与post均未指定对齐命令生成在输入 section 描述之前。顺序敏感。2.SORT([sort_by_first, sort_by_second])在输入 section 描述中发出SORT_BY_NAME、SORT_BY_ALIGNMENT、SORT_BY_INIT_PRIORITY或SORT。sort_by_first与sort_by_second的取值可为name、alignment、init_priority。若两者都未指定输入 sections 按名称排序若都指定则嵌套排序遵循 GNU ld 文档 Input Section Wildcards 中的规则。3.KEEP()通过在输入 section 描述外包一层 KEEP 命令防止链接器丢弃该放置。详见 GNU ld 文档 Input Section Keep。4.SURROUND(name)在放置前后生成符号生成的符号命名遵循_name_start与_name_end。例如name sym1时生成_sym1_start ABSOLUTE(.) ... _sym2_end ABSOLUTE(.)这些符号可在 C/C 代码中引用。顺序敏感。Flags 的组合写法添加 flags 时需要指定 scheme 中具体的section - target多个section - target用逗号分隔# 说明 # A. 实体-scheme 后跟分号 # B. section2 - target2 前有逗号 # C. section1 - target1 与 section2 - target2 需定义在 scheme1 的 entries 中 entity1 (scheme1); section1 - target1 KEEP() ALIGN(4, pre, post), section2 - target2 SURROUND(sym) ALIGN(4, post) SORT()综合示例以下 mapping 片段[mapping:name] archive: lib1.a entries: obj1 (noflash); rodata - dram0_data KEEP() SORT() ALIGN(8) SURROUND(my_sym)在链接脚本中生成. ALIGN(8) _my_sym_start ABSOLUTE(.) KEEP(lib1.a:obj1.*( SORT(.rodata) SORT(.rodata.*) )) _my_sym_end ABSOLUTE(.)由于 ALIGN 与 SURROUND 均顺序敏感若调换二者位置生成的输出变为_my_sym_start ABSOLUTE(.) . ALIGN(8) KEEP(lib1.a:obj1.*( SORT(.rodata) SORT(.rodata.*) )) _my_sym_end ABSOLUTE(.)仓库中的真实 Flags 用法esp_system/linker.lf 为系统事件esysev_shdn、esysev_initc等生成的映射条目组合了ALIGN(4, pre)、KEEP()、SORT(init_priority)与SURROUND(esysev_shdn)等标志用于在 Flash 只读数据区构建带边界符号、按初始化优先级排序且不可被 GC 丢弃的系统事件表。4. 仓库实例esp_system与freertos的真实放置规则4.1 esp_system 的条件与符号粒度映射esp_system/linker.lf 是理解真实用法的绝佳样本。它展示了基于配置的条件映射当ESP_PANIC_HANDLER_IRAM y时将panic、panic_handler、panic_arch、cache_err_int等对象放入 IRAMnoflash并进一步嵌套条件ESP_SYSTEM_HW_STACK_GUARD y时再放置debug_assist相关函数符号粒度映射reset_reason:esp_reset_reason_get_hint (noflash)、freertos_hooks:esp_vApplicationTickHook (noflash)等将单个函数放入 IRAM多条件逻辑if ESP_PANIC_HANDLER_IRAM y || ESP_BROWNOUT_USE_INTR y:使用逻辑或组合条件。例如其中一段节选[mapping:esp_system] archive: libesp_system.a entries: if ESP_PANIC_HANDLER_IRAM y: panic (noflash) panic_handler (noflash) panic_arch (noflash) cache_err_int (noflash) reset_reason:esp_reset_reason_get_hint (noflash) ... # These functions are called when the cache is disabled system_internal:esp_restart_noos (noflash) system_internal:esp_system_reset_modules_on_exit (noflash) ...这些规则与文档描述完全对应panic 处理函数必须能在 Cache 被禁用时于 IRAM 中运行esp_restart_noos等复位路径函数同样如此。4.2 freertos 的对象/归档级映射freertos/linker_common.lf 展示了归档级与对象级映射的组合[mapping:freertos_common] archive: libfreertos.a entries: if FREERTOS_IN_IRAM y: * (noflash_text) # 所有 FreeRTOS 函数放入 IRAM else: * (default) # 所有 FreeRTOS 函数放入 Flash if FREERTOS_SMP n FREERTOS_UNICORE n: tasks:xTaskIncrementTickOtherCores (noflash_text) if FREERTOS_PLACE_ISR_FUNCTIONS_INTO_FLASH n: tasks:vTaskGetSnapshot (noflash_text) if ESP_PANIC_HANDLER_IRAM y: tasks:vTaskGetSnapshot (noflash_text) tasks:uxTaskGetSnapshotAll (noflash_text) tasks:xTaskGetNext (noflash_text)这里既用* (noflash_text)实现归档级整体放置又用tasks:xTaskIncrementTickOtherCores (noflash_text)等符号级规则把特定 FreeRTOS 内核函数钉在 IRAM 中——这正是文档中符号粒度用于少量关键符号、对象/归档粒度用于批量放置的最佳实践。5. 符号粒度放置的原理与限制符号粒度放置之所以可行依赖编译器标志-ffunction-sections与-fdata-sectionsESP-IDF 默认开启这两个标志。如果用户移除它们符号粒度放置将失效。即便保留标志由于依赖编译器生成的输出 section仍存在其他限制在-ffunction-sections下每个函数被发出为独立 section且 section 名可预测地构造为.text.{函数名}与.literal.{函数名}但函数内的字符串字面量不遵循此规律它们进入合并或生成的 section 名无法按符号精确放置。在-fdata-sections下全局作用域数据被可预测地发出为.data.{变量名}、.rodata.{变量名}或.bss.{变量名}因此Type I映射条目对这些数据有效。然而函数作用域内声明的 static 数据不适用其生成 section 名由变量名与其他信息混合mangling而来无法通过符号粒度放置定位。因此文档给出的建议是将相关代码与数据组织进独立源文件改用对象粒度放置以确保放置可靠。6. 链接器脚本模板与标记语法链接器脚本模板是承载生成放置规则的骨架。它本质上是一个普通链接脚本通过特定标记语法指明生成规则插入的位置。引用某个target标记下收集到的放置规则时语法如下mapping[target] /* 引用除 SURROUND 之外的所有数据 */对于 SRAM 非连续SOC_MEM_NON_CONTIGUOUS_SRAM的芯片还额外支持arrays[target] /* 引用 SURROUND 关键字下的对象 */ mapping[target] /* 引用所有其他数据 */示例下面是一个可能的链接器脚本模板节选定义输出 section.iram0.text内部有引用目标iram0_text的标记.iram0.text : { /* Code marked as running out of IRAM */ _iram_text_start ABSOLUTE(.); /* Marker referencing iram0_text */ mapping[iram0_text] _iram_text_end ABSOLUTE(.); } iram0_0_seg非连续 SRAM 芯片的模板中还包含arrays[iram0_text]标记。假设生成器收集到以下片段定义[sections:text] .text .literal [sections:iram] .iram1 [scheme:default] entries: text - flash_text iram - iram0_text [scheme:noflash] entries: text - iram0_text [mapping:freertos] archive: libfreertos.a entries: * (noflash)则生成的链接脚本对应节选为.iram0.text : { /* Code marked as running out of IRAM */ _iram_text_start ABSOLUTE(.); /* Placement rules generated from the processed fragments, placed where the marker was in the template */ *(.iram1 .iram1.*) *libfreertos.a:(.literal .text .literal.* .text.*) _iram_text_end ABSOLUTE(.); } iram0_0_seg逐条解读生成规则*libfreertos.a:(.literal .text .literal.* .text.*)由freertosmapping 片段的* (noflash)条目生成。归档libfreertos.a下所有目标文件的所有textsections 按noflashscheme 收集到目标iram0_text并放置在模板中任何引用iram0_text标记的位置。*(.iram1 .iram1.*)由 default scheme 的iram - iram0_text条目生成。由于来自 default scheme它在同一 target 下收集的所有其他规则中排在最前。当前模板与产物路径当前使用的链接器脚本模板为 esp_system/ld/{IDF_TARGET_PATH_NAME}/sections.ld.in生成的sections.ld位于构建目录下。在 esp_system/CMakeLists.txt 中可以看到target_linker_script(${COMPONENT_LIB} INTERFACE ld/${target}/memory.ld.in) # 生成 sections.ld.in 并交由链接器脚本生成器处理 target_linker_script(${COMPONENT_LIB} INTERFACE ld/${target}/sections.ld.in PROCESS ${CMAKE_CURRENT_BINARY_DIR}/ld/sections.ld)sections.ld.in模板内部通过宏如SECTION_MAPPINGS(rtc_text)、SECTION_MAPPINGS_WITH_PADDING(flash_rodata)、ALIGNED_SYMBOL(...)引用 target。例如 esp32 的模板 中.rtc.text : { ALIGNED_SYMBOL(4, _rtc_text_start) SECTION_MAPPINGS(rtc_text) *rtc_wake_stub*.*(.literal .text .literal.* .text.*) _rtc_text_end ABSOLUTE(.); } rtc_iram_segSECTION_MAPPINGS(rtc_text)即展开为所有映射到rtc_texttarget 的放置规则.flash.rodata中则使用SECTION_MAPPINGS_WITH_PADDING(flash_rodata)模板 323-327 行在规则间插入填充以满足 Flash 只读数据段的地址对齐要求。整个流程的输入输出关系为组件 .lf 片段LDFRAGMENTS sections.ld.in 模板 │ 收集、解析、处理tools/ldgen ▼ sections.ld构建目录下的最终链接脚本→ 链接器7. 总结何时使用链接器脚本生成机制需求场景推荐做法少量关键函数进 IRAM性能/中断/Cache 禁用路径符号粒度src_file:func (noflash)整个源文件批量放置对象粒度src_file (noflash)/(rtc)整个组件/归档统一放置归档粒度* (noflash)/* (rtc)放置随 Kconfig 配置变化条件放置if CONFIG_X y: ... elif ... else ...放置区域需要对齐/排序/防裁剪/边界符号flagsALIGN()、SORT()、KEEP()、SURROUND()涉及函数内 static 数据或字符串字面量的精确放置避免符号粒度改用对象粒度组织源文件链接器脚本生成机制让 ESP-IDF 的内存布局控制从手写整个链接脚本降维为组件自描述放置意图。掌握.lf片段的语法、三种粒度、条件求值与 flags再结合 esp_system/app.lf、esp_system/linker.lf、freertos/linker_common.lf 这些真实样本即可为自己的组件定制高性能、低功耗场景下的精确内存布局。【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表