5,8K подписчиков

Большой подвох в маленькой задаче на языке C

2,4K прочитали
Сегодня будет очень интересная заметка по системному программированию. Совсем недавно в сообществе Physics.Math.Code ( в telegram, а также в группе VK ) вышла задачка по программированию на языке C.

Сегодня будет очень интересная заметка по системному программированию. Совсем недавно в сообществе Physics.Math.Code ( в telegram, а также в группе VK ) вышла задачка по программированию на языке C.

Она вызвала жаркие дискуссии здесь в VK и еще более ожесточенные споры в нашем канале в telegram. Но так получилось, что точного ответа никто не дал.

Поэтому здесь будет разбор задачи, который предоставил постоянный подписчик Виктор Дымов сообщества Physics.Math.Code. Это человек, который на высоком уровне разбирается в языке C и системном программировании. Поэтому я очень рад, что он дал мне возможность написать в массы о такой интересной задаче. Кстати, вы можете подписаться на чат в telegram Домик дедушки Суня, который ведет Виктор Дымов.

Итак, приступаем...

Задача

Будет ли корректно скомпилирован (без ошибок и ворнингов) следующий код? И если — да, то что будет выведено при его запуске?

Сегодня будет очень интересная заметка по системному программированию. Совсем недавно в сообществе Physics.Math.Code ( в telegram, а также в группе VK ) вышла задачка по программированию на языке C.-2

Остановитесь на этом моменте. Постарайтесь подумать самостоятельно. Сначала дайте ответ на бумаге. Потом попробуйте проверить себя, скормив этот код компилятору. А потом ещё раз подумайте, точно ли вы уверены в своём ответе. 😏

Проводился опрос в группе вот здесь. Было дано несколько вариантов ответов, и правильно ответили примерно 20%.

Сегодня будет очень интересная заметка по системному программированию. Совсем недавно в сообществе Physics.Math.Code ( в telegram, а также в группе VK ) вышла задачка по программированию на языке C.-3

Давайте разбираться почему так... Задача сложная. Почему ответы отличались? Дело в том, что люди компилировали на разных операционных системах, а также на разных компиляторах. Результаты, соответственно, тоже оказались разные.

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

Для начала, нужно обратить внимание на постоянный указатель (лично я его не заметил в самом начале). И казалось бы, программа должна упасть, но она работает... Почему?! Как?!

Вопрос: Первый запуск программы ничего не изменил в выводе. Возникает предположение... То есть приведение к типу (int*) делает копию указателя? И поэтому исходная не меняется (в некоторых режимах работы компилятора) ?

Ответ: Происходит вообще необычная штука... gcc просто меняет значение положив хрен на... проигнорировав const, а остальные при попытке изменить значение const или при вызове внешней процедуры просто снова кладут по адресу старое значение... То есть если компилятор не меняет такой const, то он не кладёт const в секцию .rodata, а просто при каждом подозрительном чихе выполняет инструкцию, которая кладёт старое значение в const 🤔

Вопрос: Почему же вообще запускается, ведь попытка изменить постоянный указатель? Но какая-то очень хитрая попытка?

Ответ: Ну... когда эта ошибка была обнаружена в одном HAL , возникло удивление, что ни один компилятор не выдаёт ни одного предупреждения или ошибки, и, что ещё хуже, при исполнении программы не возникает проблем типа Access violation или Segmentation fault... хотя они ожидаемы...

В комментариях в telegram канале задача вызвала бурные споры и раздаражение, потому что у всех было разное поведение :) Почитать дискуссию вы можете здесь.

Короткий ответ

На большинстве компиляторов этот код компилируется без ошибок. А вот поведение зависит от компилятора, так как оно не определено (Undefined behavior), причём один набор компиляторов может давать разный результат.

Вот, например, первый скриншот с поведением под ОС Linux.

1. Поведение под ОС Linux (gcc, g++, clang)
1. Поведение под ОС Linux (gcc, g++, clang)

Заметьте, что:
◾ сборка gcc меняет значение const,
◾ сборка g++ не меняет значение const и
◾ сборка clang (LLVM) не меняет значение const.

Второй скриншот с поведением под ОС Windows.

2. Поведение под ОС Windows (cl C и cl C++)
2. Поведение под ОС Windows (cl C и cl C++)

Сборка Microsoft'овским компилятором (версия видна на снимке) тоже может менять, а может и не менять значение const.

Проблема этого подводного камня в том, что о нём никто никогда не скажет: ни компилятор, ни операционная система. И что ещё хуже при исполнении программы не возникает проблем типа Access violation или Segmentation fault, хотя казалось бы их стоит ожидать.

Длинный ответ

Что же происходит на самом деле? На третьем скриншоте сравнивается не оптимизированный Ассемблер, генерируемый компиляторами gcc и g++.

3. Сравнение не оптимизированного Ассемблера
3. Сравнение не оптимизированного Ассемблера

Очевидно, что значение const не попало в секцию .rodata.

При сравнении кодов видно, что присвоение значения константе производится в строках 16-18. А в строках 22-25 производится изменение значение const. И в первом случае gcc проходит всю процедуру присвоения, а вот во втором случае g++ значение const принудительно устанавливает в 111 до окончания процедуры присвоения мнемоникой

movl $111, %esi

4. Сравнение оптимизированного Ассемблера
4. Сравнение оптимизированного Ассемблера

На четвертом скриншоте сравниваются сгенерированные коды с оптимизацией. И можно видеть, что присвоение значения const происходит в строке 17, затем gcc в строке 21 меняет значение игнорируя квалификатор const, а g++ в той же 17-ой строке после вызова внешней процедуры printf снова, кладёт первоначальное значение в const.

Хотя мы рассмотрели только компиляцию с набором GCC, остальные компиляторы ведут себя точно также, что вы можете проверить сами.

То есть, квалификатор const это соглашение, которое должно корректно обрабатываться компилятором, и присвоение значений переменным с квалификатором const по приведённым указателям вызывает Undefined behavior. И так как обрабатывается это компиляторами по разному, то поведение запущенного кода может быть разным. Плохо что компиляторы это обрабатывают молча и ошибок в runtime'е не возникает.

Проблема этого подводного камня в том, что о нем никто никогда не скажет ни компилятор, ни операционная система.

А зачем всё это? Эта задача имеет отношение к реальности?

После прочтения вечно недовольных комментаторов, некоторые задаются вопросом «А зачем такая задача?» , поэтому ниже дополнение ответа со случаем из практики.

Собственно эта задача возникла потому, что в HAL (Hardware Abstraction Layer) в драйвере одного из портов одной весьма промышленной железки был практически случайно замечен код, который пишет и читает байты из порта. Код выглядел примерно следующим образом:

По задумке автора этого кода, функция readByteFromPort должна взять байт из порта port и положить его в destinationBytePtr, несмотря на то, что значение по адресу destinationBytePtr объявлено как const.

Этот код на начальном этапе собирался gcc. Как понятно из объяснения, gcc в таком случае меняет переменную с квалификатором const через указатель и код работает как от него и ожидается. Но если собрать этот же код другим компилятором, который не меняет переменную с квалификатором const, то, очевидно, программа будет получать из порта одно и тоже значение совершенно независимо от того, что происходит на порту в реальности. И никаких ошибок и предупреждений ни от компилятора, ни от среды исполнения программист не получит.

Естественно, чтобы не вызывать Undefined behavior, функцию readByteFromPort следовало написать как

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

И полезно это для прикладных программистов, которые при использовании HAL/BSP/CSP считают, что именно их код источник ошибки или порт не работает. Но иногда полезно заглянуть и в файлы заголовков HAL/драйверов, чтобы убедиться, что автор драйвера не накопипастил кода с Undefined behavior. Ошибки с Undefined behavior очень трудно отловить, что и видно на данном примере.

Подробнее о квалификаторе const в стандарте C или тут

Статья написана благодаря консультации Виктора Дымова.

Понравилась статья ? Проявите активность в комментариях, поделитесь этой заметкой в социальных сетях.

Если Вам нужен репетитор по физике, математике или информатике/программированию, Вы можете написать мне или в мою группу Репетитор IT mentor в VK
Библиотека с книгами для физиков, математиков и программистов
Репетитор IT mentor в telegram