C 语言 rand() 会可能调用 malloc()
文章目录
偶然看到一篇有趣的文章,newlib(用在嵌入式系统的 C 标准库实现) 中使用 rand() 函数有可能触发 malloc() 的调用,这对于内存吃紧的 IoT(Internet of Things) 领域是很大的威胁。感兴趣可以看原文,本文是对其简单介绍。
IoT 领域的内存分配挑战
原文是一个叫 Thingsquare 的 IoT 平台,运行在只有很少内存的设备上,通常是 10 kB~32 kB。
他们的系统有 3 种内存分配机制 (静态内存通常在编译时就分配好了,这部分不重要,算是介绍他们平台的内存机制):
- memb。内存块分配器,是固定大小的静态分配的块。
- mmem。托管内存分配器,动态分配大小的内存块,可以重新从静态内存中分配避免内存碎片。
- bmem。块内存堆分配器,也是从静态内存动态分配大小的内存块。
当然还会有 C 语言的栈内存的分配。
所以在他们的平台上,不会使用 malloc/free 的内存管理机制(动态内存分配,运行时分配),因为这样可能会有未知大小的内存使用出现,以免爆内存。
对于栈内存,因为内存峰值是由运行时决定的(函数调用、局部变量等),所以在程序启动时,先把栈内存用特殊标记填充,当程序运行使用栈内存时,就会被覆盖,这样就能知道内存峰值的大小。
Bug:rand() 触发 malloc()
在这种内存严格管理之下,他们发现一些设备竟然出现预料之外的爆内存。通过添加栈内存分配的测试代码,最终锁定了一个函数 rand(),是用来生成伪随机数的。
于是开始探索 rand() 的实现,他们使用的是开源的 newlib 的标准 C 库。
|
|
源码看起来没什么问题,没什么大型的数组或结构体被创建,也没有递归调用。最后发现问题不是这段代码,而是在调用 rand() 函数的实现上。
原来是新版本的 newlib 库实现有可重入机制(Reentrancy,多个调用在多处理器可以安全并行运行,或一次调用在执行期间中断后返回可以继续安全地执行),可以同时多次地调用函数。
(上面的代码中,由于 seed 会被改变,就处于不稳定状态了,中断后再回来执行就有问题,所以属于不可重入的。)
(可重入代码(Reentry code)也叫纯代码(Pure code)是一种允许多个进程同时访问的代码。为了使各进程所执行的代码完全相同,故不允许任何进程对其进行修改。程序在运行过程中可以被打断,并由开始处再次执行,并且在合理的范围内(多次重入,而不造成堆栈溢出等其他问题),程序可以在被打断处继续执行,且执行结果不受影响。—— 线程安全和可重入的区别)
rand() 的可重入代码是以 C 的宏实现的:
|
|
而在 _REENT_CHECK_RAND48()
宏里面发现了 malloc 的踪迹:
|
|
通过反汇编编译好的代码,他们也发现确实存在对 malloc() 的调用。为了达成可重入的目标,newlib 在初始时使用 malloc() 为随机性分配了状态,以便可以被多次调用。
(题外,newlib 上有注释,rand() 和 srand() 是非线程安全的,而 rand_r() 是线程安全的。)
解决方法很简单,就是不使用 rand() 方法了,自己实现一个靠谱的,然后对生成的二进制做是否存在对 malloc() 函数调用的检查。
参考
文章作者 calssion
上次更新 2022-04-10