Концепція цих чотирьох рядків хитрого коду С


384

Чому цей код дає вихід C++Sucks? Яке поняття стоїть за ним?

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

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

Тестуйте це тут .


1
@BoBTFish технічно, так, але він працює все одно в C99: ideone.com/IZOkql
nijansen

12
@nurettin У мене були подібні думки. Але це не вина ОП, це люди, які голосують за це марне знання. Зізнається, цей предмет обфускування коду може бути цікавим, але введіть "обфускування" в Google, і ви отримаєте безліч результатів на кожній офіційній мові, яку ви можете придумати. Не зрозумійте мене неправильно, мені здається, що тут потрібно задати таке питання. Це просто завищена, оскільки не дуже корисне питання.
TobiMcNamobi

6
@ detonator123 "Ви повинні бути тут новими" - якщо ви подивитесь на причину закриття, то зможете з’ясувати, що це не так. Необхідне мінімальне розуміння явно відсутнє у вашому запитанні - "я цього не розумію, поясніть" - це не те, що вітається в Stack Overflow. Якби ви спробували щось спершу самостійно , питання не було б закритим. Google - "подвійне представлення C" або подібне, нереально.

42
Моя велика ендіанська машина PowerPC роздруковується skcuS++C.
Адам Розенфілд

27
Моє слово, я ненавиджу надумані подібні питання. Це трохи картина в пам'яті, яка буває такою ж, як і якась дурна струна. Він нікому не корисний, і все ж заробляє сотні балів повторень як для запитувача, так і для того, хто відповідає. Тим часом складні питання, які можуть бути корисними людям, заробляють, можливо, кілька балів, якщо такі є. Це своєрідна дитина-постер з того, що не так з ТА.
Carey Gregory

Відповіді:


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().


22
Чому рядок назад?
Дерек

95
@Derek x86 мало-молодшому
Angew більше не надимається SO

16
@Derek Це відбувається через платформи конкретних байт : байти абстрактних IEEE 754 подань зберігається в пам'яті відбувають адрес, так що рядок відбитки правильно. Для апаратних засобів з великою витримкою потрібно починати з іншої кількості.
dasblinkenlight

14
@AlvinWong Ви правильно, стандарт не вимагає IEEE 754 або будь-якого іншого конкретного формату. Ця програма настільки ж не портативна, як вона стає, або дуже близька до неї :-)
dasblinkenlight

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:

Основна функція не повинна використовуватися в межах програми.

Це, окрім цього, спирається на той факт, що на типовому споживчому комп’ютері a doubleдовжиною становить 8 байт і використовує певне відоме внутрішнє представлення. Початкові значення масиву обчислюються так, що при виконанні "алгоритму" остаточне значення першого doubleбуде таким, що внутрішнє представлення (8 байт) буде кодами ASCII з 8 символів C++Sucks. Потім другий елемент масиву 0.0, перший байт якого 0знаходиться у внутрішньому поданні, що робить це дійсним рядком у стилі C. Потім він надсилається на вихід, використовуючи printf().

Якщо запустити це на HW там, де деякі з перерахованих вище не містять, це призведе до сміттєвого тексту (або, можливо, навіть доступу поза межами).


25
Треба додати, що це не винахід C ++ 11 - C ++ 03 також мав basic.start.main3.6.1 / 3 з тим же формулюванням.
гострий зуб

1
Суть цього невеликого прикладу - проілюструвати, що можна зробити за допомогою C ++. Чарівний зразок за допомогою трюків UB або величезних програмних пакетів «класичного» коду.
SChepurin

1
@sharptooth Дякую за додавання цього. Я не мав на увазі інакше, я просто цитував стандарт, який я використовував.
Ендже вже не пишається SO

@Angew: Так, я так розумію, просто хотів сказати, що формулювання досить стара.
shartooth

1
@JimBalter Примітка Я сказав: "формально кажучи, міркувати неможливо", не "формально міркувати неможливо". Ви праві, що про програму можна міркувати, але вам потрібно знати подробиці компілятора, який це робив. Це було б повністю в межах прав компілятора просто усунути виклик main()або замінити його викликом API для форматування жорсткого диска чи будь-чого іншого.
Ендже вже не пишається SO

57

Мабуть, найпростіший спосіб зрозуміти код - це працювати над речами в зворотному напрямку. Почнемо з друку рядка - для балансу будемо використовувати "С ++ Скелі". Важливий момент: так само, як і оригінал, довжина рівно восьми символів. Оскільки ми будемо робити (приблизно) як оригінал, і роздруковувати його у зворотному порядку, ми почнемо, розміщуючи його у зворотному порядку. Для нашого першого кроку ми просто розглянемо цей бітовий візерунок як а 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твердження в єдиний вираз, тому тіло головного виглядає так:

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
Любіть підхід криміналістики.
райкер

24

Це просто створення подвійного масиву (16 байт), який - якщо трактувати як масив char - створює коди ASCII для рядка "C ++ Sucks"

Однак код працює не в кожній системі, він спирається на деякі з наступних невизначених фактів:


12

Наступний код друкується C++Suc;C, тому все множення є лише для двох останніх літер

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

11

Інші пояснили питання досить грунтовно, я хотів би додати зауваження, що це не визначена поведінка відповідно до стандарту.

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

Основна функція не повинна використовуватися в межах програми. Зв'язок (3.5) головного визначається реалізацією. Програма, яка визначає основну як видалену або яка визначає головну як вбудовану, статичну чи конвекспр, неправильно формується. Ім'я main не захищено інакше. [Приклад: функції членів, класи та перерахування членів можна назвати основними, як і об'єкти в інших просторах імен. —Закінчити приклад]


1
Я б сказав, що це навіть неправильно сформовано (як я це зробив у своїй відповіді) - це порушує "заповіт".
Ендже вже не пишається 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

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

Отже наш показник дорівнює (1075 + 771) = 1846 , двійковий еквівалент якого (11100110110)

Зараз наше число позитивне, тому наш біт знаків 0 .

Отже, наша змінена кількість стає:

знак біта + показник + величина (просте з'єднання бітів)

0111001101101011011000110111010101010011001010110010101101000011 

оскільки m перетворений на покажчик char, ми розділимо бітовий малюнок на шматки 8 від LSD

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 

(чий шестнадцятковий еквівалент :)

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

АСЦІЙНА ГРАФІКА Що з карти символів, як показано:

s   k   c   u      S      +   +   C 

Тепер, коли це було зроблено m [1] є 0, що означає символ NULL

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

Однак цей код не є портативним.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.