文章目录

  • 1、引出栈空间问题
  • 2、解决问题
    • 2.1、RAM空间
    • 2.2、RAM空间具体分布
    • 2.3、关于栈空间的使用
    • 2.4、栈溢出
    • 2.5、变量的消亡
    • 2.6、回到关键字static
    • 2.7、合法性的判断


1、引出栈空间问题

static关键字引出该部分内容。

为什么能从static引出来?

在使用该关键字的时候:

我们需要知道什么时候使用该关键字?

什么使用关键字?

并且我们知道函数执行的时候是在栈空间,但是我们的static修饰的关键字变量是在data段或者bss段。

还有就是我们的程序是从FLASH里面烧写的,那就是意味着所有的变量以及函数都是先出现在FLASH里面也就是ROM空间。

以上这些疑问接下来通过按键开源项目例程主意分析。

2、解决问题

2.1、RAM空间

我们知道RAM里面有栈空间、堆空间、bss、data段。

  • 栈空间(Stack)​​:存储函数调用的局部变量、参数、返回地址等,由系统自动管理,从高地址向下生长。

  • 堆空间(Heap)​​:用于动态内存分配(如 malloc),由程序员手动管理,从低地址向上生长。

  • ​.bss 段​:存储未初始化的全局变量和静态变量,程序启动时由系统自动清零。

  • ​.data 段​:存储已初始化的全局变量和静态变量,程序启动时从 Flash 复制初始值到 RAM。

但是需要声明的是在裸机开发中一般不使用堆空间,

并且函数的执行都是在==栈空间==,那说到这里还记不记得有一个栈顶空间,对的,这个栈顶空间就是给一个上限,因此栈空间的特殊性,是从上到下的,也就是高字节到低字节分配,

  • 栈是一种线性数据结构,仅允许在栈顶(Top)​进行插入(入栈)和删除(出栈)操作。类似一摞盘子,最后放上的盘子最先被取走。

这是因为在_main函数到mainARM内核还有一段代码需要执行,因此留出来的是这一段空间,然后才是我们自己写的main函数栈顶地址,就这后面的栈顶空间就可以循环利用了。

我们首先需要知道栈顶地址是怎么得到的?

2.2、RAM空间具体分布

这是整个RAM的空间:

![[Pasted image 20250711161229.png]]

栈是RAM顶部的最后一个区域,符合典型设计。这句话至关重要。

    Exec Addr    Load Addr    Size         Type   Attr      Idx    E Section Name        Object0x20000000   COMPRESSED   0x00000024   Data   RW           39    .data               main.o0x20000024   COMPRESSED   0x00000040   Data   RW          110    .data               modbus_app.o0x20000064   COMPRESSED   0x000000b5   Data   RW          183    .data               mb.o0x20000119   COMPRESSED   0x00000003   PAD0x2000011c   COMPRESSED   0x0000000c   Data   RW          267    .data               mbrtu.o0x20000128   COMPRESSED   0x00000008   Data   RW          372    .data               modbus_slave.o0x20000130   COMPRESSED   0x00000024   Data   RW          581    .data               key_drv.o0x20000154   COMPRESSED   0x00000024   Data   RW          618    .data               led_drv.o0x20000178   COMPRESSED   0x00000008   Data   RW          668    .data               ntc_drv.o0x20000180   COMPRESSED   0x00000006   Data   RW          810    .data               rh_drv.o0x20000186   COMPRESSED   0x00000002   PAD0x20000188   COMPRESSED   0x0000000c   Data   RW          964    .data               systick.o0x20000194   COMPRESSED   0x0000001c   Data   RW         1010    .data               usb2com_drv.o0x200001b0   COMPRESSED   0x00000002   Data   RW         1133    .data               portevent.o0x200001b2   COMPRESSED   0x00000002   PAD0x200001b4   COMPRESSED   0x00000018   Data   RW         1168    .data               portserial.o0x200001cc   COMPRESSED   0x00000004   Data   RW         3445    .data               mc_w.l(stderr.o)0x200001d0   COMPRESSED   0x00000004   Data   RW         3734    .data               mc_w.l(stdout.o)0x200001d4        -       0x00000100   Zero   RW          265    .bss                mbrtu.o0x200002d4   COMPRESSED   0x00000004   PAD0x200002d8        -       0x00000030   Zero   RW          580    .bss                key_drv.o0x20000308        -       0x00000014   Zero   RW          666    .bss                ntc_drv.o0x2000031c   COMPRESSED   0x00000004   PAD0x20000320        -       0x00000400   Zero   RW         3383    STACK               startup_gd32f30x_hd.o

通过工程的map文件可以看出在栈空间确定之前,首先确定的是data、bss数据占用的RAM空间,最后确定出栈空间的最低地址是多少。通过代码可以看出是0x20000320,大小是0x00000400,其中栈的大小是可以自己设定的。那么两者相加就是0x20000320 + 0x00000400 = 0x20000720

    pxMBFrameCBByteReceived                  0x2000007c   Data           4  mb.o(.data)pxMBFrameCBTransmitterEmpty              0x20000080   Data           4  mb.o(.data)pxMBPortCBTimerExpired                   0x20000084   Data           4  mb.o(.data)pxMBFrameCBReceiveFSMCur                 0x20000088   Data           4  mb.o(.data)pxMBFrameCBTransmitFSMCur                0x2000008c   Data           4  mb.o(.data)__stderr                                 0x200001cc   Data           4  stderr.o(.data)__stdout                                 0x200001d0   Data           4  stdout.o(.data)ucRTUBuf                                 0x200001d4   Data         256  mbrtu.o(.bss)__initial_sp                             0x20000720   Data           0  startup_gd32f30x_hd.o(STACK)

从最后一行代码也可以看出该工程的栈顶地址是0x20000720

并且也符合图片中的顺序。

我们现在是在裸机层面考虑,所以先不考虑堆空间。

2.3、关于栈空间的使用

​ARM Cortex-M启动流程与栈初始化​,在芯片上电或复位后,硬件自动执行以下步骤:

初始化主堆栈指针(MSP)​​:从向量表的第一个表项(地址0x00000000或0x08000000)加载MSP初始值,指向栈顶(高地址)。也就是我们常说的这一步:

![[Pasted image 20250711202933.png]]

参考链接ARM单片机启动流程(一)(详细解析)-CSDN博客

读取了栈顶地址以后,接着就是进入Rest_Handler复位函数地址,然后从这里开始执行程序,这里需要说明但是SP指向的地方。

首先,​栈顶(SP)已指向预设的栈空间顶端​(例如0x20000428),但尚未为任何函数分配栈帧。栈顶地址本身并不存储_ _main函数的入口地址,而是由硬件直接设置SP寄存器的值。

接着需要引入一个:栈帧概念:
​栈帧的创建​:
当Reset_Handler调用__main时,会在栈上为__main创建栈帧,保存返回地址(LR)和寄存器上下文。 ​栈顶(SP)此时指向__main栈帧的顶部​(低地址)

需要注意的是__main的低地址也就是main函数的高地址。

也就是下述这个例子。

![[Pasted image 20250711202134.png]]

__main函数:

  • 将已初始化的全局变量(.data段)从Flash复制到RAM。 这个地方就解决了我们所疑惑的代码烧写到ROM里面,但是那些全局变量什么的又会到RAM里面。
  • 清零未初始化的全局变量(.bss段)。
  • 初始化C运行时环境(堆、栈、库函数)。
  • 最终调用用户main()函数。

用户main()及其调用的子函数共享同一栈空间,通过SP的移动动态分配/释放栈帧,实现内存高效利用。

也就是说在整个RAM空间(不考虑堆空间),能循环利用的地方也就是栈空间,更具体来讲就是main下面的。

因为在最下面是data、bss段,往上就是堆空间,接着就是我们的栈空间了。而栈空间又分为最上面的栈顶空间是用来存__main这个栈帧空间的,接下来就是main以及可以循环利用的栈空间,全靠SP移动高效复用内存。

特性通用系统(如 Linux)嵌入式系统(无 OS)
​**main() 行为**​单次执行后退出无限循环,永不退出
​**__main 栈帧生命周期**​main() 返回后立即释放永久保留(因 main() 不退出)
栈溢出风险递归过深导致循环内局部变量过大或递归未限制
退出处理调用 atexit()、析构全局对象进入 halt 或复位
  • __main 栈帧是“永久居民”​**​:因 main() 永不返回,它作为程序生命周期的基石始终存在栈底。

  • 子栈帧是“流动工人”​​:在 main() 的循环中动态轮转,通过 SP 移动高效复用内存。

  • 循环缺失 = 系统失控​:嵌入式环境中,main() 退出即程序终结,栈帧管理失去意义。

2.4、栈溢出

在裸机嵌入式系统中,栈溢出可能覆盖bss段和data段,尤其是当栈与静态存储区相邻且无保护时。

先不考虑堆空间。

  • 栈(Stack)​​:从高地址向低地址增长(向下增长)。

  • 堆(Heap)​​:从低地址向高地址增长(向上增长)。

  • 静态存储区​:

    • data段​:存放已初始化的全局变量和静态变量。
    • bss段​:存放未初始化的全局变量和静态变量(程序启动时清零)。
  • bss段优先被覆盖​:

    • bss段通常紧邻堆区,位于栈的下方(低地址方向)。
    • 若栈溢出量较大,​最先覆盖的是bss段​(因其位置更靠近栈底)。
    • 案例​:
      在Jflash下载算法中,栈溢出导致.bss段变量被覆盖,引发Flash写入错误(如数据被篡改)。
  • data段可能被覆盖​:

    • 若bss段被完全覆盖且溢出持续,栈会进一步向下覆盖data段。
    • data段存储已初始化变量,覆盖可能导致程序逻辑错误或数据损坏​(如配置参数丢失)。

若内存布局中堆区较大或存在保护间隙(Guard Region)​,栈溢出可能仅覆盖堆区,未触及bss/data段。

某些链接脚本(Linker Script)会隔离栈与其他段,例如在栈底预留保护区。

通常bss段最先被覆盖,其次是data段(因位置更接近栈底)

并且可以通过链接脚本隔离、哨兵检测、MPU保护或静态分析,可有效预防覆盖风险。

哨兵检测(Sentinel Detection)​​ 是一种通过监控特定内存值来识别栈溢出的软件方法。其核心原理是在栈空间边界预设一个特殊标记值(哨兵值),通过定期检查该值是否被篡改来判断是否发生溢出。

设置哨兵值
在栈空间的顶部或底部​(根据栈增长方向)预留一个位置,写入特定的哨兵值(如 0xDEADBEEF)。栈通常从高地址向低地址增长(如ARM Cortex-M),哨兵值需放置在栈顶(低地址边界)。

#define STACK_SENTINEL_VALUE 0xDEADBEEF
volatile uint32_t stack_sentinel __attribute__((section(".stack"))) = STACK_SENTINEL_VALUE;

定期检查哨兵值
在系统空闲任务、定时器中断或关键任务周期中调用检测函数,验证哨兵值是否被覆盖:

void check_stack_overflow(void) {if (stack_sentinel != STACK_SENTINEL_VALUE) {// 栈溢出处理handle_overflow_error();}
}

并且哨兵检测具有滞后性、漏检风险等局限性

  • 只能在溢出发生后检测,无法预防溢出,结合栈着色(Stack Coloring)技术,填充全栈空间并计算高水位线,提前预警。
  • 若溢出未覆盖哨兵值(如局部变量过大但未触及边界),可能漏检。可在函数入口处增加栈指针范围检查。

2.5、变量的消亡

  • 静态存储区​:
    • data段​:存放已初始化的全局变量和静态变量。
    • bss段​:存放未初始化的全局变量和静态变量(程序启动时清零)。
      需要说明的是:bss和data不会释放的,会一直占用。

局部变量在函数栈帧(Stack Frame)​​ 中分配空间。当函数被调用时,编译器会移动栈指针(如 sub rsp, N 指令),为所有局部变量一次性分配内存。

函数返回时,通过指令(如 add rsp, Nmov rsp, rbp)将栈指针移回函数调用前的位置,​整个栈帧的内存被标记为“可复用”​,局部变量的存储空间随之释放

  • 释放操作是高效的指针移动,而非数据擦除(内存中可能残留原值)。
  • 若后续函数调用覆盖该栈帧,残留数据会被新数据替换。

局部变量的生命周期与其所属函数的栈空间紧密绑定,其存储空间确实随着函数栈帧的销毁而被释放。

栈空间释放后,局部变量的地址立即失效,但数据可能暂时残留。访问这些地址会导致未定义行为​(如野指针操作)。
这里也就解释了前面学习指针内容中,为什么我们要对指针指向明确的地址,就是防止野指针发生,因为有时候可能恰好就会指向我们刚好释放的栈帧空间,那不就导致数据错误了。 产生程序崩溃(段错误)、数据污染(覆盖其他变量)。

是不是这里又豁然开朗了,简直是太妙了!!!!!!!!!!

2.6、回到关键字static

使用了static就说明这个变量不会随着函数栈帧的内存被标记为“可复用”而消失。

我们使用static关键字主要有两个方面
1、控制作用域和封装

  • 限制作用域​:static 将变量作用域限定在当前文件内,其他文件无法通过 extern 访问这些变量。这避免了全局变量的“污染”,防止其他模块意外修改按键状态。
  • 封装性​:按键操作逻辑(如扫描、消抖)通常集中在同一文件中。static 变量使所有相关操作内聚,符合“高内聚、低耦合”的设计原则。
    2、​模块化设计与协作开发
  • 避免命名冲突​:全局变量可能被多人协作时的其他文件同名变量覆盖,而 static 变量仅在当前文件有效,彻底消除冲突风险。
  • 简化调试与维护​:开发者只需关注当前文件内的逻辑,无需追踪全局变量的跨文件调用链,降低认知负担。
    3、内存与生命周期管理
  • 生命周期相同,但更安全​:static 变量与全局变量均存储在静态数据区,生命周期均为整个程序运行期。但 static 通过作用域限制,提供了自动的内存隔离,避免全局变量的无约束访问。
  • 初始化保障​:static 变量默认初始化为 0(如未显式初始化),与全局变量一致,但仅在首次加载时初始化一次。
特性static Button btn1, btn2;全局变量 Button btn1, btn2;
作用域仅当前文件整个程序(所有文件)
跨文件访问不可访问可通过 extern 访问
命名冲突风险几乎为零高(需靠命名约定管理)
内存位置静态数据区(与全局变量相同)静态数据区
初始化默认 0,仅初始化一次默认 0,程序启动时初始化
适用场景模块内共享数据,无需外部暴露需跨模块共享的全局数据

因此在这里我们使用static关键字。

2.7、合法性的判断

编程思想的严谨性在这里需要体现。

即使 static 变量地址有效,若函数通过参数接收外部指针(如 button_init(&btn1, ...)),仍需检查该参数是否为空:

因此初始化的时候首先要进行检测的就是判断地址的合法性。

void button_init(Button* handle, ...) {if (!handle) return;  // 必须检查,避免外部误传 NULL
}

文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。

【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。

感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/914153.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/914153.shtml
英文地址,请注明出处:http://en.pswp.cn/news/914153.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【RK3568+PG2L50H开发板实验例程】FPGA部分 | 键控LED实验

本原创文章由深圳市小眼睛科技有限公司创作,版权归本公司所有,如需转载,需授权并注明出处(www.meyesemi.com) 1.实验简介 实验目的: 从创建工程到编写代码,完成引脚约束,最后生成 bit 流下载到…

【Python练习】039. 编写一个函数,反转一个单链表

039. 编写一个函数,反转一个单链表 039. 编写一个函数,反转一个单链表方法 1:迭代实现运行结果代码解释方法 2:递归实现运行结果代码解释选择方法迭代法与递归法的区别039. 编写一个函数,反转一个单链表 在 Python 中,可以通过迭代或递归的方式反转一个单链表。 方法 1…

BERT代码简单笔记

参考视频:BERT代码(源码)从零解读【Pytorch-手把手教你从零实现一个BERT源码模型】_哔哩哔哩_bilibili 一、BertTokenizer BertTokenizer 是基于 WordPiece 算法的 BERT 分词器,继承自 PreTrainedTokenizer。 继承的PretrainedTokenizer,具…

PID控制算法理论学习基础——单级PID控制

这是一篇我在学习PID控制算法的过程中的学习记录。在一开始学习PID的时候,我也看了市面上许多的资料,好的资料固然有,但是更多的是不知所云。(有的是写的太过深奥,有的则是照搬挪用,对原理则一问三不知&…

【Elasticsearch】function_score与rescore

它们俩都是用来“**干涉评分**”的,但**工作阶段不同、性能开销不同、能做的事也不同**。一句话总结:> **function_score** 在 **第一次算分** 时就动手脚; > **rescore** 在 **拿到 Top-N 结果后** 再“重新打分”。下面把“能干嘛”…

无广告纯净体验 WPS2016 精简版:移除联网模块 + 非核心组件,古董电脑也能跑

各位办公小能手们!今天给你们介绍一款超神的办公软件——WPS2016精简版!它有多小呢?才33MB,简直就是软件界的小不点儿!别看它个头小,功能可一点儿都不含糊,文字、表格、演示这三大功能它全都有。…

《PyWin32:Python与Windows的桥梁,解锁系统自动化新姿势》

什么是 PyWin32在 Windows 平台的 Python 开发领域中,PyWin32 是一个举足轻重的库,它为 Python 开发者打开了一扇直接通往 Windows 操作系统底层功能的大门。简单来说,PyWin32 是用于 Python 访问 Windows API(Application Progra…

vite如何生成gzip,并在服务器上如何设置开启

1. 安装插件npm install vite-plugin-compression -D2. 在 vite.config.ts 中配置TypeScriptimport { defineConfig } from vite import compression from vite-plugin-compressionexport default defineConfig({plugins: [compression({algorithm: gzip,ext: .gz,threshold: 1…

1068万预算!中国足协大模型项目招标,用AI技术驱动足球革命

中国足协启动国际足联“前进计划”下的大数据模型项目,预算1068万元。该项目将建立足球大数据分析平台,利用AI技术为国家队、青少年足球、业余球员及教练员裁判员提供精准数据分析服务,旨在通过科技手段提升中国足球竞技水平。 中国足球迎来数…

AI产品经理面试宝典第12天:AI产品经理的思维与转型路径面试题与答法

多样化思维:如何跳出单一框架解题? 面试官:AI产品常面临复杂场景,请举例说明你如何运用多样化思维解决问题? 你的回答:我会从三个维度展开:多角度拆解需求本质,多层级融合思维模式,多变量寻找最优解。比如设计儿童教育机器人时,不仅考虑功能实现(技术层),还融入情…

vscode.window对象讲解

一、vscode.window 核心架构图 #mermaid-svg-fyCxPz1vVhkf96nE {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-fyCxPz1vVhkf96nE .error-icon{fill:#552222;}#mermaid-svg-fyCxPz1vVhkf96nE .error-text{fill:#5522…

为什么一个 @Transactional 注解就能开启事务?揭秘 Spring AOP 的底层魔法

你是否也曾深陷在各种“额外”逻辑的泥潭,为了给一个核心业务方法增加日志、权限校验或缓存,而不得不将这些非核心代码硬塞进业务类中,导致代码臃肿、职责不清?是时候用代理设计模式 (Proxy Design Pattern) 来解脱了!…

《Spring 中上下文传递的那些事儿》Part 8:构建统一上下文框架设计与实现(实战篇)

📝 Part 8:构建统一上下文框架设计与实现(实战篇) 在实际项目中,我们往往需要处理多种上下文来源,例如: Web 请求上下文(RequestContextHolder)日志追踪上下文&#xf…

配置驱动开发:初探零代码构建嵌入式软件配置工具

前言在嵌入式软件开发中,硬件初始化与寄存器配置长期依赖人工编写重复代码。以STM32外设初始化为例,开发者需手动完成时钟使能、引脚模式设置、参数配置等步骤,不仅耗时易错(如位掩码写反、模式枚举值混淆)&#xff0c…

Elasticsearch混合搜索深度解析(下):执行机制与完整流程

引言 在上篇中,我们发现了KNN结果通过SubSearch机制被保留的关键事实。本篇将继续深入分析混合搜索的执行机制,揭示完整的处理流程,并解答之前的所有疑惑。 深入源码分析 1. SubSearch的执行机制 1.1 KnnScoreDocQueryBuilder的实现 KNN结果被…

Apache HTTP Server 从安装到配置

一、Apache 是什么?Apache(全称 Apache HTTP Server)是当前最流行的开源Web服务器软件之一,由Apache软件基金会维护。它以稳定性高、模块化设计和灵活的配置著称,支持Linux、Windows等多平台,是搭建个人博客…

php中调用对象的方法可以使用array($object, ‘methodName‘)?

是的,在PHP中,array($object, methodName) 是一种标准的回调语法,用于表示“调用某个对象的特定方法”。这种语法可以被许多函数(如 call_user_func()、call_user_func_array()、usort() 等)识别并执行。 语法原理 在P…

【设计模式】单例模式 饿汉式单例与懒汉式单例

单例模式(Singleton Pattern)详解一、单例模式简介 单例模式(Singleton Pattern) 是一种 创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。(对象创建型模式&…

vue3 el-table 行数据沾满格自动换行

在使用 Vue 3 结合 Element Plus 的 <el-table> 组件时&#xff0c;如果你希望当表格中的行数据文本过长时能够自动换行&#xff0c;而不是溢出到其他单元格或简单地截断&#xff0c;你可以通过以下几种方式来实现&#xff1a;方法 1&#xff1a;使用 CSS最简单的方法是通…

windows电脑远程win系统服务器上的wsl2

情况 我自己使用win11笔记本电脑&#xff0c;想要远程win11服务器上的wsl2 我这里只有服务器安装了wsl2&#xff0c;win11笔记本没有安装 因此下面提到的Ubuntu终端指的是win服务器上的wsl2终端 一定要区分是在哪里输入命令&#xff01;&#xff01; 安装SSH 在服务器上&#x…