尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32裸机中main函数为何不执行?启动流程深度解析

STM32裸机中main函数为何不执行?启动流程深度解析 1. 为什么一个简单的main()在 STM32 上会“消失”——从桌面到嵌入式的认知断层你写过#include stdio.h int main() { printf(Hello World!\n); return 0; }编译、运行、看到输出一切如常。这是 C 语言教科书的起点也是你和计算机建立的第一份契约你提供main操作系统负责加载、初始化、调用它再回收资源。但当你把这段代码放进 Keil 或 STM32CubeIDE烧录到一块 STM32F407 的开发板上按下复位键——什么都没发生。没有打印没有闪烁甚至用逻辑分析仪都抓不到任何 UART 波形。你检查了串口配置、时钟使能、引脚复用一切无误。最后你发现链接器报错undefined reference to main或者更诡异的是——程序居然跑起来了但main函数里的第一行printf根本没执行。这不是你的代码错了而是你站在了两个世界的交界处一边是 x86/Linux/Windows 构建的成熟软件生态另一边是裸金属bare-metal嵌入式世界。这里的“操作系统”不是 Windows 或 Linux而是芯片上电后那一段固化在 ROM 里的启动代码这里的“标准输入输出”不是终端窗口而是你手动配置的 USART 外设这里的“程序结束”不是进程退出而是你主动进入while(1)死循环或者触发硬件复位。main函数本身没变但它所依赖的整个执行环境被彻底重写了。热搜词里反复出现的编译器未包含main类型、[main] warn [org.apache.hadoop.util.shell] - did not find winutils.exe看似风马牛不相及实则暴露了同一个底层问题函数入口的上下文缺失。Hadoop 报错是因为 Java 运行时找不到 Windows 下的本地工具链而你的 STM32 工程报错是因为链接器找不到符合 ARM Cortex-M ABI 规范的、被正确标记为入口点的main符号。这背后牵扯的是启动流程、C 运行时库CRT、向量表、堆栈初始化、全局变量构造等一系列在桌面端被隐藏得严严实实的细节。我第一次在 STM32 上让printf成功打印出字符时花了整整三天时间——不是因为不会写 GPIO 初始化而是因为没搞懂__libc_init_array这个函数到底在哪个.o文件里、为什么它必须在main之前被调用、以及如果我把main声明成void main(void)而不是int main(void)链接器为什么会默默忽略它。这篇文章不讲怎么点亮 LED而是带你亲手拆开那个“黑盒子”看看从你按下下载按钮那一刻起你的main函数是如何穿越启动代码、跳过 C 运行时、绕过中断向量表最终在 SRAM 里获得 CPU 控制权的。无论你是刚学完翁恺老师 C 语言课的大一新生还是正在做 STM32 车载以太网协议栈的工程师只要你的代码里还有main()你就需要知道它后来去了哪里。2. 启动流程全景图从复位向量到main的七步穿越2.1 第一步复位向量——CPU 的“起床闹钟”STM32 芯片上电或复位后ARM Cortex-M 内核做的第一件事不是执行你的 C 代码而是去一个固定的内存地址读取一个 32 位的值。这个地址就是复位向量Reset Vector对所有 Cortex-M 系列芯片它恒定为0x00000004注意不是0x00000000那是主堆栈指针 MSP 的初始值。这个地址存储的是复位后 CPU 应该跳转去执行的第一条指令的地址。你可以把它理解成 CPU 的“起床闹钟”——闹铃一响它就立刻去0x00000004查看该去哪里。这个地址的内容由芯片厂商在出厂时固化在 Flash 的起始位置通常是0x08000000开始或者由你的链接脚本.ld文件精确指定。如果你打开一个标准的 STM32 启动文件比如startup_stm32f407xx.s你会看到类似这样的汇编代码.section .isr_vector,a,%progbits .type g_pfnVectors, %object g_pfnVectors: .word _estack /* Top of Stack */ .word Reset_Handler /* Reset Handler */ .word NMI_Handler /* NMI Handler */ .word HardFault_Handler /* Hard Fault Handler */ ...这里.word _estack就是存放在0x00000000的主堆栈指针初始值而.word Reset_Handler就是存放在0x00000004的复位向量。所以CPU 上电后先从0x00000000读取堆栈顶地址初始化 MSP然后从0x00000004读取Reset_Handler的地址并跳转过去执行。这个Reset_Handler就是你整个程序的真正起点它比你的main函数早出现至少七个步骤。2.2 第二步Reset_Handler—— 汇编世界的“总调度员”Reset_Handler是一个用纯汇编写的函数它的核心任务只有一个为 C 语言的执行环境铺平道路。它不关心你的业务逻辑只负责搭建舞台。典型的Reset_Handler流程如下关闭全局中断cpsid i。这是为了防止在初始化过程中被意外中断打断导致状态混乱。初始化数据段.data将 Flash 中存储的已初始化全局变量如int a 5;的初始值拷贝到它们在 RAM 中的对应位置。Flash 速度快但不能写RAM 可读可写但掉电丢失所以.data段的数据必须在启动时从 Flash “搬运”到 RAM。清零 BSS 段.bss将 RAM 中未初始化的全局变量如int b;所在区域全部置零。C 标准规定未显式初始化的全局变量默认为 0这个工作由启动代码完成。设置堆栈指针SP虽然 MSP 已在复位向量中初始化但Reset_Handler通常会再次明确设置确保万无一失。调用 C 运行时初始化函数这是最关键的一步调用SystemInit()由 ST 提供负责系统时钟、Flash 等基础外设初始化和__main由 ARM 编译器提供是 C 运行时库的入口它内部会调用__libc_init_array来执行所有__attribute__((constructor))的函数。跳转到main最后执行bl main将控制权正式交给你的 C 代码。这个过程之所以必须用汇编是因为在 C 运行时环境建立之前C 语言的许多基本设施如函数调用约定、栈帧管理都还不存在。我曾经尝试过把Reset_Handler里拷贝.data段的循环用 C 写结果编译器直接报错——因为它无法生成一个不依赖自身运行时的 C 代码。这就是为什么所有嵌入式项目都离不开那个看似枯燥的startup_*.s文件。它不是可有可无的模板而是连接硬件与软件的唯一桥梁。2.3 第三步__main与__libc_init_array—— C 运行时的“幕后推手”__main是 ARM RealView 编译器ARMCC和 ARM Clang 的一个特殊符号它不是一个用户定义的函数而是链接器自动插入的一个“胶水”函数。当你在链接命令中指定-o output.elf时链接器会悄悄地把__main的地址作为程序的入口点Entry Point而不是你写的main。__main的职责非常明确它负责执行所有 C 运行时CRT的初始化工作其中最核心的就是调用__libc_init_array。__libc_init_array是一个由编译器自动生成的函数数组的执行器。这个数组里存放的是所有被标记为constructor的函数的地址。你在代码里写的__attribute__((constructor)) void my_init_function(void) { // 这里可以做任何初始化工作 RCC-AHB1ENR | RCC_AHB1ENR_GPIOAEN; // 使能 GPIOA 时钟 }编译器就会把这个函数的地址放入一个名为__init_array_start到__init_array_end的特殊段中。__libc_init_array的作用就是遍历这个数组依次调用里面的每一个函数。这解释了为什么很多 STM32 项目里main函数之前似乎“自动”完成了时钟配置、GPIO 初始化——那很可能就是constructor函数干的。但要注意constructor的执行顺序是不确定的它只保证在main之前不保证函数之间的先后。所以如果你有两个constructor函数一个初始化时钟一个初始化 GPIO而后者依赖前者你就必须在代码里显式地加上依赖关系或者干脆把它们合并到一个函数里。这也是为什么 ST 官方的 HAL 库不推荐在constructor里做复杂的外设初始化而是把所有初始化逻辑都放在main函数里由开发者自己控制顺序。2.4 第四步向量表重定位——从 Flash 到 RAM 的“搬家”Cortex-M 的中断向量表Interrupt Vector Table, IVT默认位于 Flash 的起始地址0x08000000。但如果你的项目启用了中断并且希望在运行时动态修改中断服务函数比如实现一个可配置的中断路由你就需要把向量表搬到 RAM 里。这是因为 Flash 的写入操作非常耗时且有次数限制而 RAM 可以随时读写。向量表重定位的过程就是在SystemInit()或Reset_Handler的早期通过修改 SCB-VTORVector Table Offset Register寄存器来实现的。例如// 将向量表复制到 RAM 的 0x20000000 地址 uint32_t *vectors (uint32_t*)0x20000000; uint32_t *flash_vectors (uint32_t*)0x08000000; for(int i 0; i 48; i) { // 48 个向量包括复位、NMI、HardFault 等 vectors[i] flash_vectors[i]; } // 更新 VTOR 寄存器 SCB-VTOR 0x20000000;这个操作必须在任何中断使能之前完成否则 CPU 在响应中断时会去错误的地址查找 ISR导致 HardFault。很多初学者的 HardFault 问题根源就在于向量表重定位的时机不对或者复制的向量数量不够漏掉了 SysTick 或其他外设的向量。stm32 snmp trap v2c 代码这类网络协议栈项目往往需要频繁地注册和注销中断处理函数向量表重定位就是它们的标配。2.5 第五步堆与栈的初始化——内存管理的“基石”main函数能正常运行离不开两个关键的内存区域栈Stack和堆Heap。栈用于存储函数的局部变量、返回地址和寄存器现场堆则用于malloc/free动态内存分配。在Reset_Handler里你通常会看到类似Stack_Size EQU 0x00000400和Heap_Size EQU 0x00000200的宏定义它们定义了栈和堆的大小。链接脚本.ld文件会根据这些定义在 RAM 区域里划出两块连续的内存空间。栈从高地址向下增长堆从低地址向上增长。它们之间必须留有足够的“警戒带”否则一旦栈溢出或堆碎片化严重两者就会发生碰撞导致不可预测的崩溃。我在做一个基于 STM32 的数字温湿度计项目时就曾因为把Stack_Size设得太小只有0x200导致在调用sprintf格式化一个长字符串时栈溢出程序直接跑飞。后来我把栈大小翻倍到0x800问题迎刃而解。stm32鱼缸这类需要长时间稳定运行的项目尤其要重视堆栈大小的合理规划因为malloc分配的内存如果得不到及时释放会像滚雪球一样越积越多最终耗尽所有堆空间。2.6 第六步main的签名与返回——一个被忽略的“契约”在桌面端int main(int argc, char *argv[])是标准return 0;表示程序成功退出。但在 STM32 的裸机环境中argc和argv完全没有意义——谁来给你传递命令行参数因此绝大多数嵌入式项目的main都声明为int main(void)或void main(void)。然而这里有一个致命的陷阱void main(void)是非标准的且在某些编译器配置下会导致链接失败。原因在于链接器期望main是一个返回int的函数以便在main返回后能执行一段“清理”代码尽管在裸机环境下这段代码通常什么都不做。如果你强行声明void main(void)编译器可能会生成一个不符合 ABIApplication Binary Interface规范的函数链接器在解析符号时就会找不到匹配的main从而报错undefined reference to main。这就是热搜词编译器未包含main类型的真实含义——不是编译器“没包含”而是你写的main类型不被链接器认可。正确的做法永远是int main(void)并在末尾return 0;。至于这个return之后会发生什么答案是什么都不会发生。CPU 会执行main返回后的下一条指令而这条指令通常是链接器放置的一段无限循环while(1);或者直接跳转到一个空闲的死循环。你的程序就此进入了永恒的等待。2.7 第七步main之后的世界——从return到while(1)当你的main函数执行完毕return 0;语句被执行CPU 的程序计数器PC会跳转到main函数在栈帧中保存的返回地址。这个地址由链接器在生成可执行文件时决定。在标准的 STM32 启动流程中这个返回地址指向的是_exit函数由 libc 提供或一个自定义的__default_exit函数。_exit的典型实现就是一个无限循环void _exit(int status) { while(1) { __asm(wfi); // Wait For Interrupt进入低功耗模式 } }wfi指令会让 CPU 进入睡眠模式直到下一个中断到来才被唤醒。这比单纯的while(1);更加节能对于电池供电的stm32 车载以太网设备或stm32鱼缸控制器来说这种微小的优化能显著延长续航时间。所以main函数的结束并不意味着程序的终结而是一个优雅的休眠。这也是为什么你在main里写while(1)是最佳实践——它让你完全掌控程序的生命周期避免依赖于可能因编译器版本不同而行为各异的_exit实现。我见过一个项目因为开发者没写while(1)而是依赖_exit结果在升级了 ARM GCC 版本后新的 libc 把_exit实现成了BKPT #0断点指令导致程序一退出main就触发调试器现场直接卡死。3. 核心细节解析链接脚本、启动文件与编译器标志的“铁三角”3.1 链接脚本.ld文件——内存布局的“宪法”链接脚本是整个嵌入式构建过程中最核心、也最容易被忽视的文件。它不像 C 代码那样直观却决定了你的程序在芯片内存中的最终落位。一个典型的 STM32F407 的链接脚本STM32F407VGTx_FLASH.ld会包含以下关键部分/* 定义内存区域 */ MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 1024K RAM (xrw) : ORIGIN 0x20000000, LENGTH 128K } /* 定义输出段 */ SECTIONS { /* .isr_vector 段必须放在 Flash 的最开始紧挨着复位向量 */ .isr_vector : { . ALIGN(4); KEEP(*(.isr_vector)) /* 保留启动文件里的向量表 */ . ALIGN(4); } FLASH /* .text 段存放代码和只读数据 */ .text : { . ALIGN(4); *(.text) /* 所有代码 */ *(.rodata) /* 只读数据如字符串常量 */ . ALIGN(4); } FLASH /* .data 段已初始化的全局变量从 Flash 加载到 RAM */ .data : AT (ADDR(.text) SIZEOF(.text)) { . ALIGN(4); _sdata .; /* data 段起始地址 */ *(.data) _edata .; /* data 段结束地址 */ . ALIGN(4); } RAM /* .bss 段未初始化的全局变量全部清零 */ .bss : { . ALIGN(4); _sbss .; /* bss 段起始地址 */ *(.bss) *(COMMON) _ebss .; /* bss 段结束地址 */ . ALIGN(4); } RAM }这个脚本的精妙之处在于AT关键字。.data段的RAM表示它最终要被加载到 RAM 中运行但AT (ADDR(.text) SIZEOF(.text))表示它的初始值即.data的内容要被存放在 Flash 中紧跟在.text段之后。这正是Reset_Handler里拷贝.data段的理论依据——它知道.data的初始值在 Flash 的哪里也知道要拷贝到 RAM 的哪里。如果你不小心把.data段也写成了FLASH那么你的全局变量就永远是只读的a 5;这样的赋值将毫无效果。stm32芯片逆变器方案这类对实时性要求极高的项目常常会把关键的控制算法代码放在 RAM 中执行RAM因为 RAM 的访问速度远高于 Flash这需要在链接脚本里为.text_ram单独开辟一个段并在启动代码里手动拷贝。3.2 启动文件startup_*.s——汇编世界的“施工蓝图”启动文件是Reset_Handler的载体它是一份高度标准化的汇编代码。不同 IDEKeil、IAR、GCC生成的启动文件格式略有不同但核心逻辑一致。以 GNU ARM GCC 的startup_stm32f407xx.s为例其关键结构如下.section .isr_vector,a,%progbits .global g_pfnVectors .extern Reset_Handler .extern Default_Handler .extern NMI_Handler .extern HardFault_Handler ... g_pfnVectors: .word _estack .word Reset_Handler .word NMI_Handler .word HardFault_Handler ... /* 其他中断向量 */ .section .text.Reset_Handler .weak Reset_Handler .global Reset_Handler Reset_Handler: /* 关闭中断 */ cpsid i /* 初始化 .data 段 */ ldr r0, _sdata ldr r1, _edata ldr r2, _sidata movs r3, #0 cmp r0, r1 beq LoopCopyDataInit CopyDataInit: ldr r4, [r2, r3] str r4, [r0, r3] adds r3, r3, #4 cmp r0, r1 bcc CopyDataInit LoopCopyDataInit: /* 清零 .bss 段 */ ldr r0, _sbss ldr r1, _ebss movs r2, #0 cmp r0, r1 beq LoopFillZerobss FillZerobss: str r2, [r0] adds r0, r0, #4 cmp r0, r1 bcc FillZerobss LoopFillZerobss: /* 调用 SystemInit */ bl SystemInit /* 调用 __main进入 C 运行时 */ bl __main /* 如果 __main 返回跳转到 main */ bx lr这段代码清晰地展示了前文所述的启动流程。值得注意的是bx lr指令。lrLink Register寄存器里保存的是bl __main的返回地址也就是__main执行完毕后应该跳转回去的地方。而__main的设计就是让它在完成所有初始化后自动跳转到main函数。所以bx lr这一行就是main函数被调用的“临门一脚”。如果你在这里写成b main效果是一样的但bx lr更符合 ARM 的调用约定也更安全。keil5兼容c51和stm32安装这类多平台开发环境其启动文件的差异主要体现在.section的命名和__main的调用方式上但底层逻辑殊途同归。3.3 编译器与链接器标志——构建过程的“开关矩阵”一个完整的嵌入式构建命令远不止gcc -c main.c这么简单。它是一系列精心配置的标志的组合每一个都扮演着关键角色-mcpucortex-m4 -mfloat-abihard -mfpufpv4告诉编译器目标 CPU 架构、浮点 ABI硬浮点和浮点单元类型。如果mfpu设置错误你的float计算就会出错。-mthumb -mthumb-interwork强制使用 Thumb 指令集代码密度更高并允许 Thumb 和 ARM 指令混合使用。-Og -g3 -Wall -Wextra-Og是为调试优化的级别它在保持代码可调试性的同时进行适度优化-g3生成最详细的调试信息-Wall和-Wextra开启所有警告这是发现潜在问题的利器。c语言指针相关的很多 bug如野指针、悬空指针都会在-Wextra下被揪出来。-ffreestanding -fno-builtin-ffreestanding告诉编译器这是一个“自由站立”的环境不依赖标准库-fno-builtin禁用内置函数如memcpy强制使用你提供的或 libc 提供的实现。这对于stm32驱动下载这类需要极致可控性的项目至关重要。-T stm32f407vgtx_flash.ld指定链接脚本这是整个内存布局的源头。-specsnano.specs使用newlib-nano这个精简版 C 库它比标准newlib小得多非常适合资源受限的 MCU。c语言文件读写操作代码在嵌入式环境下几乎不可能用fopen但nano.specs保证了printf这类基础 I/O 函数的可用性。这些标志共同构成了一个“开关矩阵”任何一个开关拨错都可能导致你的main函数无法被正确识别、链接或执行。unable to find image ghcr.io/open-webui/open-webui:main locally这类 Docker 错误其本质和嵌入式链接错误一样都是“找不到指定的入口点”只是发生的层次不同而已。4. 实操过程从零开始亲手构建一个“看得见”的main流程4.1 环境准备抛弃 IDE拥抱命令行为了真正理解main的旅程我建议你暂时放下 Keil 或 STM32CubeIDE用最原始的命令行工具链来构建一个最小工程。这能让你看清每一步发生了什么。你需要GNU ARM Embedded Toolchain从 arm.com 下载最新版解压后将bin目录加入系统PATH。OpenOCD用于 JTAG/SWD 调试和烧录。一个文本编辑器VS Code 配合 Cortex-Debug 插件是绝佳选择。创建一个项目目录stm32-main-journey结构如下stm32-main-journey/ ├── startup_stm32f407xx.s # 启动文件可从 STM32CubeMX 生成 ├── system_stm32f4xx.c # 系统初始化文件 ├── main.c # 你的主程序 ├── STM32F407VGTx_FLASH.ld # 链接脚本 └── Makefile # 构建脚本4.2 编写一个“会说话”的main.c为了让main的执行变得可视化我们不点亮 LED而是让main函数在进入后通过 SWOSerial Wire Output通道发送一个独特的字符串。SWO 是 Cortex-M 的一个调试特性它能将ITM_SendChar的输出直接通过 SWD 接口发送给调试器无需额外的 UART 引脚。#include stm32f4xx.h // SWO 初始化函数 void SWO_Init(void) { // 使能 DBGMCU 时钟 RCC-APB1ENR | RCC_APB1ENR_DBGMCUEN; // 配置 SWO 引脚PA13/SWDIO GPIOA-MODER | GPIO_MODER_MODER13_1; // 复用功能 GPIOA-AFR[1] | 0x00000000; // AF0 // 配置 ITM CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; ITM-LAR 0xC5ACCE55; // 解锁 ITM ITM-TCR | ITM_TCR_ITMENA_Msk; // 使能 ITM ITM-TPR 0x00000000; // 不屏蔽任何端口 ITM-TER 0x00000001; // 使能端口 0 TPI-SPPR 2; // 设置 SWO 输出协议为 NRZ TPI-FFCR 0x00000000; // 关闭格式化 TPI-ACPR 0x00000000; // 设置波特率分频器需根据系统时钟计算 } // 一个简单的字符串发送函数 void SWO_Print(const char* str) { while(*str) { ITM_SendChar(*str); } } int main(void) { // 初始化 SWO SWO_Init(); // 发送一个独特的“签名” SWO_Print( MAIN FUNCTION STARTED \r\n); // 主循环 while(1) { // 这里可以放你的业务逻辑 for(volatile int i 0; i 1000000; i); // 简单延时 SWO_Print(Tick...\r\n); } return 0; }4.3 构建与烧录见证main的诞生编写Makefile将所有构建步骤自动化# 工具链 CC arm-none-eabi-gcc OBJCOPY arm-none-eabi-objcopy OPENOCD openocd # 目标 TARGET main.elf HEX main.hex BIN main.bin # 源文件 SOURCES startup_stm32f407xx.s system_stm32f4xx.c main.c OBJECTS $(SOURCES:.c.o) $(SOURCES:.s.o) # 编译选项 CFLAGS -mcpucortex-m4 -mfloat-abihard -mfpufpv4 -mthumb -Og -g3 -Wall -Wextra \ -ffreestanding -fno-builtin -I. -T STM32F407VGTx_FLASH.ld all: $(TARGET) $(TARGET): $(OBJECTS) $(CC) $(CFLAGS) -o $ $^ %.o: %.c $(CC) $(CFLAGS) -c -o $ $ %.o: %.s $(CC) $(CFLAGS) -c -o $ $ $(HEX): $(TARGET) $(OBJCOPY) -O ihex $ $ $(BIN): $(TARGET) $(OBJCOPY) -O binary $ $ flash: $(BIN) $(OPENOCD) -f interface/stlink.cfg -f target/stm32f4x.cfg -c program $ verify reset exit debug: $(TARGET) $(OPENOCD) -f interface/stlink.cfg -f target/stm32f4x.cfg -c init -c reset halt clean: rm -f $(OBJECTS) $(TARGET) $(HEX) $(BIN) .PHONY: all flash debug clean执行make你会看到编译器输出一系列.o文件然后链接器将它们组合成main.elf。执行make flashOpenOCD 会将二进制镜像烧录到芯片 Flash。现在打开一个终端运行st-util或使用 VS Code 的 Cortex-Debug连接到目标设备。在调试器的控制台里你应该能看到 MAIN FUNCTION STARTED 这行文字。这意味着你的main函数不仅被编译、链接、烧录而且被Reset_Handler成功调用并执行了第一行代码。你亲手构建的不再是一个黑盒而是一个透明的、可追踪的执行流程。4.4 使用 GDB 进行深度追踪main的每一步都在你眼前GDB 是理解main流程的终极武器。在 VS Code 中按F5启动调试或者在命令行中运行arm-none-eabi-gdb main.elf (gdb) target extended-remote :3333 (gdb) monitor reset halt (gdb) load (gdb) break Reset_Handler (gdb) continueGDB 会在Reset_Handler的第一条指令处停下。此时你可以用stepi单步执行汇编指令命令逐条执行cpsid i、ldr、str……亲眼看着.data段被拷贝.bss段被清零。当执行到bl __main时按step进入函数你就会跳进__main的内部看到它如何调用__libc_init_array再跳进你的my_init_function如果有的话。最后当__main执行完毕bx lr会把你带到main函数的入口。在这个过程中你可以随时用info registers查看 MSP、PC、LR 等寄存器的值用x/10xw 0x20000000查看 RAM 中的数据用disassemble查看反汇编代码。c语言基础知识入门里讲的“函数调用栈”在这里不再是抽象概念而是你屏幕上实实在在的内存地址和寄存器值。stm32定时器的初始化代码stm32控制伺服电机485的通信协议所有这些复杂逻辑都始于这个main的第一行。4.5 一个“失败”的实验故意破坏main的旅程为了加深理解我们来做一次“破坏性”实验。打开main.c把int main(void)改成void main(void)然后make。编译会成功但链接会失败报错undefined reference to main。这验证了前文的观点链接器只认int main(void)。再改回来这次打开链接脚本STM32F407VGTx_FLASH.ld把.isr_vector段的FLASH改成RAM。make会成功但烧录后程序不会运行。因为 CPU 上电后会去0x00000004
返回列表