C语言的运行环境包含了硬件环境和软件环境以及进入C语言前准备工作。1. 硬件环境1CPU处理器负责执行编译后的机器指令是运算核心。2存储器Flash/ROM存放程序代码.textRAM存放运行时数据、变量、栈、堆3时钟系统给 CPU 提供工作频率没有时钟CPU 无法执行指令。4最小供电系统给 CPU、存储器、时钟供电保证硬件工作。2. 软件环境我们这里只从嵌入式的视角去看运行环境对于windows、linux下的可执行文件可以做类比这里不做讨论。2.1 程序运行时内存结构C语言的把内存划分为以下几个区域内存区域存储内容生命周期 / 管理方式栈Stack局部变量、函数参数、返回地址、上下文自动管理函数调用创建返回销毁固定大小几 MB堆Heap动态分配内存malloc/calloc/realloc手动管理程序员调用函数申请 / 释放大小受物理内存限制数据段Data初始化非 0 的全局 / 静态变量.data程序启动时创建退出时销毁全局可见BSS 段未初始化 / 初始化为 0 的全局 / 静态变量.bss程序启动时清零退出时销毁不占可执行文件空间代码段Text程序指令、const 常量.rodata只读程序启动时加载退出时释放。注以stm32为例直接从flash读取代码没有释放流程你可能会注意到为什么是程序运行时呢这里是以C语言运行时内存分配而言对于程序存储等角度是有所区别的。如bss段实际不占用程序存储空间只会由一段代码把空间清零。其他角度的内存地址分配可以参考以下文章https://blog.csdn.net/zhy557/article/details/80832268这些不同的分区支撑着C语言的不同功能后面章节再展开说明。3. 进入C语言前准备工作3.1 从上电到main函数经历了什么我们这里以嵌入式为例Cortex-M系列上电会先经过下列流程加载SP栈顶指针 (硬件自动完成读取reset中断函数地址后进入reset中断函数 (硬件自动完成初始化运行环境为C语言提供运行环境拷贝.data段清零.bss段初始化堆栈库函数初始化进入到main函数初始化运行环境为最重要的部分。下面从Cortex-M系列实际上电启动代码进行解读感兴趣的话可以了解一下具体实现。3.2* startup.s和main.c文件这里以keil开发环境下cortex-M3为例只建两个文件main.c和startup.s启动文件。文件1startup.s作用包含上电后最开始运行的内容定义堆栈等空间芯片硬件级初始化以及__main函数的入口。我这里拷贝stm32启动文件只保留堆区栈区代码区复位区复位函数删掉其他中断函数等内容仅作为演示。; 栈区空间分配 Stack_Size EQU 0x00000400 AREA STACK, NOINIT, READWRITE, ALIGN3 Stack_Mem SPACE Stack_Size __initial_sp ; 堆区空间分配 Heap_Size EQU 0x00000400 AREA HEAP, NOINIT, READWRITE, ALIGN3 __heap_base Heap_Mem SPACE Heap_Size __heap_limit PRESERVE8 THUMB ; RESET区用于初始化栈指针及存放中断向量表。这里只保留复位中断 ; /* reset Vector Mapped to at Address 0 */ AREA RESET, DATA, READONLY EXPORT __Vectors EXPORT __Vectors_End EXPORT __Vectors_Size __Vectors DCD __initial_sp ; 栈顶指针, 固件文件开始的四个字节 DCD Reset_Handler ; 复位中断 ... ;其他中断函数地址 __Vectors_End __Vectors_Size EQU __Vectors_End - __Vectors AREA |.text|, CODE, READONLY ; 上电复位中断函数 ;/* reset Handler */ Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT SystemInit IMPORT __main LDR R0, SystemInit ;硬件级初始化如系统时钟 BLX R0 LDR R0, __main ; 将__main函数地址放到R0寄存器 BX R0 ; 跳转到__main函数 ENDP ALIGN END文件2main.cint main(void) { return 0; }这里你可能发现Reset_Handler函数是运行__main函数而不是main()函数。实际上__main用于初始化运行环境最后会从 __rt_entry_main 函数正式进入我们实现的main函数。3.3* __main函数__main函数属于C/C库函数用于初始化运行环境。注BL.W为跳转汇编指令在跳转的同时将返回地址 (跳转指令的下一条指令位置) 存储到LR寄存器中用于PC返回或者说是函数返回。我们看汇编代码能看到其下有两个函数1. __scatterload 分散加载函数负责把RW/RO数据段从装载域地址复制到运行域地址并完成了ZI运行域的初始化工作。即上文描述的拷贝.data段和清零.bss段工作。2. _rt_entry负责初始化堆栈完成库函数的初始化最后自动跳转向main()函数。注keil生成代码后能看到以下信息其中Code为程序大小RO为只读数据/常量大小RW为程序中的已初始化变量大小ZI为程序中的未初始化的变量大小。ROM占用大小 Code RO RWRAM占用大小 RW ZI3.3.1 __scatterload分散加载函数分散加载Scatter Loading编译器 / 链接器根据自定义的分散加载脚本Scatter File将程序的不同段如代码段、数据段、零初始化段分配到内存中不同地址区域如 Flash、RAM的内存布局管理机制。__scatterload: 完成RW/RO 的拷贝 (.data段拷贝)__scatterload_zeroinit完成ZI data的初始化清零.bss段源码解析__scatterload函数__scatterload: 0x08000138 A00A ADR r0, [pc, #0x2C] ; 0x08000138 0x2C 0x08000164, .data段源地址赋值给r0 0x0800013A E890C000 LDM r0, {r10-r11} ; 从 r0 指向的内存连续加载两个 32 位值到 ; r10、r11 0x0800013E 4483 ADD r10, r10, r0 ; 得到加载域信息起始地址 见最下面 ; 0x0000010C见最下面 0x08000164 0x08000270 0x08000140 4483 ADD r11, r11, r0 ; 得到加载域的信息结束地址 见最下面 ; 0x0000011C见最下面 0x08000164 0x08000280 0x08000142 F1AA0701 SUB r7, r10, #0x01 ; r7 0x08000270 - 0x01 0x0800026f ; 无函数跳转,顺序执行下一条指令进入下一个函数 ↓ ↓ ↓ __scatterload_null: 0x08000146 45DA CMP r10, r11 ; 比较源地址指针 r10 和源结束地址 r11 0x08000148 D101 BNE 0x0800014E ; 如果 r10 ! r11还有数据未拷贝则跳到 ; 0x0800014E 执行拷贝;否则进入 __rt_entry 0x0800014A F000F821 BL.W __rt_entry (0x08000190) 0x0800014E F2AF0E09 ADR.W lr, {pc}-0x07 ; 0x08000147 指向__scatterload_null开头,用 ; 于循环 0x08000152 E8BA000F LDM r10!, {r0-r3} ; 从 r10 指向的 Flash 地址一次性加载 4 个 ; 32 位字到 r0-r3. 并自动更新r10 ; 用于处理未对齐的结束地址确保最后一个字被正确拷贝 0x08000156 F0130F01 TST r3, #0x01 0x0800015A BF18 IT NE 0x0800015C 1AFB SUBNE r3, r7, r3 0x0800015E F0430301 ORR r3, r3, #0x01 0x08000162 4718 BX r3 ; 跳转到 __scatterload_zeroinit ; DCW表示它分配一段半字的内存单元。简单来说就是把半字常量数据写入该地址 0x08000164 010C DCW 0x010C ; 0x0000010C加载域信息起始地址偏移 0x08000166 0000 DCW 0x0000 0x08000168 011C DCW 0x011C ; 0x0000011C加载域信息结束地址偏移 0x0800016A 0000 DCW 0x0000加载域源起始地址到加载域源结束地址内容对应地址内容地址内容0x08000280Flash上的数据段起始地址0x20000000加载到RAM上的目的地址0x00000860数据段的总大小0x0800016c_scatterload_zeroinit函数地址​__scatterload_zeroinit函数_scatterload_zeroint: ; 这四条指令将寄存器 r3 到 r6 全部清零 ; 用于赋值给需要清零的内存 0x0800016C 2300 MOVS r3,#0x00 0x0800016E 2400 MOVS r4,#0x00 0x08000170 2500 MOVS r5,#0x00 0x08000172 2600 MOVS r6,#0x00 ; 每次循环清零 16 字节 0x08000174 3A10 SUBS r2,r2,#0x10 ; r2 - 16字节: 更新需要零初始化的内存区域长度 0x08000176 BF28 IT CS 0x08000178 C178 STMCS r1!,{r3-r6} 0x0800017A D8FB BHI 0x08000174 ; 判断是否清零完成未完成进行下一次循环 ; 处理剩余的 8 字节 0x0800017C 0752 LSLS r2,r2,#29 0x0800017E BF28 IT CS 0x08000180 C130 STMCS r1!,{r4-r5} ; 处理剩余的 4 字节 0x08000182 BF48 IT MI 0x08000184 600B STRMI r3,[r1,#0x00] 0x08000186 4770 BX lr ; 跳转回_scatterload_null再进入__rt_entry3.3.1 __rt_entry函数__rt_entry是 Keil MDK 的 C 库运行时初始化函数主要完成初始化堆栈库函数初始化如浮点运算库调用main()函数若main()函数返回执行退出处理__user_setup_stackheap 初始化堆栈地址以及SP指针位置参考https://blog.csdn.net/lushoumin/article/details/78886141https://www.cnblogs.com/jiangzhaowei/p/9240221.html