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

384

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

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

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

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

codeslayer1
источник
1
Технически, @BoBTFish, да, но он все равно работает в C99: ideone.com/IZOkql
nijansen
12
@nurettin У меня были похожие мысли. Но это не вина ОП, это люди, которые голосуют за эти бесполезные знания. Допустим, эта штука с запутыванием кода может быть интересной, но наберите в Google «обфускацию», и вы получите массу результатов на каждом формальном языке, который только сможете придумать. Не поймите меня неправильно, я нахожу нормальным задавать такой вопрос здесь. Это просто переоцененный, потому что не очень полезный вопрос.
TobiMcNamobi
6
@ detonator123 "Вы должны быть новичком здесь" - если вы посмотрите на причину закрытия, вы можете узнать, что это не так. Требуемое минимальное понимание явно отсутствует в вашем вопросе - «Я не понимаю этого, объясните это» - это не то, что приветствуется в переполнении стека. Если бы вы сначала попытались что-то сделать сами , вопрос не был бы закрыт. Это тривиально, чтобы Google "двойное представление C" или тому подобное.
42
Моя машина PowerPC с прямым порядком байтов распечатывает skcuS++C.
Адам Розенфилд
27
Клянусь словом, я ненавижу такие надуманные вопросы. Это небольшой паттерн в памяти, который совпадает с какой-то глупой строкой. Он не служит никому полезной цели, и все же он зарабатывает сотни очков повторения как для спрашивающего, так и для отвечающего. Между тем, сложные вопросы, которые могут быть полезны людям, могут заработать несколько очков, если таковые имеются. Это своего рода плакат о том, что не так с SO.
Кэри Грегори

Ответы:

494

Число 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().

dasblinkenlight
источник
22
Почему строка задом наперед?
Дерек
95
@Derek x86 - little-endian
Angew больше не гордится SO
16
@Derek Это происходит из-за платформы конкретных байт : байты абстрактных IEEE 754 представлений сохраняется в памяти убывающих адресов, так что строка отпечатки правильно. На оборудовании с большим порядком байтов нужно начинать с другого числа.
dasblinkenlight
14
@ AlvinWong Вы правы, стандарт не требует IEEE 754 или любого другого определенного формата. Эта программа настолько
непереносима,
10
@GrijeshChauhan Я использовал калькулятор IEEE754 с двойной точностью : я вставил 7709179928849219значение и вернул двоичное представление.
dasblinkenlight
223

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

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);
Адам Стельмащик
источник
8
Это постфиксный декремент. Так будет называться 771 раз.
Джек Эйдли
106

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


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

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

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

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

Angew больше не гордится SO
источник
25
Я должен добавить, что это не изобретение C ++ 11 - C ++ 03 также имел basic.start.main3.6.1 / 3 с той же формулировкой.
sharptooth
1
Цель этого небольшого примера - проиллюстрировать, что можно сделать с помощью C ++. Волшебный пример с использованием UB-трюков или огромных пакетов программ с «классическим» кодом.
Шепурин
1
@sharptooth Спасибо за добавление этого. Я не хотел подразумевать иное, я просто привел стандарт, который использовал.
Angew больше не гордится SO
@Angew: Да, я понимаю это, просто хотел сказать, что формулировка довольно старая.
sharptooth
1
@JimBalter Заметьте, я сказал «формально говоря, невозможно рассуждать», а не «невозможно формально рассуждать». Вы правы в том, что можно рассуждать о программе, но вам нужно знать детали компилятора, который использовался для этого. Было бы полностью в пределах прав компилятора просто исключить вызов main()или заменить его вызовом API для форматирования жесткого диска или чего-либо еще.
Angew больше не гордится SO
57

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

#include <stdio.h>

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

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

Это производит 3823728713643449.5. Итак, мы хотим манипулировать этим каким-то образом, что не очевидно, но легко изменить. Я буду полу-произвольно выбирать умножение на 256, что дает нам 978874550692723072. Теперь нам просто нужно написать некоторый запутанный код, чтобы разделить его на 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;

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

Джерри Гроб
источник
1
Люблю судебно-медицинский подход.
ryyker
24

Это просто создание двойного массива (16 байтов), который - если интерпретировать его как массив символов - создает коды ASCII для строки "C ++ Sucks"

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

DR
источник
12

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

double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);
Служить Лаурийссену
источник
11

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

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

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

Ю Хао
источник
1
Я бы сказал, что он даже плохо сформирован (как я и сделал в своем ответе) - он нарушает «должен».
Angew больше не гордится SO
9

Код можно переписать так:

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

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

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

Джек Эйдли
источник
3
Почему вы fвозвращаете int?
оставлено около
1
Э-э, потому что я бездумно копировал intответ в вопросе. Позвольте мне это исправить.
Джек Эйдли
1

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

(i) 1 бит для знака

(ii) 11 битов для показателя степени

(iii) 52 бита для величины

Порядок битов уменьшается с (i) до (iii).

Сначала десятичное дробное число преобразуется в эквивалентное дробное двоичное число, а затем оно выражается в виде порядка величины в двоичном виде.

Таким образом, номер 7709179928849219.0 становится

(11011011000110111010101010011001010110010101101000011)base 2


=1.1011011000110111010101010011001010110010101101000011 * 2^52

Теперь при рассмотрении величины битов 1 пренебрегают, так как все методы порядка величин должны начинаться с 1.

Таким образом, часть величины становится:

1011011000110111010101010011001010110010101101000011 

Теперь степень 2 равна 52 , нам нужно добавить к ней число смещения как 2 ^ (биты для показателя -1) -1, т.е. 2 ^ (11 -1) -1 = 1023 , поэтому наш показатель степени становится 52 + 1023 = 1075

Теперь наш код mutiplies номер с 2 , 771 раз , что делает показатель увеличится на 771

Таким образом, наш показатель равен (1075 + 771) = 1846 , бинарный эквивалент которого (11100110110)

Теперь наше число положительное, поэтому наш бит знака равен 0 .

Таким образом, наш модифицированный номер становится:

знаковый бит + экспонента + величина (простая конкатенация битов)

0111001101101011011000110111010101010011001010110010101101000011 

поскольку m преобразуется в указатель на символ, мы разделим битовую комбинацию на 8 частей от LSD

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 

(чей эквивалент Hex :)

 0x73 0x6B 0x63 0x75 0x53 0x2B 0x2B 0x43 

ASCII CHART Который из карты персонажей, как показано:

s   k   c   u      S      +   +   C 

Теперь, когда это сделано, m [1] равно 0, что означает символ NULL

Теперь предположим, что вы запускаете эту программу на машине с прямым порядком байтов (бит младшего разряда хранится в младшем адресе), так что указатель m указывает на бит младшего адреса, а затем продолжает работу, занимая биты в порциях 8 (как тип, приведенный к char * ) и printf () останавливается, если в последнем чанке установлено значение 00000000 ...

Этот код, однако, не является переносимым.

Абхишек Гош
источник