将爱赋给建筑便成为家将教学赋给建筑便成为学校将劳作赋给建筑便成为公司。将指令与数据分区赋给存储器便成为‘内存’。——《嵌入式开发之道》第3天我们成功用C语言重写了点灯程序写了代码执行了编译命令生成了一个led.bin烧录到开发板灯亮了。但在这个过程中有一个关键步骤你可能没太在意链接器ld到底做了什么为什么一定要指定-Ttext0x00000000为什么startup.s必须放在第一个今天我们就来回答这些问题。一、CPU分不清指令和数据CPU从内存里读出一个数它怎么知道这个数是指令还是数据答案是CPU分不清。在CPU眼里0x2000和0xE7FE没有区别——都是16位的二进制数。指令和数据在内存中都是0和1CPU只负责“读出来”不区分类型。如果CPU把数据当成指令去执行程序就会崩溃。解决方案很简单把指令和数据分开放。1.1指令区——.text段CPU上电后第一件事不是执行你的代码而是去地址0x00000000和0x00000004读两个数地址存放内容作用0x00000000堆栈指针初始值告诉CPU栈从哪开始见1.3节栈的详细解释0x00000004复位向量告诉CPU“程序从哪开始执行”这两个地址合起来叫向量表。你可以把它理解为一本书的目录第一个地址告诉CPU栈的起点在哪第二个地址告诉CPU程序从哪开始执行为什么叫向量因为向量在数学里表示有方向的量——这里的地址就是用来确定CPU跳转方向的所以叫向量表。向量表不仅包含启动信息还包含所有中断的入口。具体结构如下0x00000000 堆栈指针SP初始值 0x00000004 复位向量Reset 0x00000008 NMI不可屏蔽中断向量 0x0000000C HardFault硬错误向量 ... 0x00000040 EXTI0外部中断0向量向量表不仅包含复位向量还包含其他中断入口。第6天讲中断时我们再展开。向量表后面就是你的程序指令。指令按顺序排放CPU按顺序一条条往下执行。你写程序时.text段的末尾就是你最后一条指令。但问题是Flash里你写的程序后面还有别的内容——比如其他段的数据、甚至Flash中未使用的区域里面是随机值。CPU不会因为你写完了就停下来它只会一根筋地继续取指、译码、执行。如果最后一条指令不是跳转CPU就会继续往后面的地址取数把那里的数据当指令执行——这就叫程序跑飞。解决方案在最后放一条跳转指令让CPU永远跳回自己不往后面走。loop: b loop ; 跳转到自己永远循环这就是为什么所有嵌入式程序最后都有死循环——就是防止程序跑飞。掉电恢复后系统仍需继续正常运行因此指令区要安排在Flash等非易失性存储器里。指令区取名为.text。1.2数据区指令区解决了“指令存哪里”的问题。现在来回答“数据存哪里”。从原理上看CPU可以读写任何地方的数据能不能读写成功、有没有后果是另一回事——取决于该地址的硬件特性。①有初值的数据.data段假设你定义了一个全局变量int g_count 100;。这个100是初值需要保存下来——所以它被放在Flash里掉电不丢。但问题是程序运行过程中g_count的值可能被改写比如g_count。Flash不能像内存那样随时改写所以运行时必须把这个变量拷贝到SRAM里。这就是.data段的特殊之处在Flash中保存着初值100在SRAM中保存着变量的副本启动时从Flash拷贝过来之后可以随意改写启动代码是上电后最先运行的一小段汇编程序它负责在调用main函数之前把Flash中的初值拷贝到SRAM中。②无初值的数据.bss段有的数据没有初始值运行时才直接分配在可写存储器里这部分数据叫.bss段。需要在系统上电后、正式程序跑起来之前统一清零以保持程序运行的确定性。BSSBlock Started by Symbol最早是IBM 704汇编伪指令意为“从此开始保留一块未初始化存储区”。③只读数据.rodata段有的数据不仅有初值而且程序运行过程中不需要修改——比如字符串常量Hello、用关键字const修饰的全局变量。这类数据直接放在Flash中.rodata段read only data不需要拷贝到SRAM运行时直接从Flash读取既省SRAM空间又保证数据不会被意外修改。const char *s Hello; // Hello字符串放在.rodata段 const int version 0x0100; // version放在.rodata段.text和.rodata是两个独立的段它们共同存放在Flash区域中。向量表0x00000000开始本身也属于.text段的一部分。.text段包含三部分向量表 程序指令 其他只读数据。而.rodata段字符串常量、const全局变量紧跟在.text后面。两者合在一起就是最终烧录到Flash里的完整程序内容——在嵌入式开发中这份内容常被称为程序镜像可以理解为“程序在Flash中的完整副本”。1.3两种特殊情况按道理上述两种方式能适用所有情况。但有两种情况需要特别考虑事先不知道有多少数据要处理——程序执行时才知道无法提前安排怎么办要处理的数据太多存储器空间不够——怎么办针对第一种情况堆.heap在可读写的存储空间划出一段连续的地址空间。需要数据空间时就到这个地方占一些地方用完后再释放掉以备下一次重用。取名为堆.heap英文原意是杂乱的一堆、堆积物。堆通常用于运行时才知道大小的数据缓冲区如网络数据包、用户输入。在C语言中通过malloc函数来申请堆空间用free函数来释放。针对第二种情况栈设想一下我们要调用一个函数需要传给这个函数一些输入函数执行完后还要传回一个返回值同时函数本身还需要额外的数据空间来倒腾。这些都需要存储空间。比方说下面这个函数int MyFunction_A(int A, int B) { int C; if (A B) { C A B; } else { C A - B; } return C; }处理输入A和B之外还需要一个额外的变量C来临时保留结果。这个变量除了本函数关心外还有谁关心没人关心。所以我们叫它局部变量。函数执行完后A、B、C都没用了。如果程序有成百上千个函数每个函数都有自己的局部变量和参数全部安排在.data或.bss段里内存远远不够。更好的方案是用完了就释放让同一块空间被反复使用。栈的设计方案划一块连续空间K调用函数之前把参数和局部变量按顺序往里放0: A 1: B 2: C函数执行完后这块空间就释放了。下次再调用函数时又从0开始往里放——空间得到重复利用。嵌套调用的情况更复杂一些main调用MyFunction_AMyFunction_A又调用MyFunction_B0: A ← MyFunction_A的参数 1: B ← MyFunction_A的参数 2: C ← MyFunction_A的局部变量 3: D ← MyFunction_B的参数 4: E ← MyFunction_B的参数 5: F ← MyFunction_B的参数 6: G ← MyFunction_B的局部变量MyFunction_B执行完后3~6释放但0~2还要继续用到MyFunction_A结束。所以我们需要一个指针指向当前正在用的空间的边界——这个指针叫SPStack Pointer栈指针。stack英文原意是一摞盘子——先放的压在底下后放的摞在上面用的时候从上面一个个拿后进先出。往K空间放数 →压栈/进栈SP减小从K空间移除数据 →弹栈/出栈SP增大后调用的函数先返回后进先出。上述栈的工作方式示意图如下CPU专门设计了栈操作指令PUSH和POPPUSH {r1-r10} ; 将多个寄存器压入栈 POP {r1-r10} ; 从栈弹出到多个寄存器这款单片机用寄存器R13充当SP。上电后CPU从向量表地址0处读取SP的初始值——这也是我们前两天在地址0放0x20001000的原因。为什么是0x20001000这款芯片的SRAM范围是0x20000000~0x20001FFF8KB栈顶设在0x20001000留出0x20000000~0x20000FFF给.data、.bss和堆使用。栈之所以能工作的根本原因CPU在执行程序时在时间上是串行的——每一件事都有确定的先后顺序进和退的顺序是确定的栈才能工作。如果栈空间太小或者局部变量太多超过边界就会非法改写其他数据空间——这叫栈溢出。程序会异常、跑飞。// 场景一无限递归每次调用都消耗栈空间 void func(void) { func(); // 永不返回栈逐渐耗尽 } // 场景二过大的局部数组多个同类型变量打包成一组叫数组 void test(void) { char buf[10000]; // 定义一个能存10000个char的数组全部放在栈上栈只有8000字节 → 直接溢出 }数组就是连续存放的多个相同类型数据通过下标从0开始访问每一个元素。例如下面定义了一个char类型的数组一共有10000个char类型元素编号从0到9999。char buf[10000];buf[0] 3;buf[9999] 1;1.4内存划分总览常规的内存划分方案如下图所示将代码区.text和只读数据区.rodata放在Flash中本教程的芯片地址0x00000000–0x00007FFF将数据区.data/.bss/堆/栈放入SRAM中地址0x20000000–0x20001FFF。这是一个例子但其基本原则适用于所有嵌入式系统。二、C语言各要素存放在哪里还有一种特殊的变量——用static修饰的局部变量它的生命周期是整个程序存放位置取决于是否有初值。C语言要素存放位置何时确定生命周期函数代码.textFlash编译时整个程序字符串常量、const全局变量.rodataFlash编译时整个程序已初始化的全局变量.dataSRAM初值在Flash编译时整个程序已初始化的静态局部变量.dataSRAM初值在Flash编译时整个程序未初始化的全局变量.bssSRAM编译时整个程序未初始化的静态局部变量.bssSRAM编译时整个程序局部变量栈SRAM编译时大小函数执行期间静态局部变量.data或.bssSRAM编译时整个程序malloc分配的内存堆SRAM运行时malloc到free示例代码与去向对照下面这段示例代码涵盖了上述所有情况对照着看会更直观// 函数代码 → .text段 int g_init 520; // 全局变量有初值 → .data段 int g_uninit; // 全局变量无初值 → .bss段 const int version 100; // const全局变量 → .rodata段 void func(void) { int a; // 局部变量 → 栈 static int b 10; // static局部变量有初值 → .data段 static int c; // static局部变量无初值 → .bss段 const char *s Hello; // 指针s指向字符串常量Hello的首地址字符串本身在.rodata段 }三、这些数据怎么安排到内存里3.1编译时——分类打包编译器编译时根据代码中不同的元素将它们放入对应的“段”中C代码中的内容编译器放入的段函数代码.text字符串常量、const变量.rodata已初始化的全局变量.data记录初值和变量所占的字节数未初始化的全局变量.bss只记录变量所占的字节数不占文件空间注编译器只负责“分类打包”——把代码和数据按类型放入不同的段。至于这些段最终放在内存的哪个地址由链接器决定。3.2链接时——分配地址链接器拿到多个编译器生成的目标文件.o后按照链接脚本Linker Script的指示将所有段分配到具体的内存地址。链接脚本.ld就是用来描述“所有东西怎么放”的地图。MEMORY { FLASH (rx) : ORIGIN 0x00000000, LENGTH 32K SRAM (rwx) : ORIGIN 0x20000000, LENGTH 8K } SECTIONS { .text : { *(.text) } FLASH .rodata : { *(.rodata) } FLASH .data : { *(.data) } SRAM AT FLASH .bss : { *(.bss) } SRAM }链接脚本语法逐行解释语法含义MEMORY { ... }定义芯片的内存区域FLASH (rx) : ORIGIN 0x00000000, LENGTH 32KFLASH只读可执行起始地址0x00000000长度32KBSRAM (rwx) : ORIGIN 0x20000000, LENGTH 8KSRAM可读写可执行起始地址0x20000000长度8KBSECTIONS{ ... }定义芯片的各个Section.text : { *(.text) } FLASH所有.text段合并放入FLASH.rodata : { *(.rodata) } FLASH所有.rodata段合并放入FLASH.data : { *(.data) } SRAM AT FLASH运行时在SRAM初值存在FLASH启动时拷贝.bss : { *(.bss) } SRAM所有.bss段合并放入SRAM启动时清零第3天我们用的链接命令是ld -Ttext0x00000000这只是一个简化版——只指定了.text的起始地址。正式项目中链接脚本会描述所有段的完整布局。3.3运行时处理程序运行时启动代码startup.s在调用main之前完成数据区的初始化设置堆栈指针SP从向量表的0x00000000读取初始值拷贝.data段将Flash中的初值拷贝到SRAM中的.data区域清零.bss段将SRAM中的.bss区域全部清零这就是为什么C语言的全局变量在程序开始时就有正确的初值.data而未初始化的全局变量自动为0.bss。四、为什么这些分区方法今天仍然重要这套“指令区数据区”的分离设计最早源于UNIX系统的进程内存模型。ARM公司将其引入嵌入式领域后形成了今天通用的.text/.data/.bss分区标准。今天的计算机虽然内存大了很多但底层思想依然没变PC/服务器程序在硬盘指令区运行时加载到内存数据区智能手机App在Flash指令区运行时加载到RAM数据区嵌入式系统固件在Flash指令区变量在SRAM数据区你正在学习的这套分区方法不是“过时的古董”而是今天仍在使用的工业标准。理解这些分区也是理解操作系统、动态链接等高级话题的基础。五、今日核心收获概念一句话解释向量表程序启动的“目录”告诉CPU堆栈和程序在哪程序跑飞CPU执行到非法指令程序崩溃死循环程序末尾放跳转指令防止跑飞.rodata只读数据字符串常量、const变量存放在Flash中.data有初值的全局变量初值在Flash运行时在SRAM.bss无初值的全局变量启动时清零栈局部变量函数执行时分配返回时释放LIFO堆动态分配的内存运行时才确定大小malloc/free管理静态局部变量函数内用static修饰生命周期为整个程序放在.data或.bss链接脚本描述各段在内存中的布局地图明天预告站在巨人的肩膀上——使用标准库开发用更简洁的方式操作硬件。