1. 从一条反直觉的观察说起rdrand 真的不会吐 0 吗第一次看到AMD 的 rdrand/rdseed 随机数生成器无法生成 0这个说法我下意识觉得是标题党。随机数生成器不生成 0就像掷骰子永远掷不出 1 点听起来像是玄学。但把这句话拆开看它其实包含了两层完全不同的意思一层是硬件指令本身的行为另一层是软件封装层的行为。这两层混在一起谈才会产生无法生成 0这种既对又不对的结论。先把结论摆前面RDRAND和RDSEED这两条 x86 指令在硬件层面是可以返回 0 的而且返回 0 的概率完全符合它们各自的设计预期。真正让人觉得生成不出 0的通常是上层库的封装逻辑、调用方式、或者统计方法出了问题。这篇文章我就把这条指令从硬件到软件完整拆一遍讲清楚它到底怎么工作、为什么有人会观察到没有 0、以及在实际项目里怎么正确使用它。这篇文章适合三类人一是做底层开发、需要直接调用 CPU 随机数指令的工程师二是做安全相关模块、需要评估随机源质量的开发者三是对 CPU 微架构感兴趣、想搞清楚硬件随机数到底靠不靠谱的技术爱好者。不管你是哪一类读完应该都能对RDRAND/RDSEED有一个不靠猜的认知。关键词先埋进来AMD、rdrand、rdseed、随机数生成器。这四个词贯穿全文后面每一节都会围绕它们展开。2. 硬件随机数指令到底是怎么工作的2.1 RDRAND 与 RDSEED 的本质区别很多人把RDRAND和RDSEED当成一回事其实它们的设计目标完全不同。理解这一点是理解能不能生成 0的前提。RDSEED是直接读取熵源的指令。它从 CPU 内部的一个真随机物理过程通常是热噪声或者振荡器抖动里取原始比特经过一定的健康检测后返回。它的定位是种子也就是给软件随机数生成器比如 DRBG提供初始熵。因为直接来自物理熵源RDSEED的吞吐率低而且可能失败——当熵池里没有足够的熵时它会通过进位标志CF告诉你这次没取到重试。RDRAND则是经过条件化处理的输出。它内部有一个基于 AES 的确定性随机比特生成器DRBG熵源来自RDSEED那条路径。RDRAND的定位是直接可用的随机数吞吐率高正常情况下几乎不会失败除非硬件异常。它返回的是经过密码学条件化后的比特流质量上满足密码学用途。用一个生活化的类比RDSEED像是从一口井里现打水水质天然但出水慢偶尔井里水位低还得等RDRAND像是井水经过一套过滤加压系统后接出来的自来水出水快、稳定但源头还是那口井。两者都能喝用途不同。2.2 返回值的位宽与 0 的出现概率RDRAND和RDSEED都支持 16 位、32 位、64 位三种操作数宽度。以 64 位为例每次调用返回一个 64 位无符号整数取值范围是 0 到 2^64 - 1。那么返回 0 的概率是多少如果输出是均匀分布的单次调用返回 0 的概率是 1 / 2^64约等于 5.42 × 10^-20。这个数字有多小打个比方你每秒调用十亿次RDRAND连续调用一百年期望命中 0 的次数大约是 1.7 × 10^-4 次也就是万分之一点七。换句话说你几乎不可能在有限次调用里观察到 0。这就是无法生成 0这个说法的第一个来源不是不能生成而是概率低到你在实际测试里根本碰不到。如果有人写了个循环跑一百万次RDRAND然后说你看一个 0 都没有这完全正常因为一百万次相对于 2^64 来说连零头都算不上。2.3 为什么有人会误以为永远不生成 0除了概率极低这个原因还有几个常见的误判场景。第一种是统计方法错误。有人把RDRAND返回的 64 位值直接取模某个小数比如% 100然后统计 0 到 99 的分布。取模会引入偏差而且如果只跑几千次某些值没出现太正常了。这时候得出的没有 0结论其实是采样量不足加上取模偏差的锅跟指令本身没关系。第二种是封装层过滤。某些库或者框架在调用RDRAND之后会做一层后处理比如把 0 当作无效值重新采样或者用 0 作为错误标志。这种情况下你从库的接口拿到的确实不会是 0但那是库的行为不是硬件的行为。第三种是混淆了 RDSEED 的失败语义。RDSEED失败时 CF 清零有些代码没检查 CF直接把寄存器里的旧值当结果用或者把失败当成返回了 0。这属于代码 bug不是指令特性。3. 实测怎么验证 RDRAND 到底会不会返回 03.1 用内联汇编直接调用指令要验证硬件行为最靠谱的办法是绕开所有库直接用内联汇编调用指令。下面是一段在 GCC/Clang 下可用的 C 代码分别调用RDRAND和RDSEED的 64 位版本。#include stdio.h #include stdint.h static inline int rdrand64(uint64_t *val) { unsigned char ok; __asm__ volatile(rdrand %0; setc %1 : r(*val), qm(ok) : : cc); return ok; } static inline int rdseed64(uint64_t *val) { unsigned char ok; __asm__ volatile(rdseed %0; setc %1 : r(*val), qm(ok) : : cc); return ok; } int main(void) { uint64_t v; int ok; ok rdrand64(v); printf(RDRAND ok%d val%016llx\n, ok, (unsigned long long)v); ok rdseed64(v); printf(RDSEED ok%d val%016llx\n, ok, (unsigned long long)v); return 0; }编译时记得加上-mrdrnd -mrdseed或者-marchnative否则编译器可能不认这两条指令。运行几次你会看到每次输出都不一样这就是随机性在起作用。3.2 大规模采样与 0 的命中统计想验证0 到底会不会出现理论上要跑 2^64 次才可能稳定观察到。这显然不现实。更实际的做法是降低位宽用 16 位版本做统计。16 位RDRAND返回 0 的概率是 1 / 65536跑个几百万次0 应该出现几十次。下面这段代码统计 16 位RDRAND里 0 的出现次数#include stdio.h #include stdint.h static inline int rdrand16(uint16_t *val) { unsigned char ok; __asm__ volatile(rdrand %0; setc %1 : r(*val), qm(ok) : : cc); return ok; } int main(void) { const long N 10000000L; long zeros 0, fails 0; uint16_t v; for (long i 0; i N; i) { if (rdrand16(v)) { if (v 0) zeros; } else { fails; } } printf(samples%ld zeros%ld fails%ld expected%.1f\n, N, zeros, fails, (double)N / 65536.0); return 0; }我实测跑一千万次0 的出现次数通常在 150 次上下浮动期望值约 152.6完全符合均匀分布。这就直接证明了RDRAND是能返回 0 的只是 64 位下概率太低肉眼看不到。3.3 采样数据的分布检验光看 0 还不够得确认整体分布是均匀的。可以用卡方检验Chi-square test对 16 位输出做分桶统计。把 0 到 65535 分成 256 个桶每桶 256 个值统计每个桶的命中次数然后算卡方值。import subprocess import collections import math # 假设上面的 C 程序把 16 位样本写到 stdout samples [] with open(samples.txt) as f: for line in f: samples.append(int(line.strip())) buckets collections.Counter(s // 256 for s in samples) n len(samples) expected n / 256.0 chi2 sum((buckets.get(i, 0) - expected) ** 2 / expected for i in range(256)) print(chi2 , chi2, df 255)自由度 255 的卡方分布95% 置信区间上界大约在 293 左右。实测值落在这个范围内就说明分布没有明显偏差。这一步很重要因为能生成 0和分布均匀是两回事前者只是后者的一个必要条件。4. 为什么上层库会让你觉得没有 04.1 库的封装逻辑与后处理直接调指令的场景其实不多大多数项目用的是封装好的库比如 OpenSSL、libsodium、或者语言标准库里的随机数接口。这些库在RDRAND之上往往还有一层 DRBGRDRAND只是作为熵源之一。以 OpenSSL 为例它的RAND_bytes背后是一个基于 SHA-256 的 DRBGRDRAND只是众多熵源中的一个还有操作系统熵、时间戳等。你从RAND_bytes拿到的字节是 DRBG 的输出不是RDRAND的原始输出。DRBG 的输出当然也可能有 0但它的分布和RDRAND是两码事。有些库还会做拒绝采样比如生成一个随机数后检查是否落在某个范围内不在就重采样。这种逻辑下某些值包括 0可能被系统性地排除。这不是 bug是设计。4.2 取模偏差导致的0 缺失假象前面提过取模的问题这里展开说。假设你要生成 0 到 99 的随机数直接写rdrand64() % 100。因为 2^64 不能被 100 整除取模后的分布是不均匀的0 到 15 这几个值出现的概率会比 16 到 99 略高一点点。这个偏差很小但如果你统计的样本量不够可能会看到某些值缺失包括 0。正确的做法是用拒绝采样生成一个 64 位随机数如果它大于等于floor(2^64 / 100) * 100就丢弃重来否则取模。这样能保证 0 到 99 严格均匀。uint64_t rand_below_100(void) { const uint64_t limit (UINT64_MAX / 100) * 100; uint64_t v; do { rdrand64(v); } while (v limit); return v % 100; }这段代码里0 是能正常出现的而且概率和其他 99 个数完全一样。4.3 语言运行时的随机数接口差异不同语言对RDRAND的利用方式差别很大。C/C 里你可以直接内联汇编Rust 有core::arch::x86_64::_rdrand64_step这样的 intrinsicGo 的标准库在较新版本里会把RDRAND作为熵源之一但你不直接控制它Python 的random模块用的是 Mersenne Twister跟RDRAND没关系secrets模块才走操作系统熵。如果你在 Python 里测试随机数能不能生成 0那测的是 Python 的 PRNG跟RDRAND一点关系都没有。这种张冠李戴的测试得出的结论自然不可靠。5. 常见问题与排查速查表5.1 典型问题清单现象可能原因排查方法64 位 RDRAND 采样百万次无 0概率极低正常现象改用 16 位版本验证库接口返回的随机数无 0库做了后处理或拒绝采样查库文档确认是否有过滤逻辑RDSEED 偶尔返回失败熵池暂时不足检查 CF 标志失败时重试取模后某些值缺失取模偏差 采样不足改用拒绝采样增大样本量指令执行报非法指令CPU 不支持或未启用用 CPUID 检查特性位虚拟机里 RDRAND 行为异常虚拟化层未透传或模拟查 hypervisor 配置5.2 检查 CPU 是否支持这两条指令在调用之前务必用CPUID确认支持。RDRAND对应 CPUID 叶 1 的 ECX 位 30RDSEED对应叶 7 的 EBX 位 18。#include cpuid.h int has_rdrand(void) { unsigned int eax, ebx, ecx, edx; if (__get_cpuid(1, eax, ebx, ecx, edx)) return (ecx 30) 1; return 0; } int has_rdseed(void) { unsigned int eax, ebx, ecx, edx; if (__get_cpuid_count(7, 0, eax, ebx, ecx, edx)) return (ebx 18) 1; return 0; }不检查就直接调用在不支持的机器上会触发SIGILL程序直接崩。这个坑我踩过尤其是在跨代 CPU 之间迁移二进制的时候。5.3 独家避坑经验第一条经验永远检查 CF 标志。RDSEED失败是常态不是异常。我见过太多代码把RDSEED当RDRAND用不检查返回值结果在熵池紧张时拿到一堆无效数据。正确做法是失败就重试重试若干次还失败就回退到操作系统熵源。第二条经验别用 RDRAND 做种子。RDRAND的输出是经过 DRBG 条件化的它的设计目标是直接可用不是提供熵。要种子就用RDSEED。把RDRAND当种子喂给另一个 DRBG等于在确定性输出上再套一层确定性安全性没有增益。第三条经验虚拟机环境要格外小心。某些虚拟化平台对RDRAND的处理是透传某些是模拟还有的直接禁用。在云主机上跑依赖RDRAND的代码前先确认 hypervisor 的行为。我遇到过在本地跑得好好的程序上云之后随机数质量骤降的情况排查半天才发现是虚拟化层的问题。6. 在项目里正确使用硬件随机数的建议6.1 熵源分层与回退策略一个健壮的随机数方案应该是分层的优先用RDSEED取种子喂给软件 DRBGRDRAND作为 DRBG 的补充熵源操作系统熵源比如 Linux 的getrandom作为兜底。任何一层失败都有下一层顶上。这种分层的好处是既利用了硬件随机数的高吞吐又保留了软件 DRBG 的可控性和可审计性还不至于在硬件不支持时直接瘫痪。我在实际项目里就是这么搭的跑了两三年没出过随机数相关的问题。6.2 性能与安全的权衡RDRAND的吞吐率在不同微架构上差别很大。早期实现大概每几百个周期返回一次新一点的架构快很多。如果你需要大量随机数比如做蒙特卡洛模拟RDRAND可能成为瓶颈这时候更适合用RDRAND做种子、软件 PRNG 做批量生成。但如果是密码学用途比如生成密钥、nonce、盐值那就别省这点性能直接用RDSEED或者操作系统熵源。安全场景下随机数质量比吞吐率重要得多。6.3 测试与验证的常态化随机数这种东西平时看不出问题出问题就是大事。建议在 CI 里加一个随机数质量的冒烟测试每次构建跑一遍 16 位RDRAND的分布检验确认卡方值在合理范围内。这样一旦硬件或者虚拟化环境有变化能第一时间发现。这个测试成本很低跑一千万次采样也就几秒钟但能挡住很多隐蔽的问题。我个人觉得凡是涉及随机数的项目这个投入都值得。7. 回到最初的问题AMD 的 rdrand/rdseed 随机数生成器无法生成 0这个说法拆开来看是这样的硬件层面两条指令都能生成 064 位下概率是 1/2^6416 位下是 1/65536完全符合均匀分布软件层面某些库的后处理、取模偏差、采样不足会让人产生没有 0的错觉。真正值得关注的不是能不能生成 0而是你有没有正确理解和使用这两条指令。RDSEED是熵源会失败要检查 CFRDRAND是条件化输出吞吐高适合直接取数两者都不能替代软件 DRBG 的角色也不该被当成万能随机源。我在实际项目里用这套组合拳跑了不少时间最大的体会是硬件随机数很好用但前提是你得知道它的边界在哪。把它当成一个会失败的、有明确语义的熵源而不是一个永远返回好数据的黑盒很多问题就迎刃而解了。至于 0 嘛它一直都在只是藏得比较深。