Введение.


Все, кто хорошо знаком с современным С++ слышал, что начиная с С++11 в стандарт был введен спецификатор constexpr, при помощи которого можно проводить лимитированные compile-time вычисления. В последующие стандарты были добавлены if constexpr и constexpr lambdas которые в некоторой степени снимают ограничения и помогают писать код с compile time вычислениями. Сегодня мы поговорим о случайной кодогенерации в compile time.

Выбираем базу для получения «случайных» compile-time значений


Для того чтобы выбирать конкретный path для кода в compile time, нужен constexpr ГПСЧ c хорошими выходными данными, для этого я выбрал генератор xorshift, кстати xorshift проходит эмпирические тесты BigCrush (TestU01).

xorshift256++
#include <stdint.h>

/* This is xoshiro256++ 1.0, one of our all-purpose, rock-solid generators.
   It has excellent (sub-ns) speed, a state (256 bits) that is large
   enough for any parallel application, and it passes all tests we are
   aware of.

   For generating just floating-point numbers, xoshiro256+ is even faster.

   The state must be seeded so that it is not everywhere zero. If you have
   a 64-bit seed, we suggest to seed a splitmix64 generator and use its
   output to fill s. */

static inline uint64_t rotl(const uint64_t x, int k) {
	return (x << k) | (x >> (64 - k));
}


static uint64_t s[4];

uint64_t next(void) {
	const uint64_t result = rotl(s[0] + s[3], 23) + s[0];

	const uint64_t t = s[1] << 17;

	s[2] ^= s[0];
	s[3] ^= s[1];
	s[1] ^= s[2];
	s[0] ^= s[3];

	s[2] ^= t;

	s[3] = rotl(s[3], 45);

	return result;
}


/* This is the jump function for the generator. It is equivalent
   to 2^128 calls to next(); it can be used to generate 2^128
   non-overlapping subsequences for parallel computations. */

void jump(void) {
	static const uint64_t JUMP[] = { 0x180ec6d33cfd0aba, 0xd5a61266f0c9392c, 0xa9582618e03fc9aa, 0x39abdc4529b1661c };

	uint64_t s0 = 0;
	uint64_t s1 = 0;
	uint64_t s2 = 0;
	uint64_t s3 = 0;
	for(int i = 0; i < sizeof JUMP / sizeof *JUMP; i++)
		for(int b = 0; b < 64; b++) {
			if (JUMP[i] & UINT64_C(1) << b) {
				s0 ^= s[0];
				s1 ^= s[1];
				s2 ^= s[2];
				s3 ^= s[3];
			}
			next();	
		}
		
	s[0] = s0;
	s[1] = s1;
	s[2] = s2;
	s[3] = s3;
}



/* This is the long-jump function for the generator. It is equivalent to
   2^192 calls to next(); it can be used to generate 2^64 starting points,
   from each of which jump() will generate 2^64 non-overlapping
   subsequences for parallel distributed computations. */

void long_jump(void) {
	static const uint64_t LONG_JUMP[] = { 0x76e15d3efefdcbbf, 0xc5004e441c522fb3, 0x77710069854ee241, 0x39109bb02acbe635 };

	uint64_t s0 = 0;
	uint64_t s1 = 0;
	uint64_t s2 = 0;
	uint64_t s3 = 0;
	for(int i = 0; i < sizeof LONG_JUMP / sizeof *LONG_JUMP; i++)
		for(int b = 0; b < 64; b++) {
			if (LONG_JUMP[i] & UINT64_C(1) << b) {
				s0 ^= s[0];
				s1 ^= s[1];
				s2 ^= s[2];
				s3 ^= s[3];
			}
			next();	
		}
		
	s[0] = s0;
	s[1] = s1;
	s[2] = s2;
	s[3] = s3;
}


Из этого нам понадобятся только функции rotl и next, чтобы не делать вычисления слишком долгими. Передаю пламенный привет разработчикам компилятора MSVC, ввиду багов компилятора которые обнаружились в процессе написания, об этом будет сказано ниже.

Как видно из кода выше, нам нужно четыре 64 битных статических переменных для хранения состояния, для того чтобы данный код работал в compile time, нужно от них избавиться передавая случайное состояние в качестве параметров. Организовать это можно используя constexpr hash fnv1 и макрос __COUNTER__ , перепишем код в соответствии с этим и получим следующее:

compile time 'random'
#include <cstdint>
#define STRING(s) #s

// an enumeration can also be used here
template <typename T, T value>
constexpr T ensure_constexpr() {
  return value;
}
#define CONSTEXPR(x) ensure_constexpr<decltype(x), x>()

constexpr uint64_t fnv1impl(uint64_t h, const char* s) {
  return (*s == 0)
             ? h
             : fnv1impl((h * 1099511628211ull) ^ static_cast<uint64_t>(*s),
                        s + 1);
}

constexpr uint64_t fnv1(const char* s) {
  return fnv1impl(14695981039346656037ull, s);
}

template <uint64_t n>
constexpr uint64_t get_seed(const uint64_t x,
                            const uint64_t y = CONSTEXPR(fnv1(STRING(n)))) {
  return x ^ y * n;
}

#define SEED CONSTEXPR(get_seed<__COUNTER__ + 1>(fnv1(__TIME__)))

//! Rotate left by
constexpr uint64_t rotl(uint64_t x, int k) {
  return (x << k) | (x >> (64 - k));
}

//! XorShift 256 compile time random implementation
template <uint64_t a, uint64_t b, uint64_t c, uint64_t d>
constexpr uint64_t xorshift256_next() {
  uint64_t s[4] = {a, b, c, d};
  const uint64_t t = s[1] << 17;

  s[2] ^= s[0];
  s[3] ^= s[1];
  s[1] ^= s[2];
  s[0] ^= s[3];

  s[2] ^= t;

  s[3] = rotl(s[3], 45);

  return rotl(s[0] + s[3], 23) + s[0];
}

#define RND                                                           xorshift256_next<                                                       CONSTEXPR(SEED + __COUNTER__), CONSTEXPR(SEED + __COUNTER__),       CONSTEXPR(SEED + __COUNTER__), CONSTEXPR(SEED + __COUNTER__)>()


Под капотом


Теперь нам нужно убедиться, что компиляторы действительно «сворачивают» код в compile time.
Собираем код ниже используя GCC 9.2.1 в дебаге g++ _.cpp -S -masm=intel

#include "compile_random.h"
#include <iostream>

int main() {
  std::cout << RND << std::endl;
  std::cout << RND << std::endl;
}

Хочу видеть страшилки
.file "_.cpp"
.intel_syntax noprefix
.text
.section .text._Z4rotlmi,"axG",@progbits,_Z4rotlmi,comdat
.weak _Z4rotlmi
.type _Z4rotlmi, @function
_Z4rotlmi:
.LFB4:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
mov QWORD PTR [rbp-8], rdi
mov DWORD PTR [rbp-12], esi
mov eax, DWORD PTR [rbp-12]
mov rdx, QWORD PTR [rbp-8]
mov ecx, eax
rol rdx, cl
mov rax, rdx
pop rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE4:
.size _Z4rotlmi, .-_Z4rotlmi
.section .rodata
.type _ZStL19piecewise_construct, @object
.size _ZStL19piecewise_construct, 1
_ZStL19piecewise_construct:
.zero 1
.local _ZStL8__ioinit
.comm _ZStL8__ioinit,1,1
.text
.globl main
.type main, @function
main:
.LFB1528:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
call _Z16xorshift256_nextILm5485350875583313748ELm17102255921242957812ELm10344467890162128540ELm2938160962983216444EEmv
mov rsi, rax
mov edi, OFFSET FLAT:_ZSt4cout
call _ZNSolsEm
mov esi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
mov rdi, rax
call _ZNSolsEPFRSoS_E
call _Z16xorshift256_nextILm14555066043003024868ELm7653162274090525828ELm823244081203417900ELm11935748167621739468EEmv
mov rsi, rax
mov edi, OFFSET FLAT:_ZSt4cout
call _ZNSolsEm
mov esi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
mov rdi, rax
call _ZNSolsEPFRSoS_E
mov eax, 0
pop rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1528:
.size main, .-main
.section .text._Z16xorshift256_nextILm5485350875583313748ELm17102255921242957812ELm10344467890162128540ELm2938160962983216444EEmv,"axG",@progbits,_Z16xorshift256_nextILm5485350875583313748ELm17102255921242957812ELm10344467890162128540ELm2938160962983216444EEmv,comdat
.weak _Z16xorshift256_nextILm5485350875583313748ELm17102255921242957812ELm10344467890162128540ELm2938160962983216444EEmv
.type _Z16xorshift256_nextILm5485350875583313748ELm17102255921242957812ELm10344467890162128540ELm2938160962983216444EEmv, @function
_Z16xorshift256_nextILm5485350875583313748ELm17102255921242957812ELm10344467890162128540ELm2938160962983216444EEmv:
.LFB1790:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
sub rsp, 48
movabs rax, 5485350875583313748
mov QWORD PTR [rbp-48], rax
movabs rax, -1344488152466593804
mov QWORD PTR [rbp-40], rax
movabs rax, -8102276183547423076
mov QWORD PTR [rbp-32], rax
movabs rax, 2938160962983216444
mov QWORD PTR [rbp-24], rax
mov rax, QWORD PTR [rbp-40]
sal rax, 17
mov QWORD PTR [rbp-8], rax
mov rdx, QWORD PTR [rbp-32]
mov rax, QWORD PTR [rbp-48]
xor rax, rdx
mov QWORD PTR [rbp-32], rax
mov rdx, QWORD PTR [rbp-24]
mov rax, QWORD PTR [rbp-40]
xor rax, rdx
mov QWORD PTR [rbp-24], rax
mov rdx, QWORD PTR [rbp-40]
mov rax, QWORD PTR [rbp-32]
xor rax, rdx
mov QWORD PTR [rbp-40], rax
mov rdx, QWORD PTR [rbp-48]
mov rax, QWORD PTR [rbp-24]
xor rax, rdx
mov QWORD PTR [rbp-48], rax
mov rax, QWORD PTR [rbp-32]
xor rax, QWORD PTR [rbp-8]
mov QWORD PTR [rbp-32], rax
mov rax, QWORD PTR [rbp-24]
mov esi, 45
mov rdi, rax
call _Z4rotlmi
mov QWORD PTR [rbp-24], rax
mov rdx, QWORD PTR [rbp-48]
mov rax, QWORD PTR [rbp-24]
add rax, rdx
mov esi, 23
mov rdi, rax
call _Z4rotlmi
mov rdx, QWORD PTR [rbp-48]
add rax, rdx
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1790:
.size _Z16xorshift256_nextILm5485350875583313748ELm17102255921242957812ELm10344467890162128540ELm2938160962983216444EEmv, .-_Z16xorshift256_nextILm5485350875583313748ELm17102255921242957812ELm10344467890162128540ELm2938160962983216444EEmv
.section .text._Z16xorshift256_nextILm14555066043003024868ELm7653162274090525828ELm823244081203417900ELm11935748167621739468EEmv,"axG",@progbits,_Z16xorshift256_nextILm14555066043003024868ELm7653162274090525828ELm823244081203417900ELm11935748167621739468EEmv,comdat
.weak _Z16xorshift256_nextILm14555066043003024868ELm7653162274090525828ELm823244081203417900ELm11935748167621739468EEmv
.type _Z16xorshift256_nextILm14555066043003024868ELm7653162274090525828ELm823244081203417900ELm11935748167621739468EEmv, @function
_Z16xorshift256_nextILm14555066043003024868ELm7653162274090525828ELm823244081203417900ELm11935748167621739468EEmv:
.LFB1794:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
sub rsp, 48
movabs rax, -3891678030706526748
mov QWORD PTR [rbp-48], rax
movabs rax, 7653162274090525828
mov QWORD PTR [rbp-40], rax
movabs rax, 823244081203417900
mov QWORD PTR [rbp-32], rax
movabs rax, -6510995906087812148
mov QWORD PTR [rbp-24], rax
mov rax, QWORD PTR [rbp-40]
sal rax, 17
mov QWORD PTR [rbp-8], rax
mov rdx, QWORD PTR [rbp-32]
mov rax, QWORD PTR [rbp-48]
xor rax, rdx
mov QWORD PTR [rbp-32], rax
mov rdx, QWORD PTR [rbp-24]
mov rax, QWORD PTR [rbp-40]
xor rax, rdx
mov QWORD PTR [rbp-24], rax
mov rdx, QWORD PTR [rbp-40]
mov rax, QWORD PTR [rbp-32]
xor rax, rdx
mov QWORD PTR [rbp-40], rax
mov rdx, QWORD PTR [rbp-48]
mov rax, QWORD PTR [rbp-24]
xor rax, rdx
mov QWORD PTR [rbp-48], rax
mov rax, QWORD PTR [rbp-32]
xor rax, QWORD PTR [rbp-8]
mov QWORD PTR [rbp-32], rax
mov rax, QWORD PTR [rbp-24]
mov esi, 45
mov rdi, rax
call _Z4rotlmi
mov QWORD PTR [rbp-24], rax
mov rdx, QWORD PTR [rbp-48]
mov rax, QWORD PTR [rbp-24]
add rax, rdx
mov esi, 23
mov rdi, rax
call _Z4rotlmi
mov rdx, QWORD PTR [rbp-48]
add rax, rdx
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1794:
.size _Z16xorshift256_nextILm14555066043003024868ELm7653162274090525828ELm823244081203417900ELm11935748167621739468EEmv, .-_Z16xorshift256_nextILm14555066043003024868ELm7653162274090525828ELm823244081203417900ELm11935748167621739468EEmv
.text
.type _Z41__static_initialization_and_destruction_0ii, @function
_Z41__static_initialization_and_destruction_0ii:
.LFB2042:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
sub rsp, 16
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
cmp DWORD PTR [rbp-4], 1
jne .L11
cmp DWORD PTR [rbp-8], 65535
jne .L11
mov edi, OFFSET FLAT:_ZStL8__ioinit
call _ZNSt8ios_base4InitC1Ev
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:_ZStL8__ioinit
mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
call __cxa_atexit
.L11:
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE2042:
.size _Z41__static_initialization_and_destruction_0ii, .-_Z41__static_initialization_and_destruction_0ii
.type _GLOBAL__sub_I_main, @function
_GLOBAL__sub_I_main:
.LFB2043:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
mov esi, 65535
mov edi, 1
call _Z41__static_initialization_and_destruction_0ii
pop rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE2043:
.size _GLOBAL__sub_I_main, .-_GLOBAL__sub_I_main
.section .init_array,"aw"
.align 8
.quad _GLOBAL__sub_I_main
.hidden __dso_handle
.ident "GCC: (GNU) 9.2.1 20190827 (Red Hat 9.2.1-1)"
.section .note.GNU-stack,"",@progbits


Как видно из ассемблерного кода выше, функции rotl и xorshift256_next присутствуют в коде, но на самом деле это ожидаемое поведение, так как оптимизации были отключены.

Проверим с включенными оптимизациями g++ _.cpp -S -masm=intel -O2

Мне уже не страшно, я видел всё
.file "_.cpp"
.intel_syntax noprefix
.text
.section .text.startup,"ax",@progbits
.p2align 4
.globl main
.type main, @function
main:
.LFB1550:
.cfi_startproc
sub rsp, 8
.cfi_def_cfa_offset 16
mov edi, OFFSET FLAT:_ZSt4cout
movabs rsi, 6340608927850167019
call _ZNSo9_M_insertImEERSoT_
mov rdi, rax
call _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
mov edi, OFFSET FLAT:_ZSt4cout
movabs rsi, -1433878323375531419
call _ZNSo9_M_insertImEERSoT_
mov rdi, rax
call _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
xor eax, eax
add rsp, 8
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE1550:
.size main, .-main
.p2align 4
.type _GLOBAL__sub_I_main, @function
_GLOBAL__sub_I_main:
.LFB2064:
.cfi_startproc
sub rsp, 8
.cfi_def_cfa_offset 16
mov edi, OFFSET FLAT:_ZStL8__ioinit
call _ZNSt8ios_base4InitC1Ev
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:_ZStL8__ioinit
mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
add rsp, 8
.cfi_def_cfa_offset 8
jmp __cxa_atexit
.cfi_endproc
.LFE2064:
.size _GLOBAL__sub_I_main, .-_GLOBAL__sub_I_main
.section .init_array,"aw"
.align 8
.quad _GLOBAL__sub_I_main
.local _ZStL8__ioinit
.comm _ZStL8__ioinit,1,1
.hidden __dso_handle
.ident "GCC: (GNU) 9.2.1 20190827 (Red Hat 9.2.1-1)"
.section .note.GNU-stack,"",@progbits


Ура! получаем то, что ожидали, компилятор развернул 2 вызова макроса RND в 2 инструкции mov. Поведение компиляторов clang и MSVC в RELEASE MODE аналогичное. Можно переходить к compile time кодогенерации.

Для последующего понимания, нужно ознакомиться с универсальными логическими элементами
NOR (Стрелка пирса) NAND (Штрих Шеффера) при помощи которых можно построить абсолютно всю логику. Я не буду прилагать весь код для реализации операций & | ^ ++ — , разберём только один пример на основе случайной выборки шаблонной функции NAND, в остальных эмулируемых инструкциях принцип выборки такой же.

Давайте представим, что у нас есть 5 template имплементаций NAND выборку которых мы хотим рандомизировать.

//! (Not a) Or (Not b)
template <typename T>
FORCEINLINE T Nand_1(volatile T a, volatile T b) {
  return Or<T>(Not<T>(a), Not<T>(b));
}
//! Not(a And b)
template <typename T>
FORCEINLINE T Nand_2(volatile T a, volatile T b) {
  return Not<T>(And<T>(a, b));
}
//! Not ((Not a) Nor (Not b))
template <typename T>
FORCEINLINE T Nand_3(volatile T a, volatile T b) {
  return Not<T>(Nor<T>(Not<T>(a), Not<T>(b)));
}
//! (Not a) Or (Not b)
template <typename T>
FORCEINLINE T Nand_4(volatile T a, volatile T b) {
  return Or<T>(Not<T>(a), Not<T>(b));
}
//! ((Not a) And b) Xor (Not b)
template <typename T>
FORCEINLINE T Nand_5(volatile T a, volatile T b) {
  return Xor<T>(And<T>(Not<T>(a), b), Not<T>(b));
}

Для этого нам достаточно сделать следующее:

template <typename T, const uint64_t n>
FORCEINLINE volatile T NandR(volatile T a, volatile T b) {
  switch (n % 5) {
    case 0:
      return Nand_1<T>(a, b);
    case 1:
      return Nand_2<T>(a, b);
    case 2:
      return Nand_3<T>(a, b);
    case 3:
      return Nand_4<T>(a, b);
    case 4:
      return Nand_5<T>(a, b);
    default:
      return Nand_1<T>(a, b);
  }
}

Вызываться в коде это может вот так:

NandR<uint64_t, RND>(a,b)

Компилятор оставит только 1 вызов функции, который будет выбран в соответствии с результатом от макроса RND, так как число известно на момент компиляции. RND — это макрос с использованием xorshift + fnv1 + __COUNTER__ который обсуждался выше. Так же для удобства использования был написан примитивный шаблонный класс CNNInt<> который в себе содержит перегрузки необходимых операторов, чтобы улучшить читаемость кода.

Рассмотрим генерируемый ассемблерный код и DIFF двух различных компиляций для следующей функции:

uint64_t diff_me(uint64_t x) {
  using u64 = uint64_t;
  CNNInt<u64> r(std::chrono::system_clock::now().time_since_epoch().count());
  CNNInt<u64> test(x);
  test++;
  test--;
  test|=r;
  test^=r;
  test&=r;
  test^=r;
  test = ~test;
  return test.value();
}

image

image

Анализ проводился используя утилиту relyze, DIFF двух бинарников показал следующий результат:

81.13%  diff_me  -  diff_me

Посмотрим на функцию diff_me под декомпиляторами:

1. Ghidra

ulong diff_me(ulong param_1)

{
  ulong uVar1;
  ulong uVar2;
  ulong uVar3;
  ulong uVar4;
  
  uVar1 = now();
  uVar2 = ~uVar1;
  uVar3 = ~(param_1 & ~param_1 & uVar1) & (param_1 | ~param_1 & uVar1);
  uVar4 = ~uVar3;
  uVar3 = (~(uVar3 & uVar2 | uVar4) | uVar3 & uVar2 & uVar4 | uVar2) & (uVar1 & uVar3 | uVar4) |
          uVar2;
  uVar4 = ~uVar3 & uVar2;
  return (uVar1 & ~uVar3 | uVar3) & (~(uVar4 | uVar3) | uVar4 & uVar3 | uVar2);
}

В проекте в качестве примера есть еще одна функция execute_code, ее Ghidra декомпилировать не смогла.

2. Hopper

int _Z7diff_mem(long arg0) {
    rax = std::chrono::_V2::system_clock::now();
    rax = (rax & !((!(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) & !rax | !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax))) | !(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) & !rax & !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax)) | !rax) & (rax & !(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) | !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax))) | !rax) | !!((!(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) & !rax | !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax))) | !(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) & !rax & !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax)) | !rax) & (rax & !(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) | !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax))) | !rax)) & (!(!((!(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) & !rax | !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax))) | !(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) & !rax & !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax)) | !rax) & (rax & !(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) | !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax))) | !rax) & !rax | !!((!(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) & !rax | !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax))) | !(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) & !rax & !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax)) | !rax) & (rax & !(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) | !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax))) | !rax)) | !((!(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) & !rax | !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax))) | !(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) & !rax & !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax)) | !rax) & (rax & !(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) | !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax))) | !rax) & !rax & !!((!(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) & !rax | !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax))) | !(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) & !rax & !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax)) | !rax) & (rax & !(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax) | !(!(arg0 & !arg0 & rax) & (arg0 | !arg0 & rax))) | !rax) | !rax);
    return rax;
}

3. RetDec

// Address range: 0x401320 - 0x4013a3
// Demangled:     diff_me(unsigned long)
int64_t _Z7diff_mem(int64_t a1) {
    // 0x401320
    g14 = a1;
    int64_t v1 = g14; // 0x401321
    int64_t v2 = function_401030(); // 0x401324 _ZNSt6chrono3_V212system_clock3nowEv();
    g14 = 0;
    return -1 - ((v2 & -1 - v1 | v1) & v2);
}

Бонусы


1. Репозиторий проекта.

2. Вернемся к MSVC компилятору (тулчейн версии 16.4.2), компиляция примера из проекта съедает огромное количество памяти и занимает большое количество времени, я зарепортил баг компиляторной команде MSVC, буду рад если вы проголосуете за исправление данного бага.

Потребление памяти при компиляции тестового проекта:



Стоит отметить, что сборка тестового проекта используя Clang 9.0 и GCC 9.2.1 происходит практически моментально и не кушает такое количество памяти :)

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


  1. kunix
    03.01.2020 15:06

    forceinline — наше все!
    Вообще, это конечно колхоз.
    Обфускацию должен делать компилятор.


    1. Dinisoid Автор
      03.01.2020 15:11

      Спасибо, воспринимайте это как POC, для людей кто не хочет модифицировать конкретный backend LLVM :)


      1. kunix
        03.01.2020 15:27

        Впрочем, не всегда можно скомпилять себе тулчейн.
        Я тоже этим всем занимаюсь.
        Часто приходится инлайнить обфусцированный код для создания строк, при помощи forсeinline и alloca.
        В коде это выглядит как sprintf(buf, CSTRA(«top secret»), ...);

        Заинлайненый alloca в цикле — мой любимый баг :)


  1. avdx
    04.01.2020 20:33

    А какой смысл использования ГСПЧ, если функция xorshift256_next не сохраняет состояние? Т.е. получается, что каждый следующий вызов RND никак не зависит от предыдущего — при каждом новом вызове просто инициализируется новый ГПСЧ, который генерирует одно число. Не проще ли напрямую использовать хэш от __COUNTER__ и __TIME__?


    1. Dinisoid Автор
      04.01.2020 22:10

      Можно делать и так как вы говорите, но качество рандома будет хуже чем у xorshift256, это основная причина почему сделано именно так, можете провести эксперимент с TestU01 если есть возможность и интерес. Так же посмотрите макрос SEED в проекте.


      1. avdx
        04.01.2020 22:23

        Насколько я понимаю, при использовании этого (как и многих других) ГПСЧ, каждый вызов next() должен использовать состояние, сохраненное предыдущим вызовом. Только в этом случае можно говорить о какой то случайной последовательности и, соответственно, ее свойствах.
        Здесь же при каждом вызове RND генерируется новое состояние с помощью этого макроса SEED, которое потом никуда не сохраняется и соответственно при следующем вызове xorshift256_next() никак не используется. Т.е. каждое новое случайное число определяется новым SEED и зависит от него, а не от свойств ГПСЧ.


        1. kunix
          04.01.2020 22:34

          Dinisoid просто использовал ГПСЧ вместе хеш-функции.


        1. Dinisoid Автор
          04.01.2020 22:38

          avdx, Вы правильно считаете, но наличие сохраненного состояния не гарантирует вам хорошей рандомизации если за основу взят неправильный принцип его генерации, состояние — это только одна важная часть PRNG. Передо мной не стояла задача сохранять стейт xorshift256, в данном случае просто получить вменяемые compile time значения, чтобы не получать повторения одних и тех же инструкций на большОм количестве операций.


          1. avdx
            04.01.2020 23:22

            Задача понятна. Но генерируемая таким образом последовательность (назовем ее X), не является последовательностью, генерируемой указанным ГПСЧ, а является последовательностью, полученной из первых чисел последовательностей, генерируемых разными ГПСЧ, инициализированными случайным образом. Соответственно нельзя утверждать, что X обладает свойствами последовательностей, генерируемых данным ГПСЧ. Тогда какой смысл в его использовании вообще?
            Я думаю, если сгенерировать тестовую последовательность описанным способом (без сохранения состояния), то указанный в статье тест она пройдет не лучше, чем последовательность, сгенерированная хэш функцией, используемой для генерации состояний. Поэтому я и написал, что проще использовать сразу результат хэш функции. Хотя возможно у этой хэш функции случайность вообще плохая и присутствуют какие то явные патерны, тогда первое число, генерируемое ГПСЧ, действительно может обладать большей случайностью, но все же вся последовательность не будет обладать свойствами, обеспечиваемыми этим генератором.


            1. yleo
              04.01.2020 01:35

              Использовав xorshift Dinisoid пальнул из пушки по воробьям и устроил пытки MSVC. Для подобной задачи хватило-бы простейшего миксера. Причем использование макросов, а не новомодных constexpr, доставило бы компилятору намного меньше мук ;)


              Тем не менее, более-менее приличный ГПСЧ использованный подобным образом (без состояния, с инициализацией от натуральной последовательности) пройдет все упомянутые тесты, как и более-менее приличная хэш-функция.