编译后,从C代码将不同类型的数据放入不同的内存段。即:.text
,.data
,.bss
,栈和堆。我只想知道这些段中的每一个在微控制器存储器中的位置。也就是说,给定存储器类型为RAM,NVRAM,ROM,EEPROM,FLASH等,哪些数据将进入哪种类型的存储器。
我在这里找到了类似问题的答案,但是他们无法解释每种不同内存类型的内容。
任何帮助都将受到高度赞赏。提前致谢!
编译后,从C代码将不同类型的数据放入不同的内存段。即:.text
,.data
,.bss
,栈和堆。我只想知道这些段中的每一个在微控制器存储器中的位置。也就是说,给定存储器类型为RAM,NVRAM,ROM,EEPROM,FLASH等,哪些数据将进入哪种类型的存储器。
我在这里找到了类似问题的答案,但是他们无法解释每种不同内存类型的内容。
任何帮助都将受到高度赞赏。提前致谢!
Answers:
.text段包含实际代码,并被编程到微控制器的闪存中。当有多个不连续的闪存块时,可能会有多个文本段;例如,起始向量和中断向量位于存储器的顶部,并且代码从0开始;或引导程序和主程序的单独部分。
可以在函数或过程外部分配三种类型的数据。第一个是未初始化的数据(历史上称为.bss,还包括0个初始化数据),第二个是未初始化的数据(非bss)或.data。历史上,“ bss”这个名字源于大约60年前在汇编器中使用的“以符号开头的块”。这两个区域都位于RAM中。
在编译程序时,会将变量分配给这两个常规区域之一。在链接阶段,所有数据项将一起收集。所有需要初始化的变量将保留一部分程序存储器以保留初始值,并且在调用main()之前,通常将通过一个称为crt0的模块来初始化变量。通过相同的启动代码将bss节初始化为全零。
在一些微控制器中,有较短的指令允许访问RAM的第一页(前256个位置,有时称为第0页)。这些处理器的编译器可以保留关键字,例如near
指定要放置在此处的变量。同样,也有一些微控制器只能通过指针寄存器(需要额外的指令)来引用某些区域,并指定了此类变量far
。最后,某些处理器可以一点一点地寻址内存的一部分,编译器将有一种方法来指定该部分(例如关键字bit
)。
因此,可能还有其他段,如.nearbss和.neardata等,在这些段中收集了这些变量。
函数或过程外部的第三种数据类型类似于初始化变量,只是它是只读的,不能被程序修改。在C语言中,这些变量使用const
关键字表示。它们通常作为程序闪存的一部分存储。有时,它们被标识为.rodata(只读数据)段的一部分。在使用哈佛架构的微控制器上,编译器必须使用特殊指令来访问这些变量。
堆栈和堆都放在RAM中。根据处理器的体系结构,堆栈可能会变大或变小。如果长大,它将被放置在RAM的底部。如果它变小,它将被放置在RAM的末尾。堆将使用未分配给变量的剩余RAM,并向堆栈的相反方向增长。堆栈和堆的最大大小通常可以指定为链接器参数。
放置在堆栈中的变量是在函数或过程中定义的,没有关键字的任何变量static
。它们曾经被称为自动变量(auto
关键字),但是不需要该关键字。从历史上讲,auto
之所以存在是因为它是C之前的B语言的一部分,因此需要它。功能参数也放置在堆栈上。
这是RAM的典型布局(假设没有特殊的第0页部分):
在闪存问世之前,EEPROM(电可擦可编程只读存储器)用于存储程序和const数据(.text和.rodata段)。现在,只有少量(例如2KB至8KB字节)的EEPROM可用(如果有的话),它通常用于存储配置数据或需要在断电加电时保留的其他少量数据周期。这些变量在程序中未声明为变量,而是使用微控制器中的特殊寄存器写入。EEPROM也可以在单独的芯片中实现,并可以通过SPI或I²C总线进行访问。
ROM本质上与Flash相同,区别在于ROM是在工厂编程的(用户不能编程)。它仅用于非常大容量的设备。
NVRAM(非易失性RAM)是EEPROM的替代产品,通常实现为外部IC。如果常规RAM由电池备份,则可以认为它是非易失性的。在这种情况下,不需要特殊的访问方法。
尽管可以将数据保存到闪存,但闪存的擦除/编程周期数量有限(1000到10,000个),因此并不是真正为此设计的。它还要求立即擦除内存块,因此仅更新几个字节是不方便的。它用于代码和只读变量。
EEPROM在擦除/编程周期上有更高的限制(100,000至1,000,000),因此在此方面要好得多。如果微控制器上有可用的EEPROM,并且EEPROM足够大,那么您就可以在其中保存非易失性数据。但是,在写入之前,您还必须先擦除块(通常为4KB)。
如果没有EEPROM或它太小,则需要一个外部芯片。一个32KB的EEPROM 只有66¢,可以擦除/写入1,000,000次。具有相同擦除/编程操作次数的NVRAM昂贵得多(x10)。NVRAM的读取速度通常比EEPROM快,但写入速度较慢。它们可以一次写入一个字节,也可以成块写入。
更好的替代方法是FRAM(铁电RAM),它具有无限的写入周期(100万亿),并且没有写入延迟。它与NVRAM的价格大致相同,32KB约为5美元。
普通嵌入式系统:
Segment Memory Contents
.data RAM Explicitly initialized variables with static storage duration
.bss RAM Zero-initialized variables with static storage duration
.stack RAM Local variables and function call parameters
.heap RAM Dynamically allocated variables (usually not used in embedded systems)
.rodata ROM const variables with static storage duration. String literals.
.text ROM The program. Integer constants. Initializer lists.
另外,通常有用于启动代码和中断向量的单独的闪存段。
说明:
如果变量声明为static
或驻留在文件范围内(有时简称为“全局”),则该变量具有静态存储持续时间。C有一条规则,指出程序员没有明确初始化的所有静态存储持续时间变量都必须初始化为零。
每个隐式或显式初始化为零的静态存储持续时间变量都以结尾.bss
。而那些显式初始化为非零值的结果以结尾.data
。
例子:
static int a; // .bss
static int b = 0; // .bss
int c; // .bss
static int d = 1; // .data
int e = 1; // .data
void func (void)
{
static int x; // .bss
static int y = 0; // .bss
static int z = 1; // .data
static int* ptr = NULL; // .bss
}
请记住,嵌入式系统最常见的非标准设置是“最小启动”,这意味着程序将跳过具有静态存储持续时间的对象的所有初始化。因此,最好不要编写依赖于此类变量的初始化值的程序,而应在首次使用它们之前在“运行时”中设置它们。
其他细分的示例:
const int a = 0; // .rodata
const int b; // .rodata (nonsense code but C allows it, unlike C++)
static const int c = 0; // .rodata
static const int d = 1; // .rodata
void func (int param) // .stack
{
int e; // .stack
int f=0; // .stack
int g=1; // .stack
const int h=param; // .stack
static const int i=1; // .rodata, static storage duration
char* ptr; // ptr goes to .stack
ptr = malloc(1); // pointed-at memory goes to .heap
}
在优化过程中,可以传递到堆栈中的变量通常可能最终存储在CPU寄存器中。根据经验,任何不带地址的变量都可以放置在CPU寄存器中。
请注意,指针比其他变量稍微复杂一点,因为它们允许两种不同的const
,具体取决于所指向的数据应该是只读的,还是指针本身应该是只读的。了解它们之间的差异非常重要,这样当您希望指针处于闪存状态时,指针就不会意外地进入RAM。
int* j=0; // .bss
const int* k=0; // .bss, non-const pointer to const data
int* const l=0; // .rodata, const pointer to non-const data
const int* const m=0; // .rodata, const pointer to const data
void (*fptr1)(void); // .bss
void (*const fptr2)(void); // .rodata
void (const* fptr3)(void); // invalid, doesn't make sense since functions can't be modified
对于整数常量,初始化程序列表,字符串文字等,根据编译器的不同,它们可能以.text或.rodata结尾。最终,它们可能是:
#define n 0 // .text
int o = 5; // 5 goes to .text (part of the instruction)
int p[] = {1,2,3}; // {1,2,3} goes to .text
char q[] = "hello"; // "hello" goes to .rodata
.data
通常在闪存中具有一个所谓的加载地址,用于存储初始值,而在RAM中具有一个所谓的虚拟地址(在微控制器中不是真正的虚拟地址),在执行期间将变量存储在其中。在main
开始之前,会将初始值从加载地址复制到虚拟地址。您不需要存储零,因此.bss
不需要存储其初始值。sourceware.org/binutils/docs/ld/…– starblue 2013