STM32【8】堆栈和段的概念(1)

目录

段的概念_重定位的引入

1. 全局变量和局部变量引入栈的概念

这里面有一个全局变量,一个局部变量

现在问2个问题:

- 1.全局变量保存在哪里?Flash还是RAM?

它的初始值肯定是保存在flash上,运行的时候,因为他是可读可写,所以运行时它肯定是在内存里

- 2.局部变量保存在哪里?

我们看看反汇编,就一清二楚了

C函数的第1个参数,放在寄存器R0里(这个是定死的,第一个参数在R0,第二个参数在R2.。。。。)

  1. tmp在栈里

  2. 用val也就是r0来设置栈里的内容

    tmp变量的地址是多少? "sp+0"

可以看到局部变量的地址跟栈密切相关

参数个数小于4个的话都会用寄存器来传递 ,至于大于4个参数的话,就是其他的方式,具体没有研究过,不过肯定没有这个效率高。

可以得出结论:

  1. 局部变量的地址跟栈相关
  2. SP的初始值是多少?可以影响后续的局部变量的地址
  3. 函数的调用深度,也会影响到栈
  4. 调用者的局部变量有多、有少,这也会影响到栈

我们再看看全局变量:

在返回的文件里面就可以看到全局变量的地址,他的初始值保存在哪里?

我们用一个比较特殊的数值来看看,static int g_a = 0x12345678;

这个全局变量的地址,在代码中怎么体现?

下图中,红框里面的两个数值,哪一个是全局变量g_a的地址?

去 0x800016c 处,读到一个数值0x20000000,这个数值就是g_a的地址

现在我们知道了:

  1. 局部变量的地址跟栈相关

  2. 全局变量的地址,对应的是内存(严格来说是RAM),但是它的地址是怎么确定的呢?

你链接程序时,需要指定"数据段"的起始地址:
我们使用这种简单的办法来指定"可读可写的内存"的基地址,基地址是0x20000000

在我们这个简单的程序里只有一个全局变量g_a,所以g_a的地址是0x20000000

在工程设置里,我们只是指定了内存的大小,这块内存怎么使用?

一部分用来保存全局变量,一部分用来当做栈,还可能有一部分没有用到

选择内存哪里当做栈?随你高兴,只要不会覆盖全局变量的空间就可以。

比如我们可以这样设置:

栈往下增长,如果定义一个局部变量:char [1000000000]会发生什么事?

执行时会发生什么事情?

这个情况比较极端,但是可以看到:

1.我们无法限定 栈的使用范围,如果你的程序写得不好,栈就会溢出

我们只能指定栈的TOP地址,无法限定栈往下增长的范围

2.这也就是我们为什么一般把栈的TOP地址设定在内存的最高位置的原因

尽可能让栈往下增长时,可以使用更多的空间。

二、堆是什么?

堆是什么?

  1. 堆是一块内存

  2. 这块内存一开始是空闲的,也就是没人使用

  3. 我们可以从这块内存里分配出一小块来使用

  4. 我们也可以在使用完后,把这一小块内存放回去以便再次分配

堆是空闲内存吗?不是,可以从里面划分出来使用,划分出来的这块被使用的内存,也属于堆

堆:程序员自己管理的内存,可以从中申请内存,可以回收内存

你申请的内存,你想用来做什么都可以。但是,全局变量的地址是在连接时就确定了,所以它不在堆上。

深入讲解

  1. FreeRTOS里定义了一个全局数组:
    用数组的方式申请一大块内存,里面可以放任何的东西,注意不要溢出!
  2. 很多程序,是使用"空闲内存"
    比如:全局变量从0x20000000开始,你设置栈执行一块0x200的内存,剩下的内存,如果你可以管理起来,能从中分配内存,它就是堆

当然,对于堆,我们管理它的时候肯定是要事先确定它的起始地址、结束地址

你从堆里划分空间时,从头部开始划分,或者从尾部开始划分,没有什么不同。

看看下面的代码:

c 复制代码
int heap_start = 0x20004000;
int heap_end   = 0x20005000;

void *malloc(int len)
{
   void *ret = heap_start;
   heap_start += len;
   return ret;
}

这段代码分配得到的内存,能够释放吗?

释放:就是让这块内存能再次使用,再次malloc

假设第1次:buf1 = malloc(10),

假设第2次:buf1 = malloc(20);

现在想去释放buf1,你能写出一个 free 函数吗?void free(void *buf);

写不出,为什么?

比如:free(buf1); 我没办法根据buf1这个参数知道buf1有多大

就是说不知道回收、释放多大的空间

从上面的图里,知道buf1的大小吗?

可能会告诉我:buf2 - buf1,就是大小

但是:

c 复制代码
void a()
{
 char *buf1 = malloc(10);

 b();

 free(buf1); // 根本无法访问另一个函数的局部变量buf2
}

void b()
{
 char *buf2 = malloc(20);
}

在我们这个场景里面,你没有办法知道buf2的地址,就没办法通过buf2-buf1确定buf1的长度

在实际应用中,你甚至不知道第2个malloc的地址是存在哪个变量里

所以,我们这个简单的malloc函数没有对应的free函数

我们使用的这个管理方法是有问题的,它可以分配空间,但是无法回收空间。

我们看看官方的malloc函数时怎么做的

我们看看上面的malloc方法

  1. buf1 = malloc(10);
  2. 分配的空间大小=10+头部
  3. 在头部里记录大小

多了个记录内存使用信息的头部用来管理

这种方法巧妙地把分配的内存的长度,记录了下来以后 free(buf1)时,怎么得到buf1的长度?

从头部读取buf1的长度记录

keil自带的malloc、free,大概就是这种方法

当然,内部实现会复杂得多

三. 段的概念

程序分为下面几个段(也就是连续空间分类存储):

代码段、只读数据段、可读可写的数据段、BSS段。

c 复制代码
char g_Char = 'A';           // 可读可写,不能放在ROM上,应该放在RAM里
const char g_Char2 = 'B';    // 只读变量,可以放在ROM上
int g_A = 0;   // 初始值为0,干嘛浪费空间保存在ROM上?没必要
int g_B;       // 没有初始化,干嘛浪费空间保存在ROM上?没必要

所以,程序分为这几个段:

  • 代码段(RO-CODE):
    存放程序的二进制机器指令,如函数代码。此段属性为只读,防止程序运行时被意外修改。C语言中所有函数的实现代码均存储在此区域。
  • 可读可写的数据段(RW-DATA):
    存放已初始化且非零值的全局变量和静态变量,例如:int global_var = 100;。程序启动时需将数据从ROM复制到RAM中,保证运行时的可修改性
  • 只读的数据段(RO-DATA):
    存储常量数据,例如字符串字面量、const修饰的全局变量等。该段 同样为只读,加载时通常映射到ROM或内存的只读区域。例如:const int g_const = 10;会被分配到此段。
  • BSS段或ZI段:
    存储未初始化或初始值为零的全局变量和静态变量(如int uninit_var;或static int zero_var = 0;)。此段在程序加载时由系统自动清零,无需占用ROM空间,仅记录所需内存大小
  • 堆(Heap)
    动态内存分配区域,通过malloc/free管理。其地址空间向上增长,由程序员显式控制生命周期
  • 栈(Stack)
    存放函数调用的局部变量、参数及返回地址。其地址空间向下增长,由编译器自动分配和释放,具有LIFO(后进先出)特性

动态分配区(堆区)的溢出通常不会直接影响静态分配区 (如.data段、.bss段、.rodata段),但存在间接影响的可能性。以下是具体分析:

一、内存布局隔离性

  1. 静态分配区的固定性

    静态分配区(.data、.bss、.rodata)的地址在程序启动时已确定 ,且位于内存的低地址到中地址区域 ,与堆区(动态分配区)的高地址区域物理隔离。例如:

    • .data段存放已初始化的全局变量(如int g_var = 10;)。
    • .bss段存放未初始化的全局变量(如int g_uninit;)。
    • .rodata段存放只读数据(如字符串常量"Hello")。
  2. 堆区的动态扩展方向

    堆区通过malloc等函数动态分配内存,其地址向高地址扩展 ,与静态分配区无直接相邻性。因此,常规的堆溢出(如malloc分配的缓冲区溢出)通常只会覆盖堆内的相邻动态内存块,而非静态区。


二、溢出影响的边界条件

极端情况下,动态分配区溢出可能间接影响静态区:

  1. 堆内存管理结构破坏

    堆溢出可能篡改堆内存管理器(如malloc的元数据),导致后续分配行为异常。若攻击者通过溢出修改堆管理器的指针,可能触发任意地址写入,理论上可覆盖静态区的数据。例如:

    c 复制代码
    // 堆溢出篡改堆元数据
    char *heap_ptr = malloc(16);
    strcpy(heap_ptr, overflow_data);  // 溢出覆盖堆元数据
    free(heap_ptr);  // 触发异常的内存回收逻辑
  2. 特殊内存布局的嵌入式系统

    在内存资源受限的嵌入式系统中,若堆区与静态区地址空间重叠或紧邻,溢出可能直接覆盖静态区。但这类设计在通用操作系统中极为罕见。

  3. 全局函数指针篡改

    若静态区中存在函数指针(如static void (*func_ptr)();),且堆溢出导致该指针被覆盖,可能通过func_ptr()调用攻击者注入的代码,间接影响静态区关联的逻辑。


三、防护机制与开发建议

  1. 内存隔离技术

    • 地址空间随机化(ASLR):随机化堆、栈、静态区的基址,增加溢出预测难度。
    • 数据执行保护(DEP):标记静态区为不可执行(NX位),防止注入代码运行。
  2. 编码规范

    • 对动态分配的内存进行边界检查 (如使用strncpy代替strcpy)。
    • 使用内存安全语言(如Rust)或工具(如AddressSanitizer)检测堆溢出。

四、总结

场景 是否影响静态区 说明
常规堆溢出 ❌ 否 溢出仅影响堆内部结构或相邻动态内存块
堆元数据篡改 ⚠️ 可能 通过任意地址写入可间接修改静态区
特殊内存布局系统 ⚠️ 可能 嵌入式系统中堆与静态区紧邻时可能受影响
函数指针/全局变量覆盖 ✅ 是 若静态区存在可写指针或敏感数据,可能被间接篡改

结论 :在标准编程实践和现代操作系统保护下,动态分配区溢出不会直接影响静态分配区,但需警惕间接攻击路径。开发者应优先采用安全编程实践和防护技术。

相关推荐
KaiGer66638 分钟前
图解AUTOSAR_SWS_SPIHandlerDriver
嵌入式硬件
国科安芯2 小时前
汽车电气架构中的电源架构
人工智能·嵌入式硬件·fpga开发·架构·汽车
程序员的世界你不懂2 小时前
Tomcat生产服务器性能优化
服务器·性能优化·tomcat
promising-w2 小时前
【keil】单步调试
stm32·单片机·嵌入式硬件
andylauren3 小时前
STM32 MODBUS-RTU主从站库移植
stm32·单片机·嵌入式硬件
是lamune3 小时前
stm32F103C8T6引脚定义
stm32·单片机·嵌入式硬件
0南城逆流03 小时前
【STM32】知识点介绍二:GPIO引脚介绍
stm32·单片机·嵌入式硬件
沐欣工作室_lvyiyi7 小时前
基于单片机的智能奶茶机(论文 +源码)
stm32·单片机·嵌入式硬件·物联网·智能家居
猪猪童鞋8 小时前
STM32F4单片机SDIO驱动SD卡
stm32·单片机·嵌入式硬件·sd卡驱动·sdio总线
Tlog嵌入式9 小时前
[项目]基于FreeRTOS的STM32四轴飞行器: 十六.激光测距定高功能
stm32·单片机·嵌入式硬件·mcu·iot