安卓手机做网站服务器网站建设与维护项目六

张小明 2026/1/13 8:38:59
安卓手机做网站服务器,网站建设与维护项目六,兰州网站优化推广,装修建材网站各位编程领域的同仁们#xff0c;大家好#xff01;今天#xff0c;我们将深入探讨一个在 C 开发#xff0c;尤其是在嵌入式系统、操作系统内核或任何需要精细内存控制的场景中至关重要的主题——链接脚本#xff08;Linker Scripts#xff09;。你是否曾好奇#xff0c…各位编程领域的同仁们大家好今天我们将深入探讨一个在 C 开发尤其是在嵌入式系统、操作系统内核或任何需要精细内存控制的场景中至关重要的主题——链接脚本Linker Scripts。你是否曾好奇当你编写的 C 代码编译链接后那些.text、.data、.bss段最终是如何被放置到物理内存中的特定位置的我们通常将代码视为抽象的逻辑单元但最终它们必须在实际的硬件上找到自己的归宿。链接脚本正是这座桥梁它赋予我们对程序内存布局的终极控制权。1. 引言为什么我们需要链接脚本在软件开发中我们通常将代码编译成目标文件Object Files然后由链接器将这些目标文件以及库文件组合成最终的可执行程序。在这个过程中链接器不仅仅是简单地将各个部分拼接起来它还要完成符号解析、地址重定位以及最重要的——决定程序在内存中的布局。对于大多数桌面应用程序操作系统的虚拟内存管理系统为我们提供了一个抽象且相对宽松的环境我们很少需要关心代码和数据在物理内存中的精确位置。然而在以下场景中这种精细控制变得不可或缺嵌入式系统开发微控制器通常具有不同类型的内存区域例如只读的闪存Flash ROM用于存储程序代码和常量数据以及可读写的RAM用于存储变量和堆栈。我们需要精确地将代码、初始化的数据、未初始化的数据分别放置到这些特定的内存区域。操作系统内核开发内核需要直接管理物理内存其自身的代码和数据必须放置在预定义的、固定的物理地址上以确保启动和运行的正确性。高性能计算和内存优化为了最大化缓存命中率或利用特定内存区域的访问速度优势可能需要将关键代码或数据放置在特定的物理地址。内存映射I/OMemory-Mapped I/O硬件寄存器通常映射到特定的物理内存地址。链接脚本可以帮助我们将 C 变量或结构体直接定位到这些地址从而方便地通过指针访问硬件。安全考虑将某些敏感代码或数据放置在受保护的内存区域或者将不同的模块隔离开来以增强系统的安全性。链接脚本就是为满足这些需求而生。它是一种由链接器通常是 GNUld解析的特殊指令文件用于描述输出文件的内存布局。通过编写链接脚本我们可以精确定义各种程序段Sections在内存中的起始地址、大小以及它们之间的相对顺序。2. 编译与链接的流程概览在深入链接脚本之前让我们快速回顾一下 C 代码从源代码到可执行文件的基本流程。这有助于我们理解链接器在整个链条中的位置和作用。预处理Preprocessing编译器首先运行预处理器处理#include、#define等指令生成一个“纯净”的 C 源代码文件。编译Compilation编译器将预处理后的 C 代码翻译成汇编代码。汇编Assembly汇编器将汇编代码翻译成机器码并将其打包成目标文件Object File。目标文件通常是ELF(Executable and Linkable Format) 格式在 Linux/Unix 系统上或PE(Portable Executable) 格式在 Windows 系统上。链接Linking链接器将一个或多个目标文件以及任何必要的库文件静态库或共享库组合成一个最终的可执行文件或共享库。我们的关注点主要在链接阶段。链接器接收目标文件作为输入并根据链接脚本如果提供了的话生成最终的输出文件。2.1 目标文件Object File的内部结构为了更好地理解链接脚本我们需要对目标文件特别是 ELF 格式的内部结构有一个基本认识。一个典型的 ELF 目标文件包含以下关键部分ELF Header描述文件的整体信息如文件类型可重定位文件、可执行文件等、目标架构、入口点地址等。Section Header Table (SHT)描述文件中所有节Section的信息包括节的名称、类型、大小、在文件中的偏移量、内存中的对齐要求等。Program Header Table (PHT)仅存在于可执行文件和共享库中描述如何将文件中的节加载到内存中以创建进程映像。Sections这是我们关注的核心它们包含了程序的代码、数据、符号表、重定位信息等。常见的节包括.text包含可执行的机器码。.data包含已初始化的全局变量和静态变量。.rodata包含只读数据如字符串字面量、const修饰的全局变量。.bss包含未初始化的全局变量和静态变量。这些变量在程序启动时由加载器或启动代码清零。.symtab符号表包含程序中定义和引用的所有符号函数名、变量名。.strtab字符串表存储符号名和其他字符串。.rel.text,.rel.data重定位表记录需要链接器修正的地址引用。.init,.fini包含构造函数和析构函数的代码。.ctors,.dtorsC 构造函数和析构函数指针列表。.eh_frame异常处理帧信息。.debug_*调试信息如.debug_info,.debug_line等。重要概念输入节Input Section来自目标文件或库文件的节。输出节Output Section链接器将多个输入节合并后在最终可执行文件中形成的节。链接脚本就是定义这些输出节如何布局的。3. 链接脚本的基础语法与核心指令链接脚本使用一种类似于 C 语言的语法但其目的完全不同。它不是用来编写程序逻辑而是用来指导链接器如何组织内存。一个链接脚本通常包含以下几个核心指令ENTRY()定义程序的入口点。MEMORY{}定义目标系统的内存区域。SECTIONS{}定义输出文件的节布局这是链接脚本最核心的部分。让我们逐一剖析这些指令。3.1ENTRY(symbol)定义程序入口点ENTRY命令用于指定程序执行的起始点。这通常是一个函数的名称例如ENTRY(_start)。在大多数 C/C 程序中_start是由 C 运行时库CRT提供的它会进行一些初始化工作然后调用main函数。ENTRY(_start)3.2MEMORY{}定义内存区域MEMORY命令用于定义目标系统可用的物理内存区域。每个内存区域都有一个起始地址origin,org或o和一个长度length,len或l。这些区域可以是不同类型如 Flash、RAM或具有不同访问属性的内存。MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 1024K /* 闪存区域可读可执行 */ RAM (rwx) : ORIGIN 0x20000000, LENGTH 128K /* 内存区域可读可写可执行 */ }FLASH和RAM这是你为内存区域定义的名称可以是任何你喜欢的标识符。(rx)和(rwx)这是区域的属性标志。r可读w可写x可执行a可分配默认i初始化默认ORIGIN或org或o区域的起始物理地址。LENGTH或len或l区域的总长度。3.3SECTIONS{}定义输出节布局SECTIONS命令是链接脚本的核心它定义了最终输出文件中的各个节Output Sections如何组织以及它们将包含哪些来自输入文件Input Sections的内容。SECTIONS块内部由一系列的输出节定义组成。每个输出节定义都指定了其名称、属性以及它包含的输入节。SECTIONS { /* 输出节定义 */ .text : { /* 输入节描述 */ KEEP(*(.text.startup)) /* 保留特定的启动代码 */ *(.text) /* 将所有输入文件中的 .text 节放入这里 */ *(.text.*) /* 包含所有以 .text. 开头的子节 */ } FLASH AT FLASH /* 运行时和加载时地址都在 FLASH 区域 */ .data : { _sdata .; /* 定义符号 _sdata 为 .data 节的起始地址 */ *(.data) } RAM AT FLASH /* 运行时在 RAM但加载时在 FLASH */ .bss (NOLOAD) : { _sbss .; *(.bss) *(COMMON) /* 处理 COMMON 符号 */ _ebss .; } RAM /* 运行时在 RAM不从文件加载 (NOLOAD) */ /* ... 其他节 ... */ }3.3.1.(点) – 位置计数器在SECTIONS块内部.符号是一个特殊变量表示当前位置计数器Location Counter。它表示当前输出节中下一个字节将被放置的地址。你可以在链接脚本中使用.来计算地址、设置对齐或者定义符号。. ALIGN(4); /* 将位置计数器对齐到4字节边界 */ _my_symbol .; /* 定义一个符号 _my_symbol 为当前地址 */3.3.2 输出节的属性每个输出节可以有以下重要属性 region_name运行时地址VMA – Virtual Memory Address指定该输出节将放置在哪个MEMORY区域。这是程序运行时访问该节的地址。例如.text FLASH表示.text节将在FLASH区域中运行。AT region_name加载时地址LMA – Load Memory Address指定该输出节在可执行文件中的存储位置以及加载器将其加载到内存中的位置。对于嵌入式系统特别是.data节VMA 和 LMA 通常是不同的。例如.data RAM AT FLASH表示.data节的初始化值存储在FLASH中LMA但在程序启动时会被复制到RAM中运行VMA。(NOLOAD)不加载此属性表示该节不需要在程序启动时从文件中加载到内存。它通常用于.bss节因为.bss节只包含未初始化的数据加载器或启动代码会将其清零而不是从文件中复制内容。ALIGN(alignment)对齐强制输出节的起始地址对齐到指定的字节边界。3.3.3 输入节描述在输出节内部我们使用模式匹配来指定要包含哪些输入节。*(.text)** 匹配所有输入文件中的.text节。* 是通配符表示所有输入文件。filename.o(.data)仅匹配filename.o文件中的.data节。*(.text .text.*)匹配所有输入文件中的.text节和所有以.text.开头的子节如.text.startup。KEEP()用于防止链接器对指定的输入节进行垃圾回收garbage collection。即使该节没有被显式引用它也会被保留。这对于一些特殊的启动代码或中断向量表非常有用。SORT()用于对匹配到的输入节进行排序。例如SORT(.text.*)可以按字母顺序排列所有.text.*节。COMMON这是一个特殊的关键字用于处理 C 语言中的“common”符号未初始化的全局变量其定义可能分散在多个文件中。链接器会为它们分配空间。3.3.4 定义符号你可以在链接脚本中使用symbol expression;的形式来定义符号。这些符号可以在 C/C 代码中通过extern声明来引用从而获取特定节的起始地址、结束地址或大小。SECTIONS { .data : { _sdata .; /* .data 节的起始地址 */ *(.data) _edata .; /* .data 节的结束地址 */ } RAM AT FLASH .bss (NOLOAD) : { _sbss .; /* .bss 节的起始地址 */ *(.bss) *(COMMON) _ebss .; /* .bss 节的结束地址 */ } RAM }在 C 代码中extern char _sdata; extern char _edata; extern char _sbss; extern char _ebss; void startup_init() { // 复制 .data 段 char *src _sdata; // LMA for .data is _sdata char *dest _sdata; // VMA for .data is also _sdata here, // but in embedded systems, src would be LMA and dest would be VMA. // Lets refine this example later for clarity. // For now, lets assume LMA VMA to simplify, but the concept holds. // The actual copy loop would look like this for LMA ! VMA: // extern char __data_load_start__; // LMA start // extern char __data_start__; // VMA start // extern char __data_end__; // VMA end // for (char *s __data_load_start__, *d __data_start__; d __data_end__; s, d) { // *d *s; // } // 清零 .bss 段 for (char *p _sbss; p _ebss; p) { *p 0; } }4. 实际案例分析与代码实践现在让我们通过几个具体的例子来演示链接脚本的强大功能。4.1 案例一简单的桌面程序内存布局对于一个运行在操作系统之上的桌面程序我们通常不需要复杂的内存映射因为操作系统会处理大部分细节。但为了理解基础我们可以看一个简单的链接脚本它将.text、.rodata、.data、.bss顺序放置。假设我们有一个main.cpp文件// main.cpp #include iostream const char* message Hello, Linker Scripts!; // .rodata int global_initialized_var 123; // .data int global_uninitialized_var; // .bss void print_message() { std::cout message std::endl; } int main() { print_message(); global_uninitialized_var 456; std::cout Initialized var: global_initialized_var std::endl; std::cout Uninitialized var (now 456): global_uninitialized_var std::endl; return 0; }simple_app.ld(链接脚本):/* simple_app.ld */ ENTRY(_start) /* 假设由C运行时库提供 */ /* 定义一个单一的内存区域通常是RAM因为OS会处理虚拟内存到物理内存的映射 */ MEMORY { RAM (rwx) : ORIGIN 0x00001000, LENGTH 256M } SECTIONS { /* 将所有代码段 .text 放置在 RAM 区域 */ .text : { KEEP(*(.text.startup)) *(.text) *(.text.*) *(.glue_7t) *(.glue_7) *(.vfp11_veneer) *(.ARM.extab) *(.ARM.exidx) } RAM /* 将所有只读数据段 .rodata 放置在 .text 之后也在 RAM 区域 */ .rodata : { *(.rodata) *(.rodata*) } RAM /* 将所有初始化数据段 .data 放置在 .rodata 之后在 RAM 区域 */ .data : { *(.data) *(.data.*) } RAM /* 将所有未初始化数据段 .bss 放置在 .data 之后在 RAM 区域 */ .bss : { *(.bss) *(.bss.*) *(COMMON) /* 处理 COMMON 符号 */ } RAM /* 其他常见的节例如堆和栈 */ . ALIGN(4); /* 对齐到4字节 */ .stack_dummy : { *(.stack) } RAM .heap_dummy : { *(.heap) } RAM /* 丢弃一些不需要的调试或链接器内部节 */ /DISCARD/ : { *(.comment) *(.ARM.attributes) *(.note.GNU-stack) *(.gnu.attributes) *(.pdr) *(.debug*) } }编译和链接g -c main.cpp -o main.o ld -o simple_app -T simple_app.ld main.o -lc -lgcc # -lc -lgcc 链接标准库使用objdump -h simple_app或readelf -S simple_app可以查看最终的可执行文件节信息验证其布局是否符合链接脚本的定义。$ readelf -S simple_app There are 15 section headers, starting at offset 0x3640: Section Headers: [Nr] Name Type Address Off Size ES Flg Lk Inf Al [ 0] NULL 0000000000000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 0000000000001000 001000 0001b3 00 AX 0 0 4 [ 2] .rodata PROGBITS 00000000000011b4 0011b4 000019 00 A 0 0 4 [ 3] .data PROGBITS 00000000000011d0 0011d0 000004 00 WA 0 0 4 [ 4] .bss NOBITS 00000000000011d4 0011d4 000004 00 WA 0 0 4 # ... 其他省略 ...从输出可以看到.text从0x1000开始.rodata紧随其后然后是.data最后是.bss。这正是链接脚本所期望的。4.2 案例二嵌入式系统中的 Flash/RAM 分离这是链接脚本最典型的应用场景。微控制器通常将代码和常量数据存储在非易失性存储器如 Flash中而变量和堆栈则存储在易失性存储器如 RAM中。关键概念加载地址 (LMA) vs. 运行地址 (VMA)LMA (Load Memory Address)节在非易失性存储器如 Flash中的物理位置。当程序烧录到设备中时节就位于这些地址。VMA (Virtual Memory Address)节在程序运行时实际占用的内存地址。对于 Flash 中的代码和只读数据LMA 和 VMA 通常是相同的。但对于初始化的数据 (.data)它的初始值存储在 Flash 中LMA但在程序启动时需要被复制到 RAM 中运行VMA。未初始化的数据 (.bss) 只存在于 RAM 中VMA没有 LMA。假设我们有一个简单的微控制器其内存布局如下Flash起始地址0x08000000大小128KBRAM起始地址0x20000000大小32KBembedded.ld(链接脚本):/* embedded.ld */ ENTRY(_start) /* 程序的入口点通常在启动文件中定义 */ /* 定义内存区域 */ MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 128K RAM (rwx) : ORIGIN 0x20000000, LENGTH 32K } SECTIONS { /* 中断向量表 (通常在 Flash 的起始位置) */ .isr_vector : { KEEP(*(.isr_vector)) /* 确保中断向量表不被优化掉 */ } FLASH /* 代码段运行时和加载时都在 FLASH */ .text : { . ALIGN(4); /* 对齐 */ *(.text) *(.text.*) *(.glue_7t) *(.glue_7) *(.vfp11_veneer) *(.ARM.extab) *(.ARM.exidx) /* 异常处理信息 */ *(.init) *(.fini) PROVIDE_HIDDEN (__preinit_array_start .); KEEP (*(.preinit_array)) PROVIDE_HIDDEN (__preinit_array_end .); PROVIDE_HIDDEN (__init_array_start .); KEEP (*(SORT(.init_array.*))) KEEP (*(.init_array)) PROVIDE_HIDDEN (__init_array_end .); PROVIDE_HIDDEN (__fini_array_start .); KEEP (*(SORT(.fini_array.*))) KEEP (*(.fini_array)) PROVIDE_HIDDEN (__fini_array_end .); KEEP (*crtbegin.o(.ctors)) KEEP (*(EXCLUDE_FILE(*crtend.o) .ctors)) KEEP (*(SORT(.ctors.*))) KEEP (*crtend.o(.ctors)) KEEP (*crtbegin.o(.dtors)) KEEP (*(EXCLUDE_FILE(*crtend.o) .dtors)) KEEP (*(SORT(.dtors.*))) KEEP (*crtend.o(.dtors)) *(.eh_frame) /* 异常帧 */ . ALIGN(4); _etext .; /* 定义 _etext 符号表示代码段结束地址 */ } FLASH /* 只读数据段运行时和加载时都在 FLASH */ .rodata : { . ALIGN(4); *(.rodata) *(.rodata*) . ALIGN(4); } FLASH /* 初始化的数据段 (.data) */ /* 加载时在 FLASH运行时在 RAM */ .data : AT (ADDR(.rodata) SIZEOF(.rodata)) /* LMA在rodata之后 */ { . ALIGN(4); _sdata .; /* _sdata 是 .data 节在 RAM 中的 VMA 起始地址 */ *(.data) *(.data.*) . ALIGN(4); _edata .; /* _edata 是 .data 节在 RAM 中的 VMA 结束地址 */ } RAM /* 未初始化的数据段 (.bss)只在 RAM 中不从 FLASH 加载 */ .bss (NOLOAD) : { . ALIGN(4); _sbss .; /* _sbss 是 .bss 节在 RAM 中的 VMA 起始地址 */ *(.bss) *(.bss.*) *(COMMON) . ALIGN(4); _ebss .; /* _ebss 是 .bss 节在 RAM 中的 VMA 结束地址 */ } RAM /* 堆栈段在 RAM 中 */ .stack_start : { . ALIGN(8); /* 通常堆栈需要较大对齐 */ _stack_bottom .; /* 栈底 */ . . 0x400; /* 假设分配 1KB 栈空间 */ _stack_top .; /* 栈顶 */ } RAM /* 堆段在 RAM 中紧随堆栈之后或在 RAM 剩余空间中 */ .heap_start : { . ALIGN(8); _heap_start .; } RAM .heap_end : { _heap_end .; } RAM /* 丢弃不需要的调试信息和属性节 */ /DISCARD/ : { *(.comment) *(.ARM.attributes) *(.note.GNU-stack) *(.gnu.attributes) *(.pdr) *(.debug*) } }C 启动代码 (startup.cpp 或 startup.s):为了让上述链接脚本正确工作我们需要一段启动代码通常是汇编或精简 C来执行以下任务复制.data段将.data节从 FlashLMA复制到 RAMVMA。清零.bss段将.bss节在 RAM 中清零。设置堆栈指针初始化主堆栈指针。调用 C 构造函数执行全局对象的构造函数.init_array。跳转到main()调用应用程序的main函数。// startup_code.cpp (简化版仅展示 .data 和 .bss 处理) extern C { // 这些符号由链接脚本提供 extern unsigned int _etext; // .text 节的结束地址 (LMA VMA in FLASH) extern unsigned int _sdata; // .data 节在 RAM 中的 VMA 起始地址 extern unsigned int _edata; // .data 节在 RAM 中的 VMA 结束地址 extern unsigned int _sbss; // .bss 节在 RAM 中的 VMA 起始地址 extern unsigned int _ebss; // .bss 节在 RAM 中的 VMA 结束地址 extern unsigned int _stack_top; // 栈顶地址 // C main 函数声明 int main(); // 全局构造函数和析构函数数组 extern void (*__preinit_array_start [])(void) __attribute__((weak)); extern void (*__preinit_array_end [])(void) __attribute__((weak)); extern void (*__init_array_start [])(void) __attribute__((weak)); extern void (*__init_array_end [])(void) __attribute__((weak)); extern void (*__fini_array_start [])(void) __attribute__((weak)); extern void (*__fini_array_end [])(void) __attribute__((weak)); // 默认中断处理函数 void Default_Handler() { while(1); // 无限循环 } // 复位处理函数作为程序的入口点 void Reset_Handler() __attribute__((noreturn)); void Reset_Handler() { // 1. 复制 .data 段 // _etext 是 Flash 中 .text 节的结束地址紧随其后是 .data 的 LMA unsigned int *src _etext; // LMA of .data unsigned int *dest _sdata; // VMA of .data while (dest _edata) { *dest *src; } // 2. 清零 .bss 段 dest _sbss; while (dest _ebss) { *dest 0; } // 3. 调用 C 全局构造函数 // 按照链接脚本中的顺序先是 preinit_array然后是 init_array unsigned int i; unsigned int count; count (unsigned int)__preinit_array_end - (unsigned int)__preinit_array_start; for (i 0; i count; i) { __preinit_array_start[i](); } count (unsigned int)__init_array_end - (unsigned int)__init_array_start; for (i 0; i count; i) { __init_array_start[i](); } // 4. 调用 main 函数 main(); // 如果 main 返回则进入无限循环 while(1); } } // extern C // 假设我们有一个简单的 main 函数 int main() { // 应用程序代码 volatile int x 10; static int y 20; // .data static int z; // .bss y; z x y; // 假设有一些硬件初始化和主循环 while(1) { // ... } } // 定义中断向量表 (假设是 Cortex-M 微控制器) // 通常这部分会在汇编启动文件中完成这里用C伪代码表示 // _stack_top 是由链接脚本定义的栈顶地址 // Reset_Handler 是复位向量 // Default_Handler 是其他中断的默认处理 void (* const g_pfnVectors[])(void) __attribute__ ((section(.isr_vector))) { (void (*)(void))_stack_top, // 栈顶地址 Reset_Handler, // 复位向量 Default_Handler, // NMI Default_Handler, // HardFault // ... 其他中断向量 ... };编译和链接 (示例命令):# 假设使用 ARM GCC 工具链 arm-none-eabi-g -c -mcpucortex-m4 -mthumb -g -O0 startup_code.cpp -o startup_code.o arm-none-eabi-g -c -mcpucortex-m4 -mthumb -g -O0 main.cpp -o main.o arm-none-eabi-g -mcpucortex-m4 -mthumb -g -O0 -nostdlib -T embedded.ld startup_code.o main.o -o firmware.elf-nostdlib不链接标准库因为我们自己提供了_startReset_Handler和内存初始化。-T embedded.ld指定链接脚本。通过arm-none-eabi-objdump -h firmware.elf或arm-none-eabi-readelf -S firmware.elf检查firmware.elf你会看到.text和.rodata的 LMA 和 VMA 都在0x0800xxxx(Flash)而.data的 LMA 在0x0800xxxx(Flash) 且 VMA 在0x2000xxxx(RAM).bss只有 VMA 在0x2000xxxx(RAM)。4.3 案例三自定义节和内存映射 I/O有时我们需要将特定的变量或数据结构放置在固定的、由硬件定义的内存地址上例如微控制器的外设寄存器。假设 GPIO 端口 A 的数据寄存器位于地址0x40020010。gpio_access.ld(链接脚本片段):/* ... */ MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 128K RAM (rwx) : ORIGIN 0x20000000, LENGTH 32K PERIPHERALS (rwx) : ORIGIN 0x40020000, LENGTH 0x10000 /* 假设外设区域 */ } SECTIONS { /* ... 其他节 ... */ /* 自定义节用于放置 GPIOA_DR 变量 */ .gpioa_dr_section : { KEEP(*(.gpioa_dr)) /* 确保这个节不会被垃圾回收 */ } PERIPHERALS AT PERIPHERALS /* 加载和运行都在外设区域 */ /* ... */ }C 代码 (main.cpp):#include cstdint // 定义一个结构体来表示 GPIO 端口 A 的寄存器 // 这里的地址是 GPIOA_BASE (0x40020000) // DR 寄存器通常在基址 0x10 的偏移处 struct GPIOA_Registers { volatile uint32_t MODER; // 0x00 volatile uint32_t OTYPER; // 0x04 volatile uint32_t OSPEEDR; // 0x08 volatile uint32_t PUPDR; // 0x0C volatile uint32_t IDR; // 0x10 (Input Data Register) volatile uint32_t ODR; // 0x14 (Output Data Register) // ... 其他寄存器 }; // 使用 __attribute__((section(.gpioa_dr))) 将变量放置到自定义节中 // 并将其地址设置为 GPIOA_Registers 的基址 // 注意这里需要配合链接脚本将 .gpioa_dr 节的 VMA 设定为 0x40020000 // 或者更常见的方式是直接使用指针强制转换 GPIOA_Registers *const GPIOA reinterpret_castGPIOA_Registers*(0x40020000); // 如果是单个寄存器可以直接定义 // volatile uint32_t GPIOA_ODR __attribute__((section(.gpioa_odr_section))) 0; // 并在链接脚本中 .gpioa_odr_section : { *(.gpioa_odr_section) } PERIPHERALS AT 0x40020014 int main() { // 直接通过指针访问 GPIO 寄存器 // 例如设置 GPIOA 的第 5 位为高电平 (假设 ODR 控制输出) GPIOA-ODR | (1 5); // 例如读取 GPIOA 的输入数据寄存器 uint32_t input_state GPIOA-IDR; while(1) { // ... } return 0; }说明对于内存映射 I/O更常见和安全的做法是直接使用reinterpret_cast将基地址强制转换为指向寄存器结构体的指针而不是通过链接脚本来放置 C 变量。因为编译器可能会对变量进行优化或者变量本身的大小和布局可能与硬件寄存器不完全匹配。然而链接脚本仍然可以在以下场景发挥作用强制特定数据结构在某个固定地址如果你有一个特殊的查找表或配置数据必须位于某个预设的物理地址链接脚本可以确保这一点。分配未使用的内存区域为自定义用途将一部分 RAM 定义为特殊的缓冲区通过链接脚本将其命名并在 C 中通过符号访问其起始和结束地址。4.4 案例四丢弃不需要的节在最终的可执行文件中有时会包含一些我们不希望保留的节例如调试信息 (.debug_*)、编译器或链接器内部使用的属性节 (.comment,.ARM.attributes)。使用/DISCARD/命令可以将这些节从输出文件中移除从而减小文件大小。SECTIONS { /* ... 其他节 ... */ /DISCARD/ : { *(.comment) *(.ARM.attributes) *(.note.GNU-stack) *(.gnu.attributes) *(.pdr) *(.debug_info) *(.debug_aranges) *(.debug_pubnames) *(.debug_pubtypes) *(.debug_abbrev) *(.debug_line) *(.debug_str) *(.debug_loc) *(.debug_frame) *(.debug_macinfo) *(.debug_weaknames) *(.debug_funcnames) *(.debug_isinfo) *(.debug_ranges) *(.debug_types) *(.debug_macro) } }这样做可以生成更小的固件文件这在存储空间有限的嵌入式系统中非常有用。5. 链接脚本的进阶特性除了上述核心指令链接脚本还提供了一些高级功能以满足更复杂的内存布局需求。5.1PROVIDE()和PROVIDE_HIDDEN()这两个函数用于在链接脚本中定义符号。PROVIDE()定义的符号是全局可见的而PROVIDE_HIDDEN()定义的符号是隐藏的这意味着它们不会在动态链接时导出但可以在静态链接时被引用。它们通常用于标记节的起始和结束地址。SECTIONS { .text : { _text_start .; *(.text) _text_end .; } FLASH }等价于SECTIONS { .text : { PROVIDE(_text_start .); *(.text) PROVIDE(_text_end .); } FLASH }5.2ALIGN()和ALIGN_WITH_INPUT()ALIGN(expression)将当前位置计数器对齐到expression指定的地址边界。例如ALIGN(4)将地址对齐到4字节。ALIGN_WITH_INPUT()根据下一个输入节的对齐要求来对齐当前位置计数器。5.3KEEP()我们已经提到过KEEP()它强制链接器保留指定的输入节即使没有代码显式引用它。这对于中断向量表、特殊的启动代码或者需要固定位置的数据块非常重要。SECTIONS { .isr_vector : { KEEP(*(.isr_vector)) } FLASH }5.4SORT()SORT()函数用于对匹配到的输入节进行排序。这在某些情况下有助于优化代码或数据布局。SORT(.text.*)按字母顺序排列所有以.text.开头的输入节。SORT_BY_NAME(.init_array.*)按名称排序。SORT_BY_ALIGNMENT(COMMON)按对齐要求排序。SORT_BY_INIT_PRIORITY(.init_array.*)按初始化优先级排序C11。5.5ASSERT(expression, message)ASSERT命令用于在链接时进行断言检查。如果expression求值为假链接器将输出message并终止链接。这对于验证内存区域是否足够大、地址是否正确对齐等非常有用。MEMORY { RAM (rwx) : ORIGIN 0x20000000, LENGTH 32K } SECTIONS { /* ... */ .bss (NOLOAD) : { _sbss .; *(.bss) *(COMMON) _ebss .; } RAM ASSERT((_ebss - ORIGIN(RAM)) LENGTH(RAM), RAM memory overflow for .bss section!); }5.6OUTPUT_FORMAT和OUTPUT_ARCHOUTPUT_FORMAT(bfdname)指定输出文件的格式如elf32-littlearm、binary等。OUTPUT_ARCH(arch)指定输出文件的目标架构如arm、i386等。这些通常在文件开头定义用于指导链接器生成特定格式和架构的文件。OUTPUT_FORMAT(elf32-littlearm) OUTPUT_ARCH(arm) ENTRY(_start) /* ... */6. 与 C/C 代码的交互链接脚本中定义的符号例如_sdata、_edata等可以在 C/C 代码中通过extern关键字进行访问。它们在 C/C 中被视为char类型的指针或unsigned int取决于你的习惯其值就是链接器分配的内存地址。// 在 C 代码中 extern C { // 确保 C 编译器不对这些符号进行名称修饰 extern char _sdata; extern char _edata; extern char _sbss; extern char _ebss; } void initialize_memory() { // 复制 .data 段 // 假设 __data_load_start__ 是 .data 节在 Flash 中的 LMA // _sdata 是 .data 节在 RAM 中的 VMA // 这需要链接脚本中定义 __data_load_start__ extern char __data_load_start__; char *src __data_load_start__; char *dest _sdata; while (dest _edata) { *dest *src; } // 清零 .bss 段 dest _sbss; while (dest _ebss) { *dest 0; } }注意extern CC 编译器会对函数名和变量名进行“名称修饰”name mangling以支持函数重载等特性。然而链接脚本定义的符号是纯 C 风格的没有修饰。因此在 C 代码中引用这些符号时必须使用extern C来告诉编译器不要修饰这些名称以便链接器能够正确解析它们。7. 调试链接脚本问题调试链接脚本可能是一个挑战但有一些工具和技巧可以帮助你。生成链接器映射文件 (.map文件)这是调试链接脚本最重要的工具。使用ld -M -o output_file -T linker_script.ld ... output.map命令链接器会生成一个详细的映射文件其中列出了所有输入文件、它们贡献的节、输出节的地址和大小以及所有符号的地址。仔细检查.map文件可以发现内存重叠、节放置错误、地址不符合预期等问题。objdump -h或readelf -S这些工具可以显示可执行文件或目标文件中的所有节的名称、类型、地址、大小和属性。用于验证链接脚本是否按预期生成了输出节。nm列出目标文件或可执行文件中的所有符号及其地址。可以用来检查链接脚本中定义的符号是否正确导出。ld --verbose打印链接器使用的内置链接脚本和搜索路径。ASSERT()在链接脚本中加入ASSERT()语句可以在链接时进行一些基本检查提前发现问题。逐步简化如果你的链接脚本很复杂遇到问题时可以尝试将其简化到一个最小的工作版本然后逐步添加功能定位问题所在。8. 最佳实践和注意事项了解目标硬件在编写链接脚本之前务必彻底了解目标微控制器的内存映射、Flash 和 RAM 的起始地址、大小以及任何特殊的外设内存区域。从模板开始不要从零开始编写链接脚本。许多微控制器厂商或开发环境会提供基于其硬件的默认链接脚本你可以以此为基础进行修改。使用有意义的名称为内存区域、节和符号使用清晰、描述性的名称提高可读性。注释详细注释你的链接脚本解释每个部分的目的特别是复杂的地址计算或特殊处理。版本控制将链接脚本纳入你的版本控制系统与源代码一起管理。避免硬编码尽量使用链接脚本提供的函数如ORIGIN(),LENGTH(),SIZEOF(),ADDR()进行地址和大小的计算而不是硬编码数字这能提高脚本的可维护性。对齐始终注意节的对齐要求。不正确的对齐可能导致性能下降甚至在某些架构上引发硬件错误。内存溢出检查结合ASSERT()和.map文件确保你的代码和数据不会超出可用的内存区域。理解 LMA 和 VMA这是嵌入式系统中最重要的概念之一。确保你清楚哪些节需要 LMA ! VMA以及启动代码如何处理它们。9. 链接脚本的精髓链接脚本是连接高级编程语言世界与底层硬件世界的桥梁。它赋予了开发者对程序内存布局的极致控制这在资源受限或需要特定硬件交互的场景中是不可或缺的。从简单的桌面程序到复杂的嵌入式系统和操作系统内核理解和掌握链接脚本是成为一名真正系统级程序员的关键一步。它不仅仅是配置工具更是一种深入理解计算机体系结构和程序执行机制的强大思维模型。通过精心设计的链接脚本我们可以优化性能、增强安全性、实现灵活的内存管理并最终打造出高效、可靠的软件系统。希望这次讲座能帮助大家深入理解链接脚本的奥秘。谢谢
版权声明:本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

网站变灰色代码申请域名费用

随着数字化转型加速与网络威胁复杂度的指数级增长,安全运营模式也从单品防御、规则驱动逐步迈向数据驱动与人工智能赋能的智能防御时代。各大网络安全厂商推出自己的安全垂域大模型如:奇安信QAX-GPT安全机器人、深信服安全GPT、360安全Agent、天融信天问…

张小明 2026/1/7 5:00:46 网站建设

阿里巴巴网站的营销策略外贸网站的域名

大公司那套复杂流程,搬到小团队里真的管用吗?别开玩笑了!条条框框太多,反而会拖垮团队的活力。小团队最宝贵的,就是那份灵活和高效,一旦被繁琐制度束缚,优势瞬间消失。今天,咱们就甩…

张小明 2026/1/12 12:32:43 网站建设

桂林网站建设兼职高端网站设计教程

LangFlow自动化测试方案设计:确保工作流稳定可靠 在AI应用开发日益普及的今天,越来越多团队借助大语言模型(LLM)构建智能客服、自动化报告生成、知识问答系统等复杂流程。然而,当开发从“写代码”转向“搭积木”——使…

张小明 2026/1/8 7:27:15 网站建设

河南省通信管理局网站备案电话中山市网站建设公司

文章目录系统截图项目技术简介可行性分析主要运用技术介绍核心代码参考示例结论源码lw获取/同行可拿货,招校园代理 :文章底部获取博主联系方式!系统截图 怕一天Python-flask-django_去cbwm 企业财务发票支票管理系统哄-flask-django_6nsn 企业员工…

张小明 2026/1/8 5:29:04 网站建设

自适应响应式网站源码做网站为什么要投资钱

城通网盘直连解析器:3步告别繁琐下载流程 【免费下载链接】ctfileGet 获取城通网盘一次性直连地址 项目地址: https://gitcode.com/gh_mirrors/ct/ctfileGet 还在为城通网盘复杂的下载流程而烦恼吗?广告等待、验证码输入、页面跳转...这些繁琐操作…

张小明 2026/1/10 0:53:14 网站建设

eclipse静态网站开发网站内页关键词密度

第一章:智慧文旅新标杆——无人值守核销系统的时代机遇随着物联网、人工智能与移动支付技术的深度融合,智慧文旅正迎来前所未有的变革。无人值守核销系统作为其中的关键应用,正在重塑景区、博物馆、文化场馆的票务管理与游客服务模式。该系统…

张小明 2026/1/13 1:20:40 网站建设