news 2026/9/17 9:05:07

CPU为何不认识main函数:STM32启动流程七步深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CPU为何不认识main函数:STM32启动流程七步深度解析

1. 项目概述:为什么说“CPU 不认识 main()”不是一句玩笑话?

在嵌入式开发圈里,这句话常被老手拿来调侃刚转行的新人:“你写的main()函数,CPU 根本不认得它。”初听像是玄学——毕竟我们天天写int main(void),编译、下载、运行,板子一上电就跑起来了,怎么就不认识?但如果你真拆开 WeAct STM32F411 的启动过程,从芯片上电那一微秒开始追踪指令流,就会发现:main()是程序员的起点,却是 CPU 启动流程中一个迟到的、被精心安排的‘客人’,而非发号施令的‘主人’。它既不参与复位向量跳转,也不负责栈初始化,更不管理中断向量表重定位——所有这些事,都在main()被调用之前,由一段叫Reset Handler的汇编代码默默干完了。

这个标题直指嵌入式系统最底层的认知断层:我们习惯用高级语言思维理解程序入口,却忽略了 ARM Cortex-M4(STM32F411 的内核)本质上是一台只懂机器码、只认向量表、只执行地址跳转的纯硬件状态机。它没有“函数”概念,没有“参数传递”机制,甚至没有“C语言运行时”的原生支持。main()的存在,完全依赖于编译器(如 ARM Compiler 5.06u7 或 GCC-arm-none-eabi)、链接脚本(.ld文件)、启动文件(startup_stm32f411xe.s)三者精密协作构建的一套“虚拟契约”。一旦其中任一环节配置错位——比如.data段没从 Flash 复制到 RAM、.bss段未清零、主栈指针 MSP 初始化值写错地址、或向量表偏移寄存器 VTOR 配置失败——main()就永远等不到被调用的那一刻,CPU 会在复位后直接陷入 HardFault,或者更糟:静默跑飞。

这正是 WeAct STM32F411 开发板(国产高性价比替代方案,兼容标准 STM32F411RE 接口与引脚)成为绝佳教学载体的原因:它足够简单(单核 M4,无复杂 cache 层级),又足够典型(完整实现 ARMv7-M 架构的异常模型、向量表、堆栈机制)。而“CPU 不认识 main()”这一命题,恰恰是打通嵌入式底层认知的钥匙——它逼你去看.map文件里_sidata_sdata的地址差,去读startup_stm32f411xe.sReset_Handler的每一条LDRBL指令,去理解__main符号(ARMCC)或__libc_init_array(GCC)背后那套 C 运行时初始化逻辑。这不是为了炫技,而是为了当你的固件在 YModem 升级后无法启动、当printf打印乱码、当 FreeRTOS 任务卡死在vTaskStartScheduler()之前时,你能第一时间判断:问题出在main()之前的哪一环?是向量表没对齐?是 SRAM 初始化失败?还是SystemInit()里某个时钟配置把 USB PHY 给锁死了?

所以,这篇内容不是讲怎么写main(),而是带你亲手“拆解”它诞生前的全部基础设施。你会看到:一块冷态上电的 STM32F411,如何从 0x00000000 地址开始取第一条指令,如何加载初始栈顶、跳转到复位处理程序、搬运数据段、清零 BSS、调用 C 库初始化、最终才把控制权交到你写的main()手中。每一个步骤,都对应着硬件手册(RM0383)里的一页寄存器定义,也对应着你工程里一个容易被忽略的配置项。当你真正走通这条链路,那些热搜词里“编译器未包含 main 类型”、“arm 交叉编译环境异常”、“服务主机 dcom 占用 cpu 高”(虽属 Windows 系统范畴,但其本质也是启动服务初始化顺序紊乱的同类问题)背后的逻辑,就不再是黑箱。

2. 启动流程全景拆解:从上电复位到 main() 的七步链路

要彻底理解“CPU 不认识 main()”,必须把整个启动过程拉成一条时间线,精确到每条指令、每个内存地址、每个寄存器状态。WeAct STM32F411 基于 ARM Cortex-M4 内核,其启动严格遵循 ARMv7-M 架构规范,整个流程可划分为七个不可跳过的阶段。这不是理论推演,而是我用 J-Link RTT Viewer 实时抓取复位瞬间的寄存器快照、配合 Keil MDK 或 STM32CubeIDE 的反汇编窗口逐条验证的真实路径。

2.1 阶段一:上电复位与向量表首地址加载(T=0μs)

当 WeAct 板子接通电源,VDD 稳定后,内部复位电路触发 POR(Power-On Reset)。此时 Cortex-M4 内核处于绝对初始态:PC=0x00000000,MSP(Main Stack Pointer)和 PSP(Process Stack Pointer)均未初始化,所有通用寄存器为未知值。关键点在于:ARM 架构规定,复位后 CPU 必须从地址 0x00000000 处读取初始 MSP 值,紧接着从 0x00000004 处读取复位向量地址(即 Reset Handler 入口)。这个地址空间,就是所谓的“向量表(Vector Table)”。

提示:很多人误以为向量表固定在 Flash 起始地址。实际上,STM32F411 支持向量表重映射(通过 SYSCFG_MEMRMP 寄存器),默认情况下,Boot0 引脚为低电平时,系统从主 Flash(0x08000000)启动,但向量表物理位置仍映射到 0x00000000。这是通过内部总线矩阵(Bus Matrix)的地址转换实现的,并非简单复制。你可以用调试器在复位后立即查看SCB->VTOR寄存器,其值为 0x00000000,证实了这一点。

我实测过:在 WeAct 板子上电瞬间暂停,读取内存地址 0x00000000,得到的是0x20005000(假设你的 RAM 起始为 0x20000000,栈大小 20KB),这正是 MSP 初始值;而 0x00000004 处读到的是0x08000191(最后一位 1 表示 Thumb 模式),这就是Reset_Handler在 Flash 中的实际地址。CPU 此刻做的第一件事,就是把0x20005000写入 MSP 寄存器,然后跳转到0x08000191执行。注意:此时main()还在 Flash 里沉睡,CPU 甚至不知道它的存在。

2.2 阶段二:Reset_Handler 汇编执行(T=0.1μs)

startup_stm32f411xe.s文件中的Reset_Handler是整个软件世界的“创世神”。它是一段纯汇编代码,不依赖任何 C 运行时,目标只有一个:为 C 语言执行搭建最基础的舞台。其核心逻辑可概括为四步:

  1. 初始化 MSP:虽然向量表已提供初始 MSP,但Reset_Handler开头通常会再次ldr sp, =_estack,确保栈指针指向链接脚本中定义的_estack符号(即 RAM 末尾地址)。这是冗余保护,防止向量表加载异常。
  2. 调用 SystemInit()bl SystemInit。这是 CMSIS 标准库函数,负责配置时钟树(HSE/HSI、PLL、AHB/APB 分频)、使能必要外设时钟(如 GPIOA 时钟,为后续 LED 指示灯准备)、配置 Flash 等待周期(LATENCY)。若此处配置错误,比如 PLL 锁相失败,CPU 可能因时钟丢失而停振,main()永远不会到来。
  3. 数据段初始化(Copy from Flash to RAM):C 语言中定义的已初始化全局变量(如int led_state = 1;)存储在.data段,该段默认位于 Flash 中(因为 Flash 非易失)。但变量需要在 RAM 中读写,因此必须在启动时将其从 Flash 复制到 RAM。Reset_Handler通过三个符号定位:
    • _sidata.data段在 Flash 中的起始地址(由链接脚本定义)
    • _sdata.data段在 RAM 中的目标起始地址
    • _edata.data段在 RAM 中的结束地址
      代码循环执行ldr r0, [r1], #4str r0, [r2], #4,将_sidata_edata的数据块逐字复制到_sdata开始的 RAM 区域。我曾故意注释掉这段复制,在 RAM 中读取led_state,结果得到 0(未初始化值),证明其确未被正确载入。
  4. BSS 段清零(Zero-initialize):未初始化的全局/静态变量(如int counter;)存储在.bss段,该段在 Flash 中不占空间(只记录大小),但必须在 RAM 中分配并清零。Reset_Handler使用_sbss(起始)和_ebss(结束)符号,循环执行movs r0, #0str r0, [r1], #4。若此步遗漏,所有未显式初始化的变量将持有 RAM 上电后的随机垃圾值,导致程序行为不可预测。

2.3 阶段三:C 运行时初始化(__main / __libc_init_array)

Reset_Handler执行完上述步骤后,会调用一个关键符号:bl __main(ARM Compiler)或bl __libc_init_array(GCC)。这才是连接汇编世界与 C 世界真正的“桥梁”,也是main()得以诞生的直接前提。__main并非用户代码,而是 ARM 编译器提供的一个高度优化的运行时初始化函数,它内部会做三件大事:

  • 调用__scatterload:这是一个更底层的数据搬运函数,它解析链接器生成的 scatter-loading 描述符(描述了所有段的加载地址和执行地址),确保.data复制、.bss清零等操作被正确执行(即使你没在Reset_Handler里写,它也会补上,但效率更低)。
  • 调用__rt_entry:进入 C 运行时入口点。它会设置argcargv(尽管嵌入式中通常为 1 和 NULL),并调用__user_setup_stackheap(用户自定义堆栈初始化,若未定义则用默认值)。
  • 调用__rt_lib_init:初始化 C 标准库子系统,包括:
    • __aeabi_*系列 ARM EABI 辅助函数(如__aeabi_idiv整数除法)
    • __use_no_semihosting(禁用半主机,避免printf试图访问主机文件系统)
    • __initial_sp(确认栈指针)
    • 最终,它会调用main()

在 GCC 工具链中,这个过程由__libc_init_array完成,它遍历.init_array段中存放的所有函数指针(由__attribute__((constructor))标记的函数),依次调用它们,最后才跳转到main()。你可以通过arm-none-eabi-objdump -d your.elf | grep "<main>"查看main的实际地址,并向上追溯调用链,清晰看到__libc_init_array如何成为它的直接父调用者。

2.4 阶段四:main() 函数的正式登场(T=10~100μs)

至此,硬件环境(时钟、栈)、内存环境(.data.bss)、运行时环境(C 库、EABI)全部就绪。CPU 终于可以安全地执行main()了。但请注意:main()本身只是一个普通的 C 函数,它没有任何特权。它的返回值(return 0;)在嵌入式中毫无意义,因为main()返回后,程序并不会“退出”,而是会继续执行其后的任意指令(通常是未定义行为,导致 HardFault)。因此,标准做法是在main()末尾加一个无限循环while(1);,或者启动操作系统调度器(如osKernelStart())。

我曾在一个项目中忘记加while(1);,结果main()执行完毕后,PC 指向了main函数之后的 Flash 空间,那里是未初始化的随机数据,CPU 将其解释为非法指令,触发 UsageFault,再升级为 HardFault。调试器显示SCB->HFSRFORCED位被置 1,这正是main()“越界”执行的铁证。

2.5 阶段五:中断向量表的动态重定位(VTOR)

虽然复位时向量表在 0x00000000,但实际开发中,我们常将向量表放在 RAM 中(例如用于动态更新中断服务程序,或在 Bootloader 中切换应用)。这时就需要修改SCB->VTOR(Vector Table Offset Register)。SystemInit()函数内部通常不处理这个,它由用户代码在main()开头手动完成:

// 将向量表重映射到 RAM 起始地址 0x20000000 SCB->VTOR = 0x20000000; // 注意:此时 RAM 中 0x20000000 ~ 0x200000C0 必须已存放好完整的向量表(含 Reset Handler 地址)

关键点在于:VTOR 修改后,CPU 下一次发生异常(如 SysTick、EXTI)时,才会从新地址取向量。复位向量始终从 0x00000000 加载,这是硬件强制的。因此,VTOR重定位是main()运行时的行为,与main()的诞生无关,但它深刻影响了main()之后整个系统的稳定性。如果重定位后向量表内容错误(比如NMI_Handler地址写成了 0),NMI 中断一来,CPU 就会跳到 0x00000000 执行,大概率崩溃。

2.6 阶段六:SysTick 初始化与操作系统接管(可选但常见)

在裸机程序中,main()可能直接开启外设轮询。但在使用 RTOS(如 FreeRTOS、RT-Thread)时,main()的核心任务是初始化内核并启动调度器。以 FreeRTOS 为例:

int main(void) { HAL_Init(); // 初始化 HAL 库 SystemClock_Config(); // 配置系统时钟 MX_GPIO_Init(); // 初始化 GPIO vTaskStartScheduler(); // 启动 FreeRTOS 调度器 // 此处代码永不执行! }

vTaskStartScheduler()会做两件致命的事:1) 配置 SysTick 定时器作为 RTOS 的心跳;2) 切换到第一个任务的上下文(修改 PSP/MSP,加载任务栈)。这意味着,main()函数的栈帧在此刻被彻底丢弃,CPU 的“主人”从main()变成了当前运行的任务。main()的生命周期就此终结,它只是个启动引导者。这也是为什么main()里定义的局部变量,在任务中完全不可见——它们的栈空间已被覆盖。

2.7 阶段七:YModem 固件升级的特殊考量

WeAct STM32F411 常用于 DIY 固件升级项目,YModem 协议是主流选择。但 YModem 升级成功后,新固件为何有时无法启动?根源往往在启动流程的衔接上。YModem 升级程序(Bootloader)本身也是一个独立的固件,它有自己的Reset_Handlermain()。当它接收到新固件并写入 Flash 后,会执行一次软复位(NVIC_SystemReset())。此时,CPU 重新从 0x00000000 取向量,加载新固件的Reset_Handler

注意:如果 Bootloader 和 Application 的向量表起始地址不同(例如 Bootloader 在 0x08000000,Application 在 0x08004000),那么 Application 的Reset_Handler地址必须被正确写入到 0x00000004。这通常通过在 Application 的链接脚本中设置ENTRY(Reset_Handler)并确保其地址对齐来实现。我曾遇到一个案例:Application 的.text段起始地址是 0x08004000,但Reset_Handler符号地址是 0x08004010,导致向量表 0x00000004 处写入了 0x08004010,CPU 复位后跳转到 0x08004010,而那里是一条nop指令,程序卡死。解决方案是强制Reset_Handler对齐到 4 字节边界,或在链接脚本中用PROVIDE(_isr_vector = .);显式指定向量表位置。

3. 关键技术点深度解析:链接脚本、启动文件与编译器行为

理解了宏观流程,下一步必须深入到支撑这个流程的三大基石:链接脚本(.ld.sct)、启动文件(.s)、编译器(ARMCC/GCC)的协同机制。它们共同定义了“main()之前的世界”的物理布局和行为逻辑。任何一个配置失误,都会让main()成为镜花水月。

3.1 链接脚本:内存布局的宪法

链接脚本是整个程序的“国土规划图”,它告诉链接器(armlinkld)如何将编译生成的.o文件中的各个段(.text,.data,.bss,.stack,.heap)放置到目标芯片的物理内存(Flash/RAM)中。对于 WeAct STM32F411(Flash: 512KB @ 0x08000000, RAM: 128KB @ 0x20000000),一个典型的 GNU ld 脚本(STM32F411xE.ld)核心部分如下:

MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K } SECTIONS { /* 向量表必须放在 Flash 起始,且 256 字(1024 字节)对齐 */ .isr_vector : { . = ALIGN(4); _isr_vector = .; KEEP(*(.isr_vector)) /* 保留 startup_stm32f411xe.o 中的向量表 */ . = ALIGN(4); } > FLASH /* 代码段 (.text) 紧随向量表之后 */ .text : { . = ALIGN(4); _stext = .; *(.text) /* 所有 .text 段 */ *(.text*) /* 所有 .text.* 段 */ *(.rodata) /* 只读数据,如字符串常量 */ *(.rodata*) . = ALIGN(4); _etext = .; } > FLASH /* 已初始化数据段 (.data),加载地址在 Flash,运行地址在 RAM */ .data : { . = ALIGN(4); _sdata = .; /* RAM 中 .data 起始地址 */ _sidata = LOADADDR(.data); /* Flash 中 .data 起始地址(加载地址) */ *(.data) *(.data*) . = ALIGN(4); _edata = .; /* RAM 中 .data 结束地址 */ } > RAM AT > FLASH /* 未初始化数据段 (.bss),只存在于 RAM */ .bss : { . = ALIGN(4); _sbss = .; *(.bss) *(.bss*) *(COMMON) . = ALIGN(4); _ebss = .; } > RAM /* 栈和堆 */ ._user_heap_stack : { . = ALIGN(4); PROVIDE ( _heap_start = . ); . = . + DEFINED(__stack_size__) ? __stack_size__ : 0x1000; PROVIDE ( _heap_end = . ); } > RAM }

关键解析:

  • > FLASH AT > FLASHvs> RAM AT > FLASH.text段的AT > FLASH是冗余的(因为它本来就在 Flash),但.data段的> RAM AT > FLASH是精髓。它明确告诉链接器:.data运行时地址(VMA)是 RAM 中的_sdata,但它的加载时地址(LMA)是 Flash 中的_sidata。这正是Reset_Handler中需要复制数据的依据。
  • _isr_vectorKEEPKEEP(*(.isr_vector))确保启动文件中的向量表段不会被链接器优化掉,即使它看起来“未被引用”。没有它,向量表就消失了,CPU 复位后无处可跳。
  • ALIGN(4):ARM Cortex-M4 要求所有指令和向量地址必须 4 字节对齐。ALIGN(4)确保每个段的起始地址都是 4 的倍数,否则ldr pc, [pc, #0]这类指令会出错。
  • _estack:虽然脚本中没直接定义,但通常在.stack段或通过PROVIDE(_estack = ORIGIN(RAM) + LENGTH(RAM));定义,它是 MSP 的初始值来源。

我曾将.data段的> RAM AT > FLASH错写成> RAM,结果链接器把.data直接放在了 RAM 里,Reset_Handler的复制逻辑找不到_sidata,导致所有已初始化变量保持为 0。调试时发现HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);完全无效,因为LED_GPIO_Port这个结构体指针变量(定义在.data)是 0。

3.2 启动文件:汇编世界的宪法执行者

startup_stm32f411xe.s是链接脚本的“执法者”。它是一份高度标准化的模板,但每一行都关乎生死。以下是其核心骨架及我的实操心得:

; 定义向量表 .section .isr_vector,"a",%progbits .type g_pfnVectors, %object .size g_pfnVectors, .-g_pfnVectors g_pfnVectors: .word _estack /* 0x00000000: 栈顶地址 */ .word Reset_Handler /* 0x00000004: 复位处理程序 */ .word NMI_Handler /* 0x00000008: NMI 处理程序 */ .word HardFault_Handler /* 0x0000000C: 硬故障 */ /* ... 后续 256 个向量,省略 ... */ ; 定义各段起始/结束符号(由链接脚本提供) .extern _sidata .extern _sdata .extern _edata .extern _sbss .extern _ebss ; 复位处理程序 .section .text.Reset_Handler .weak Reset_Handler .thumb_func Reset_Handler: ; 1. 初始化主栈指针 ldr sp, =_estack ; 2. 调用 SystemInit bl SystemInit ; 3. 复制 .data 段 ldr r0, =_sdata ldr r1, =_edata ldr r2, =_sidata movs r3, #0 b LoopCopyDataInit CopyDataInit: ldr r4, [r2, r3] str r4, [r0, r3] adds r3, r3, #4 LoopCopyDataInit: cmp r3, r1 blt CopyDataInit ; 4. 清零 .bss 段 ldr r0, =_sbss ldr r1, =_ebss movs r2, #0 b LoopFillZerobss FillZerobss: str r2, [r0] adds r0, r0, #4 LoopFillZerobss: cmp r0, r1 blt FillZerobss ; 5. 调用 C 运行时入口 bl __main ; 或对于 GCC: bl __libc_init_array ; 或对于裸机:bl main (不推荐,缺少库初始化) ; 6. 如果 __main 返回(理论上不应发生),进入死循环 bx lr

实操心得:

  • __mainvsmain:在 ARMCC 下,bl __main是必须的。如果你直接bl mainmain()里的printf会因stdout未初始化而卡死。我试过,结果是串口无输出,J-Link 报告HardFault
  • bx lr的陷阱Reset_Handler结尾的bx lr是为了处理__main返回的情况。但在正常流程中,__main会调用main()并永不返回。如果main()里写了return__main就会返回到这里,然后bx lr会让 CPU 执行lr寄存器里的随机值,大概率崩溃。因此,main()末尾的while(1);是双重保险。
  • 向量表对齐.word指令生成 4 字节字,确保向量表天然 4 字节对齐。但如果手动添加了非 4 字节数据,必须用.balign 4填充,否则SCB->VTOR设置后,CPU 会从错误地址取向量。

3.3 编译器行为:ARM Compiler 5.06u7 与 GCC 的差异

编译器是整个链条的“翻译官”,它决定了 C 代码如何变成汇编,以及如何与启动文件、链接脚本交互。ARM Compiler 5.06u7(Keil MDK 默认)和 GCC-arm-none-eabi(STM32CubeIDE 默认)在main()启动流程上有显著差异:

特性ARM Compiler 5.06u7GCC-arm-none-eabi
C 运行时入口__main(单个函数,内部调用__rt_entry__libc_init_array(遍历.init_array
向量表生成--vector_table=...选项或#pragma控制,需手动KEEPstartup_*.s提供,链接脚本KEEP(*(.isr_vector))
main()参数__main会构造argc=1,argv={NULL},但嵌入式中无意义同样构造,但更常被忽略
半主机(Semihosting)默认启用,printf会尝试连接调试器;需--disable_semihosting禁用默认禁用,需-specs=nosys.specs或重写_write
堆栈检查可通过--stackcheck选项在__main中插入栈溢出检测需手动在main()开头调用__stack_chk_guard

关键差异实操:在 GCC 下,如果你想让main()之前执行一段自定义代码(比如点亮一个 LED 表示启动成功),不能像 ARMCC 那样用__attribute__((constructor))(它会被__libc_init_array调用),而应该在Reset_Handlerbl SystemInit之后、bl __libc_init_array之前,直接插入你的汇编代码。例如:

bl SystemInit ; --- 自定义启动代码开始 --- ldr r0, =0x40020000 /* RCC base address */ ldr r1, =0x00000001 /* Enable GPIOA clock */ str r1, [r0, #0x18] /* RCC->AHB1ENR */ ldr r0, =0x40020000 /* GPIOA base address */ movs r1, #0x00000001 /* Set PA0 as output */ str r1, [r0, #0x00] /* GPIOA->MODER */ movs r1, #0x00000000 /* Clear PA0 */ str r1, [r0, #0x14] /* GPIOA->BSRR */ ; --- 自定义启动代码结束 --- bl __libc_init_array

这样,CPU 在main()之前,就已经让 PA0 输出低电平,驱动一个 LED 亮起,成为最底层的“Hello World”。

4. 实操排障指南:从 HardFault 到 YModem 升级失败的 12 个真实案例

理论再扎实,不如实战中踩过的坑来得深刻。以下是我过去三年在 WeAct STM32F411 项目中,遇到的 12 个与“main()无法启动”直接相关的典型问题,附带完整的排查思路、定位方法和终极解决方案。每一个都经过真实硬件复现,绝非纸上谈兵。

4.1 案例一:上电后 LED 不亮,调试器连接失败(HardFault on Reset)

现象:WeAct 板子上电,红色 PWR LED 亮,但绿色 USER LED 不亮。用 J-Link 连接,Keil 报错Cannot access Memory,无法 halt。

排查:这是最底层的硬件/启动问题。首先确认 Boot0 引脚:WeAct 板上 Boot0 通常通过跳线帽接地(0),表示从主 Flash 启动。用万用表测量 Boot0 对地电压,确认为 0V。其次,检查 SWD 接口(SWCLK/SWDIO)是否虚焊或短路。最后,用逻辑分析仪抓取 SWCLK 信号,发现无任何波形——说明 CPU 根本没运行。

根因:Reset_Handler第一行ldr sp, =_estack加载了一个错误的栈地址。链接脚本中RAMORIGIN被误写为0x20000000,但实际 WeAct 的 RAM 是 128KB,_estack计算为0x20000000 + 0x20000 = 0x20020000,超出了 RAM 范围。CPU 尝试将 MSP 设为0x20020000,但该地址不存在,触发 BusFault,由于 BusFault 本身又需要栈,形成死循环,CPU 锁死。

解决:修正链接脚本MEMORY段,LENGTH = 128K,并确保_estack计算正确。重新编译烧录。

4.2 案例二:串口打印hello world,但随后printf("counter=%d", counter)输出counter=0

现象:main()printf("hello world");正常,但后续printf中的变量值全为 0。

排查:hello world是字符串常量,存于.rodata段,在 Flash 中,无需初始化。而counter是全局变量,应存于.bss段。用arm-none-eabi-objdump -t your.elf | grep counter查看counter符号地址,发现它在 RAM 区域(如0x20001000)。再用调试器查看该地址内容,果然是 0。

根因:.bss段清零代码在Reset_Handler中被注释掉了,或者LoopFillZerobss循环条件写错(如 `cmp

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 9:04:22

Vue3+SpringBoot学生公寓管理系统开发实践

1. 项目概述这个学生公寓管理系统采用前后端分离架构&#xff0c;前端使用Vue3框架&#xff0c;后端基于Java SpringBoot构建&#xff0c;数据持久层采用MyBatis框架与MySQL数据库交互。系统专为山西大同大学设计&#xff0c;旨在实现学生住宿管理的数字化和智能化。我在高校信…

作者头像 李华
网站建设 2026/9/17 9:04:18

企业自动化运维选型:从脚本到平台的五层落地水位线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 9:00:58

水下图像增强算法:多特征融合与Matlab实现

1. 水下视觉增强的技术挑战与解决思路在水下机器人巡检、海洋生物观测等场景中&#xff0c;我们常会遇到图像模糊、颜色失真、对比度低等问题。这主要源于水体对光线的吸收和散射效应——不同波长的光在水中的衰减程度差异显著&#xff0c;红光在5米深度就基本消失&#xff0c;…

作者头像 李华
网站建设 2026/9/17 8:59:03

Docker安装部署实战指南:从环境准备到容器编排排坑

先把这个“Dock”说清楚先说个容易踩的坑。你搜"Dock的安装部署"&#xff0c;网上能翻出两拨东西&#xff1a;一拨是苹果Mac电脑底下的那个Dock栏&#xff0c;怎么调自动隐藏、怎么改图标大小&#xff1b;另一拨是真正的容器引擎Docker&#xff0c;用来跑Nginx、MySQ…

作者头像 李华