伪随机比特序列:从LFSR构造到随机性验证与工程选型

📅 2026/8/26 8:34:02
伪随机比特序列:从LFSR构造到随机性验证与工程选型
我第一次真正被伪随机比特序列Pseudorandom Bit Sequences逼到认真研究是在做通信系统仿真的时候。当时直接用开发语言自带的rand()函数生成扩频码跑出来的误码率曲线漂亮得不行结果换一台机器重跑所有结果全变了。排查了一整天才意识到问题不在代码而在伪随机这三个字上。伪随机比特序列本质上是用确定性的算法生成一段 0/1 比特流但要求它在统计性质上足够接近真随机掷硬币的结果同时只要种子相同序列就能完整复现。这个特性让它在扩频通信、蒙特卡洛仿真、密码学、随机数测试等领域到处都是。这篇文章就围绕这个主题把构造方法、随机性验证、工程选型和踩过的坑一次讲清楚适合正在搞通信仿真、接触序列设计或者被各种随机数生成器质量折磨过的朋友。1. 为什么工程实践里离不开伪随机比特1.1 真随机的成本困境硬件熵源不是到处都有先问一个基础问题既然要随机为什么不用真随机真随机源确实存在比如放大电阻热噪声、利用二极管雪崩效应或者基于量子过程的物理熵源。但工程里大规模使用真随机源有三个现实障碍。第一是出数速率不够稳定好的硬件熵源每秒能产生几十到几百兆比特已经算不错但通信系统里的扩频码生成、仿真里的海量随机采样往往需要每秒吉比特级别差距明显。第二是不可复现同一个场景测试两次如果用真随机源两次输入完全不一样复现故障、对比算法效果、核对实验数据全都变得困难。第三是成本不是每块板卡都舍得放一颗专用熵源芯片很多嵌入式设备连一个真正的随机源都没有。所以工程中大量使用的是伪随机比特序列。它本质上是把一小段真实熵种子通过确定性算法扩展成一条很长的比特流。种子只有几十位生成的序列却可以有几百万甚至几十亿位这是伪随机生成器最核心的价值用少量的真随机性换取大量统计上足够随机的比特。1.2 伪不代表假伪随机比特序列的两个核心性质很多人一看到伪字就直觉认为不如真随机这是一个常见的误解。伪随机比特序列在设计目标上要求两个性质同时成立缺一不可。第一个性质是统计性上接近真随机。也就是说序列里的 0 和 1 数量要大致均衡连续出现多个相同比特的情况要符合概率规律序列的自相关性要很低不能有明显的周期和模式。如果一个序列能被简单的统计测试找出明显偏差比如 0 占了 70%那就不能叫一个合格的伪随机序列。第二个性质是可复现性。同样的种子任何时候重新生成都能得到完全相同的序列。这一点在通信系统里极其关键CDMA 扩频通信中收发双方只要约定好种子就能独立生成完全一致的伪随机码不需要传输整条码序列。如果用真随机源两端还得额外同步整个码序列通信效率直接崩盘。蒙特卡洛仿真也一样固定种子可以让实验在别人机器上原样重跑这是科研和工程调试的基本要求。这两个性质决定了伪随机序列的使用方式种子当参数序列当资源。1.3 香农视角伪随机与信息熵的关系从信息论的角度看伪随机序列的熵其实很低因为它完全由种子决定内部状态空间有限信息熵不会超过状态空间的对数。比如一个 32 位 LFSR 的状态空间是 2^32它的熵最多就是 32 比特。但它对外表现出的随机性是让不知道种子的观察者无法区分它和真随机。这正是现代密码学里伪随机数生成器PRNG的定义核心如果一个生成器的输出在多项式时间内没有区分器能把它和等长真随机串区分开那么它在计算意义上就是伪随机的。也就是说伪随机比特序列的价值不在于拥有真正的随机性而在于在可接受的计算预算内表现得和真随机一样好。这个视角很重要它把问题从够不够随机转换成了在什么场景下够用后面讲统计测试和密码学安全生成器都是围绕这个视角展开的。2. LFSR与m序列伪随机比特最经典的底层引擎如果只能从零开始掌握一种伪随机比特序列构造方法我会选 LFSR。它结构简单、资源占用少、运行速度快是通信领域里绕不开的基础模块也是理解后面所有进阶设计的地基。2.1 移位寄存器的工作机制从移位到反馈LFSR 全称 Linear Feedback Shift Register线性反馈移位寄存器。它的核心结构是一个 n 位移位寄存器加上若干抽头位的异或反馈逻辑。每个时钟周期寄存器整体右移一位最高位用选定的抽头位异或结果填充。由于比特只有 0 和 1异或就是模 2 加法。以 n4、反馈多项式 x^4 x^3 1 为例抽头对应的寄存器位是第 3 位和第 4 位。给定初始状态 0001每个周期的反馈值等于第 3 位和第 4 位异或。用代码表示更直观def lfsr_step(state, mask, n): # mask 里某一位置1表示该位参与反馈异或 feedback bin(state mask).count(1) 1 state (state 1) | (feedback (n - 1)) return state这里最关键的一点是初始状态不能是全零。如果所有寄存器位都是 0每一步移位和反馈结果都还是 0序列永远停在零状态整个生成器就死了。实际工程中可以通过在种子中加入固定的有效位来保证这一点比如约定种子最高位永远为 1。2.2 反馈多项式与本原多项式为什么能产生最大周期LFSR 的周期完全由反馈多项式的选择决定。n 位寄存器一共有 2^n 个状态去掉全零状态后理论上的最大周期是 2^n - 1。能达到这个最大周期的 LFSR前提是反馈多项式是本原多项式。本原多项式是有限域上的不可约多项式它的根满足特定的阶条件能够产生整个乘法群的非零元素。简单理解本原多项式能让内部状态按顺序遍历除全零外的所有 2^n - 1 个状态之后才回到起点状态转移图是一条完整的环。如果反馈抽头选错了状态转移图会分裂成若干个小环周期会大幅缩短。实际工程中不需要自己推导本原多项式直接查表的场景更多。给一份常用抽头表寄存器位数 n常用本原多项式抽头位置3x^3 x^2 1[3, 2]4x^4 x^3 1[4, 3]5x^5 x^3 1[5, 3]7x^7 x^6 1[7, 6]8x^8 x^6 x^5 x^4 1[8, 6, 5, 4]16x^16 x^14 x^13 x^11 1[16, 14, 13, 11]31x^31 x^28 1[31, 28]32x^32 x^22 x^2 x 1[32, 22, 2, 1]这些值来自常见的工程参考表比如 Xilinx 的应用笔记使用时按表格选抽头基本不会错。我自己的习惯是如果项目对周期要求不苛刻优先选位数适中的表项比如 16 位或 31 位寄存器资源消耗小周期也足够长。这里的为什么值得多说一句本原多项式选择的是反馈抽头而这些抽头本质上是决定状态转移矩阵的结构。只有本原多项式才能保证状态图是一个大环这直接影响序列的随机性和抗截断能力。2.3 最大长度序列的三条经典性质当 LFSR 以本原多项式运行、初始状态非零时它产生的周期为 2^n - 1 的序列叫最大长度序列通常称为 m 序列。m 序列在统计性质上非常漂亮它满足三条经典性质第一是平衡性。在一个完整周期内1 出现的次数比 0 多 1 次。因为非零状态共有 2^n - 1 个每个状态对应一个输出位遍历完全部状态后1 正好多一个。这和理想随机序列0 和 1 各占一半非常接近偏差只有 1/(2^n - 1)。第二是游程特性。所谓游程就是连续相同比特的一段比如 000 就是一个长度为 3 的 0 游程。在 m 序列的一个完整周期里长度为 k 的游程数量占总游程数量的 1/2^k这正好是无偏随机序列的理论分布。第三是自相关特性。周期自相关函数在偏移为 0 时值为 1非零偏移时恒为 -1/NN 是序列周期。这相当于一个近似 Dirac 冲激的函数说明序列和自身错位版本几乎不相关。这个性质在扩频通信里太重要了因为接收机就是靠搜索自相关峰值来完成同步捕获的尖锐的自相关意味着同步点好找、误锁率低。不过这里必须提前打个预防针m 序列有这些优秀的统计性质但它的线性结构非常薄弱。通信领域用它做扩频码没问题但如果想拿它当流密码后面马上会被打穿。3. 从像随机到不可预测进阶序列设计m 序列统计上很漂亮但它有一个致命伤完全线性。这一节要说的就是从统计上随机进化到计算上不可预测的过程这也是伪随机比特序列从通信领域走向密码学领域的关键一步。3.1 线性复杂度为什么m序列挡不住Berlekamp-Massey密码学里衡量一个序列可预测性的重要指标叫线性复杂度定义是生成该序列所需的最短 LFSR 的长度。m 序列的线性复杂度只有 n等于它自己的寄存器长度。也就是说攻击者只要连续观察到 2n 个输出比特就可以用 Berlekamp-Massey 算法在多项式时间内恢复出整个 LFSR 的反馈多项式进而生成后续所有比特。这个过程不复杂LFSR 的输出与内部状态之间是线性关系观察 2n 个连续比特相当于拿到了 2n 个线性方程未知数是反馈系数和状态位高斯消元就能解出来。Berlekamp-Massey 算法只是把这件事做得更高效、更系统化而已。所以在密码学场景下直接拿 m 序列当流密钥流等于裸奔。任何声称我的序列统计测试全过了所以安全的说法都忽略了线性复杂度这个维度。统计测试只能证明看起来均匀不能证明不可预测。3.2 非线性组合生成器打破线性结构既然线性是弱点一个自然的想法是把多个 LFSR 的输出用非线性函数组合起来让输出序列的线性复杂度变大抵抗 Berlekamp-Massey 攻击。这类结构叫非线性组合生成器。最简单的例子是 Geffe 生成器由三个 LFSR 组成输出逻辑为output (L1 AND L2) XOR ((NOT L1) AND L3)当 L1 为 1 时输出 L2当 L1 为 0 时输出 L3。这样输出关于内部状态的表达式是非线性的线性复杂度会比单个 LFSR 高不少。但这里有一个教学里经常强调、工程里则容易忽略的坑非线性组合并不天然等于安全。Geffe 生成器就存在严重的相关泄漏输出与 L2 的相关系数、与 L3 的相关系数都不是 0攻击者可以用统计方法逐段恢复出各 LFSR 的种子。这种攻击叫相关攻击它的存在说明设计非线性组合生成器时必须分析相关免疫性、代数阶等指标不能简单把几个 LFSR 拼起来就认为安全。我在实际项目中的体会是如果是教学或演示用非线性组合生成器展示线性复杂度提升了没问题但如果要做真正的密码产品还是应该选择有严格安全证明的现成算法比如 AES-CTR 或 ChaCha20而不是自己设计组合逻辑。3.3 BBS生成器把安全性建立在因数分解困难上在密码学意义上的伪随机比特生成器里Blum Blum ShubBBS是一个绕不开的经典。它把安全性建立在合数分解的困难性上只要大整数分解问题还算不动输出就与真随机不可区分。算法流程很简单选两个大素数 p 和 q都满足 p ≡ 3 (mod 4)、q ≡ 3 (mod 4)令 n p * q选一个与 n 互素的种子 s计算 x_0 s^2 mod n每次迭代计算 x_{i1} x_i^2 mod n输出比特取 x_{i1} 的最低位。Python 实现非常简洁def bbs_generate(p, q, seed, num_bits): n p * q x pow(seed, 2, n) bits [] for _ in range(num_bits): x pow(x, 2, n) bits.append(x 1) return bits这个生成器的优点是安全论证清晰它的不可区分性直接归约到二次剩余问题和因数分解困难性。缺点是性能太差每一次迭代都是一次大整数模乘生成一个比特就要一次昂贵的运算比现在常用的流密码要慢几个数量级。所以 BBS 在实际系统中很少被直接用作高速随机源更多出现在安全协议的理论框架和密码学课程作业里。我自己在带项目时的一个判断标准是如果你需要向审计方解释这个生成器为什么安全那张安全性归约到因数分解困难问题的证明就是最大的说服力如果只看吞吐量和资源消耗那就老老实实选现代流密码。伪随机比特序列的设计从来不是越复杂越好而是在安全需求与性能约束之间找平衡。4. 判断够不够随机统计测试实战生成器做出来之后怎么验证它够不够随机这是最容易出问题的环节。很多人把测一下 p-value 看是否大于 0.01当成全部实际上统计测试的完整流程比这复杂不少而且测试本身有严格的统计学前提。4.1 p-value到底在说什么NIST SP 800-22 这类统计测试套件的核心逻辑是假设检验。零假设是被检测序列来自理想随机生成器然后对序列计算一个统计量再根据统计量的分布算出 p-value如果序列真的是随机的出现当前统计结果甚至更极端结果的概率是多大。通常取显著性水平 α 0.01。如果 p-value 小于 0.01意味着在序列是随机的前提下出现这种结果的概率不到 1%于是拒绝零假设判定序列非随机。反之如果 p-value 大于等于 0.01则不能拒绝零假设也就是说测试没有发现非随机证据。但要注意不能拿单个序列的单个测试结果下结论。随机性测试的结论是有概率错误的就算序列确实随机100 次测试中出现 1 次 p-value 小于 0.01 也是正常波动这叫第一类错误。正确做法是生成足够多的独立样本序列对每个样本跑测试然后看两个指标样本通过比例是否落在置信区间内以及 p-value 的分布是否均匀。NIST 文档里对这两个指标都给了明确判据这一点后面会展开。4.2 NIST SP 800-22测试清单每个测试在查什么NIST SP 800-22 包含 15 个测试模块每个测试从不同角度检验序列是否偏离理想随机特性。我把最常用的列成一张表测试名称检测重点频数测试Monobit整个序列里 0 和 1 的比例是否足够均衡块内频数测试固定长度块内 1 的比例是否异常游程测试连续相同比特段的分布是否符合随机期望最长连续 1 游程测试块内最长全 1 游程长度是否极端二进制矩阵秩测试固定长度子序列之间是否存在线性依赖离散傅里叶变换测试序列中是否存在过强的周期成分非重叠模板匹配测试某些固定模板是否出现得过于频繁重叠模板匹配测试允许重叠条件下模板频率是否正常通用统计测试基于无损压缩思路检测序列是否可压缩近似熵测试相邻长度模板的频率差异是否异常累积和测试随机游走过程中的最大偏移是否过大随机游动测试随机游走访问特定状态的总次数是否异常串行测试m 位模板的出现频率是否均匀逐个手写全部测试的代码量很大实际项目中通常直接用现成工具比如 GitHub 上开源的 Python 版 NIST 测试实现或者用调用底层统计库的封装。但理解每个测试在查什么是必要的因为当你看到某一项测试失败时需要知道它暗示了序列的哪种结构问题。比如频数测试失败说明 0/1 不均衡离散傅里叶变换测试失败说明有周期性成分线性复杂度测试失败说明序列可能被短 LFSR 逼近。4.3 实际测试中容易误判的情况我见过不少团队在随机性验证上犯的错误这里集中说几个最常见的。第一个是样本量不足就下结论。有人只生成一条 1 万比特的序列跑完 15 项测试全过就宣布生成器随机性没问题。实际上 NIST 推荐每个生成条件测试至少 100 条独立序列每条长度至少 1e6 比特才能对通过率做出有意义的判断。样本太少时测试的统计功效很低很多问题测不出来。第二个是忽略通过率置信区间。假设测试 100 条序列显著性水平 0.01那么通过率至少应该在 99% 附近但允许一点波动。NIST 文档给出了一条经验公式通常要求通过率不低于 96% 才算正常。如果 100 条里只有 90 条通过即使每条单独的 p-value 都大于 0.01整体也说明生成器有偏。第三个是误把连续片段当独立样本。从同一个生成器里连续取 100 段各 1e6 比特如果生成器存在长程相关性这些片段之间并不独立测试结果会失真。正确做法是对不同种子生成 100 条序列或者在不同状态点采样。第四个是只测统计测试不测线性复杂度和不可预测性。前面说过m 序列可以通过大量统计测试但线性复杂度只有 n拿来做密码用途照样被秒破。统计测试只回答像不像随机不回答能不能预测两者是不同维度的问题。5. Python手写一个伪随机比特生成器并完整验证理论聊完直接上手写一个 LFSR 生成器并跑一遍从周期检测到统计验证的完整流程。这一节的所有代码我都按可直接复制运行的方式给出方便你对照复现。5.1 实现一个可配置的LFSR先实现一个可配置的 LFSR 类。构造参数包括寄存器位数、反馈抽头列表和种子并处理种子全零的异常情况import math class LFSR: def __init__(self, n, taps, seed): assert seed ! 0, 种子不能为0否则序列永久停滞在全零 assert seed (1 n), 种子位数超出寄存器容量 self.n n self.mask 0 for t in taps: self.mask | 1 (t - 1) self.state seed self.limit (1 n) - 1 def next_bit(self): feedback bin(self.state self.mask).count(1) 1 self.state ((self.state 1) | (feedback (self.n - 1))) self.limit return feedback def next_bits(self, count): return [self.next_bit() for _ in range(count)]注意bin(...).count(1)用来计算抽头位中 1 的个数奇偶性就是异或结果。这个写法比循环逐位异或更清晰性能也不错。测试时用 n7、抽头 [7, 6]、种子 1周期应该是 2^7 - 1 127lfsr LFSR(7, [7, 6], seed1) bits lfsr.next_bits(500) print(前32位:, bits[:32])5.2 实现周期检测确认最大长度要确认 LFSR 是否达到最大周期可以记录初始状态然后不断产生比特直到状态回到初始状态并发生一次后停表def lfsr_period(n, taps, seed): lfsr LFSR(n, taps, seed) start_state lfsr.state count 0 while True: lfsr.next_bit() count 1 if lfsr.state start_state: return count print(周期:, lfsr_period(7, [7, 6], seed1)) print(理论最大周期:, 2**7 - 1)运行结果应该是 127。如果抽头改成 [7, 5] 这种非本原抽头周期会明显缩短你可以自己试试对比。这个抽头选错导致周期缩水的问题在工程里很难一眼看出来因为统计测试可能仍然通过但系统在某些时刻会突然出现明显的周期性重复。周期检测是排查这类问题的第一步。5.3 实现频数测试和游程测试接下来手写两个最基础的随机性测试频数测试和游程测试。频数测试检查 0/1 数量是否均衡游程测试检查相邻比特翻转次数是否符合预期。def monobit_test(bits): n len(bits) ones sum(bits) s ones - (n - ones) s_obs abs(s) / math.sqrt(n) p_value math.erfc(s_obs / math.sqrt(2)) return p_value def runs_test(bits): n len(bits) ones sum(bits) # 游程测试的前置条件是频数测试通过 if abs(ones - n / 2) / math.sqrt(n) 2: return 0.0 pi ones / n v 1 sum(1 for i in range(1, n) if bits[i] ! bits[i - 1]) numerator abs(v - 2 * n * pi * (1 - pi)) denominator 2 * math.sqrt(2 * n) * pi * (1 - pi) p_value math.erfc(numerator / denominator) return p_value这里math.erfc是互补误差函数NIST 里很多测试的 p-value 最终都归结到 erfc 的计算。调用方式很简单bits LFSR(16, [16, 14, 13, 11], seed0xACE1).next_bits(100000) print(Monobit p-value:, round(monobit_test(bits), 4)) print(Runs p-value:, round(runs_test(bits), 4))对于合格的生成器这两项 p-value 一般都会落在 0.01 到 1 之间。5.4 完整验证一个实际用例并分析结果为了更贴近真实测试流程我按 100 条独立序列、每条 1e6 比特的标准来验证一个 16 位 LFSR统计通过率def evaluate_lfsr(n, taps, num_samples100, seq_len1000000): mono_pass 0 runs_pass 0 for seed in range(1, num_samples 1): lfsr LFSR(n, taps, seed) bits lfsr.next_bits(seq_len) mono_pass monobit_test(bits) 0.01 runs_pass runs_test(bits) 0.01 return mono_pass, runs_pass res evaluate_lfsr(16, [16, 14, 13, 11]) print(Monobit 通过率: {}/100.format(res[0])) print(Runs 通过率: {}/100.format(res[1]))实际跑出来的结果LFSR 的频数测试和游程测试通过率通常都在 96/100 以上说明它在统计上是合格的。但如果你拿同样长度的输出跑一个线性复杂度测试就会发现大量序列的线性复杂度都远低于序列长度的一半这说明它从不可预测性角度完全不合格。这个对比很直观地解释了统计随机和密码安全的区别。如果你手头有 NIST 的完整测试套件也可以把生成的比特写进文件跑全套 15 项测试。对 LFSR 这组序列我可以直接预判大部分统计测试会通过跟手写的两个测试结果一致但重点还是要看结构化弱点而不是只看统计测试的绿灯。6. 工程应用中的选型经验与避坑指南写到最后一部分结合实际项目经验聊选型。伪随机比特序列没有最好的生成器只有在某个场景下最合适的生成器。需求不同选择完全不同。6.1 不同场景该选哪种序列一张选型表应用场景推荐方案核心原因CDMA 扩频 / 同步捕获m 序列、Gold 序列自相关尖锐互相关可控硬件实现成本极低流密码 / 密钥生成AES-CTR、ChaCha20有公开的安全论证软件实现效率高蒙特卡洛仿真MT19937、PCG、xoshiro256**周期长、均匀性好、速度快科研教学演示LFSR、BBS结构简单便于观察性质和验证理论这里展开说一下 Gold 序列。它由两个周期相同的 m 序列按位异或生成选择特定的本原多项式组合可以让不同相位之间的互相关值达到理论下界也就是互相干扰最小。在 CDMA 系统里用户之间靠不同码序列区分m 序列虽然自相关好但两两互相关不可控Gold 序列牺牲了少量自相关性能换来稳定的互相关上界所以多用户场景里更常用。密码学场景则完全不同。AES-CTR 和 ChaCha20 的本质是把密钥扩展成一条不可预测的比特流它们不是传统意义上的序列设计但输出的比特流完全符合伪随机比特序列的定义。它们有密码学界的长期分析和审计出问题被修复的概率比自研方案低得多。6.2 最容易踩的坑把这几年的经验整理成一份避坑清单每一条都对应一个我实际遇到或帮别人定位过的故障。第一LFSR 种子选成全零。这是最隐蔽的低级错误。种子全零时所有状态和输出都是 0序列没有任何随机性但程序不会报错只有跑到统计测试阶段才会发现异常。我的习惯是在初始化代码里直接断言种子非零并且约定高位恒为 1从机制上排除全零状态。第二反馈抽头选错导致周期缩短。抽头不是随便几个位置组合就行必须对应本原多项式。选错之后序列可能看起来还是有随机性但周期只有预期的一小截系统运行到某个时刻后会发现重复模式。排查办法就是做周期检测拿理论周期和实测周期对比。第三把统计测试通过当成密码安全。这一点前面反复强调但工程里依然频繁出现。m 序列可以轻松通过 NIST 大量测试但线性复杂度只有寄存器长度B-M 算法一打就穿。结论很简单做通信扩频可以用 m 序列做密码加密必须用密码学安全生成器。第四连续取段当独立样本。做统计测试时应该用不同种子生成独立序列而不是从同一生成器的长输出里切片段。因为生成器可能存在长程相关连续片段不是独立样本测试的置信度会虚高。第五位序处理不一致。同一套 LFSR 算法在 C 语言和 FPGA Verilog 里实现如果移位方向和抽头编号没有对齐同样的种子生成的序列完全不同。联调时两端生成的码序列对不上通信就建立不起来。项目启动时就把位序约定写清楚可以省掉大量排查时间。6.3 我的实践体会最后分享一点个人经验。我在通信和密码工程里待得越久越觉得伪随机比特序列这个主题最核心的不是某一个算法而是需求分析先问清楚这个场景要的是统计级随机还是密码级随机再决定用哪类生成器。需要可复现的仿真就固定种子需要抗攻击的密钥流就选 AES-CTR 或 ChaCha20需要低资源高速率的扩频码就去查本原多项式表做 m 序列或 Gold 序列。生成器本身不复杂真正容易出问题的是边界条件和验证方法。种子全零、抽头错误、位序不一致、测试样本不足这些问题我都在不同项目里见过。遇到类似问题先别急着换算法回到最基础的周期检测和统计测试往往很快就能定位。伪随机序列设计是一门把细节抠到位的工程原理清楚之后剩下的都是耐心和体系化的验证。