目录
- 段的概念_重定位的引入
-
- [1. 全局变量和局部变量引入栈的概念](#1. 全局变量和局部变量引入栈的概念)
- 二、堆是什么?
- [三. 段的概念](#三. 段的概念)
段的概念_重定位的引入
1. 全局变量和局部变量引入栈的概念

这里面有一个全局变量,一个局部变量
现在问2个问题:
- 1.全局变量保存在哪里?Flash还是RAM?
它的初始值肯定是保存在flash上,运行的时候,因为他是可读可写,所以运行时它肯定是在内存里
- 2.局部变量保存在哪里?
我们看看反汇编,就一清二楚了

C函数的第1个参数,放在寄存器R0里(这个是定死的,第一个参数在R0,第二个参数在R2.。。。。)
-
tmp在栈里
-
用val也就是r0来设置栈里的内容
tmp变量的地址是多少? "sp+0"
可以看到局部变量的地址跟栈密切相关
参数个数小于4个的话都会用寄存器来传递 ,至于大于4个参数的话,就是其他的方式,具体没有研究过,不过肯定没有这个效率高。
可以得出结论:
- 局部变量的地址跟栈相关
- SP的初始值是多少?可以影响后续的局部变量的地址
- 函数的调用深度,也会影响到栈
- 调用者的局部变量有多、有少,这也会影响到栈
我们再看看全局变量:
在返回的文件里面就可以看到全局变量的地址,他的初始值保存在哪里?
我们用一个比较特殊的数值来看看,static int g_a = 0x12345678;
这个全局变量的地址,在代码中怎么体现?
下图中,红框里面的两个数值,哪一个是全局变量g_a的地址?
去 0x800016c 处,读到一个数值0x20000000,这个数值就是g_a的地址
现在我们知道了:
-
局部变量的地址跟栈相关
-
全局变量的地址,对应的是内存(严格来说是RAM),但是它的地址是怎么确定的呢?
你链接程序时,需要指定"数据段"的起始地址:
我们使用这种简单的办法来指定"可读可写的内存"的基地址,基地址是0x20000000
在我们这个简单的程序里只有一个全局变量g_a,所以g_a的地址是0x20000000
在工程设置里,我们只是指定了内存的大小,这块内存怎么使用?
一部分用来保存全局变量,一部分用来当做栈,还可能有一部分没有用到
选择内存哪里当做栈?随你高兴,只要不会覆盖全局变量的空间就可以。
比如我们可以这样设置:
栈往下增长,如果定义一个局部变量:char [1000000000]会发生什么事?
执行时会发生什么事情?
这个情况比较极端,但是可以看到:
1.我们无法限定 栈的使用范围,如果你的程序写得不好,栈就会溢出
我们只能指定栈的TOP地址,无法限定栈往下增长的范围
2.这也就是我们为什么一般把栈的TOP地址设定在内存的最高位置的原因
尽可能让栈往下增长时,可以使用更多的空间。
二、堆是什么?
堆是什么?
-
堆是一块内存
-
这块内存一开始是空闲的,也就是没人使用
-
我们可以从这块内存里分配出一小块来使用
-
我们也可以在使用完后,把这一小块内存放回去以便再次分配
堆是空闲内存吗?不是,可以从里面划分出来使用,划分出来的这块被使用的内存,也属于堆
堆:程序员自己管理的内存,可以从中申请内存,可以回收内存。
你申请的内存,你想用来做什么都可以。但是,全局变量的地址是在连接时就确定了,所以它不在堆上。
深入讲解
- FreeRTOS里定义了一个全局数组:
用数组的方式申请一大块内存,里面可以放任何的东西,注意不要溢出!
- 很多程序,是使用"空闲内存"
比如:全局变量从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方法
- buf1 = malloc(10);
- 分配的空间大小=10+头部
- 在头部里记录大小
多了个记录内存使用信息的头部用来管理
这种方法巧妙地把分配的内存的长度,记录了下来以后 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段),但存在间接影响的可能性。以下是具体分析:
一、内存布局隔离性
-
静态分配区的固定性
静态分配区(.data、.bss、.rodata)的地址在程序启动时已确定 ,且位于内存的低地址到中地址区域 ,与堆区(动态分配区)的高地址区域物理隔离。例如:
.data
段存放已初始化的全局变量(如int g_var = 10;
)。.bss
段存放未初始化的全局变量(如int g_uninit;
)。.rodata
段存放只读数据(如字符串常量"Hello"
)。
-
堆区的动态扩展方向
堆区通过
malloc
等函数动态分配内存,其地址向高地址扩展 ,与静态分配区无直接相邻性。因此,常规的堆溢出(如malloc
分配的缓冲区溢出)通常只会覆盖堆内的相邻动态内存块,而非静态区。
二、溢出影响的边界条件
在极端情况下,动态分配区溢出可能间接影响静态区:
-
堆内存管理结构破坏
堆溢出可能篡改堆内存管理器(如
malloc
的元数据),导致后续分配行为异常。若攻击者通过溢出修改堆管理器的指针,可能触发任意地址写入,理论上可覆盖静态区的数据。例如:c// 堆溢出篡改堆元数据 char *heap_ptr = malloc(16); strcpy(heap_ptr, overflow_data); // 溢出覆盖堆元数据 free(heap_ptr); // 触发异常的内存回收逻辑
-
特殊内存布局的嵌入式系统
在内存资源受限的嵌入式系统中,若堆区与静态区地址空间重叠或紧邻,溢出可能直接覆盖静态区。但这类设计在通用操作系统中极为罕见。
-
全局函数指针篡改
若静态区中存在函数指针(如
static void (*func_ptr)();
),且堆溢出导致该指针被覆盖,可能通过func_ptr()
调用攻击者注入的代码,间接影响静态区关联的逻辑。
三、防护机制与开发建议
-
内存隔离技术
- 地址空间随机化(ASLR):随机化堆、栈、静态区的基址,增加溢出预测难度。
- 数据执行保护(DEP):标记静态区为不可执行(NX位),防止注入代码运行。
-
编码规范
- 对动态分配的内存进行边界检查 (如使用
strncpy
代替strcpy
)。 - 使用内存安全语言(如Rust)或工具(如AddressSanitizer)检测堆溢出。
- 对动态分配的内存进行边界检查 (如使用
四、总结
场景 | 是否影响静态区 | 说明 |
---|---|---|
常规堆溢出 | ❌ 否 | 溢出仅影响堆内部结构或相邻动态内存块 |
堆元数据篡改 | ⚠️ 可能 | 通过任意地址写入可间接修改静态区 |
特殊内存布局系统 | ⚠️ 可能 | 嵌入式系统中堆与静态区紧邻时可能受影响 |
函数指针/全局变量覆盖 | ✅ 是 | 若静态区存在可写指针或敏感数据,可能被间接篡改 |
结论 :在标准编程实践和现代操作系统保护下,动态分配区溢出不会直接影响静态分配区,但需警惕间接攻击路径。开发者应优先采用安全编程实践和防护技术。