Концепция этих четырех линий сложного кода C

Почему этот код дает вывод C++Sucks? Какова концепция этого?

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

int main() {
    m[1]--?m[0]*=2,main():printf((char*)m);    
}

Проверьте здесь.

Ответ 1

Число 7709179928849219.0 имеет следующее двоичное представление в виде 64-разрядного double:

01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011
+^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- --------

+ показывает положение знака; ^ экспоненты и - мантиссы (т.е. значение без экспоненты).

Поскольку в представлении используется двоичный показатель и мантисса, удвоение числа увеличивает показатель экспоненты на единицу. Ваша программа делает это точно 771 раз, поэтому показатель степени, начинающийся с 1075 (десятичное представление 10000110011), становится 1075 + 771 = 1846 в конце; двоичное представление 1846 равно 11100110110. Полученный шаблон выглядит следующим образом:

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011
-------- -------- -------- -------- -------- -------- -------- --------
0x73 's' 0x6B 'k' 0x63 'c' 0x75 'u' 0x53 'S' 0x2B '+' 0x2B '+' 0x43 'C'

Этот шаблон соответствует строке, которую вы видите напечатанной, только назад. В то же время второй элемент массива обращается в нуль, обеспечивая нулевой ограничитель, делая строку подходящей для перехода к printf().

Ответ 2

Более читаемая версия:

double m[2] = {7709179928849219.0, 771};
// m[0] = 7709179928849219.0;
// m[1] = 771;    

int main()
{
    if (m[1]-- != 0)
    {
        m[0] *= 2;
        main();
    }
    else
    {
        printf((char*) m);
    }
}

Он рекурсивно называет main() 771 раз.

В начале m[0] = 7709179928849219.0, который стоит для C++Suc;C. В каждом вызове m[0] удваивается, чтобы "восстановить" последние две буквы. В последнем вызове m[0] содержит ASCII char представление C++Sucks и m[1] содержит только нули, поэтому он имеет null terminator для строки C++Sucks. Все в предположении, что m[0] хранится на 8 байтах, поэтому каждый char занимает 1 байт.

Без рекурсии и незаконного вызова main() это будет выглядеть так:

double m[] = {7709179928849219.0, 0};
for (int i = 0; i < 771; i++)
{
    m[0] *= 2;
}
printf((char*) m);

Ответ 3

Формально говоря, невозможно рассуждать об этой программе, потому что она плохо сформирована (т.е. не является законной C++). Он нарушает С++ 11 [basic.start.main] p3:

Функция main не должна использоваться внутри программы.

В остальном он полагается на то, что на типичном потребительском компьютере a double имеет длину 8 байтов и использует некоторое известное внутреннее представление. Начальные значения массива вычисляются так, что, когда выполняется "алгоритм", конечное значение первого double будет таким, что внутреннее представление (8 байтов) будет кодами ASCII из 8 символов C++Sucks, Второй элемент в массиве - это 0.0, чей первый байт 0 во внутреннем представлении, что делает его допустимой строкой в ​​стиле C. Затем он отправляется на вывод с помощью printf().

Запуск этого процесса на HW, где некоторые из вышеперечисленных действий не выполняются, приведет к тому, что вместо этого появится текст мусора (или, возможно, даже доступ за пределами границ).

Ответ 4

Возможно, самый простой способ понять код - это работать через все наоборот. Мы начнем с строки для печати - для баланса мы будем использовать "С++ Rocks". Важнейший момент: точно так же, как оригинал, он ровно восемь символов. Поскольку мы собираемся сделать (примерно), как оригинал, и распечатать его в обратном порядке, мы начнем с ввода его в обратном порядке. Для нашего первого шага мы просто рассмотрим этот битовый шаблон как double и распечатаем результат:

#include <stdio.h>

char string[] = "skcoR++C";

int main(){
    printf("%f\n", *(double*)string);
}

Это дает 3823728713643449.5. Итак, мы хотим каким-то образом манипулировать этим, что не очевидно, но легко отменить. Я полу-произвольно выбираю умножение на 256, что дает нам 978874550692723072. Теперь нам просто нужно написать некоторый obfuscated код для деления на 256, а затем распечатать отдельные байты в обратном порядке:

#include <stdio.h>

double x [] = { 978874550692723072, 8 };
char *y = (char *)x;

int main(int argc, char **argv){
    if (x[1]) {
        x[0] /= 2;  
        main(--x[1], (char **)++y);
    }
    putchar(*--y);
}

Теперь у нас есть много кастингов, передающих аргументы (рекурсивный) main, которые полностью игнорируются (но оценка, чтобы получить приращение и декремент, имеет решающее значение), и, конечно, это абсолютно произвольно выглядящее число, чтобы скрыть факт что то, что мы делаем, действительно довольно просто.

Конечно, поскольку весь смысл - обфускация, если мы чувствуем, что мы можем предпринять и другие шаги. Например, мы можем воспользоваться оценкой короткого замыкания, чтобы превратить наш оператор if в одно выражение, поэтому тело main выглядит так:

x[1] && (x[0] /= 2,  main(--x[1], (char **)++y));
putchar(*--y);

Для тех, кто не привык к запутанному коду (и/или кодовому гольфу), это начинает выглядеть довольно странно - вычисление и отбрасывание логического and некоторого бессмысленного числа с плавающей запятой и возвращаемого значения из main, который даже не возвращает значение. Хуже того, не осознавая (и не задумываясь) о том, как работает оценка короткого замыкания, может быть даже не сразу очевидным, как это позволяет избежать бесконечной рекурсии.

Наш следующий шаг, вероятно, заключался бы в том, чтобы отделить печать каждого персонажа от поиска этого символа. Мы можем сделать это довольно легко, создав правильный символ как возвращаемое значение из main и распечатав, что возвращает main:

x[1] && (x[0] /= 2,  putchar(main(--x[1], (char **)++y)));
return *--y;

По крайней мере, для меня это кажется запутанным, поэтому я оставлю его на этом.

Ответ 5

Он просто создает двойной массив (16 байтов), который - если интерпретируется как массив char - создает коды ASCII для строки "С++ Sucks"

Однако код не работает в каждой системе, он опирается на некоторые из следующих undefined фактов:

  • double имеет ровно 8 байт
  • endianness

Ответ 6

Следующий код печатает C++Suc;C, поэтому полное умножение выполняется только для последних двух букв

double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);

Ответ 7

Код можно переписать следующим образом:

void f()
{
    if (m[1]-- != 0)
    {
        m[0] *= 2;
        f();
    } else {
          printf((char*)m);
    }
}

В результате он создает набор байтов в массиве double m, который соответствует символам С++ Sucks, за которыми следует нулевой терминатор. Они обфускали код, выбирая двойное значение, которое при удвоении 771 раз производит в стандартном представлении набор байтов с нулевым терминатором, предоставляемым вторым членом массива.

Обратите внимание, что этот код не будет работать под другим представлением endian. Кроме того, вызов main() строго запрещен.

Ответ 8

Другие подробно объяснили этот вопрос, я хотел бы добавить примечание о том, что это undefined поведение в соответствии со стандартом.

С++ 11 3.6.1/3 Основная функция

Функция main не должна использоваться внутри программы. Связь (3.5) main определяется реализацией. Программа, которая определяет главную как удаленную или объявляет основную строку, статическую или constexpr, плохо сформирована. Основное имя не зарезервировано. [Пример: функции-члены, классы и перечисления можно назвать главными, а также сущностями в других пространствах имен. -end пример]

Ответ 9

В основном это просто умный способ скрыть строку "С++ Sucks" (обратите внимание на 8 байтов) в первом двойном значении, которое рекурсивно умножается на два, пока секундные двойные значения не достигнут нуля (771 раз).

Умножение двойных значений 7709179928849219.0 * 2 * 711 приводит к "С++ Sucks", если вы интерпретируете байтовое значение double как string, которое printf() делает с литой. И printf() не сбой, потому что второе двойное значение равно "0" и интерпретируется как "\ 0" printf().