随机数真的是随机的么? ---让菠菜、彩票等裤衩赔穿的漏洞

  日常业务中,经常会生成随机数,这里以C++为例,常见的随机数生成方法如下:原理很简单,先生成一个随机数生成器,指定种子,然后生成随机数;

#include <iostream>
#include <windows.h>

int main()
{
    printf("time(0)=%d",time(0));
    srand(0);
    for(int i=0;i<=10;i++){
        printf("%d\n",rand());
    }
    system("pause");
    
}

  根据随机数的定义,每次生成的数不一样才能叫随机数。然而上面这段代码真是这样的么?我们看看效果了:生产的10个数确实不一样啊,这不就是我们想要的效果么? 感兴趣的读者可以自行尝试多生成一些数,看看每次生成的数是不是都不一样!

  

        然而这样就结束了么?图样图森破!同样的代码,我们多运行几次实例看看了,效果如下:不同的时间点我生成了4个不同的实例,然后每次生成实例产生的随机数都是一样的,这是不是很“毁三观”啊!

  

   我明明想生成随机数,为啥每次生成实例产生的数据都是一样的了?换句话说:srand生成随机数的原理是啥了?

   计算机的随机数都是由伪随机数,即是由小M多项式序列生成的,其中产生每个小序列都有一个初始值,即随机种子。(注意: 小M多项式序列的周期是65535,即每次利用一个随机种子生成的随机数的周期是65535,当你取得65535个随机数后它们又重复出现了。)我们知道 rand() 函数可以用来产生随机数,但是这不是真正意义上的随机数,是一个伪随机数,是根据一个数(我们可以称它为种子)为基准以某个递推公式推算出来的一系列数,当这系列数很大的时候,就符合正态公布,从而相当于产生了随机数,但这不是真正的随机数,所以我们运行不同实例时产生的“随机数”居然是一样的,都是相同种子惹的祸!所以我们把种子用当前时间来代替,重新更改后的代码如下:

#include <iostream>
#include <windows.h>

int main()
{
    int seed = time(0);
    printf("seed=%d", seed);
    srand(seed);
    for(int i=0;i<=10;i++){
        printf("%d\n",rand());
    }
    system("pause");
}

  多运行几个实例,每次产生的数确实不一样了:这样就万事大吉、高枕无忧了?

    

     前面说了:相同的种子,产生的伪随机数是一样的,是不是也可以反过来猜想: 根据伪随机数倒推种子了?比如我先在看到的数字是22843、6380、25403、7962,是不是能倒推出种子是16210637719041了?一旦倒推出种子,我是不是也能成功预测第5个数字是13894了?穷举找seed的代码如下:

int num[] = { 22843,6380,25403,7962 };
    int seed = time(0);
    bool bfind = false;
    while (seed--) 
    {
        srand(seed);
        for (int i=0;i<4;i++) 
        {
            if (num[i]!=rand()) 
            {
                bfind = false;
                break;
            }
            bfind = true;
        }
        printf("%d is not the seed,continue!\n", seed);
        if (bfind) 
        {
            printf("find the seed:%d \n", seed);
            break;
        }
           
    }

  一旦找到seed,就能用这个seed继续生成剩下的数字,准确预测了!

  注意:实战时,因为不知道菠菜站点启动服务器的时间,所以这个计算量较大,一般都是用服务器+多线程跑的,这里只是介绍最核心的原理,感兴趣的小伙伴可以自行尝试!

===================================分割线====================================

  怎么才能得到尽可能随机的数了?这就要回到随机数的定义了!我个人简单理解:所谓随机数,就是没有规律、无法精准预测、琢磨不透生成规律的数!为了达到这个效果,这里“不走寻常路”地用多线程(不考虑同步)去读写数据,原因很简单:

  多个线程之间如果不用互斥、信号量等同步,强行在“同一时间”读写同一个内存的数据,那么问题来了:哪个线程先读写?哪个线程后读写?内存被读写后的结果是啥?就我个人浅薄的知识理解,影响最终结果的关键因素如下:

  •   cpu的内部缓存:cpu有3级缓存,每级缓存存放的数据和共享的范围是不一样的,内存数据都会被先读到缓存后再进入cpu的运算单元处理
  •        线程的调度:这个是由操作系统实现的;调度的算法,包括但不限于先进先出、最短耗时任务优先、时间片轮转、最大最小公平、multi-level feedback、多CPU核场景下MFQ等,调度机制异常复杂! 

       机制越复杂,人为预测(甚至是猜测)准确的可能性就越小,这不就是“随机”要达到的效果么?所以这里尝试用多线程读写同一内存的方式生产随机数,核心代码如下:

  

#include <pthread.h>

int num = 0;
pthread_mutex_t mutex;

void* inc_num(void* arg) {
    //pthread_mutex_lock(&mutex);
    for (int i= 0; i <= 10000; i++) {
        num++;
    }
    //pthread_mutex_unlock(&mutex);
    return NULL;
}

void* dec_num(void* arg) {
    //pthread_mutex_lock(&mutex);
    for (int i = 0; i <= 10000; i++) {
        num--;
    }
    //pthread_mutex_unlock(&mutex);
    return NULL;
}

void main() {
    pthread_mutex_init(&mutex,NULL);
    pthread_t thread_id[50];
    for (int i = 0; i < 50; i++) {
        if (i % 2) {
            pthread_create(thread_id + 1, NULL, inc_num, NULL);
        }
        else {
            pthread_create(thread_id + 1, NULL, dec_num, NULL);
        }
    }
    for (int i = 0; i < 50; i++) {
        pthread_join(thread_id[i],NULL);
    }
    printf("num is %d",num);
    pthread_mutex_destroy(&mutex);
}

  代码的原理很简单:生成50个线程,奇数号线程增加num,偶数号线程减少num,但是线程完全不使用同步机制!由于上述的cpu缓存、os线程调度机制很复杂,人为预测准确的概率较小,反正我尝试过多次后还没遇到重复的结果!

 

1、https://www.runoob.com/w3cnote/cpp-rand-srand.html  C++ rand 与 srand 的用法

2、https://zhuanlan.zhihu.com/p/97071815 内核调度算法

posted @ 2021-05-15 15:59  第七子007  阅读(988)  评论(0编辑  收藏  举报