Всем привет!

Сам я криптографией не занимаюсь, но кому то вполне может пригодится мое небольшое исследование. Решил разобраться со встроенными в процессор функциями RDRAND и RDSEED. Компилятор Delphi сказал Undeclared identifier. Хмм. Уже давно существует BMI, BMI2, AVX, AVX2 и даже AVX-512, а дельфийцы остановились на SSE4.2. Не беда. Скомпилируем код сами.

Сначала сделал проверку на поддержку данных функций процессором. Конечно же CPUID. Использовать CPUID можно начиная с первых Pentium процессоров. Надеюсь никто не додумается запустить CPUID на 486 машине, ибо ее там еще не было. Кстати RDRAND и RDSEED до процессоров IvyBridge также не существует.

function CPU_support_RDRAND: Boolean;
asm
  mov rax, $01
  cpuid
  test ecx, 40000000h //тестируем 30-й бит
  setne al
end;

function CPU_support_RDSEED: Boolean;
asm
  mov rcx, 0
  mov rax, $07 //страница №7
  cpuid
  test ebx, 40000h //тестируем 18-й бит
  setne al
end;

Оказалось, что мой Core i7 G6950X Extreme поддерживает данные функции. Поэтому дальше решил скомпилировать байт-код вручную. Для опытных приведу код REX и REX.W префиксов. Возможно вы захотите записать результат в другой регистр:

const
  REX_RDRAND32: Byte = $F0; //(11b:REG, 110b:OPCODE, 000b:EAX) 
  REX_RDSEED32: Byte = $F8; //(11b:REG, 111b:OPCODE, 000b:EAX)
  REX_W_RDRAND64: Byte = $48; //(11b:REG, 110b:OPCODE, 000b:RAX)
  REX_W_RDSEED64: Byte = $48; //(11b:REG, 111b:OPCODE, 000b:RAX)

Функции могут работать как в 32-х битном режиме, так и в 64-х битном. Поэтому сделал обе и даже в двух вариантах. В итоге получилось 4 функции:

function RDRand32: DWord;
asm
 @Retry:
  db $0F, $C7, $F0 //RDRAND EAX (CF = 1 говорит о корректности данных)
  jnc @Retry
end;

function RDSeed32: DWord;
asm
 @Retry:
  db $0F, $C7, $F8 //RDSEED EAX (CF = 1 говорит о корректности данных)
  jnc @Retry
end;

function RDRand64: QWord;
asm
  .NOFRAME

 @Retry:
  db $48, $0F, $C7, $F0  //RDRAND RAX (CF = 1 говорит о корректности данных)
  jnc @Retry
end;

function RDSeed64: QWord;
asm
  .NOFRAME

 @Retry:
  db $48, $0F, $C7, $F8 //RDSEED RAX (CF = 1 говорит о корректности данных)
  jnc @Retry
end;

По скорости они медленней, чем библиотечная функция Random. RDRand примерно на 35%, а RDSeed процентов на 50% и даже более, но качество уникальности генерируемых значений значительно выше. На данном ресурсе есть неплохие статьи на эту тему, ну а моя миссия (сделать функции доступными в Delphi) завершена. В Lazarus не тестировал, но скорее всего будет работать без каких либо изменений. В конец объявления функции нужно лишь добавить резервное слово assembler.

Здесь исходные тексты тестового консольного приложения. Там можно найти прототип функций Random32 и Random64 на основе встроенных в процессор. Возможно это то, что вы искали. Всем пока!

Комментарии (15)


  1. mistergrim
    23.02.2019 03:41
    +2

    Надеюсь никто не додумается запустить CPUID на 486 машине, ибо ее там еще не было.

    На каких-то не было, а на каких-то и была.


  1. powerman
    23.02.2019 04:54
    +2

    качество уникальности генерируемых значений значительно выше

    Интересно, кто и как это определил?


    Вообще, как раз для криптографии очень нежелательно использовать один источник случайных чисел, особенно встроенный в процессор. Не знаю, как под виндой, но в линухе используется смесь из нескольких источников, чтобы возможные закладки в любом из них были нивелированы: вышеупомянутый RDRAND плюс события от клавы/мыши/диска/etc.


    1. eDmk Автор
      23.02.2019 11:32

      Эти функции сертифицированы.
      RDRAND: Стандарт NIST SP 800-90A
      RDSEED: Стандарт NIST SP 800-90B and NIST SP800-90C.
      Стандарты конечно не наши, но все же.


      1. powerman
        23.02.2019 16:22
        +1

        И как это отвечает на мой вопрос? Наличие сертификата говорит только о том, что кто-то вложил ресурсы в сертификацию. Сама по себе сертификация качество продукта не изменяет, и ничего не говорит о качестве этого продукта относительно других.


        1. eDmk Автор
          23.02.2019 16:36

          И как это отвечает на мой вопрос?

          Вы пишите: используется смесь из нескольких источников...
          В этой статье как раз описывается как устроены данные функции:
          habr.com/ru/post/128666
          Они тоже берут данные из нескольких источников.


          1. powerman
            23.02.2019 16:58

            Это не имеет значения, потому что работа RDRAND полностью контролируется одной компанией, и провести аудит как она на самом деле работает в конкретном куске кремния — крайне проблематично.


            1. eDmk Автор
              23.02.2019 19:23

              Я вас ни к чему не агитирую.


    1. Nagg
      23.02.2019 16:06

      Была даже петиция на тему убрать rdrand из /dev/random :-) но Торвальдс заявляет что если там есть бэкдор то это никак не повляет т.к используются и другие способы как вы упомянули.


      1. powerman
        23.02.2019 16:23

        Я в курсе. И, на мой взгляд, он абсолютно прав. Что не отменяет возможности того, что RDRAND содержит бэкдор.


  1. scg
    23.02.2019 09:48

    Первый раз вижу инструкцию, которая не возвращает сразу правильное значение. Прям асинхронный ассемблер какой-то получается: пока генерируется случайное число процессор может заняться чем-то еще.


    1. DistortNeo
      23.02.2019 12:37
      +1

      Видимо, вы не застали математический сопроцессор x87 в те времена, когда он был отдельным чипом.


  1. Yermack
    23.02.2019 10:19

    Здраствуйте, увидел у Вас похожие штуки и решил спросить. Где почитать и найти таблицы с объяснениями всех команд, чисто чтоб подобное
    .text
    Filename: In[35]
    pushq %rbp
    movq %rsp, %rbp
    Source line: 1
    mulsd %xmm0, %xmm0
    popq %rbp
    retq
    nopw (%rax,%rax)

    не представлялось какой-то магией, чтоб можно было такую вещь окинуть взором и сделать вывод, де, тут сумма и цикл, можно сделать и лучше


    1. eDmk Автор
      23.02.2019 10:45

      На сайте Intel . А как обычно все делают: опыт — сын ошибок трудных. В вашем случае вроде ассемблер AT&T.


  1. masterspline
    23.02.2019 23:34

    > По скорости они медленней, чем библиотечная функция Random.

    Я написал функцию для заполнения 32-х байт случайными данными. Получилось:
    Haswell 1400 тиков процессора (3.3 гигатика в секунду, Linux на голом железе)
    Sandy Bridge EP 45 тиков (1.7 гигатика, Xeon в VMWare на Aruba)

    Таким образом на разных процессорах разница на 2 порядка (1400 тактов и 45) при разнице в тактовой частоте процессоров всего в 2 раза. Однако, я не знаю, как оценить влияние виртуализации во втором случае (кроме как запустить на голом железе). Так что выкладывайте свои результаты (проц, количество тактов, количество гигатиков по результатам калибровки).

    Код программы
    /*
     * Copyright 2019 Alex Syrnikov <san@masterspline.eu>
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
    
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    // clang++-7 -O3 -Wall -Wextra -pedantic -march=native -mtune=native -o rdrand32bytes ./rdrand.cpp
    
    #include <cstdio>
    #include <cstdint>
    
    #include <chrono>
    #include <thread>
    #include <algorithm>
    
    inline uint64_t rdtsc()
    {
        uint32_t tickl, tickh;
        __asm__ __volatile__("rdtsc":"=a"(tickl),"=d"(tickh));
        return ( static_cast<uint64_t>(tickh) << 32) | tickl;
    }
    
    void calibrate_tsc()
    {
        printf( "tsc calibration\n" );
    
        auto chrono_begin = std::chrono::steady_clock::now();
        uint64_t begin_tsc = rdtsc();
        std::this_thread::sleep_for( std::chrono::milliseconds(100) );
        uint64_t end_tsc = rdtsc();
        auto chrono_end   = std::chrono::steady_clock::now();
        std::chrono::duration<double> diff = chrono_end - chrono_begin;
    
        auto ticks_per_second = static_cast<size_t>((end_tsc-begin_tsc)/diff.count());
        printf( "tsc per second %zd\n", ticks_per_second );
    }
    
    int main()
    {
        calibrate_tsc();
    
        uint64_t begin_tsc, end_tsc;
    
        uint8_t rnd[32];
        std::fill( rnd, rnd+sizeof(rnd), 0 );
    
        begin_tsc = rdtsc();
        for( size_t i = 0; i < sizeof(rnd)/8; ++i )
            __builtin_ia32_rdrand64_step( reinterpret_cast<unsigned long long*>(rnd+8*i) );
        end_tsc = rdtsc();
        printf( "rdrand64 for buffer got %ld ticks\n", end_tsc-begin_tsc );
    
        printf( "rnd:" );
        for( auto el : rnd )
        {
            printf( " %02x", el );
        }
        printf( "\n" );
    
        return 0;
    }
    


    1. eDmk Автор
      24.02.2019 01:04

      Проц Intel i7 G6950X Extreme 3.0ГГц (3.4 ГГц при работе). Разгон до 4 ГГц выключил.
      Монитор откалиброван, а больше мне ничего калибровать не надо.

      Генерация данных:

      Заголовок спойлера
      Всего итераций (x64): 100 000 000
      
      Функция: RDRand32
      Время: 8,47 сек.
      11 807 769,51 в сек.
      
      Функция: RDRand64
      Время: 8,25 сек.
      12 121 212,12 в сек.
      
      Функция: System.Random
      Время: 0,23 сек.
      427 350 427,35 в сек.
      
      Всего итераций (x64): 10 000 000
      
      Функция: RDSeed32
      Время: 9,86 сек.
      1 014 301,65 в сек.
      
      Функция: RDSeed64
      Время: 9,86 сек.
      1 014 198,78 в сек.