- PVSM.RU - https://www.pvsm.ru -
Вызов: Прежде чем лезть под кат, скомпилируйте в голове заголовок статьи, что он дает на выходе?
![main(){printf(&unix["-021%six-012-0"], (unix)[«have»]+«fun»-0x60);} - 1 image](http://www.pvsm.ru/images/2016/10/28/main-printf-unix-quot-021-six-012-0-quot--unix-have-fun-0x60.jpg)
Когда я в очередной раз просматривал книгу «Expert C programming», я вдруг наткнулся на раздел «light relief» в международном конкурсе на самый запутанный код на Си (IOCCC [1]). Это соревнование по написанию как можно более нечитабельного кода. То, что такие конкурсы устраиваются для Си, наверное, говорит что-что об этом языке. Мне хотелось увидеть работы участников этого соревнования. Не найдя никакой информации в интернете, я решил поискать их самостоятельно.
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 [2], которая разрабатывает Электронный сервис передач заключенным [3] и реализовала вирусную рассылку информации [4].]
![main(){printf(&unix["-021%six-012-0"], (unix)[«have»]+«fun»-0x60);} - 2 image](http://www.pvsm.ru/images/2016/10/28/main-printf-unix-quot-021-six-012-0-quot--unix-have-fun-0x60-2.jpg)
В «Expert C programming» об этом говорится следующее:
Избегайте любого использования препроцессора Си, которое изменяет базовый язык.
![main(){printf(&unix["-021%six-012-0"], (unix)[«have»]+«fun»-0x60);} - 3 main(){printf(&unix["-021%six-012-0"], (unix)[«have»]+«fun»-0x60);} - 3](http://www.pvsm.ru/images/2016/10/28/main-printf-unix-quot-021-six-012-0-quot--unix-have-fun-0x60-3.jpg)
Одним из первых победителей в 1987 году был Дэвид Корн, создатель Korn shell’а (что не так с этими shell-райтерами?), который написал всего одну строчку:
main(){printf(&unix["21%six12"], (unix)["have"]+"fun"-0x60);}
Вот и все. Попробуйте скомпилировать это. Что будет выведено?
Этот код не запустится на Майкрософте (подсказка!), но вот ссылка на онлайн компилятор [5], который справится с этой задачей. Там добавлено несколько строк, чтобы оно заработало, но в остальном — все то же самое.
Код всего лишь выводит:
unix
Но почему? В коде есть что-то, что выглядит как массив с названием unix, но он не был декларирован. Тогда unix — это ключевое слово? Оно каким-то образом выводит имя переменной?
Я вслепую попытался проверить это, добавив:
printf(unix);
И он вывел мне ошибку, сказав, что printf принимает char *, а не int.
Когда я вывел эту переменную как int, стало понятно, что ее значение равно 1. Это натолкнуло меня на мысль, что она была переопределена, как если бы код был скомпилирован в Unix-системе. Поискав на gcc source code [6], я нашел, что это run-time target specification [7]. Это объясняет, почему код не запустится на Windows.
unix — это просто 1. Переписав, получим:
main(){printf(&1["21%six12"], (1)["have"]+"fun"-0x60);}
Итак, unix не было названием переменной. Но тогда как же работает 1[]? Я уже видел подобное раньше, и это один из моих любимых фактов о языке Си.
![main(){printf(&unix["-021%six-012-0"], (unix)[«have»]+«fun»-0x60);} - 4 image](http://www.pvsm.ru/images/2016/10/28/main-printf-unix-quot-021-six-012-0-quot--unix-have-fun-0x60-4.gif)
Си берет начало в языке BCPL. Его создатель — доктор Мартин Ричардс, писал [8]:
Оператор косвенного обращения! принимает указатель в качестве аргумента и возвращает содержание ячейки, на которую тот указывает. Если 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("%dn%dn", x[1], 1[x]);
Тогда что есть 1["21%six12"]? Написав в привычном виде, увидим доступ к элементам массива через оператор индексирования:"21%six12"[1]. Все равно нетипично, но уже понятно, что этоarray[index], хотя, как правило, строковые литералы так не используют. Но это работает, поэтому попробуем следующее:
printf("%cn", "hello, world"[1]);
Давайте перепишем только первый массив, пока разбираемся с этим.
main() {
char str[] = "21%six12";
printf(&str[1], (1)["have"]+"fun"-0x60);
}
Все еще работает так же. Посмотрев на str, я задумался о , который является null character’ом (или NUL character’ом?). Я думал, что строковые литералы в Си по умолчанию имеют null character. Поглядим, что получится, если мы удалим его:
printf("%s", "21%six12");
Выводит:
%six
Я использую форматирование строк "%s", потому что строка, которую я пытаюсь вывести, содержит форматирующий символ %. (Небольшая подсказка: не выводите такие строки как printf(myStr), когда они имеют форматирующие символы. Вывод через %s показан выше.)
Кажется, оно по-прежнему работает без . Может, в каком-нибудь пре-ANSI Си нужно было самому добавлять null character'ы в строковые литералы? Думаю, нет, так как другие строки в программе их не имеют. Или так просто выглядит более запутанно? Ладно, оставим пока этот .
Раз уж остановились на этой строке, давайте посмотрим на ее остальную часть. xxx — представление каждого символа в восьмеричной системе счисления, 21 — некий управляющий символ, а 12 — символ перевода строки или n, как мы его привыкли видеть, в конце выводимых строк.
Зная, что 21 — это всего один символ, поймем, что str[1] есть %. Тогда &str[1] — строка, начинающаяся с %. Значит строкой на самом деле может быть просто %sixn, без управляющего символа, который не понятно зачем тут нужен.
main() {
char str[] = "%sixn";
printf(str, (1)["have"]+"fun"-0x60);
}
Первая строка, передаваемая в printf, это форматирующая строка, %s означает «помести следующую строку вместо этой». Так как эта строка заканчивается на ix, можно предположить, что следующая переданная в printf строка каким-то образом должна выглядеть как un. Запросто избавимся от массива character’ов, который мы использовали, чтобы передать форматирующую строку, и получим:
main() {
printf("%sixn", (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("%sixn", "un");
}
Вот и читабельный код.
Мне нравится, когда в запутанности кода большую роль играет семантика, то есть, когда, например, используют определенное слово unix, чтобы сбить вас с толку и заставить подумать, что оно переопределено и каким-то образом выводит свое имя. Символ 21 похож на инвертированный 12 и может заставить вас считать, что он необходим, хотя, по факту, не используется. Тут так же есть форматирующая строка %six, содержащая слово «six», видимо, чтобы вы приняли %s не за форматирование, а за что-нибудь другое.
Перевод: Алена Карнаухова
Автор: Edison
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/203732
Ссылки в тексте:
[1] IOCCC: http://ioccc.org
[2] Edison: https://www.edsd.com/
[3] Электронный сервис передач заключенным: https://www.edsd.ru/ehlektronnyj-servis-peredach-zaklyuchennym
[4] вирусную рассылку информации: https://www.edsd.ru/virusnaya-rassylka-informacii
[5] компилятор: http://ideone.com/QxS783
[6] gcc source code: https://github.com/gcc-mirror/gcc/blob/master/gcc/config/linux.h#L50
[7] run-time target specification: https://gcc.gnu.org/onlinedocs/gcc-3.3/gccint/Run-time-Target.html
[8] писал: http://exple.tive.org/blarg/2013/10/22/citation-needed/
[9] Стратегическая речь Пола Грэма на Defcon 2005: «Неравенство и риск»: https://habrahabr.ru/company/edison/blog/313542/
[10] Питер Хинченс про Optimistic Merging: Сначала люди, потом код. Соберите правильное сообщество, и оно напишет нужный код: https://habrahabr.ru/company/edison/blog/313606/
[11] Источник: https://habrahabr.ru/post/313598/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.