当前位置:首页 » 《随便一记》 » 正文

嵌入式 C 编程必备(1):清晰区分数据段、代码段、堆、栈段和 BSS 段

15 人参与  2024年12月23日 16:01  分类 : 《随便一记》  评论

点击全文阅读


目录

一、数据段(Data Segment)

1.1. 定义与特性

1.2. 代码示例

二、代码段(Code Segment/Text Segment)

2.1. 定义与特性

2.2. 代码示例

三、堆栈段(Stack Segment)

3.1. 定义与特性

3.2. 代码示例

四、堆段(Heap Segment) 

4.1. 堆段的基本概念

4.2. 堆段的特点

4.3. 堆段的应用场景

4.4. 堆段的管理与优化

4.5. 代码示例

五、BSS段(Block Started by Symbol Segment)

5.1. 定义与特性

5.2. 代码示例

六、总结


在嵌入式开发中,进程在运行时会占用一定数量的内存空间,这些空间不仅用来存放从磁盘载入的程序代码,还用于存储各种运行时所需的数据。进程内存空间包含五种数据区:代码区存储只读指令,数据区存储已初始化全局/静态变量,BSS段存储未初始化全局/静态变量(程序执行前自动初始化为零),堆区用于动态内存分配,栈区存储局部变量、调用参数及返回地址,由操作系统自动管理,各数据区共同支持程序执行。

一、数据段(Data Segment)

1.1. 定义与特性

数据段是程序中用于存放已初始化的全局变量和静态变量的一块内存区域。这些变量在程序编译时就已经确定了其大小和初始值,并在程序加载到内存时,这些初始值会被复制到数据段中相应的位置。数据段的内容在程序运行期间通常保持不变,除非程序通过代码显式地修改这些变量的值。

数据段属于静态内存分配,意味着在程序运行之前,操作系统或编译器就已经为数据段分配了足够的内存空间。与堆栈段和堆段不同,数据段的内存分配和释放是由编译器在编译时确定的,而不是在程序运行时动态分配的。

全局变量:全局变量是在函数外部声明的变量,它们在整个程序中都可见。已初始化的全局变量会被存储在数据段中。

静态变量:静态变量包括静态全局变量和静态局部变量。静态全局变量与全局变量类似,但它们的链接性(作用域)仅限于定义它们的文件。静态局部变量是在函数内部声明的,但使用static关键字修饰的变量。它们的生命周期贯穿整个程序运行期间,但作用域仅限于声明它们的函数。已初始化的静态变量也会被存储在数据段中。

初始化:数据段中的变量在程序加载到内存时会被初始化为编译时确定的值。如果变量没有显式初始化,则它们的初始值是未定义的(对于基本数据类型,通常是随机值)。然而,在BSS段中,未初始化的全局变量和静态变量会被自动初始化为0(或NULL,对于指针变量)。

1.2. 代码示例

以下是一个简单的C程序示例,展示了数据段中全局变量和静态变量的使用:

#include <stdio.h>// 已初始化的全局变量,存储在数据段中int global_var = 42;// 未初始化的全局变量,实际上存储在BSS段中,但这里为了说明数据段的概念,我们一并提及int uninit_global_var; // 未初始化,但在BSS段中会被初始化为0void function() {    // 已初始化的静态局部变量,存储在数据段中(尽管是局部变量,但由于是static,其生命周期贯穿整个程序)    static int static_local_var = 100;        // 未初始化的静态局部变量,实际上在第一次使用时会被初始化为0(但这里我们显式初始化了)    static int uninit_static_local_var = 0; // 实际上,由于初始化了,它也存储在数据段中        printf("Global variable: %d\n", global_var);    printf("Static local variable: %d\n", static_local_var);        // 修改静态局部变量的值    static_local_var += 10;        // 修改全局变量的值    global_var += 5;}int main() {    printf("Uninitialized global variable (initially 0): %d\n", uninit_global_var);        function();        printf("After function call:\n");    printf("Global variable: %d\n", global_var);    // 注意:我们不能直接访问uninit_static_local_var,因为它在function函数的作用域内        return 0;}

运行结果:

在这个示例中,global_varstatic_local_var都是已初始化的变量,它们会被存储在数据段中。而uninit_global_var虽然是一个全局变量,但它是未初始化的,实际上会被存储在BSS段中(在程序加载时会被初始化为0)。uninit_static_local_var虽然是一个静态局部变量且未初始化,但在这个示例中我们显式地初始化了它,所以它也会被存储在数据段中。然而,如果它没有显式初始化,那么它会在第一次使用时被初始化为0(这通常是由编译器在编译时处理的,而不是在运行时)。

请注意,BSS段和数据段在物理上可能是连续的,或者它们可能只是内存布局中的逻辑划分。在实际的内存布局中,它们可能并不总是严格区分的。此外,不同的编译器和操作系统可能会有不同的内存布局和初始化行为。

二、代码段(Code Segment/Text Segment)

2.1. 定义与特性

代码段是程序中用于存放执行代码的内存区域。这个区域在程序运行前就已经确定了其大小,并且通常被设置为只读,以防止程序意外地修改自己的指令。然而,在某些特定的架构或操作系统中,代码段可能被设置为可写,但这通常是不推荐的做法,因为它可能引发安全漏洞。

代码段包含了程序的执行指令,这些指令是编译器从源代码编译而来的机器码。此外,代码段还可能包含一些只读的数据,如字符串常量。这些字符串常量在程序中以字面量的形式出现,并被编译器放置在代码段中,因为它们在程序运行期间不会被修改。

执行指令:代码段中的主要内容是程序的执行指令。这些指令按照程序的逻辑顺序排列,当程序运行时,CPU会按照这些指令的顺序执行。

只读属性:代码段通常被设置为只读,这是为了保护程序的完整性。如果代码段可以被写入,那么恶意软件可能会通过修改代码段中的指令来篡改程序的行为。因此,将代码段设置为只读是一种安全措施。

字符串常量:在C和C++等语言中,字符串常量是以字面量的形式出现的,如"Hello, World!"。这些字符串常量在编译时被放置在代码段中,并且被设置为只读。这意味着不能通过指针来修改这些字符串的内容。

代码段的位置:在程序的内存布局中,代码段通常位于较低的地址空间。这是因为代码段的大小在编译时就已经确定,并且不会改变,因此可以将其放置在固定的位置。

2.2. 代码示例

以下是一个简单的C程序示例,展示了代码段中指令和字符串常量的使用:

#include <stdio.h>void print_message() {    printf("Hello, World!\n");}int main() {    print_message();    return 0;}

在这个示例中,print_message函数和main函数的指令都被存储在代码段中。此外,字符串常量"Hello, World!\n"也被放置在代码段中,并且被设置为只读。当print_message函数被调用时,CPU会跳转到该函数在代码段中的起始地址,并按照指令的顺序执行。

需要注意的是,虽然我们在代码中直接引用了字符串常量,但我们不能通过指针来修改它的内容。例如,以下代码是未定义行为,并且可能会导致程序崩溃:

char *str = "Hello, World!\n";str[0] = 'h'; // 未定义行为,尝试修改只读内存

在大多数现代操作系统和编译器中,尝试修改代码段或其中的字符串常量会导致程序崩溃或安全漏洞。因此,我们应该始终遵守这些规则,不要尝试修改只读内存区域的内容。 

三、栈段(Stack Segment)

3.1. 定义与特性

栈段是程序中一个非常重要的内存区域,用于存放函数调用过程中产生的各种数据,包括局部变量、函数参数、函数返回地址以及临时数据。栈段遵循后进先出(LIFO, Last In First Out)的原则,这意味着最后压入栈的数据会最先被弹出。

以下是栈段的主要特性:

后进先出原则:这是栈操作的核心原则。当一个新的函数调用发生时,相关的数据会被压入栈中;当函数返回时,这些数据会按照相反的顺序被弹出栈。

栈的大小限制:栈的大小通常由操作系统在程序启动时预先分配,并有一个固定的上限。例如,在Windows操作系统中,默认的栈大小可能是2MB或更大,但这个值可以通过链接器选项或系统调用进行调整。

自动管理:栈的分配和释放是由编译器自动管理的。程序员不需要显式地申请或释放栈空间,这大大简化了内存管理的工作。

函数调用支持:栈段是函数调用机制的核心。当函数被调用时,函数的返回地址、参数以及局部变量都会被压入栈中。当函数返回时,这些信息会从栈中弹出,并恢复调用前的状态。

局部变量:在函数内部声明的变量被称为局部变量。这些变量在函数被调用时分配在栈上,并在函数返回时释放。

函数参数:函数调用的参数通常也被存储在栈上。当函数被调用时,参数的值会被压入栈中,并在函数内部通过指针或寄存器访问。

函数返回地址:当函数被调用时,当前执行的指令的下一条指令的地址(即函数返回后应该继续执行的地址)会被压入栈中。这个地址在函数返回时被弹出,并作为下一条执行的指令。

临时数据:在函数执行过程中,可能需要存储一些临时数据。这些数据也会被分配在栈上,并在不再需要时被释放。

3.2. 代码示例

以下是一个简单的C程序示例,展示了栈段在函数调用过程中的作用:

#include <stdio.h> void functionB(int b) {    int local_var_B = b + 10; // 局部变量,存储在栈上    printf("Function B: local_var_B = %d\n", local_var_B);} void functionA(int a) {    int local_var_A = a * 2; // 局部变量,存储在栈上    functionB(local_var_A); // 调用函数B,函数B的参数和局部变量也会存储在栈上    printf("Function A: local_var_A = %d\n", local_var_A);} int main() {    int main_var = 5; // 局部变量,存储在栈上    functionA(main_var); // 调用函数A,函数A的参数和局部变量也会存储在栈上    printf("Main: main_var = %d\n", main_var);    return 0;}

运行结果: 

在这个示例中,main函数调用了functionA,而functionA又调用了functionB。每次函数调用时,相关的局部变量、参数和返回地址都会被压入栈中。当函数返回时,这些数据会从栈中弹出,并恢复调用前的状态。这个过程是自动由编译器和操作系统管理的,程序员不需要显式地处理栈的分配和释放。

需要注意的是,虽然栈提供了方便的内存管理机制,但如果不正确地使用栈(如递归调用过深导致栈溢出),可能会导致程序崩溃或安全漏洞。因此,程序员应该谨慎地管理栈的使用,确保不会超出栈的大小限制。

四、堆段(Heap Segment) 

堆段(Heap)在程序的内存布局中扮演着至关重要的角色,特别是在需要动态内存分配的场景中。

4.1. 堆段的基本概念

堆段是程序内存布局中的一个区域,用于存储大型数据结构和具有动态生命周期的对象。与堆栈段不同,堆段允许在程序执行期间随时分配和释放内存,这使得它成为处理动态内存需求的理想选择。

4.2. 堆段的特点

动态内存分配:堆段允许在程序运行时动态地分配和释放内存。这意味着开发者可以根据需要在程序执行期间随时申请或释放内存空间。灵活性:堆段提供了一个灵活的区域来存储各种大小和类型的数据结构。这使得它成为处理复杂数据结构(如链表、树、图等)和大型对象的理想选择。手动管理:与堆栈段不同,堆段的内存管理需要由开发者手动进行。这包括分配内存(如使用mallocnew等函数)和释放内存(如使用freedelete等函数)。因此,开发者需要谨慎地管理堆内存,以避免内存泄漏和野指针等问题。内存碎片:由于堆段允许频繁地分配和释放内存,这可能导致内存碎片的产生。内存碎片是指内存中存在的一些小块空闲区域,这些区域由于太小而无法被有效利用。内存碎片会降低内存的利用率,并可能导致内存分配失败。

4.3. 堆段的应用场景

大型数据结构:当需要存储大型数据结构(如链表、树、图等)时,堆段是一个理想的选择。这些数据结构通常具有动态的大小和复杂的结构,需要灵活的内存分配策略来支持。动态对象:在面向对象编程中,堆段常用于存储具有动态生命周期的对象。这些对象可能在程序的不同部分之间传递和共享,因此需要灵活的内存管理策略来支持。临时数据存储:在某些情况下,程序可能需要临时存储一些数据。这些数据可能具有不确定的大小和生命周期,因此堆段是一个合适的选择。例如,在处理用户输入或文件I/O时,可能需要动态地分配内存来存储临时数据。

4.4. 堆段的管理与优化

内存分配与释放:开发者需要谨慎地管理堆内存,确保在不再需要时及时释放内存。这可以通过使用智能指针(如C++中的std::unique_ptrstd::shared_ptr)或垃圾回收机制(如Java和Python中的垃圾回收器)来实现。内存碎片处理:为了减少内存碎片的产生,开发者可以采取一些策略来优化内存分配。例如,可以使用内存池来预先分配一块连续的内存区域,并在需要时从中分配小块内存。此外,还可以使用紧凑算法来重新排列内存块,以减少碎片的产生。性能监控与调优:在开发过程中,开发者可以使用性能监控工具来跟踪和分析堆内存的使用情况。这有助于发现潜在的内存泄漏和性能瓶颈,并采取相应的措施进行优化。

堆段是程序内存布局中的一个重要区域,它提供了灵活的内存分配策略来支持动态内存需求。然而,开发者需要谨慎地管理堆内存,以避免内存泄漏、野指针和内存碎片等问题。通过合理的内存分配与释放策略、内存碎片处理以及性能监控与调优措施,可以确保程序的稳定性和性能。

4.5. 代码示例

以下是一个简单的C语言代码示例,用于演示如何在堆段上动态分配和释放内存。此示例创建了一个整型数组,并在堆上为其分配内存,随后对其进行操作,并最终释放分配的内存。

#include <stdio.h>#include <stdlib.h>int main() {    int n;    printf("请输入数组的大小: ");    scanf("%d", &n);    // 在堆上动态分配内存以存储n个整型数据    int *array = (int *)malloc(n * sizeof(int));    if (array == NULL) {        // 如果内存分配失败,则打印错误信息并退出程序        fprintf(stderr, "内存分配失败\n");        return 1;    }    // 初始化数组并打印其内容    for (int i = 0; i < n; i++) {        array[i] = i * i; // 将数组元素设置为i的平方        printf("array[%d] = %d\n", i, array[i]);    }    // 对数组进行某些操作(此处省略具体操作,仅作为示例)    // ...    // 释放之前分配的堆内存    free(array);    array = NULL; // 将指针设为NULL以避免野指针问题(这是一个良好的编程习惯)    printf("内存已成功释放\n");    return 0;}

输出结果:

输入数组大小:程序首先提示用户输入一个整数,表示所需数组的大小。动态内存分配:使用malloc函数在堆上动态分配一个整型数组所需的内存。malloc的参数是所需内存的总字节数,这里通过n * sizeof(int)计算得出。如果malloc返回NULL,则表示内存分配失败,此时程序会打印错误信息并退出。数组初始化和打印:通过循环,将数组的每个元素初始化为其索引的平方值,并打印出来。内存释放:使用free函数释放之前分配的堆内存。将指针设置为NULL以避免后续可能产生的野指针问题(虽然在这个简单的示例中程序在释放内存后立即结束,但在更复杂的应用中这是一个好习惯)。程序结束:打印一条消息表示内存已成功释放,并正常结束程序。

请注意,在实际开发中,对于动态分配的内存,务必确保在不再需要时及时释放,以避免内存泄漏。同时,在释放内存后将指针设置为NULL可以作为一种额外的安全保障措施。

五、BSS段(Block Started by Symbol Segment

5.1. 定义与特性

BSS段(Block Started by Symbol Segment)是程序中一个特殊的内存区域,用于存放未初始化的全局变量和静态变量。这些变量在程序编译时并不占据磁盘空间(因为它们只包含未初始化的数据,即默认为0或NULL的值),但在程序加载到内存时,系统会为它们分配空间,并自动初始化为0或NULL。以下是BSS段的主要特性:

静态内存分配:BSS段中的变量属于静态内存分配,它们在程序的整个生命周期内都存在,不会因为函数的调用和返回而消失。

自动初始化为0:未初始化的全局变量和静态变量在BSS段中占据空间,当程序开始执行时,这些变量会被自动初始化为0(对于数值类型)或NULL(对于指针类型)。

不占用磁盘空间:与代码段和数据段不同,BSS段在程序的可执行文件中并不占据实际的磁盘空间。这是因为BSS段只包含未初始化的变量,这些变量的值在程序加载时可以被简单地初始化为0,而无需在磁盘上存储这些值。

段表记录大小:虽然BSS段不占用磁盘空间,但它在程序的段表中有一个条目,用于记录BSS段的大小。这个大小信息在程序加载时被用来为BSS段分配内存。

全局变量:在函数外部声明的变量被称为全局变量。如果全局变量未被初始化,则它们会被存储在BSS段中。

静态变量:在函数内部声明的静态变量(使用static关键字)也具有静态存储期。这些变量在函数被调用之间保持其值,并且如果它们未被初始化,则也会被存储在BSS段中。

初始化:在程序开始执行之前,操作系统或加载器会遍历BSS段,并将所有变量初始化为0或NULL。这个过程是自动的,程序员不需要显式地进行初始化。

5.2. 代码示例

以下是一个简单的C程序示例,展示了BSS段中未初始化的全局变量和静态变量的使用:

#include <stdio.h>// 未初始化的全局变量,存储在BSS段中int global_var;void function() {    // 未初始化的静态变量,存储在BSS段中    static int static_var;        // 第一次调用时,这些变量会被初始化为0    // 随后的调用中,这些变量会保持上一次的值(但在这个例子中,由于它们未被修改,所以仍然是0)    global_var++;    static_var++;        printf("Function called:\n");    printf("  global_var = %d\n", global_var);    printf("  static_var = %d\n", static_var);}int main() {    // 调用函数三次,观察全局变量和静态变量的变化    for (int i = 0; i < 3; i++) {        function();    }        return 0;}

输出结果: 

在这个示例中,global_var是一个未初始化的全局变量,而static_var是一个未初始化的静态变量。它们都被存储在BSS段中,并在程序开始执行时被初始化为0。当function函数被调用时,这些变量会被递增,并打印出它们的值。由于这些变量具有静态存储期,所以它们在函数调用之间保持其值。然而,在这个特定的例子中,由于我们只在每次调用时递增这些变量而没有其他操作,所以它们的值在每次调用后都会简单地增加1。 

六、总结

在程序的内存布局中,数据段、代码段、堆段、栈段和BSS段各自扮演着不可或缺的角色,共同支撑着程序的顺畅运行。

数据段存储内容:已初始化的全局变量和静态变量。特性:这些变量在程序的整个生命周期内都有效,且它们的初始值在程序加载时由可执行文件提供。代码段存储内容:程序的执行代码以及只读常数变量(如字符串常量和浮点常数)。特性:代码段通常是只读的,以防止程序意外地修改自身的指令。常数变量也被视为代码的一部分,因为它们的值在编译时确定,且在程序运行时不会改变。栈段存储内容:函数调用时的局部变量、函数参数、返回地址以及临时数据。特性:堆栈段遵循后进先出(LIFO)的原则,即最后压入栈的数据会最先被弹出。这使得堆栈段成为处理函数调用和返回的理想数据结构。堆段: 存储内容:程序在运行时动态分配的内存区域,通常用于存储动态分配的变量(如使用mallocnew分配的变量)。特性:堆段的大小在程序运行时是可变的,由程序员通过动态内存分配函数来管理。它允许程序在需要时分配内存,并在不再需要时释放内存,从而提供了灵活的内存管理机制。BSS段存储内容:未初始化的全局变量和静态变量。特性:这些变量在程序开始执行前会被自动初始化为0(或NULL,对于指针变量)。虽然BSS段在程序的可执行文件中不占用磁盘空间,但它在程序的内存布局中占据一定的空间。

这些段在程序的内存布局中各有其独特的位置和作用,它们共同协作以支持程序的正常运行。在嵌入式开发中,深入了解这些段的区别和特性对于优化程序性能、高效管理内存资源以及精准调试程序都至关重要。

此外,值得注意的是,不同的操作系统和编译器可能会对内存布局和段的实现细节有所差异。因此,在进行具体的嵌入式开发时,开发者需要参考目标平台和编译器的文档,以确保正确地理解和使用这些内存段。


点击全文阅读


本文链接:http://m.zhangshiyu.com/post/205767.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1