Obfuscated C Code Contest 2006. Пожалуйста, объясните sykes2.c

Как работает эта C-программа?

main(_){_^448&&main(-~_);putchar(--_%64?32|-~7[__TIME__-_/8%8][">'txiZ^(~z?"-48]>>";;;====~$::199"[_*2&8|_/64]/(_&2?1:8)%8&1:10);}

Он компилируется как есть (проверен на gcc 4.6.3). Он печатает время компиляции. В моей системе:

    !!  !!!!!!              !!  !!!!!!              !!  !!!!!! 
    !!  !!  !!              !!      !!              !!  !!  !! 
    !!  !!  !!              !!      !!              !!  !!  !! 
    !!  !!!!!!    !!        !!      !!    !!        !!  !!!!!! 
    !!      !!              !!      !!              !!  !!  !! 
    !!      !!              !!      !!              !!  !!  !! 
    !!  !!!!!!              !!      !!              !!  !!!!!!

Источник: sykes2 - часы в одной строке, sykes2 авторские подсказки

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

sykes2.c:1:1: warning: return type defaults to ‘int’ [-Wreturn-type]
sykes2.c: In function ‘main’:
sykes2.c:1:14: warning: value computed is not used [-Wunused-value]
sykes2.c:1:1: warning: implicit declaration of function ‘putchar’ [-Wimplicit-function-declaration]
sykes2.c:1:1: warning: suggest parentheses around arithmetic in operand of ‘|’ [-Wparentheses]
sykes2.c:1:1: warning: suggest parentheses around arithmetic in operand of ‘|’ [-Wparentheses]
sykes2.c:1:1: warning: control reaches end of non-void function [-Wreturn-type]

Ответы

Ответ 1

Пусть де-обфускация.

Отступы:

main(_) {
    _^448 && main(-~_);
    putchar(--_%64
        ? 32 | -~7[__TIME__-_/8%8][">'txiZ^(~z?"-48] >> ";;;====~$::199"[_*2&8|_/64]/(_&2?1:8)%8&1
        : 10);
}

Представляем переменные, чтобы распутать этот беспорядок:

main(int i) {
    if(i^448)
        main(-~i);
    if(--i % 64) {
        char a = -~7[__TIME__-i/8%8][">'txiZ^(~z?"-48];
        char b = a >> ";;;====~$::199"[i*2&8|i/64]/(i&2?1:8)%8;
        putchar(32 | (b & 1));
    } else {
        putchar(10); // newline
    }
}

Обратите внимание, что -~i == i+1 из-за двойного дополнения. Поэтому имеем

main(int i) {
    if(i != 448)
        main(i+1);
    i--;
    if(i % 64 == 0) {
        putchar('\n');
    } else {
        char a = -~7[__TIME__-i/8%8][">'txiZ^(~z?"-48];
        char b = a >> ";;;====~$::199"[i*2&8|i/64]/(i&2?1:8)%8;
        putchar(32 | (b & 1));
    }
}

Теперь обратите внимание, что a[b] совпадает с b[a] и снова применяет изменение -~ == 1+:

main(int i) {
    if(i != 448)
        main(i+1);
    i--;
    if(i % 64 == 0) {
        putchar('\n');
    } else {
        char a = (">'txiZ^(~z?"-48)[(__TIME__-i/8%8)[7]] + 1;
        char b = a >> ";;;====~$::199"[(i*2&8)|i/64]/(i&2?1:8)%8;
        putchar(32 | (b & 1));
    }
}

Преобразование рекурсии в цикл и кратковременное упрощение:

// please don't pass any command-line arguments
main() {
    int i;
    for(i=447; i>=0; i--) {
        if(i % 64 == 0) {
            putchar('\n');
        } else {
            char t = __TIME__[7 - i/8%8];
            char a = ">'txiZ^(~z?"[t - 48] + 1;
            int shift = ";;;====~$::199"[(i*2&8) | (i/64)];
            if((i & 2) == 0)
                shift /= 8;
            shift = shift % 8;
            char b = a >> shift;
            putchar(32 | (b & 1));
        }
    }
}

Это выводит один символ на итерацию. Каждый 64-й символ выводит новую строку. В противном случае он использует пару таблиц данных, чтобы выяснить, что выводить, и помещает либо символ 32 (пробел), либо символ 33 (a !). Первая таблица (">'txiZ^(~z?") представляет собой набор из 10 растровых изображений, описывающих внешний вид каждого символа, а вторая таблица (";;;====~$::199") выбирает соответствующий бит для отображения из растрового изображения.

Вторая таблица

Начнем с изучения второй таблицы int shift = ";;;====~$::199"[(i*2&8) | (i/64)];. i/64 - номер строки (от 6 до 0) и i*2&8 равен 8, если f i равно 4, 5, 6 или 7 mod 8.

if((i & 2) == 0) shift /= 8; shift = shift % 8 выбирает либо восьмеричную цифру (для i%8= 0,1,4,5), либо нижнюю восьмеричную цифру (для i%8= 2,3,6,7) значения таблицы. Таблица сдвига выглядит следующим образом:

row col val
6   6-7 0
6   4-5 0
6   2-3 5
6   0-1 7
5   6-7 1
5   4-5 7
5   2-3 5
5   0-1 7
4   6-7 1
4   4-5 7
4   2-3 5
4   0-1 7
3   6-7 1
3   4-5 6
3   2-3 5
3   0-1 7
2   6-7 2
2   4-5 7
2   2-3 3
2   0-1 7
1   6-7 2
1   4-5 7
1   2-3 3
1   0-1 7
0   6-7 4
0   4-5 4
0   2-3 3
0   0-1 7

или в табличной форме

00005577
11775577
11775577
11665577
22773377
22773377
44443377

Обратите внимание, что автор использовал нулевой ограничитель для первых двух записей таблицы (подлый!).

Это спроектировано после семисегментного дисплея с 7 как пробелы. Таким образом, записи в первой таблице должны определять загораемые сегменты.

Первая таблица

__TIME__ - это специальный макрос, определенный препроцессором. Он расширяется до строковой константы, содержащей время, в которое выполнялся препроцессор, в форме "HH:MM:SS". Обратите внимание, что он содержит ровно 8 символов. Обратите внимание, что 0-9 имеют значения ASCII с 48 по 57, а : имеет значение ASCII 58. Выходной файл составляет 64 символа на строку, поэтому он оставляет 8 символов на символ __TIME__.

7 - i/8%8 является, таким образом, индексом __TIME__, который в настоящее время выводится (требуется 7-, потому что мы итерируем i вниз). Таким образом, t выводится символ __TIME__.

a заканчивается в двоичном выражении следующим образом: в зависимости от входа t:

0 00111111
1 00101000
2 01110101
3 01111001
4 01101010
5 01011011
6 01011111
7 00101001
8 01111111
9 01111011
: 01000000

Каждый номер представляет собой растровое изображение, описывающее сегменты, которые освещаются на нашем семисегментном дисплее. Так как символы все 7-бит ASCII, высокий бит всегда очищается. Таким образом, 7 в таблице сегментов всегда печатает как пробел. Вторая таблица выглядит так: 7 как пробелы:

000055  
11  55  
11  55  
116655  
22  33  
22  33  
444433  

Итак, например, 4 - это 01101010 (набор бит 1, 3, 5 и 6), который печатается как

----!!--
!!--!!--
!!--!!--
!!!!!!--
----!!--
----!!--
----!!--

Чтобы показать, что мы действительно понимаем код, пусть немного отрегулирует вывод этой таблицы:

  00  
11  55
11  55
  66  
22  33
22  33
  44

Это кодируется как "?;;?==? '::799\x07". Для художественных целей мы добавим 64 к нескольким символам (поскольку используются только младшие 6 бит, это не повлияет на вывод); это дает "?{{?}}?gg::799G" (обратите внимание, что 8-й символ не используется, поэтому мы можем сделать все, что захотим). Поместите нашу новую таблицу в исходный код:

main(_){_^448&&main(-~_);putchar(--_%64?32|-~7[__TIME__-_/8%8][">'txiZ^(~z?"-48]>>"?{{?}}?gg::799G"[_*2&8|_/64]/(_&2?1:8)%8&1:10);}

получаем

          !!              !!                              !!   
    !!  !!              !!  !!  !!  !!              !!  !!  !! 
    !!  !!              !!  !!  !!  !!              !!  !!  !! 
          !!      !!              !!      !!                   
    !!  !!  !!          !!  !!      !!              !!  !!  !! 
    !!  !!  !!          !!  !!      !!              !!  !!  !! 
          !!              !!                              !!   

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

Ответ 2

Отформатируйте это для удобства чтения:

main(_){
  _^448&&main(-~_);
  putchar((--_%64) ? (32|-(~7[__TIME__-_/8%8])[">'txiZ^(~z?"-48]>>(";;;====~$::199")[_*2&8|_/64]/(_&2?1:8)%8&1):10);
}

Итак, запустив его без аргументов, _ (argc условно) 1. main() будет рекурсивно называть себя, передавая результат -(~_) (отрицательный побитовый NOT of _), так что это действительно будет 448 рекурсий (только условие, когда _^448 == 0).

Взяв это, он напечатает 7 64-символьных широких линий (внешнее тернарное состояние и 448/64 == 7). Так что пусть переписывает его немного чище:

main(int argc) {
  if (argc^448) main(-(~argc));
  if (argc % 64) {
    putchar((32|-(~7[__TIME__-argc/8%8])[">'txiZ^(~z?"-48]>>(";;;====~$::199")[argc*2&8|argc/64]/(argc&2?1:8)%8&1));
  } else putchar('\n');
}

Теперь 32 является десятичным для пространства ASCII. Он либо печатает пробел, либо "!" (33 - "!", Следовательно, "&1" в конце). Позвольте сосредоточиться на blob в середине:

-(~(7[__TIME__-argc/8%8][">'txiZ^(~z?"-48]) >>
     (";;;====~$::199"[argc*2&8|argc/64]) / (argc&2?1:8) % 8

Как сказал другой плакат, __TIME__ - время компиляции для программы, и это строка, поэтому происходит некоторая строковая арифметика, а также использование двунаправленного индекса массива: a [b] - это так же, как b [a] для массивов символов.

7[__TIME__ - (argc/8)%8]

Это выберет один из первых 8 символов в __TIME__. Затем он индексируется в [">'txiZ^(~z?"-48] (0-9 символов равно 48-57 десятичным). Символы этой строки должны быть выбраны для их значений ASCII. Эта же символьная обработка кода ASCII продолжается через выражение, что приводит к печати либо '' или '!' в зависимости от местоположения в символьном символе.

Ответ 3

Добавляя к другим решениям, -~x равен x+1, потому что ~x эквивалентен (0xffffffff-x). Это равно (-1-x) в дополнении 2s, поэтому -~x -(-1-x) = x+1.

Ответ 4

Я de-obfuscated по модулю арифметики столько, сколько мог, и удалил reccursion

int pixelX, line, digit ;
for(line=6; line >= 0; line--){
  for (digit =0; digit<8; digit++){
    for(pixelX=7;pixelX > 0; pixelX--){ 
        putchar(' '| 1 + ">'txiZ^(~z?"["12:34:56"[digit]-'0'] >> 
          (";;;====~$::199"[pixel*2 & 8  | line] / (pixelX&2 ? 1 : 8) ) % 8 & 1);               
    }
  }
  putchar('\n');
}

Развернув его немного больше:

int pixelX, line, digit, shift;
char shiftChar;
for(line=6; line >= 0; line--){
    for (digit =0; digit<8; digit++){
        for(pixelX=7;pixelX >= 0; pixelX--){ 
            shiftChar = ";;;====~$::199"[pixelX*2 & 8 | line];
            if (pixelX & 2)
                shift = shiftChar & 7;
            else
                shift = shiftChar >> 3;     
            putchar(' '| (">'txiZ^(~z?"["12:34:56"[digit]-'0'] + 1) >> shift & 1 );
        }

    }
    putchar('\n');
}