略坑的 rand

下午在水群的时候无意看见一位学弟的提问,为什么自己写的C语言程序中前后两次使用rand获取的随机数是相同的。代码嘛具体记不全了,这里就贴一个简化版本吧(可能简化太多了)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>
#include <time.h>

int main()
{
    int a = 0, b = 0;

    srand(time(NULL));
    a = rand() % 100;

    srand(time(NULL));
    b = rand() % 100;

    printf("A is %d, B is %d\n");
    return 0;
}

运行得到以下的结果:

1
A is 46, B is 46

熟悉C语言的朋友应该知道,C的标准库中的rand函数生成随机数(准确讲是伪随机数)使用的是线性同余法,只要指定的种子一样,不管你运行多少次程序,rand得到的数列是一模一样的。通常为了方便获取系统的时间戳当作种子,而时间戳的精度是到秒,很明显运行这几条指令所需的时间远远不及一秒。两个相同的种子,两次rand,理所当然地得到两个相同的“随机数”。解决方案很简单,只初始化一次种子即可。也有群友提出了其它方案,在两个srand中间插入sleep函数,这样完全也可以。

既然提到了随机数,我便开始想别的东西,让我产生随机数我会怎么整呢?由于我是忠实的Linux党,直接从 /dev/random 或 /dev/urandom 读出随机数不好嘛。可能使用的流程会稍稍变复杂一丢丢,打开文件,从中读取若干字节,但我觉得完全没有什么不妥。关于以上提到的两个伪设备,又有很多能扯的地方。有一篇文章很棒,至少讲清了两者的区别还有很多玄学问题:/dev/urandom 不得不说的故事

Windows 下自然有密码学专用的随机数产生函数,不过以 Win32 API 的尿性,没个一堆的参数你别想用它。这里就不提它了,因为还有更好玩的东西。

硬件随机数 RDRAND

之前无意中见到近些年的 CPU 支持硬件随机数指令,Intel 是第三代酷睿架构(IvyBridge),而 AMD 是从2015年之后的产品。这条指令是 RDRAND。调用这个指令只会修改 rax/eax,从理论上讲比那些软件实现的不知道高到哪里去了。

但是有一个很尴尬的问题,该如何调用该指令呢?直接写汇编再编译或者内联汇编都是可行的,该指令的内联汇编看起来是比较复杂的(其实是我比较菜)。后发较新的 GCC 现有现成的库可以使用,那为什么不调库呢?写了一个 demo 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>
#include <immintrin.h>

int main()
{
  int a = 0, b = 0;
  printf("Before rdrand\n");
  printf("A is %d, B is %d\n", a, b);

  _rdrand32_step(&a);
  _rdrand32_step(&b);
  printf("After rdrand\n");
  printf("A is %d, B is %d\n", a, b);
}

这样看起来十分简单是不是,根据产生的随机数的长度不同,共有三个不同的函数(省略了很多的 GCC 的特有标识,以便观察):

1
2
3
int _rdrand16_step(unsigned short *__p);
int _rdrand32_step(unsigned int *__p);
int _rdrand64_step(unsigned long long *__p);

但是编译却遇到一些问题:

1
2
3
4
5
6
7
8
gen_rand.c: In function ‘main’:
/usr/lib/gcc/x86_64-pc-linux-gnu/9.2.1/include/immintrin.h:168:1: error: inlining failed in call to always_inline ‘_rdrand32_step’: target specific option mismatch
  168 | _rdrand32_step (unsigned int *__P)
      | ^~~~~~~~~~~~~~
gen_rand.c:11:3: note: called from here
   11 |   _rdrand32_step(&b);
      |   ^~~~~~~~~~~~~~~~~~

看着有点懵,直接问 google,找到一个类似的,只要指定处理器架构就好了(当年天真的以为只有 SIMD 指令和架构相关,太 Native 了!!!),我编译用的指令为:

1
gcc gen_rand.c -o gen_rand -march=skylake

由于我的处理器是六代酷睿,就指定了 skylake,其实指定为 lvybridge 之后的版本都是可以滴。运行,结果如下:

1
2
3
4
Before rdrand
A is 0, B is 0
After rdrand
A is 1445042752, B is 1967589868

算成成功了。突然又有一个大胆的想法,把这个程序丢在不支持的处理器上运行会怎样呢,好奇ing。。。

越看这个硬件随机数感觉越厉害,原理大致是利用电阻热噪声取得硬件真随机数。但为什么 C 的标准库还坚持用那么 Low 的实现呢。旧硬件不支持,算是吧。又 baidu 了一会,发现 AMD 一直在这个指令上翻车。先是老 APU A6-6310 只要从休眠或睡眠状态唤醒,产生的随机数就不那么随机了,不但如此还会导致无法再次进入休眠或睡眠状态;后有 Ryzen 3000 系列坚持 0xffffffff 是随机度最高的,质量最好的随机数。

额,都是 AMD 的锅,我们天天 Yes 支持你也要抓紧修复 Bug 啊。可见硬件随机数发生器也是存在不少问题的,Linux 内核不从 RDRAND 获取看来也是大有原因。最后一个问题,世界上真的有真随机数吗(陷入沉思),你怎么看呢?

参考

AMD五年前老APU被发现Bug:休眠恢复后随机数生成错误

血泪控诉:曝光数月的 AMD 微代码 bug 毁掉了我的周末