STM32 启动汇编学习笔记:GNU ld 学习续篇
STM32 启动汇编学习笔记GNU ld 学习续篇前篇GNU ld 链接脚本学习笔记24 步。本文整理前篇生成之后的学习内容从第 25 步继续编号。将后续分散讲解的寄存器、指令、符号和启动流程按知识依赖重新组织便于复习编号不代表原聊天逐条编号。学习环境沿用前篇Windows / PowerShell、STM32F407ZGCortex-M4F、Arm GNU GCC、GNU ld、FreeRTOS GCC ARM_CM4F 移植层。主要研究文件为 startup_stm32f407xx.S 和 STM32F407ZG.ld。学习目标是看懂并维护当前启动文件能够解释程序从哪里开始、进入 main 前做了什么、出问题时检查哪里。本文没有修改启动代码具体地址示例用于教学不代表每次重新构建后的地址。第 25 步启动文件的总体职责当前启动文件完成以下工作提供中断向量表。提供复位入口 Reset_Handler。设置启动栈指针。调用 SystemInit。将 .data 初始内容从 Flash 复制到 RAM。将 .bss 清零。调用 C 运行库初始化函数。调用 main并处理 main 意外返回的情况。为未实现的中断提供默认处理函数。当前代码的关键部分如下.section .text.Reset_Handler,ax,%progbits .global Reset_Handler .type Reset_Handler,%function .thumb_func Reset_Handler: ldr sp, _estack bl SystemInit ldr r0, _sidata ldr r1, _sdata ldr r2, _edata 1: cmp r1, r2 bcs 2f ldr r3, [r0], #4 str r3, [r1], #4 b 1b 2: ldr r1, _sbss ldr r2, _ebss movs r3, #0 3: cmp r1, r2 bcs 4f str r3, [r1], #4 b 3b 4: bl __libc_init_array bl main bl _exit 5: b 5b .size Reset_Handler, .-Reset_Handler先理解总体顺序再逐步阅读每条指令。不要求记住所有 ARM 汇编指令。第 26 步认识 CPU 寄存器寄存器是 CPU 内部用于保存数据或地址的存储单元。当前 Cortex-M4 的 r0r12 是 32 位通用寄存器每个保存 4 字节。名称本阶段需要了解的作用r0r12保存运算数据、地址或临时结果sp / r13栈指针实际使用 MSP 或 PSP 取决于处理器状态lr / r14普通函数调用时通常保存返回地址pc / r15程序计数器与接下来执行的位置有关例如movs r3, #0将立即数 0 放入 r3。#表示直接给出的数值该指令还会更新相应条件标志。寄存器本身并不区分“这是地址”还是“这是普通整数”其含义取决于代码如何使用它。第 27 步取得地址和读取数据比较下面两句ldr r0, _sidata ldr r3, [r0]第一句将链接符号 _sidata 的值放入 r0。这里 _sidata 表示 Flash 地址。第二句读取 r0 所指内存中的一个 32 位数据放入 r3。假设_sidata 0x080121BC 该位置保存的 32 位数值 115200执行后r0 0x080121BC r3 115200对应的 C 语言理解constuint32_t*ptr(constuint32_t*)0x080121BC;uint32_tvalue*ptr;关键区别r0 是寄存器里的值[r0] 表示通过该值访问内存。第 28 步使用 str 写入内存str r3, [r1]将 r3 中的 32 位数据写入 r1 指向的位置不改变 r1 和 r3 的值。假设 r1 为 0x20000000r3 为 115200执行后 RAM 的 0x200000000x20000003 保存这个数值。一次复制可以写成ldr r3, [r0] /* 从 Flash 读取 4 字节 */ str r3, [r1] /* 将这 4 字节写入 RAM */数据经过的路径是Flash → r3 → RAM。第 29 步后索引寻址与地址自动递增ldr r3, [r0], #4 str r3, [r1], #4每句都先访问原地址再把地址寄存器增加 4 字节。寄存器执行前执行后r00x080121BC0x080121C0r10x200000000x20000004注意这里访问 4 字节是 ldr/str 操作一个 32 位字的结果末尾 #4 指定的是操作后地址增加多少字节。str r3, [r1] /* 写入后 r1 不变 */ str r3, [r1], #4 /* 写入后 r1 加 4 */第 30 步比较、条件跳转与数字标签1: cmp r1, r2 bcs 2f ldr r3, [r0], #4 str r3, [r1], #4 b 1b 2:cmp r1, r2比较两个寄存器并更新条件标志不修改它们的值。紧接着的 bcs 根据进位标志决定跳转对于这次比较表示无符号的 r1 r2。写法含义1:数字局部标签1b向前面的代码寻找最近的 1:b 为 backward2f向后面的代码寻找最近的 2:f 为 forwardb 1b无条件跳回 1:这里 bcs 不是“随时自动比较 r1 和 r2”它使用前面 cmp 设置的标志。例如 r1 初值 0x20000000r2 为 0x2000000C循环复制 3 次、共 12 字节r1 达到结束边界时退出该边界本身不会被写入。第 31 步完整理解 .data 复制开始时ldr r0, _sidata /* Flash 源地址 */ ldr r1, _sdata /* RAM 当前目标地址 */ ldr r2, _edata /* RAM 结束边界 */循环逻辑等价于以下伪代码其中 src、dst、end 按 uint32_t 指针理解while(dstend){*dst*src;}_sidata、_sdata、_edata 都是链接地址符号不是保存这些地址的普通 C 全局变量。启动代码通过符号取得最终地址。此过程使以下全局变量在进入 main 前获得初始值uint32_tbaudrate115200;第 32 步完整理解 .bss 清零ldr r1, _sbss ldr r2, _ebss movs r3, #0 3: cmp r1, r2 bcs 4f str r3, [r1], #4 b 3b 4:r1 指向当前待清零位置r2 表示结束边界r3 始终保存零。每轮向 RAM 写入一个 32 位零并推进地址。例如清零范围为 [0x20000400, 0x2000040C)则需要写入三次覆盖 12 字节。与 .data 的区别.bss 不需要 Flash 源地址因为写入值固定为零。uint32_terror_count;这类全局变量在 main 开始前为零依赖的就是对应运行环境初始化过程。第 33 步bl、lr 与函数返回bl __libc_init_array bl main bl _exitbl 跳转执行函数并把返回地址记录到 lr。返回位置是 bl 后面的下一条指令不是调用前的位置也不是 bl 自己。例如 bl main 的返回位置对应后面的 bl _exit。Cortex-M 的返回地址还包含 Thumb 状态所需的最低位信息。指令作用b 标签跳转不为这次跳转记录返回地址bl 函数跳转并更新 lrbx lr普通函数返回的一种常见方式__libc_init_array 初始化相关运行库设施并执行初始化函数表然后进入 main。当前程序正常运行时调度器不应返回如果 main 返回启动代码调用 _exit。若 _exit 也返回最后的5: b 5b会使 CPU 留在循环中。第 34 步嵌套调用为什么需要保存 lr调用关系main → FunctionA → FunctionB。进入 FunctionA 时lr 保存返回 main 的位置。FunctionA 执行 bl FunctionB 又会更新 lr所以必须先保存原返回地址。教学示例FunctionA: push {r4, lr} bl FunctionB pop {r4, pc}push 保存 r4 和原 lr共 8 字节。bl 调用 FunctionB并将 lr 更新为返回 FunctionA 的位置。pop 恢复 r4将原返回地址装入 pc返回 main。该示例假定入口栈已满足 8 字节对齐压入 8 字节后继续保持对齐。实际编译器选择保存哪些寄存器要根据函数实现和调用约定确定。任务函数运行时通常使用 PSP 栈启动阶段和异常处理函数使用 MSP。普通嵌套函数调用与异常进入、异常返回的硬件机制不能完全混同后者属于进阶内容。第 35 步默认中断与弱符号当前默认处理函数Default_Handler: b ..表示当前指令位置这条指令不断跳回自己。默认中断关系可以这样建立.weak USART1_IRQHandler .thumb_set USART1_IRQHandler, Default_Handler.weak 声明弱符号.thumb_set 将其设为 Default_Handler 的 Thumb 函数别名。当 C 文件提供同名强定义时voidUSART1_IRQHandler(void){/* 实际中断处理 */}链接器选择这个强定义。覆盖发生在链接阶段。如果误写为 USART1_IRQHandle名称不一致无法覆盖默认入口。程序停在 Default_Handler 时应检查触发的中断、函数名以及对应实现是否参与构建。第 36 步.word 与中断向量表.word _estack .word Reset_Handler .word NMI_Handler .word HardFault_Handler在当前 ARM 汇编中每条 .word 放入 32 位内容占 4 字节四条共 16 字节。相对向量表的偏移保存的内容0x00初始 MSP 值0x04Reset_Handler 入口地址0x08NMI_Handler 入口地址0x0CHardFault_Handler 入口地址.word Reset_Handler是保存入口地址的数据不是调用指令。真正调用函数可以使用 bl。对于本工程常规主 Flash 启动布局向量表链接在 0x08000000。硬件复位实际按芯片启动映射读取启动向量STM32 的启动地址别名映射应与 Boot 配置一起理解不能把该链接地址概括为所有启动模式下 CPU 固定访问的物理地址。第 37 步.section 与 .ld 的配合向量表声明.section .isr_vector,a,%progbits复位代码声明.section .text.Reset_Handler,ax,%progbits.section 指定后续内容所属输入段。a 表示需要在目标内存分配空间。x 表示可执行内容。%progbits 表示段含有实际内容。链接脚本负责收集并放置.isr_vector : { KEEP(*(.isr_vector)) } FLASH .text : { *(.text .text.*) } FLASH.section 本身没有指定函数必须位于某个固定地址。汇编器决定内容属于哪个输入段链接器按 .ld 决定最终位置。第 38 步函数属性、标签和 .size.global Reset_Handler .type Reset_Handler,%function .thumb_func Reset_Handler: /* CPU 执行的指令 */ .size Reset_Handler, .-Reset_Handler写法作用.global让符号对其他目标文件可见.type …,%function标记符号为函数.thumb_func标记后续 Thumb 函数入口名称:标记当前位置.size记录符号大小这些声明不是 CPU 执行的机器指令。.global 不会创建一个普通全局变量也不会调用函数。.size Reset_Handler, .-Reset_Handler表示用当前位置减去函数起点。例如从 0x08001000 到 0x0800103A大小为 0x3A即 58 字节。这个大小描述符号覆盖的代码范围不是运行时需要的栈大小。第 39 步.s 和 .S在 GCC 根据文件扩展名选择处理流程的默认规则下后缀处理方式.s汇编源码直接汇编.S先进行 C 预处理再汇编大写 .S 支持例如#include startup_config.h #ifdef ENABLE_EARLY_INIT bl EarlyInit #endif流程为.S → 预处理 → 汇编 → .o → 链接 → ELF。#ifdef 由预处理器处理.section 由汇编器处理。大写 .S 不代表另一种 CPU 指令集。显式指定输入语言等工具参数时可以覆盖扩展名默认规则。第 40 步链接符号引用与重定位汇编文件使用ldr r1, _sdata bl SystemInit汇编时通常还不知道这些外部符号的最终地址。目标文件保留符号引用和所需重定位信息链接器完成段布局后处理这些引用。例如链接器从 .ld 得到 _sdata 0x20000000再修正相关位置使指令执行后 r1 获得这个值。修正的位置可能位于指令或用于加载地址的常量数据中。这也是为什么启动文件引用的 _sdata 必须与 .ld 中定义的符号名称完全一致。第 41 步两类常见链接错误undefined reference缺少可用定义例如启动文件调用 SystemInit但它的实现没有参与链接。常见原因名称拼写错误。只有声明没有实现。实现所在 .c 文件没有加入构建。实现被条件编译排除。需要跨文件调用的函数被声明为 static。文件存在不代表它已参与构建。multiple definition多个强定义同名如果 a.c 和 b.c 都提供普通的 SystemInit 定义通常会发生重复定义错误。定义情况通常结果引用符号却找不到可用定义undefined reference同名普通强定义出现两次multiple definition默认弱定义与同名强定义并存使用强定义第 42 步对齐与复制步长必须一致当前 .data 边界.data : { . ALIGN(4); _sdata .; *(.data .data.*) . ALIGN(4); _edata .; } RAM AT FLASH先对齐再记录边界保证范围可以按 4 字节处理。假设 .data 中只有一个 5 字节数组uint8_tconfig[5]{1,2,3,4,5};如果起点为 0x20000000数组结束边界是 0x20000005ALIGN(4) 将段结束边界推进到 0x20000008。sizeof(config) 5 字节 .data 段大小 8 字节包含 3 字节填充 启动复制 2 次每次 4 字节变量大小和段大小不一定相等。当前 .bss 起止采用 8 字节对齐也满足按 4 字节清零的要求。不能只修改启动循环步长或段边界而不检查另一侧的约定。第 43 步SystemInit 和 main 的边界当前工程在 .data 复制、.bss 清零之前调用 SystemInit。此时它不应依赖尚未完成初始化的可写全局/static 变量。即使早期主动写入全局变量也可能被后续初始化覆盖uint32_tboot_flag;voidSystemInit(void){boot_flag1;}后续清零 .bss 会把 boot_flag 改回零。阶段工作边界SystemInit必要的早期寄存器与系统配置避免依赖尚未初始化的数据main 及其调用函数在 C 运行环境建立后完成外设、应用对象、任务等初始化具体初始化顺序取决于启动文件和 SDK不能假定所有芯片都与本工程相同。第 44 步ldr 符号 与常量池ldr r0, _sidata这是加载符号值的伪指令。汇编器选择实际实现方式如果不能用合适的指令直接构造该值可以将其放入附近的常量池再生成读取常量的指令。概念上类似ldr r0, nearby_value /* 实际控制流必须避免顺序执行到常量池 */ nearby_value: .word _sidata上面只是原理示意不是可直接复制使用的完整函数。如果 _sidata 0x080121BC从常量池取得地址 0x080121BC → r0 再执行 ldr r3, [r0] → 读取该 Flash 地址的数据不是所有 ldr 常量 都必然生成常量池访问。要确认实际生成指令需要查看反汇编这属于下一阶段实践。已完成的学习范围与后续方向本阶段完成了“读懂当前启动文件”的基础学习。复习时应能沿以下顺序解释代码向量表提供初始 MSP 和复位入口 → Reset_Handler 设置 SP → SystemInit → 复制 .data → 清零 .bss → __libc_init_array → main并能区分 CPU 指令、汇编器声明、预处理语句和链接脚本规则。反汇编验证、HardFault 现场分析、异常硬件压栈、FreeRTOS 上下文切换是后续方向本篇不将它们列为已经完成的实践。练习答案速查问题答案r0 保存地址后ldr r3, [r0] 得到什么r0 仍保存地址r3 得到该地址处的 32 位数据str r3, [r1] 是否改变 r1不改变str r3, [r1], #4 做什么先写入再将 r1 增加 4 字节.bss 清零为何不需要 Flash 源地址写入值固定为零bl 返回到哪里调用指令之后的下一条指令嵌套调用会覆盖 lr 吗会因此需要保存仍需使用的原返回地址拼错 IRQHandler 名称能覆盖默认入口吗不能符号名称必须匹配四条 ARM .word 占多少字节16 字节.section 是否直接指定某个固定地址本文写法没有最终地址由链接脚本决定.size 表示代码大小还是栈大小指定符号范围的大小此处为函数代码范围.S 与 .s 的默认区别.S 先预处理.s 直接汇编.ld 改了符号名但启动文件没改会怎样原符号找不到定义时链接报错启动文件基础是否学完已覆盖当前文件的主要机制仍可用反汇编和上板验证深化对前面讲解的精确补充RAM 是物理存储器栈只是其一种用途普通对象和栈指针的地址计算不能混淆。ldr/str 的访问宽度与后索引增量是两个概念末尾 #4 只描述地址更新。lr 在普通函数调用中保存返回地址异常处理中的 lr 可能是 EXC_RETURN不能总按普通代码地址解释。异常处理函数使用 MSP但从 PSP 任务进入异常时硬件保存被打断现场会使用被打断上下文的栈。此前“中断使用 MSP”描述的是处理函数执行阶段。Memory 的 r/w/x 属性用于链接器的区域匹配等布局工作并不会自动设置 MPU 或硬件读写执行权限。对 NOLOAD 段即使 objdump 显示某个 LMA 数值也不能据此认定 Flash 中有需要复制的内容要结合 CONTENTS、LOAD 等属性判断。已保存的学习示例数值可能随构建变化应以本次生成的 ELF/MAP 为准。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →