嵌入式common

startup.c是做什么的?

startup.c通常是指嵌入式系统中的启动代码,它的主要作用是初始化系统并启动应用程序

在启动过程中,会进行一些初始化操作,如设置堆栈、复制数据段、清零BSS段、设置中断向量表等。此外,startup.c还会加载应用程序,将控制转移到应用程序的入口处,开始执行应用程序的功能。

因此,startup.c非常重要,涉及到整个系统的启动和运行。不同的嵌入式平台和芯片厂商会有不同的startup.c代码,但它们的基本逻辑和流程是类似的。

启动文件为什么会比main先启动?

这个问题总结起来就是CPU启动时根据外部boot引脚状态从(起始地址+0x4)地址加载数据,赋值给PC(执行程序寄存器,r15),而编译器将startup.s文件中的ResetHander函数的地址编译到(起始地址+0x4)内部,所以startup.s被先调用。

详细的解释需要参考Cortex-M系列的知识,下面来详细说明。首先参考STM32F1x的手册2.4启动配置这句话

那么就可以理解从0x04地址执行代码,这是由芯片设计时规定的,至于如何实现就是集成电路相关的知识了,这里不在说明,那么就是第二个问题,ResetHander如何被编译到0x04偏移地址的,这就涉及中断向量表的知识,下面来自于<Cortex-M3权威指南>关于异常的说明章节。

从上面可以看出,在0x04偏移地址中定位的就是复位向量,而在代码startup.s中,就可以看到

1
2
3
4
5
__Vectors       DCD     __initial_sp               ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
...
__Vectors_End

也就是说由内核特性和STM芯片的定义,上电时的入口地址一定是Reset_Handler,而Reset_Handler就放置在在启动文件startup.s中,这就是启动文件startup.s比main先调用的总的原因.

简略回答: startup比main更先调用,确实和编译器设置有关,但更多的是程序本来如此。

详细回答:背景知识程序的生成步骤一般分成四步 preprocess(.c) -> compile(.S) -> assemble (.o) -> link,具体可阅读 The Four Stages of Compiling a C Program 。

Linker Script的基本知识: Simple Linker Script Example具体例子下面的截图是Atmel SAMD10D14AM的souce code,第一张是 Linker Script (samd10d14am_flash.ld),第二张是 Startup file (startup_samd10.c)。接下来解释highlight部分的含义。

Linker Script Vector Table

Vectors就是所谓Exception Table。对于ARM处理器Cortex M系列,Table的第一条是Stack Pointer的地址,第二条就是最重要的ISR for reset的地址

左边的Linker Script指定把 section(“.vector”) 放在生成程序的.text代码段,一般也就是flash的起始地址。从keyword MEMORY的定义来看,flash rom的起始地址是0x00000000,大小是0x00000800 (16K),刚好符合官方文档的描述 https://web.eecs.umich.edu/~prabal/teaching/resources/eecs373/ARM_Cortex_AppNote179.pdf

1.1 Nested Vectored Interrupt Controller (NVIC)The initial stack pointer and the address of the reset handler must be located at 0x0 and 0x4 respectively. These values are then loaded into the appropriate CPU registers at reset.

Reset_Handler()这个ISR就是CPU power on或者reset后的第一个routine,里面会先做一些简单初始化,比如Intialize the C libray(这样我们才能在后头的代码里直接用strcpy()等常见函数),最后跳往我们最熟悉的main()。

其余的ISR都是optional,可以留白,会自动被Dummy_Handler填充,因为keyword weak。结论启动文件startup比main()先启动,是因为程序的启动顺序就是这样的,**CPU power on / reset -> Reset_Handler() -> main()**。

BSS段是做什么的?

BSS段(Block Started by Symbol)是指程序中未被初始化的全局变量和静态变量所占用的一段内存空间。在程序加载运行时,操作系统会自动分配一块内存空间用于存放BSS段。

BSS段中的变量被默认初始化为0,因此,在程序编写中不需要为它们显式地赋初值。BSS段对于节省内存空间和程序运行效率都有很大的帮助。这是因为,程序中的未初始化变量会在BSS段中被统一地设置为0,而不需要在程序中存储这些变量的具体值。这样,在程序执行过程中,BSS段所占空间可以被多次重用并动态分配,从而使得程序的内存利用率更高

RAM中的各个段

RAM内存

RAM内存包括:

  • 代码段(text)
  • 数据段(data)
  • bss段
  • 堆栈段(head stack)

编译器编译结果分析

编译结果有代码段(text)、数据段(data)、bss段。

  • 代码段(.text)是可执行指令的集合
  • 数据段 (.data) 表示已经初始化不为0的存放在静态区的数据(全局 or 静态)
  • .bss段 表示未初始化的或为0的存放在静态区的数据(全局 or 静态)。

从可执行程序的角度来说,如果一个数据未被初始化,就不需要为其分配空间,所以.data 和.bss 的区别就是: .bss 并不占用可执行文件的大小,仅仅记录需要用多少空间来存储这些未初始化的数据,而不分配实际空间

所以代码段(text)、数据段(data)这两者相加共同构成可执行文件的大小,dec也就是文件大小(hex也是文件大小,只不过是16进制表示的)。

堆栈

堆 heap

堆保存函数内部动态分配(malloc 或 new)的内存,是另外一种用来保存程序信息的数据结构。 堆是先进先出(FIFO)数据结构。堆的地址空间是向上增加,即当堆上保存的数据越多,堆的地址越高。动态内存分配。 注意:堆内存需要程序员手动管理内存,通常适用于较大的内存分配,如频繁的分配较小的内存,容易导致内存碎片化

栈 stack

栈保存函数的局部变量(不包括 static 修饰的变量),参数以及返回值。是一种后进先出(LIFO)的数据结构。 在调用函数或过程后,系统会清除栈上保存的局部变量、函数调用信息及其他信息。 栈的另外一个重要特征是,它的地址空间 向下减少,即当栈上保存的数据越多,栈的地址越低。静态内存分配

注意:由于栈的空间通常比较小,一般 linux 程序只有几 M,故局部变量,函数入参应该避免出现超大栈内存使用,比如超大结构体,数组等,避免出现 stack overflow。

总结

段名 存储属性 内存分配
代码段
.text
存放可执行程序的指令,存储态和运行态都有 静态
数据段
.data
存放已初始化(非零初始化的全局变量和静态局部变量)的数据,存储态和运行态都有 静态
bss段
.bss
存放未初始化(未初始化或者0初始化的全局变量和静态局部变量)的数据,存储态和运行态都有 静态

heap
动态分配内存,需要通过malloc手动申请,free手动释放,适合大块内存。容易造成内存泄漏和内存碎片。运行态才有 动态

stack
存放函数局部变量和参数以及返回值,函数返回后,由操作系统立即回收。栈空间不大,使用不当容易栈溢出。运行态才有 静态

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
using namespace std;

int a = 0;//初始化的全局变量:保存在数据段
char *p1;//未初始化的全局变量:保存在BSS段

int main()
{
int b;//未初始化的局部变量:保存在栈上
char s[] = "abc";
/* "abc"为字符串常量保存在常量区;数组保存在栈上,
并将常量区的"abc\0"复制到该数组中。这个数组可以随意修改而不会有任何隐患,
而"123"这个字符串依然会保留在静态区中。
*/

char *p2;//p2保存在栈上
char *p3 = "123456";//p3保存在栈上,"123456\0"保存在data区的read-only部分
//注意:如果令p3[1] = 9; 则程序崩溃,指针可以访问但不允许改变常量区的内容
/* 声明了一个指针p3并指向"123456\0"在静态区中的地址,事实上,p3应该声明为
char const *,以免可以通过p3[i]='\n'这一类的语法去修改这个字符串的内容。如果这样做了,在支持“常量区”的系统中可能会导致异常,在“合并相同字符串”的编译方法下会导致其它地方的字符串常量古怪地发生变化。
*/

static int c = 0;//初始化的静态局部变量:保存在数据区(数据段)

p1 = (char *)malloc(sizeof(char) * 10);//分配的10字节区域保存在堆上
p2 = (char *)malloc(sizeof(char) * 20);//分配的20字节区域保存在堆上

strcpy(p1, "123456");
//"123456\0"放在常量区,编译器可能会将它与p3所指向"123456"优化成一个地方

return 0;
}
作者

Gavin

发布于

2023-03-21

更新于

2023-03-21

许可协议

CC BY-NC-SA 4.0

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×