偶然看到一篇有趣的文章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 库。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int
rand_r (unsigned int *seed)
{
        long k;
        long s = (long)(*seed);
        if (s == 0)
          s = 0x12345987;
        k = s / 127773;
        s = 16807 * (s - k * 127773) - 2836 * k;
        if (s < 0)
          s += 2147483647;
        (*seed) = (unsigned int)s;
        return (int)(s & RAND_MAX);
}

源码看起来没什么问题,没什么大型的数组或结构体被创建,也没有递归调用。最后发现问题不是这段代码,而是在调用 rand() 函数的实现上。

原来是新版本的 newlib 库实现有可重入机制(Reentrancy,多个调用在多处理器可以安全并行运行,或一次调用在执行期间中断后返回可以继续安全地执行),可以同时多次地调用函数。

(上面的代码中,由于 seed 会被改变,就处于不稳定状态了,中断后再回来执行就有问题,所以属于不可重入的。)

(可重入代码(Reentry code)也叫纯代码(Pure code)是一种允许多个进程同时访问的代码。为了使各进程所执行的代码完全相同,故不允许任何进程对其进行修改。程序在运行过程中可以被打断,并由开始处再次执行,并且在合理的范围内(多次重入,而不造成堆栈溢出等其他问题),程序可以在被打断处继续执行,且执行结果不受影响。—— 线程安全和可重入的区别)

rand() 的可重入代码是以 C 的宏实现的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int
rand (void)
{
  struct _reent *reent = _REENT;

  /* This multiplier was obtained from Knuth, D.E., "The Art of
     Computer Programming," Vol 2, Seminumerical Algorithms, Third
     Edition, Addison-Wesley, 1998, p. 106 (line 26) & p. 108 */
  _REENT_CHECK_RAND48(reent);
  _REENT_RAND_NEXT(reent) =
     _REENT_RAND_NEXT(reent) * __extension__ 6364136223846793005LL + 1;
  return (int)((_REENT_RAND_NEXT(reent) >> 32) & RAND_MAX);
}

而在 _REENT_CHECK_RAND48() 里面发现了 malloc 的踪迹:

1
2
3
4
5
6
7
8
9
/* Generic _REENT check macro.  */
#define _REENT_CHECK(var, what, type, size, init) do { \
  struct _reent *_r = (var); \
  if (_r->what == NULL) { \
    _r->what = (type)malloc(size); \
    __reent_assert(_r->what); \
    init; \
  } \
} while (0)

通过反汇编编译好的代码,他们也发现确实存在对 malloc() 的调用。为了达成可重入的目标,newlib 在初始时使用 malloc() 为随机性分配了状态,以便可以被多次调用。

(题外,newlib 上有注释,rand() 和 srand() 是非线程安全的,而 rand_r() 是线程安全的。)

解决方法很简单,就是不使用 rand() 方法了,自己实现一个靠谱的,然后对生成的二进制做是否存在对 malloc() 函数调用的检查。

参考