内存安全翻车现场的罪魁祸首,根源就在这三步里?

简介: 今天的任务是把内存翻车现场的"罪魁祸首"挨个揪出来,讲透内存安全问题的根源到底在哪儿。看不懂,算我的!#内存安全 #内存泄漏 #野指针 #内存越界 #原理 #知识分享 #C++ #内存管理 #数据竞争 #悬空指针 #UAF #内存释放 #堆内存

文章封面.jpeg

温馨提示:另有配套视频版,附带图解演示,观感不同,欢迎到各大视频平台搜索同名标题或【蛋先生说识】

术语约定:下文提到的无前缀修饰的 Runtime 是指 C/C++ 那种「甩手掌柜」——工具给你、逻辑你自己负责;不是 JVM 那种「啥都管」的接管型 Runtime

开场

丹尼尔:蛋兄,上回(请在各平台搜索"明明都是源码到CPU,各语言中间原理咋不是一个套路?")你以"内存安全"为维度列举了一些编程语言的设计思路,今天能跟我系统地讲讲"内存安全"吗?

蛋先生:当然!内存安不安全,其实就是程序怎么用内存的问题。程序用内存,就三步:先申请,再读写,最后用完释放。就这么个过程

┌──────────┐     ┌──────────┐     ┌──────────┐
│① 申请内存 │ ──→ │② 读写内存 │ ──→ │③ 用完释放 │
└──────────┘     └──────────┘     └──────────┘

丹尼尔:听着挺简单的啊,咋就能出事呢?

蛋先生:如果这三步全都由程序员说了算(比如 C/C++ 里的堆内存),那问题就来了。你想想,程序员也是个凡人,他能保证每次都按流程走吗?

丹尼尔:应该……会吧?申请了再读,读完再释放,天经地义啊

蛋先生:( ╯▽╰) 他大概率不是故意的,但架不住写岔了呀!今天咱就按这简单的三步,把这些"翻车现场"挨个捋一遍

内存申请,如果忘记了会怎样?

丹尼尔:那先从第一步——内存申请开始,这步会出现啥问题呢?

蛋先生:如果你直接绕过这一步,你觉得会怎样?

丹尼尔:没申请就用?这也太离谱了吧!

蛋先生:在 C/C++ 里就有这种离谱的现象——野指针的一种来源:未初始化指针。你声明了一个局部的指针变量 int* p;,本意是让它指向一块申请好的堆内存。但你忘了申请,它就装着一个不确定的地址(通常是栈上该位置残留的旧数据)。然后你写 *p = 100;——指向的对象可能不存在,这跟"跳过申请去使用"是同一回事

丹尼尔:那会报错吗?

蛋先生:要看情况!如果那块地址不在当前进程的有效可写内存区域内,就会立刻触发错误崩溃;反之,它会悄悄篡改那块地址上的内存,表面一切正常,过很久才在别处炸开

内存使用,时间空间错了会怎样?

丹尼尔:那要是我申请了,是不是就安全了?

蛋先生:这就得引出两个维度了:空间维度和时间维度

丹尼尔:先说空间维度呗

蛋先生:打个比方,数组就像一排储物格,共 10 个,编号 0 到 9。你在堆上申请了这排格子,指针的起点没错——它就指向第 0 格,稳稳落在已分配的堆区里。但你偏移一算错,代码里写了 scores[10],访问地址直接飞到了格子区外面。偏偏旁边紧挨着的,是别人的格子

┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐        ┌─────┬─────┬─────┬─────┬─────┬─────┐
│ 0   │ 1   │ 2   │ 3   │ 4   │ 5   │ 6   │ 7   │ 8   │ 9   │══════▶│ 10  │ 11  │ 12  │ 13  │ 14  │ 15  │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘        └─────┴─────┴─────┴─────┴─────┴─────┘
   ↑                                                                   ↑                               
   │ scores[0] 指针起点,稳稳落在自己区                                  │ scores[10] 越界!飞进邻居格子区 
   │                                                                   │                               
   你申请的区域:0 ~ 9                                                邻居的区域:10 ~ …

丹尼尔:哇塞!这不是偷窥吗?

蛋先生:还有更厉害的——"越界写"。还是这排格子,你写上 scores[10] = 100 这句代码,就能往邻居格子里乱涂乱画了

丹尼尔:这么狠的吗?

蛋先生:这些操作,程序表面上可能一切正常运行,也可能直接崩溃,这种问题,排查起来能把人逼疯

丹尼尔:好家伙!那时间维度又是咋回事?

蛋先生:这就是大名鼎鼎的数据竞争(Data Race)了。多个线程并发读写同一块内存,其中至少一个是写操作,又没有加锁、也没用原子操作之类的同步手段

丹尼尔:同时抢一块地儿,会怎样?

蛋先生:谁先谁后,全看造化。结果就是程序时好时坏,极难复现咯

内存释放,如果忘记了会怎样?

丹尼尔:那释放这一步,我掐指一算,应该就是忘了释放导致的问题是吧

蛋先生:果然神机妙算!哈哈!这就是"慢性病"——内存泄漏(Memory Leak)。前台登记本上永远标记着"已住",这块地儿就永远空不出来了

丹尼尔:那最后岂不是要爆了?

蛋先生:对喽!程序正常运行,但内存占用持续飙升。直到某天系统忍无可忍,无需再忍,直接给你一个 Out Of Memory,把进程直接带走

释放了,我再访问会怎样?

丹尼尔:还有没有更狠的?

蛋先生:最玄乎的,压轴登场——释放后再访问,专业术语叫 Use After Free(UAF)

丹尼尔:释放了再访问?不就相当于没申请就访问吗?为何单独拎出来讲?

蛋先生:你认为的释放,是不是内存数据被清理了,空间还回去了?

丹尼尔:不然呢?

蛋先生:但实际上,释放只是"逻辑删除",Runtime 只是在"内存管理账本"上改一下空闲标记位

丹尼尔:啊!我都释放了,为啥数据不清理呢?

蛋先生:都是为了性能。数据为啥不擦呢?因为挨个字节擦一遍太费 CPU了,所以 Runtime 干脆懒得擦;那空间为啥不还呢?因为小块内存归还给操作系统也得来回折腾,不如留在手里复用。等下次有人申请到这块地,新数据一写,把旧的直接盖掉就完事了

丹尼尔:这么说,释放后再访问,是可以访问到数据咯?

蛋先生:没错,但访问到什么数据,全看人品!运气好读到旧数据(看着正常),运气差读到新对象的数据(脏数据),运气更差直接崩溃。这是极难排查的"幽灵问题"

丹尼尔:你刚刚提到"内存管理账本",是在哪里实现的?

蛋先生:像 C++ 这种能让程序员显式摸到内存的语言,分配和释放内存的工具都是 Runtime 提供的。所以 Runtime 手里攥着一本"内存账本",记录哪块被占了、哪块空着

丹尼尔:但好像它只记账啊,没啥实质作用。你看,释放时它又不清理数据

蛋先生:没错,这本账只为了指导"内存复用",也就是下次分配时好知道该把哪块地给你

丹尼尔:那读写的时候,它怎么不翻翻账本防着点?

蛋先生:翻账本是要花时间的!So,为了极致性能,读写时根本没有查账本的逻辑,就信程序员手里的指针,指哪打哪

所以,该怎么办?

丹尼尔:哎,照这么看,要想搞好内存安全问题,对程序员的要求还是挺高的啊

蛋先生:所以聪明的语言们就换了个思路——不让程序员显式碰内存!"接管型 Runtime"因此而生

丹尼尔:接管了会怎样?

蛋先生:分配、访问、释放,全由 Runtime 一手包办,程序员压根没有机会跳过分配、越界乱写、忘了释放。这下好了,那些翻车现场理论上就可以从根上就杜绝了

丹尼尔:妙啊!那具体怎么接管的?

蛋先生:嘿嘿,且听下回分解!

写在最后

亲们,都到这了,要不,点赞 / 收藏 / 关注支持下呗 o( ̄▽ ̄)d

目录
相关文章
|
2月前
|
存储 人工智能
你瘦不下来但大模型可以:量化原理了解一下
此量化不是搞钱的量化,是大模型「瘦身」的秘诀。本期聚焦 GPTQ / AWQ / llama.cpp 等主流量化方案的底层基石——对称 INT4 / INT8 量化原理。从 scale 怎么算、到浮点与整数怎么互转,一条公式一条公式推给你看 - 看不懂,算我的 ( ´・ᴗ・ )
101 2
|
SQL 数据库 容器
软件体系结构 - 元组演算
【4月更文挑战第7天】软件体系结构 - 元组演算
472 2
|
设计模式 JSON 架构师
你真的需要防腐层吗?DDD 系统间的7种关系梳理与实践
当提到系统间交互的时候,人们都会想到大名鼎鼎的防腐层,用来防止其他系统的模型变更对本系统造成影响。但是在实践这个模式的过程中,我们常常会遇到问题。此时我们也应该考虑下其他的系统交互方式。
28680 12
你真的需要防腐层吗?DDD 系统间的7种关系梳理与实践
|
12月前
|
机器学习/深度学习 人工智能 索引
RAG 切片利器 LumberChunker 是如何智能地把文档切割成 LLM 爱吃的块
RAG 里的文档应该怎么切割比较好呢?按固定的字符数或词数?按句?按段落?加个重叠窗口?还是 ...
514 1
RAG 切片利器 LumberChunker 是如何智能地把文档切割成 LLM 爱吃的块
|
存储 前端开发 测试技术
DDD - 六边形架构和CQRS架构
DDD - 六边形架构和CQRS架构
2138 0
|
安全 数据库
通过E-R理解 主键和外键的关系
实例 现有课程和教师两个实体,课程实体的属性有课程名称、课程编号、课程属性、考试类型;教师实体的属性包括姓名、工号、职称;一门课程可以有多个教师,且每一位教师可以教授多门课程。教师每教授一门课有课序号。
7810 1
通过E-R理解 主键和外键的关系
|
消息中间件 人工智能 资源调度
云上AI推理平台全掌握 (5):大模型异步推理服务
针对大模型推理服务中“高计算量、长时延”场景下同步推理的弊端,阿里云人工智能平台 PAI 推出了一套基于独立的队列服务异步推理框架,解决了异步推理的负载均衡、实例异常时任务重分配等问题,确保请求不丢失、实例不过载。
|
存储 Linux API
软件体系结构 - 嵌入式系统(2)- 嵌入式操作系统
软件体系结构 - 嵌入式系统(2)- 嵌入式操作系统
709 0
|
自然语言处理 JavaScript Java
《鸿蒙HarmonyOS应用开发从入门到精通(第2版)》学习笔记——HarmonyOS架构介绍
HarmonyOS采用分层架构设计,从下至上分为内核层、系统服务层、框架层和应用层。内核层支持多内核设计与硬件驱动;系统服务层提供核心能力和服务;框架层支持多语言开发;应用层包括系统及第三方应用,支持跨设备调度,确保一致的用户体验。
1603 81
|
存储 自然语言处理 算法
【北京大学 软件工程】四、结构化分析方法
结构化分析方法是一种系统化的软件开发方法学,旨在通过使用问题域术语建立系统的功能模型,以明确“系统必须做什么”。该方法包括结构化分析、设计和程序设计三个主要部分。其核心工具是数据流图(DFD),用于表达系统功能模型,并结合数据字典定义数据流和数据存储。此外,还使用加工小说明(如判定表或判定树)描述加工逻辑。 结构化分析过程遵循自顶向下、逐步求精的原则,首先建立系统环境图确定边界,然后通过分解加工、分派数据流和引入文件来细化模型。整个过程中需确保模型平衡和信息组织的复杂性控制。最终输出为需求规格说明书(SRS),确保需求的正确性、无二义性、完整性和可验证性等特性。
2048 1

热门文章

最新文章