Intereting Posts
Что определяет тип данных, который будет использоваться для хранения временного значения? Что может привести к сбою закрытия (2) с помощью EIO для файла только для чтения? Почему компилятор генерирует дополнительные sqrts в скомпилированном ассемблере Пожалуйста, объясните, что делает этот код (someChar-48) Измените страtagsю malloc для 2D-массива, чтобы malloc преуспел передача unsigned char массива в строковые функции Что считается EOF в stdin? Как определить состояние процесса (т. Е. Если это зомби) C Предупреждение: функция возвращает адрес локальной переменной Что делает функция remquo и для чего ее можно использовать? Может ли memset () вызываться с нулевым указателем, если размер равен 0? C: Код сигнала: Адрес не отображается (1) mpirecv странное поведение scanf для короткого int Поиск индекса сложности функции предупреждение: игнорирование возвращаемого значения ‘realloc’, объявленного с атрибутом warn_unused_result

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

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

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

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

Число 7709179928849219.0 имеет двоичное представление в виде 64-битного double :

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

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

Поскольку в представлении используется двоичный показатель и мантисса, удвоение числа увеличивает показатель экспоненты на единицу. Ваша программа делает это точно 771 раз, поэтому показатель, 10000110011 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() .

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

 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-символьное представление C++Sucks и m[1] содержит только нули, поэтому он имеет нулевой ограничитель для строки C++Sucks . Все в предположении, что m[0] хранится на 8 байтах, поэтому каждый символ принимает 1 байт.

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

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

Отказ от ответственности: этот ответ был отправлен в исходную форму вопроса, в которой упоминался только C ++ и включал заголовок C ++. Преобразование вопроса в чистый C было сделано сообществом, без ввода от первоначального искателя.


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

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

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

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

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

 #include  char string[] = "skcoR++C"; int main(){ printf("%f\n", *(double*)string); } 

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

 #include  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; 

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

Он просто создает двойной массив (16 байт), который – если интерпретируется как массив символов – создает коды ASCII для строки «C ++ Sucks»,

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

  • double имеет ровно 8 байт
  • порядок байт

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

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

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

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

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

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

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

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

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

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

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