00:00
欢迎来到,大先生说事听不懂,算我的。兄弟姐妹们,今天的任务就是把内存安全翻车现场的罪魁祸首挨个揪出来,看看内存安全问题的根源到底在哪里?那内存安不安全,说白了就是程序怎么用内存的问题。程序用内存就三步,先申请,再读写,最后用完示范。就这么个过程,看着挺简单的,咋就能出示呢?如果这步全都由程序员说了算,比如CCR加里的堆内存,那问题就来了,程序员也是凡人,他大概率不是故意的,但架不住写差了呀。今天咱就按这简单的三步把这些翻车现场挨个捋一遍。对了,先立个俗语约定,接下来说到的没有前缀修饰的乱唱,指的是CC加那种甩手掌柜,只给你提供工具,但不干涉你的逻辑。
01:05
不是这边那种啥都管的接管形状看哦。先从第一步内存申请说起,如果你直接绕过这一步没申请就用,会是什么情况呢?听着有点离谱,但在西西压家里还真有这种现象,就是野指针的一种来源,未初始化指针代码中声明了一个局部变量指针P,本意是想让它指向一块申请好的对内存,但忘了申请,它就装置一个不确定的地址,通常是站上该位置残留的旧数据,然后你往里面写入100这个数据。问题就在于指向的对象可能根本不存在,这跟跳过申请去使用是同一回事,那会报错吗?得看情况,如果那块地址不在当前进程的有效可写内存区域内,立刻就会触发错误,当场崩溃,反之。
02:05
它会悄悄篡改那块地址上的内存,表面一切正常,过很久才在别处炸开,跟个定时炸弹似的,炸不炸,啥时候炸,全凭运气。那申请了是不是就安全了呢?天真的这一步又引出两个维度,空间维度和时间维度。先看空间维度,打个比方,数组就像一排储物格,共10个编号,0~9。你在堆上申请了这排格子指针的起点没做,它就指向地灵格,稳稳的落在已分配的堆区里,但你偏移搞错了,代码里使用了索引时,访问地址直接飞到格子区外面了,偏偏旁边紧挨着的是别人的格子。阅借读是不是有点偷窥的意思?那越界写就更过分了,直接在邻居格子里乱涂乱画了。这些操作程序表面上可能一切正常。
03:05
也可能直接崩溃,这种问题排查起来能把人逼疯,你根本不知道炸点是不是在这。再说时间维度,这就是大名鼎鼎的数据竞争了,多个县城并发读写同一块内存,其中至少一个是写操作,又没家属也能用原子操作之类的同步手坏,同时抢一块地,随先随后,全看造化,结果就是程序时好时坏,极难复现,昨天跑的好好的,今天一跑就崩,你上哪说理去?那示范这一步掐指一算,你也应该算出是忘了示范是吧?这就是慢性病内存泄露,Memory Li前台登记本上永远标记着已注,这块地就永远都空不出来了。那最终结局就是爆了,程序正常运行,但内存占用持续飙升,直到某天系统。
04:05
忍无可忍,无需再忍,直接给你一个auto top memory, 把进城当场带走,还有没有更狠的呢?最玄乎的压轴登场,释放后再访问,专业俗语叫use after free u AF. 你可能觉得释放了再访问不就相当于没申请就访问吗?为啥单独拎出来讲呢?这就得显9这一个认知了。你以为的示范是不是数据被清理了,空间还回去了?但实际上示范只是逻辑删除,乱探只是在内存管理账本上改一下空闲的标志位而已。我猜你可能想问,数据为什么不擦除呢?空间为什么不还呢?答案是为了性能,挨个这节插一遍太费CPU了,所以卵探就懒得插了,手块内存归还给操作系统也得。
05:05
还为枝腾,不如留在手里复用,等下次有人申请到这块地,新数据一写,把旧的直接盖掉完事。所以释放后再访问是能访问到数据的,但访问到什么数据全看人品,运气好读到旧数据看着正常,运气差呢,不到新对象的数据属于脏数据,运气更差的直接崩溃。这是极难排查的幽灵问题。那这个内存管理账本是在哪里实现的呢?像C加加这种能让程序员显示摸到内存的语言,分配和释放内存的工具都是乱time提供的。所以乱time手里揣着一本内存账本,记录着哪块被占了,哪块控制,但这个账本只是为了指导内存复用,下次分配时好知道该把哪块地给你读,写的时候为了极致性能,它根本不翻。
06:05
账本,因为翻账本是要花时间的,所以就信程序员手里的指针指哪打哪。讲到这儿你该明白了,CC加又搞好内存安全问题,对程序员还是要求挺高的,所以聪明的语言们就换了个思路,不让程序员显示泵内存的。于是接管乱探应运而生,分配、访问、释放全由接管型乱探一手包办,程序员压根就没有跳过分配、越界乱写,忘了释放的犯错机会。这下好了,那些翻车现场理论上是可以从根上杜绝的,具体怎么结果的呢?嘿嘿,且听下回分解,拜拜。
我来说两句