Этот сайт — моя персональная записная книжка. Интересна мне, по большей части, история, своя жизнь и немного программирование.

Маленькая сбойная программа на Си

Попалась мне тут статья про задачку, которую автору задали в университете — написать самую маленькую (по объёму текста) программу на Си, которая компилируется, но выполняется с ошибкой. Прежде чем читать, я попробовал написать такую самостоятельно.

Сначала я подумал о рекурсии:

main(){main();}

Для экономии байт можно использовать стандарт Си89, где всё, у чего нет при определении задания типа, имеет тип int — это позволяет убрать тип функции main.

Программа компилируется, но, как и задумывалось, падает с ошибкой:

bolk@note ~$ gcc-14 -std=c89 small.c && ./a.out
Segmentation fault: 11

Дальше я подумал, что вряд ли в статье используется такой очевидный способ и, перебрав в уме несколько вариантов, решил сосредоточится на делении на ноль. Лучший вариант, который мне пришёл в голову, вот такой, он на один байт меньше:

main(a){a/=0;}

Очевидная идея — поделить число на ноль и никуда не присваивать, работать не будет, компилятор просто выкинет константное выражение. Поэтому его надо или присвоить переменной или вернуть. return занимает больше присваивания, но куда бы мы могли присвоить? Нужна же переменная.

Переменную можно взять из аргументов функции main, в первый её аргумент записывается число параметров при вызове. Если ноль поделить на ноль, это ошибки не вызовет, но там всегда не ноль, потому что нулевой параметр всегда существует и содержит имя программы.

Поэтому мы можем поделить его на ноль и программа упадёт при вызове:

bolk@note ~$ gcc-14 -std=c89 -Wno-div-by-zero small.c && ./a.out
Floating point exception: 8

Но самая короткая сбойная программа оказалась почти втрое меньше. Выглядит она так:

main;

Она компилируется, хоть и с замечанием, и падает, как и требуется по условию:

bolk@note ~$ gcc-14 -std=c89 small.c && ./a.out
small.c:1:1: warning: data definition has no type or storage class
    1 | main;
      | ^~~~
Bus error: 10

Тут определяется статическая переменная main с неявным заданием типа int, которая, как все такие переменные, инициализируется нулём. Поскольку компоновщик видит имена, но ничего не знает о типах, он связывает адрес переменной с адресом точки входа в программу. Когда программа запускается, управление передаётся по адресу ноль, что вызывает ошибку.

Ошибка вызывается, кстати, потому что адрес ноль является зарезервированным значением в Си. Ноль обозначает, что указатель пустой (имеет значение NULL), поэтому при обращении к нему срабатывает специальная ловушка, вызывающая ошибку. Правда это возможно только на платформах с защитой памяти.

Добавлено: Оказалось, что я неправильно разобрался в происходящем.

1 комментарий
Evgeny Sureev 17 дн

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

Евгений Степанищев 17 дн

У меня в голове что-то тут не сходится — main же должна содержать адрес для исполнения, а ней ноль. Или я чего-то не понимаю. Надо, наверное, ассемблер почитать, который получается при компиляции.