main(){printf(&unix["\021%six\012\0"], (unix)[«have»]+«fun»-0x60);}

Розважаємося, «розплутуючи» код на мові СіДзвінок: Перш ніж лізти під кат, скомпілюйте в голові заголовок статті, що він дає на виході?

image
Коли я в черговий раз переглядав книгу «Expert C programming», я раптом натрапив на розділ «light relief» у міжнародному конкурсі на самий заплутаний код на Сі (IOCCC). Це змагання по написанню як можна більш нечитабельного коду. Те, що такі конкурси влаштовуються для Сі, напевно, говорить що про це мовою. Мені хотілося побачити роботи учасників цього змагання. Не знайшовши ніякої інформації в інтернеті, я вирішив пошукати їх самостійно.

IOCCC був придуманий Стівеном Борном, коли він вирішив використовувати препроцесор Сі та написати Unix shell, як би на мові Сі, але більше схожому на мову Algol-68, з його явними закінченнями операторів, наприклад:

if
...
fi 

Він домігся цього, зробивши:

#define IF if(
#define THEN ){
#define ELSE } else {
#define FI ;}

Що дозволило йому писати так:

IF *s2++ == 0
THEN return(0);
FI


[Підтримка публікації — компанія Edison, яка розробляє Електронний сервіс передач ув'язненим і реалізувала вірусну розсилку інформації.]

image
У «Expert C programming» про це говориться наступне:

Уникайте будь-якого використання препроцесора Сі, яке змінює базовий мову.



Одним із перших переможців у 1987 році був Девід Корн, творець Korn shell'а (що не так з цими shell-райтерами?), який написав всього один рядок:

main(){printf(&unix["\021%six\012\0"], (unix)["have"]+"fun"-0x60);}

От і все. Спробуйте скомпілювати це. Що буде виведено?

Цей код не запуститься на Майкрософті (підказка!), але ось посилання на онлайн компілятор, який впорається з цим завданням. Там написано кілька рядків, щоб воно запрацювало, але в іншому — все те ж саме.

Код всього лише виводить:

unix

Але чому? У коді є щось, що виглядає як масив з назвою
unix
, але він не був декларований. Тоді
unix
— це ключове слово? Воно якимось чином виводить ім'я змінної?

Я наосліп спробував перевірити це, додавши:

printf(unix);

І він вивів мені помилку, сказавши, що
printf
бере
char *
, а не
int
.

Коли я вивів цю змінну
int
, стало зрозуміло, що її значення дорівнює 1. Це наштовхнуло мене на думку, що вона була перезаписати, як якщо б код був скомпільований в Unix-системі. Пошукавши на gcc source code, я знайшов, що це run-time target specification. Це пояснює, чому код не запускається на Windows.

unix
— це просто 1. Переписавши, отримаємо:

main(){printf(&1["\021%six\012\0"], (1)["have"]+"fun"-0x60);}

Отже,
unix
не було назвою змінної. Але тоді як же працює 1[]? Я вже бачив подібне раніше, і це один з моїх улюблених фактів про мову Сі.

image
Сі бере початок у мові BCPL. Його творець — доктор Мартін Річардс, писав:

Оператор непрямого звернення! приймає покажчик як аргумент і повертає зміст комірки, на яку він вказує. Якщо v — вказівник, то !(v+i) звертатиметься до комірки з адресою v+i. Бінарна версія оператора! визначена так, що v!i = !(v+i). v!i веде себе як індексоване уявлення, де v — одновимірний масив, а i — індекс типу integer. Зауважте, що в мові BCPL v5= !(v+5) =! (5+v) = 5!v. Те ж відбувається і в мові Сі: v[5] = 5[v].
Іншими словами, індекси просто складаються з покажчиками, а так як додавання коммутативно, то коммутативен і оператор індексування. Спробуємо змінити і це теж:

int x[] = {1, 2, 3};
printf("%d\n%d\n", x[1], 1[x]);

Тоді що є
1["\021%six\012\0"]
? Написавши в звичному вигляді, побачимо доступ до елементів масиву через оператор індексування:
"\021%six\012\0"[1]
. Все одно нетипово, але вже зрозуміло, що це
array[index]
, хоча, як правило, рядкові літерали так не використовують. Але це працює, тому спробуємо наступне:

printf("%c\n", "hello, world"[1]); 

Давайте перепишемо тільки перший масив, поки розбираємося з цим.

main() {
char str[] = "\021%six\012\0";
printf(&str[1], (1)["have"]+"fun"-0x60);
}

Все ще працює так само. Подивившись на
str
, я задумався про
\0
, який є null character'ом (або NUL character'му?). Я думав, що рядкові літерали в Сі мають null character. Подивимося, що вийде, якщо ми видалимо його:

printf("%s", "\021%six\012");

Виводить:

%six

Я використовую форматування рядків
"%s"
, тому що рядок, яку я намагаюся вивести, містить форматувальний символ
%
. (Невелика підказка: не виводьте такі рядки
printf(myStr)
, коли вони мають форматирующие символи. Висновок через
%s
показаний вище.)

Здається, воно як і раніше працює без
\0
. Може, в якомусь пре-ANSI Сі потрібно було самому додавати null character'и в рядкові літерали? Думаю, немає, так як інші рядки в програмі їх не мають. Чи так просто виглядає більш заплутано? Гаразд, залишимо поки цей
\0
.

Раз вже зупинилися на цьому рядку, давайте подивимося на її решту.
\xxx
— подання кожного символу в вісімковій системі числення,
\021
— якийсь керуючий символ, а
\012
— символ перекладу рядка або
\n
, як ми його звикли бачити, в кінці виведених рядків.

Знаючи, що
\021
— це всього один символ, зрозуміємо, що
str[1]
%
. Тоді
&str[1]
— рядок, що починається з
%
. Значить рядком насправді може бути просто
%six\n
, без керуючого символу, який не зрозуміло навіщо тут потрібний.

main() {
char str[] = "%six\n";
printf(str, (1)["have"]+"fun"-0x60);
}

Перша рядок, що передається в
printf
, це форматирующая рядок,
%s
означає «помісти наступний рядок замість цієї». Так як ця рядок закінчується на
ix
, можна припустити, що наступна передана в
printf
рядок якимось чином має виглядати як
un
. Запросто позбудемося масиву character'ів, який ми використовували, щоб передати форматирующую рядок, і отримаємо:

main() {
printf("%six\n", (1)["have"]+"fun"-0x60);
}

У наступному рядку маємо:
(1)["have"]+"fun"-0x60
. Тут є
un
, що міститься в слові
fun
, так що давайте розберемо її.

Знову бачимо цей трюк з індексуванням:
(1)["have"]
. Круглі дужки навколо 1 не потрібні. Знову ж таки, це вимагалося в старому Сі або зроблено для більшої нечитабельності?
"have"[1]
— це
a
. У шістнадцятковому представленні вона виглядає як 0x61, віднімаємо 0x60. Тоді залишиться
1+"fun"
.

Так само, як раніше,
"fun"
розшифровується як
char *
. Додаток 1 дає нам рядок, що починається з другого символу, тобто
un
. Тоді все перетворюється на це:

main() {
printf("%six\n", "un");
}

Ось і читабельний код.

Мені подобається, коли в заплутаності коду велику роль відіграє семантика, тобто, коли, наприклад, використовують певне слово
unix
, щоб збити вас з пантелику і змусити подумати, що воно перевизначено і якимось чином виводить своє ім'я. Символ
\021
схожий на інвертований
\012
може змусити вас вважати, що він необхідний, хоча, за фактом, не використовується. Тут так само є форматирующая рядок
%six
, яка містить слово «six», мабуть, щоб ви отримали %s не за форматування, а за що-небудь інше.

Переклад: Олена Карнаухова

Читати ще
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.