main函数之前究竟发生了什么?

探讨了在main函数执行前,嵌入式系统启动代码如何初始化C/C++资源,并将静态变量从Flash映射到RAM。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

//=====================================================================
//TITLE:
//    main函数之前究竟发生了什么?
//AUTHOR:
//    norains
//DATE:
//    Friday  3-December-2010
//Environment:
//    MDK 4.1
//=====================================================================

 

     当使用MFC时,我们会认为入口函数是:: InitInstance;当使用WIN32 API时,我们会认为入口函数是WinMain;当我们写个纯粹的C++程序时,入口函数又变成了main;可当我们进入到嵌入式领域,却发现main函数之前还有一段启动代码!

 

     究竟在main函数之前,发生了什么?如果你觉得已经明白了这个过程,那么请试着回答这个问题:程序是存储到FLASH中的,运行时static变量地址是指向RAM,那么这些static变量的初始值是如何映射到RAM中的?

 

     我们以STM32F10x的启动代码为例,先看看其完整的源码:


     一些旁枝末节和本文的主题无关,我们先不要去理会,只需要知道这个启动代码是设置向量表,然后跳转到__main函数。跳转具体到代码段部分如下:

 

     当大家看到__main函数时,估计应该有不少人认为这个是main函数的别名或是编译之后的名字,否则在启动代码中再也无法找到和main相关的字眼了。可事实是,__main和main是完全两个不同的函数!如果这还不足以让你诧异,那么再告诉你另一个事实:你无法找到__main代码,因为这个是编译器自动创建的!

 

     如果你对此还半信半疑,可以查看MDK的文档,会发现有这么一句说明:It is automatically created by the linker when it sees a definition of main()。简单点来说,当编译器发现定义了main函数,那么就会自动创建__main。

 

     __main函数的出身我们基本搞清楚了,那么现在的问题是,它和main又有什么关系呢?其实__main主要做这么两件事:初始化C/C++所需的资源,调用main函数。初始化先暂时不说,但“调用main函数”这个功能能够让我们解决为什么之前的启动代码调用的是__main,最后却能转到main函数的疑惑。

 

     初始化C/C++所需的资源,如果脱离了具体情况,实在很难解释清楚,还是先看看编译出来的汇编代码片段:
 

 

     凡是以__rt开头的,都是用来初始化C/C++运行库的;而以__scatterload开头,则是根据离散文件的定义,将代码中的变量映射到相应的内存位置。而回答本文开头的问题,关键就在于__scatterload_copy函数!

 

     我们在STM32F10x平台举个简单的例子,首先要明白一点是,该平台的的flash地址以0x08000000为起始,主要是存储代码;而SRAM是以0x20000000为起始,也就是内存。然后C/C++有这么一行代码:

 

     当我们程序开始跑起来的时候,通过IDE发现,g_iVal被映射到内存地址0x20000000,数值为一个随机数0xFFFFBE00,而不是代码中设置的12,如图:


 

     我们让程序继续往下执行,当执行完毕__scatterload_copy之后,我们发现g_iVal这时候已经变成我们所需要的初始值了:


 

     接下来就是C/C++库的初始化,最后就是进入到main函数,而此时已经是万事俱备。

 

     如果大家只是局限于桌面应用的开发,因为编译出来的程序带有很多操作系统的特性,所以会给我们理解程序的运行带来很大的迷惑,也只有步入嵌入式领域,没有操作系统的支持下赤裸裸地奔跑在CPU之上,才能更好地理解软件是如何运行起来的,也只有这时候我们才能够更清楚知道,原来main函数并不是起点。

<think>嗯,用户问的是单片机在进入main函数之前执行了哪些操作。这个问题看起来挺基础的,但作为新手可能不太清楚启动过程的具体步骤。首先,我需要回忆一下单片机的启动流程。记得之前学过,单片机上电后并不是直接跳到main函数的,而是有一些初始化工作要先完成。 那第一步应该是复位向量吧?当单片机通电或者复位时,处理器会从复位向量地址获取初始的程序计数器值,也就是复位处理函数的地址。这个地址通常指向启动代码,比如汇编写的启动文件。这部分可能包括设置堆栈指针,初始化静态存储区的变量,比如.data和.bss段。.data段存放已初始化的全局变量,而.bss段是未初始化的,需要清零。 接下来可能还有时钟系统的初始化,比如配置内部或外部时钟源,设置PLL倍频,调整系统时钟频率。这确保单片机的时钟正确运行,之后外设才能正常工作。然后可能初始化中断向量表,把中断服务函数的地址加载到对应的位置,这样当中断发生时,处理器能正确跳转。 还有可能执行一些硬件外设的初始化,比如看门狗定时器的配置,避免在启动过程中触发复位。之后才会跳转到main函数执行用户代码。用户可能没有意识到这些底层步骤,但了解这些对调试和优化程序有帮助,比如变量未初始化的问题可能和启动代码有关。 需要确认这些步骤的正确性,比如是否先设置堆栈,再初始化数据段,然后是时钟和中断,最后到main。可能还需要提到不同的单片机厂商可能有不同的启动流程,但大体步骤类似。另外,C语言环境初始化,比如初始化静态变量和全局变量,可能是在启动代码里完成的。还有,是否调用了库函数的初始化函数,比如标准库的初始化,这些也可能在main之前执行。 要确保回答结构清晰,分步骤说明,并用例子帮助理解。可能还需要指出这些步骤是由启动文件(如startup.s)处理的,开发者通常不需要修改,但在某些情况下可能需要自定义,比如优化启动速度或特定硬件配置。</think>单片机在进入`main()`函数前,需要完成硬件初始化和运行环境搭建。以下是分步骤说明: --- ### 一、复位向量跳转 1. **复位触发** 单片机上电或复位时,CPU从固定地址(复位向量地址)读取**复位处理函数指针**,跳转到启动代码(通常由汇编编写,如`startup.s`或`crt0.s`)。 --- ### 二、硬件初始化阶段 2. **设置堆栈指针(SP)** 根据芯片手册配置堆栈起始地址(例如:`__initial_sp`),为后续函数调用和局部变量分配空间。 3. **初始化静态存储区** - **.data段**:将已初始化的全局变量从Flash复制到RAM。 ```c /* 伪代码示例 */ memcpy(&_sdata, &_edata, &_ldata); ``` - **.bss段**:将未初始化的全局变量清零。 ```c memset(&_sbss, 0, &_ebss - &_sbss); ``` --- ### 三、系统级配置 4. **时钟系统初始化** - 启用内部/外部时钟源(如HSI、HSE)。 - 配置PLL倍频,设置系统主频(如STM32中通过RCC模块配置)。 - 分配时钟到外设(如AHB、APB总线)。 5. **中断向量表初始化** - 将中断服务函数(ISR)地址加载到中断向量表。 - 启用中断控制器(如NVIC)。 6. **外设预配置** - 关闭看门狗(如独立看门狗IWDG)。 - 初始化必要外设的默认状态(如GPIO默认模式)。 --- ### 四、跳转至用户代码 7. **调用`main()`函数** 最终通过汇编指令`BL main`或C函数调用进入用户编写的`main()`。 --- ### 补充说明 - **启动文件的作用**:上述操作通常由芯片厂商提供的启动文件(如STM32的`startup_stm32fxxx.s`)自动完成。 - **定制化需求**:若需优化启动速度或修改默认行为(如提前启用某个外设),需手动修改启动文件。 - **调试技巧**:若程序在进入`main()`前崩溃,需检查堆栈配置、.data/.bss段复制是否正常。 --- 通过这一系列操作,单片机确保硬件环境、内存状态和中断系统就绪,为`main()`函数的执行提供可靠基础。
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值