Еще раз. Критерием "баг это или не баг" я считаю статус в багзилле GCC.
И что дальше? В самом начале вы писали:
Только вы определитесь, вы конкретный компилятор Си ругаете, ли сам Си?
Я продемонстрировал вам, что поведение компилятора до исправления "бага" и после не противоречит стандарту. Более того, компилятор продолжает порождать вызовы memcmp/memcpy/memset/memmove. По этой причине пример в статье вполне закономерный, и подобная оптимизация могла произойти в любом другом компиляторе.
Не нравится официальный стандарт - изобретите свой стандарт и сделайте там так, как вам нравится.
Зачем? Создавать еще один одноименный язык высокого уровня и вводить людей в заблуждение я не собираюсь, а Си, который Ритчи сделал изначально, на данный момент практически бесполезен.
Совсем-совсем все, т.е. 100% программ вдруг стали неработающими? Неработающими с точки зрения стандарта C89? А с точки зрения K&R они точно все были работающими?
Хорошо, раз уж вы обратились к истокам, то вот вам, к примеру, мануал по языку Си, написанный Ритчи до того, как был принят стандарт:
Там, к примеру, рукой создателя языка написано следующее:
C supports four fundamental types of objects: characters, integers, single-, and double-precision floating-point numbers.
Characters (declared, and hereinafter called, char) are chosen from the ASCII set; they occupy the rightmost seven bits of an 8-bit byte. It is also possible to interpret chars as signed, 2’s complement 8-bit numbers.
Integers (int) are represented in 16-bit 2’s complement notation
Это очень похоже на текст стандарта? Насколько я могу судить, эволюция у языка была потрясающая. Как думаете, подобные изменения хорошо сказались на работоспособности программ, написанных с использованием этого документа?
Вот в Java размер типа int как раз указан, выходит что Java это язык низкого уровня?
Я этого не говорил. Все, что я сказал - это то, что Паскаль - это язык высокого уровня, и именно поэтому размер числового типа вводить необязательно.
Это не отменяет того факта, что программы на языке Паскаль могут перестать работать из-за того, что тип Integer вдруг стал другим, и не вмещает в себя нужный диапазон. Так что же получается, "Паскаль должен умереть"?
Во-первых, как я уже сказал, Паскаль - это не Си, и такое поведение в нем всегда было определено как Error. Во-вторых, прочитайте внимательно тот текст стандарта Паскаля, который я процитировал. Описанное там поведение сильно отличается от того, что говорит о знаковом переполнении стандарт Си. Если реализация Паскаля соответствует пунктам 2 или 3, то тогда ваша программа в худшем случае аварийно завершит исполнение, в то время как аналогичная программа на Си может, согласно стандарту, сделать что-угодно. В случае, если реализация следует пункту 1, то тогда вам нужно обращаться к документации компилятора, где это поведение должно быть четко определено. Опять же, стандарт Си такое требование не накладывает.
Я о том, что Си и Паскаль можно компилировать в достаточно эффективный машинный код для фон-неймановских архитектур.
Что такое достаточно эффективный код для фоннеймановских архитектур? И при чем тут упомянутые вами в предыдущем сообщении абстракции?
Что вообще такое язык высокого и язык низкого уровня?
Хорошо, я уточню используемую мною терминологию.
Когда я говорю, что язык Си - это язык высокого уровня, я имею в виду то, что он абстрагирован от конкретной реализации компилятора и компьютерной архитектуры. Как говорит сам стандарт, его описание содержит "unambiguous and machine-independent definition of the language C".
Когда же я говорю, что Си - это язык низкого уровня, я подразумеваю, что его описание напрямую зависит от реализации компилятора и особенностей компьютера PDP-11. Как пишет сам Ритчи в упомянутом ранее мануале:
This paper is a manual only for the C language itself as implemented on the PDP-11.
Т.е. такой язык практически неотличим по семантике от упомянутых вами языков ассемблера и машинного языка. Наличие, к примеру, явных конструкций ветвления и циклов ситуацию не меняет - с достаточно мощным макропроцессором их можно добавить и в ассемблер. От этого абстрагированным от конкретной архитектуры (высокоуровневым) он не станет.
Писать интерпретаторы ассемблера незаконно?
Этого я тоже не говорил. Я сказал только то, что возможность трансляции текста языка высокого уровня в машинный код (или какой-либо текст на другом языке) не имеет никакого отношения к самому языку.
Я язык Си не считаю языком низкого уровня, язык низкого уровня для меня это ассемблер, машинный код.
Вы, к вашему счастью, не считаете, а огромное количество других программистов (в том числе и создатель языка, если верить написанному им же мануалу) считают или считали иначе. Статья написана именно для таких людей с целью предостеречь их от ошибок. Если мыслить о языке Си в терминах его реализации, предполагать, какой код сгенерирует компилятор, то тогда он действительно становится неотличим по семантике от того же ассемблера. При этом до 89-ого года писать на нем код каким-либо другим образом было невозможно - четкого, абстрагированного от конкретной архитектуры описания не существовало. За 20 лет жизни языка появилось огромное количество программ, которые в момент выхода стандарта оказались формально нерабочими. Причем из-за особенностей текста стандарта (в том числе благодаря постоянному использованию неопределенного поведения) это произошло практически незаметно. Разгребать последствия этих решений мы будем еще очень долго.
Багом это является по причине того, что в багзилле GCC признали, что это баг.
Багом эту ситуацию обозвал Richard Biener, который в конечном итоге просто добавил к флагу -ffreestanding отключение конкретной оптимизации циклов, которая не позволяла реализовать memcpy на языке Си.
В том же обсуждении другой разработчик gcc упоминает, что "проблема" остается актуальной до сих пор, так как компилятор продолжает добавлять вызовы memcpy во freestanding окружении, но уже в других местах:
Note that the compiler emits calls to memcpy for struct copies anyway, so if there is a problem it is a long-standing one.
И это, как уже было отмечено ранее, не противоречит ни документации gcc, ни стандарту Си. То, что реализация компилятора не позволила реализовать на нем memcpy не говорит о том, что gcc - это неправильный компилятор языка Си. Это поведение абсолютно валидно с точки зрения стандарта - никто вам такой функционал предоставлять не обязан.
Эту точку зрения подтверждает все тот же Richard Biener:
-fno-builtin-XXX does not prevent GCC from emitting calls to XXX. It only makes GCC not assume anything about existing calls to XXX.
Так что исправление "бага" - это просто костыль, который позволяет не писать memcpy на языке ассемблера. По сути ничего остального в работе компилятора он не меняет. Т.е. он все еще может теоретически добавить рекурсивный вызов в ваше определение функции memcpy, если вы напишите код определенным образом.
Ну допустим не меняли бы они ничего, выпустили бы вместо C89 новый частично совместимый с Си язык
Что вы имеете в виду под частично совместимым? C89 - это не частично совместимый с Си язык, а его единственно верный, как уверяет комитет, вариант:
The need for a single clearly defined standard had arisen in the C community due to a rapidly expanding use of the C programming language and the variety of differing translator implementations that had been and were being developed. The existence of similar but incompatible implementations was a serious problem for program developers who wished to develop code that would compile and execute as expected in several different environments.
Как говорится, благими намерениями вымощена дорога в ад.
старые программы портировали бы на этот "Extended-C" а сам исконно-посконный Си забросили бы
Разница тут в том, что когда вы переписываете программу на новом языке, вы не ожидаете, что семантика у него будет такая же, как и у старого. Тут же случилось так, что все написанные к 89-ому году работающие программы на Си неожиданно в одну секунду оказались, согласно стандарту, неработающими. Только никто этого не заметил, и проблема как раз в том, что программы людьми действительно не портировались (а этих программ было много - один 4.3BSD чего стоил). Все продолжали писать так, как делали это раньше. Последствия неожиданной метаморфозы языка Си из низкоуровневого (по сути ассемблера) в высокоуровневый язык программирования стали очевидны далеко не сразу. А большинство из них неочевидны до сих пор - сколько UB сейчас хранится в исходных кодах легиона программ - страшно себе представить.
Вот например в паскале у Integer какой размер в байтах?
Паскаль всегда был и остается языком высокого уровня, поэтому размер типа Integer там не указан намеренно. Причем переполнение числа является, согласно стандарту Паскаля, ошибкой (Error):
D.47 6.7.2.2 It is an error if an integer operation or function is not performed according to the mathematical rules for integer arithmetic.
Причем сам Error стандарт Паскаля определяет гораздо мягче, чем Undefined Behavior у Си. В частности, такие ограничения накладывает документ на обработку Error-ов:
treat each violation that is designated an error in at least one of the following ways:
1) there shall be a statement in an accompanying document that the error is not reported, and a note referencing each such statement shall appear in a separate section of the accompanying document; 2) the processor shall report the error or the possibility of the error during preparation of the program for execution and in the event of such a report shall b e able to continue further processing and shall be able to refuse execution of the program-block; 3) the processor shall report the error during execution of the program
Т.е., если компилятор Паскаля явно не указал в своей документации иное поведение, он обязан сообщить об ошибке либо во время исполнения программы, либо еще во время ее компиляции.
В любом случае Паскаль, в отличие от Си, никогда за свою историю не определялся как язык низкого уровня. Поэтому то же знаковое переполнение всегда было ошибкой, и никто из людей, кто знаком с документацией языка, не будет спорить об обратном. Попытки обойти это ограничение посредством инструментов компилятора являются хаками и к самому языку отношения не имеют.
Какие компилируемые в машинный код языки программирования, сопоставимые по уровню абстракции с тем же Си или паскалем
Где в стандартах Си или Паскаля указано, что они компилируется в машинные коды? Для языка высокого уровня безразлично, каким образом будут транслироваться написанные на нем программы. Тем более, что интерпретаторы Си тоже существуют.
Не получится одновременно писать и на низкоуровневом языке Си, и на высокоуровневом - это оксюморон. Печальные последствия такого программирования мы наблюдаем постоянно. Именно это, а не что-то другое, и явилось причиной, почему мы хотим, чтобы Си умер.
Багом GCC является то, что использование некоторых флагов не позволяет эту оптимизацию отключить.
Как багом может являться документированное поведение компилятора? Ни один из указанных автором поста флагов не гарантирует, что компилятор не будет вставлять вызовы memcpy в код. Даже упомянутый "-fno-tree-loop-distribute-patterns" не позволяет утверждать, что компилятор не добавит вызов memcpy куда-нибудь еще, так как этот флаг лишь отключает конкретную оптимизацию циклов.
То, что по стандарту (т.е. не используя специальных опций компилятора) нельзя написать свою реализацию memcpy, не является серьезной проблемой языка Си, т.к. во-первых такие функции не надо каждый день писать и во-вторых, это решается специальными флагами компилятора.
Проблема - это программа, которая не работает. А не работает она в том числе потому, что комитет стандартизаторов посчитал возможном полностью изменить семантику языка, который к тому моменту существовал уже 20 лет.
Как я указал выше - даже с помощью флагов gcc (как минимум тех, которые указаны на багзилле) невозможно гарантированно реализовать memcpy так, чтобы в нем не было вызовов функции memcpy. Это может сработать для одной версии компилятора, а для другой - нет.
Данный пример нужен для того, чтобы показать, что человек не в состоянии предсказать, какую оптимизацию совершит компилятор. Пытаться думать как он - это бесполезная трата времени. Поэтому чрезвычайно важно не допускать ошибки и не ждать того момента, когда они вылезут наружу.
Под который нужно было бы переписывать весь старый код?
Был старый код, а стал неработающий код. Если у комитета была задача сломать как можно больше кода (и старого, и нового), то они ее достигли с большим успехом.
Потому что под Си написано много кода, и его хотелось бы не переписывать, а дорабатывать.
Ну правильно, давайте бесконечно дорабатывать работающие программы - делать нам что ли больше нечего?!
А кто сказал, что такой язык никто не сделал?
Я не говорил. Я осуждаю комитет не за то, что они не сделали, а за то, что они сделали - абсолютно новый Си, который выдает себя за старый Си.
Введение в заблуждение в чем заключается? Си времен K&R был хорошим, а потом стал плохим?
Си, созданный Ритчи, был языком для конкретной машины. Как только он вылез за рамки PDP-11, начались проблемы. Компиляторы Си под новые архитектуры постоянно ломали старый код. А стандарт еще больше подлил масла в огонь - внешне язык практически не изменился, но при этом связь с PDP-11 перестал иметь вообще. Но им было этого мало, и чтобы залатать часть неработающих оптимизаций, они добавили ключевое слово volatile. Без него компиляторы не могли понять, какие обращения в память можно оптимизировать, а какие - нет:
"But all bets are off if you do arithmetic or comparisons with pointers pointing to different arrays. If you're lucky, you'll get obvious nonsense on all machines. If you're unlucky, your code will work on one machine but collapse mysteriously on another."
Разница между тем, что написано тут, и что в стандарте очевидна. Программирование на Си до C89 предполагало понимание того, какой код генерирует компилятор под целевую архитектуру. Это было хоть как-то возможно потому, что сами компиляторы были гораздо проще, да и портировать нужно было намного меньше. Но бесконечно так продолжаться просто не могло. Для Си того времени просто немыслима возможность наличия двух указателей с одинаковым содержимым, проверка на равенство для которых возвращает ложь. Разумеется речь идет про плоскую память. А вот для стандартного Си - это вполне нормально, так как подобное сравнение - это неопределенное поведение. И на самом деле еще хорошо, что такое сравнение ложь возвращает, а ведь может еще, если верить стандарту, вам исходный код удалить или монитор сжечь.
Скорее так: если нужно реализовывать memcpy и прочие функции из стандартной библиотеки Си, нужно использовать специальные флаги компилятора или прагмы, не дающие компилятору соптимизировать это в рекурсию. Программисты редко когда свой memcpy реализовывают, так что это не то чтобы большая проблема.
Вообще это была ирония, потому что по сути обмануть компилятор - это тоже решение. Только далеко не самое адекватное по очевидным причинам.
Now, if this replacement still happens when you compile with -nostdlib, that would be a bug since it becomes legal code in that case.
Это не баг, -nostdlib работает так, как написано в документации gcc:
-nostdlib
Do not use the standard system startup files or libraries when linking. No startup files and only the libraries you specify will be passed to the linker. The compiler may generate calls to memcmp, memset, memcpy and memmove. These entries are usually resolved by entries in libc. These entry points should be supplied through some other mechanism when this option is specified.
Так или иначе вопрос не в том, как заставить компилятор не делать какие-то оптимизации, а в том, валидны ли эти оптимизации с точки зрения языка. И да, они абсолютно валидны. Единственный момент тут в том, что если вы сообщите компилятору, что у вас freestanding окружение (для gcc нужен флаг -ffreestanding), то тогда все, что связано со стандартной библиотекой, будет зависеть от реализации компилятора:
In a freestanding environment (in which C program execution may take place without any benefit of an operating system), the name and type of the function called at program startup are implementation-defined. There are otherwise no reserved external identifiers. Any library facilities available to a freestanding program are implementation-defined.
Но это совсем не означает, что в таком случае компилятор не может самостоятельно добавлять в код вызовы функций стандартной библиотеки. Однако implementation-defined behavior разработчики компилятора уже должны документировать, что они и сделали для упомянутого флага -nostdlib.
Для переносимости и для более агрессивной оптимизации.
Ну так и сделали бы новый, прекрасный, переносимый, легко оптимизируемый язык. Зачем было брать прибитый ржавыми гвоздями к архитектуре PDP-11 Си и вводить в заблуждение кучу людей?
А где она разрешима? Кроме ассемблера, я не слышал о других языках, которые бы давали такой уровень контроля. Как такой язык должен выглядеть, если это будет не ассемблер?
Любой, в документации/спецификации/стандарте которого написано, что он на такое способен. И да, скорее всего это будет только ассемблер, но это не такой уж и плохой вариант. Никто не мешает вам линковаться с объектными файлами, написанными на асме. Решение с флагами компилятора тоже неплохое, но только при условии, что вы действительно уверены, что оно сработает как нужно. Но в рамках самого языка Си решить эту проблему невозможно - в этом и был смысл примера, который мы дали в тексте.
Если размер байта больше 8 бит (такое вполне бывает и разрешено стандартом), ничего страшного не произойдет. Если хочется получить UB, лучше использовать макрос CHAR_BIT
Да, стандарт действительно не устанавливает верхнюю планку для размера char, однако минимальный размер - это всегда 8 (2.2.4.2 Numerical limits):
The values given below shall be replaced by constant expressions suitable for use in #if preprocessing directives. Their implementation-defined values shall be equal or greater in magnitude (absolute value) to those shown, with the same sign.
* maximum number of bits for smallest object that is not a bit-field (byte) CHAR_BIT 8
Так что использование явной константы 8 в качестве размера для битового сдвига достаточно, чтобы вызвать UB в большинстве случаев, в том числе и при компиляции примера gcc для x86 (что и было продемонстрировано).
Формально вы, конечно, правы, и чтобы словить UB во всех возможных ситуациях, нужно использовать макрос, но идея тут была в другом. На самом деле пример со сдвигом нужен, чтобы показать, что даже для той архитектуры, для которой известны размер минимально адресуемой ячейки памяти (8 бит) и поведение инструкции битового сдвига, нет гарантии, что будет сгенерирован тот код, который ожидает программист.
Только вы определитесь, вы конкретный компилятор Си ругаете, ли сам Си?
Спасибо за отличную ссылку, вот этот комментарий порадовал особенно:
We are not presently experiencing this issue in musl libc, probably because the current C memcpy code is sufficiently overcomplicated to avoid getting detected by the optimizer as memcpy.
Т.е. реализация memcpy должна быть настолько сложной, чтобы компилятор не смог оптимизировать ее исходный код - прекрасно!
Почему вы считаете, что это баг компилятора? Компилятор ведет себя в соответствии со стандартом (4.1.2 Standard headers):
Each library function is declared in a header, whose contents are made available by the #include preprocessing directive. The header declares a set of related functions, plus any necessary types and additional macros needed to facilitate their use. Each header declares and defines only those identifiers listed in its associated section. All external identifiers declared in any of the headers are reserved, whether or not the associated header is included. All external identifiers that begin with an underscore are reserved. All other identifiers that begin with an underscore and either an upper-case letter or another underscore are reserved. If the program defines an external identifier with the same name as a reserved external identifier, even in a semantically equivalent form, the behavior is undefined.
Да и на самом деле мы ругаем не вакуумный язык Си, а то, что его концепция и история вводит людей в заблуждение о его сущности. И это заблуждение поддерживалось в том числе комитетом стандартизаторов - для чего было вводить столько UB, перекладывая ответственность на создателей компиляторов, и делать вид, что с Си ничего не произошло? Си, который создал Ритчи, и тот Си, который определен в стандарте - это абсолютно разные языки, только внешне они практически неотличимы. Первый - это язык для конкретной машины PDP-11, второй - абстрагированный язык высокого уровня.
А еще существует вероятность, что какие-то куски пароля останутся в каких-нибудь регистрах, как это проконтролировать? Может тогда на ассемблере писать, ну что уж точно ничего случайно не просочилось? И кстати пароли в открыдом виде не хранят, а хранят их хэши.
Мы упомянули о том, что пароль (или его часть) может остаться в регистре. На самом деле ответ прост - в рамках самого языка Си эта проблема неразрешима. В тексте стандарта даже, очевидно, слово "stack" не упоминается - об очищении чего тогда вообще может идти речь? Локальные переменные могут находиться где-угодно, и исключительно средствами Си это проконтролировать невозможно. Подробнее об этом написано тут (ссылка также есть в статье):
Насчет хэшей - мы использовали пароль, чтобы не вдаваться лишний раз в подробности. Это простой пример, смысл которого в том, что порой бывает важно очищать все места хранения потенциально опасных данных, и что Си в этом вопросе никак помочь не может.
В тексте речь шла про оптимизации компилятора gcc. Второй ваш результат как раз и совпадает с тем, что я сказал выше - оба сравнения возвращают ложь, хотя как минимум два указателя имеют абсолютно одинаковое содержимое.
Если бы в стандарте языка было указано, что вся целочисленная арифметика явным образом использует дополнительный код, то для тех архитектур, которые ее не поддерживают, пришлось бы имитировать такое поведение. Впрочем, тут нет ничего чрезвычайно страшного - компиляторам языка Си и без этого приходится программно реализовывать те операции, которые целевые архитектуры также не поддерживают. Так, например, для тех машин, которые не содержат инструкций для целочисленного деления и не только, gcc использует специальную библиотеку libgcc. Подробнее можно почитать здесь:
Я этого не говорил да и полагаю, что они оба просто высокоуровневые. Ни один из них не более или не менее высокоуровневый, чем другой, так как нормальных критериев я придумать не могу. Да и какой смысл? Вообще я имел в виду немного другое - большое количество программистов предполагают, что операции в Си будут имеют ровно такое же поведение, как и соответствующие им инструкции на целевой архитектуре. Т.е. если вы скомпилировали код под x86, то битовый сдвиг на размер переменной должен оставить ее нетронутой. В теории и на практике это так не работает. Поэтому Си - это не язык низкого уровня. То, как в стандарте языка определяется битовый сдвиг или знаковое переполнение не влияет на то, станет он от этого более высокого или низкого уровня (если только прямо не сказано, какую инструкцию должен использовать компилятор при трансляции). Да, неопределенное поведение позволяет совершать более агрессивные оптимизации, но всему есть предел. Си просто переполнен UB, приличная часть которого вообще имеет мало смысла. Конкретно проверять знаковое переполнение перед каждой потенциально опасной операцией гораздо неудобнее, чем это делать после.
Спасибо большое за ссылки и комментарий! К сожалению, в текст одной статьи невозможно уместить все сразу, тем более, что бездна UB неисчерпаема. Идея была в том, чтобы провести неискушенного в тонкостях стандарта читателя от достаточно тривиальных ошибок до гораздо более изощренных и опасных случаев неопределенного поведения. Безусловно, говоря о том, что Си должен умереть, я сам не надеюсь на то, что это произойдет. Смысл в том, что вокруг этого языка существует огромное количество серьезных заблуждений, и вызваны они отнюдь не только непрофессионализмом тех, кто на нем пишет. И учитывая историю Си, и то, как написан стандарт, неудивительно, что ошибки неопределенного поведения стали обыденностью. Я сам долгое время считал, что Си - это своего рода кроссплатформенный ассемблер. Потому я полагаю, что концепция языка, который создает столь опасную иллюзию , является по меньшей мере неудачной. И в том случае, если Си остается с нами надолго (а у меня нет оснований считать иначе), очень важно, чтобы программисты четко осознавали то, какой именно инструмент они используют.
Да, по сути верно. Единственная загвоздка тут в том, что знаковое переполнение, ровно как и многие другие случаи неопределенного поведения, большинством программистов считалось правомерным. В этой позиции есть смысл, если думать о Си как языке низкого уровня. В действительности это не так. Отсюда и огромное количество сломанного кода, и требование откатить изменения компилятора.
Уже наличие сборщика мусора в языке автоматически исключает возможность стать заменой языку Си. Также необходим zero runtime, т.е. возможность полного отказа от использования стандартной библиотеки. Иначе просто нереально будет писать ядра операционных систем или прошивки для микроконтроллеров. А это именно те области, где для Си нет альтернатив.
И что дальше? В самом начале вы писали:
Я продемонстрировал вам, что поведение компилятора до исправления "бага" и после не противоречит стандарту. Более того, компилятор продолжает порождать вызовы memcmp/memcpy/memset/memmove. По этой причине пример в статье вполне закономерный, и подобная оптимизация могла произойти в любом другом компиляторе.
Зачем? Создавать еще один одноименный язык высокого уровня и вводить людей в заблуждение я не собираюсь, а Си, который Ритчи сделал изначально, на данный момент практически бесполезен.
Хорошо, раз уж вы обратились к истокам, то вот вам, к примеру, мануал по языку Си, написанный Ритчи до того, как был принят стандарт:
https://www.bell-labs.com/usr/dmr/www/cman.pdf
Там, к примеру, рукой создателя языка написано следующее:
Это очень похоже на текст стандарта? Насколько я могу судить, эволюция у языка была потрясающая. Как думаете, подобные изменения хорошо сказались на работоспособности программ, написанных с использованием этого документа?
Я этого не говорил. Все, что я сказал - это то, что Паскаль - это язык высокого уровня, и именно поэтому размер числового типа вводить необязательно.
Во-первых, как я уже сказал, Паскаль - это не Си, и такое поведение в нем всегда было определено как Error. Во-вторых, прочитайте внимательно тот текст стандарта Паскаля, который я процитировал. Описанное там поведение сильно отличается от того, что говорит о знаковом переполнении стандарт Си. Если реализация Паскаля соответствует пунктам 2 или 3, то тогда ваша программа в худшем случае аварийно завершит исполнение, в то время как аналогичная программа на Си может, согласно стандарту, сделать что-угодно. В случае, если реализация следует пункту 1, то тогда вам нужно обращаться к документации компилятора, где это поведение должно быть четко определено. Опять же, стандарт Си такое требование не накладывает.
Что такое достаточно эффективный код для фоннеймановских архитектур? И при чем тут упомянутые вами в предыдущем сообщении абстракции?
Хорошо, я уточню используемую мною терминологию.
Когда я говорю, что язык Си - это язык высокого уровня, я имею в виду то, что он абстрагирован от конкретной реализации компилятора и компьютерной архитектуры. Как говорит сам стандарт, его описание содержит "unambiguous and machine-independent definition of the language C".
Когда же я говорю, что Си - это язык низкого уровня, я подразумеваю, что его описание напрямую зависит от реализации компилятора и особенностей компьютера PDP-11. Как пишет сам Ритчи в упомянутом ранее мануале:
Т.е. такой язык практически неотличим по семантике от упомянутых вами языков ассемблера и машинного языка. Наличие, к примеру, явных конструкций ветвления и циклов ситуацию не меняет - с достаточно мощным макропроцессором их можно добавить и в ассемблер. От этого абстрагированным от конкретной архитектуры (высокоуровневым) он не станет.
Этого я тоже не говорил. Я сказал только то, что возможность трансляции текста языка высокого уровня в машинный код (или какой-либо текст на другом языке) не имеет никакого отношения к самому языку.
Вы, к вашему счастью, не считаете, а огромное количество других программистов (в том числе и создатель языка, если верить написанному им же мануалу) считают или считали иначе. Статья написана именно для таких людей с целью предостеречь их от ошибок. Если мыслить о языке Си в терминах его реализации, предполагать, какой код сгенерирует компилятор, то тогда он действительно становится неотличим по семантике от того же ассемблера. При этом до 89-ого года писать на нем код каким-либо другим образом было невозможно - четкого, абстрагированного от конкретной архитектуры описания не существовало. За 20 лет жизни языка появилось огромное количество программ, которые в момент выхода стандарта оказались формально нерабочими. Причем из-за особенностей текста стандарта (в том числе благодаря постоянному использованию неопределенного поведения) это произошло практически незаметно. Разгребать последствия этих решений мы будем еще очень долго.
Багом эту ситуацию обозвал Richard Biener, который в конечном итоге просто добавил к флагу -ffreestanding отключение конкретной оптимизации циклов, которая не позволяла реализовать memcpy на языке Си.
В том же обсуждении другой разработчик gcc упоминает, что "проблема" остается актуальной до сих пор, так как компилятор продолжает добавлять вызовы memcpy во freestanding окружении, но уже в других местах:
И это, как уже было отмечено ранее, не противоречит ни документации gcc, ни стандарту Си. То, что реализация компилятора не позволила реализовать на нем memcpy не говорит о том, что gcc - это неправильный компилятор языка Си. Это поведение абсолютно валидно с точки зрения стандарта - никто вам такой функционал предоставлять не обязан.
Эту точку зрения подтверждает все тот же Richard Biener:
Так что исправление "бага" - это просто костыль, который позволяет не писать memcpy на языке ассемблера. По сути ничего остального в работе компилятора он не меняет. Т.е. он все еще может теоретически добавить рекурсивный вызов в ваше определение функции memcpy, если вы напишите код определенным образом.
Что вы имеете в виду под частично совместимым? C89 - это не частично совместимый с Си язык, а его единственно верный, как уверяет комитет, вариант:
Как говорится, благими намерениями вымощена дорога в ад.
Разница тут в том, что когда вы переписываете программу на новом языке, вы не ожидаете, что семантика у него будет такая же, как и у старого. Тут же случилось так, что все написанные к 89-ому году работающие программы на Си неожиданно в одну секунду оказались, согласно стандарту, неработающими. Только никто этого не заметил, и проблема как раз в том, что программы людьми действительно не портировались (а этих программ было много - один 4.3BSD чего стоил). Все продолжали писать так, как делали это раньше. Последствия неожиданной метаморфозы языка Си из низкоуровневого (по сути ассемблера) в высокоуровневый язык программирования стали очевидны далеко не сразу. А большинство из них неочевидны до сих пор - сколько UB сейчас хранится в исходных кодах легиона программ - страшно себе представить.
Паскаль всегда был и остается языком высокого уровня, поэтому размер типа Integer там не указан намеренно. Причем переполнение числа является, согласно стандарту Паскаля, ошибкой (Error):
Причем сам Error стандарт Паскаля определяет гораздо мягче, чем Undefined Behavior у Си. В частности, такие ограничения накладывает документ на обработку Error-ов:
Т.е., если компилятор Паскаля явно не указал в своей документации иное поведение, он обязан сообщить об ошибке либо во время исполнения программы, либо еще во время ее компиляции.
В любом случае Паскаль, в отличие от Си, никогда за свою историю не определялся как язык низкого уровня. Поэтому то же знаковое переполнение всегда было ошибкой, и никто из людей, кто знаком с документацией языка, не будет спорить об обратном. Попытки обойти это ограничение посредством инструментов компилятора являются хаками и к самому языку отношения не имеют.
Где в стандартах Си или Паскаля указано, что они компилируется в машинные коды? Для языка высокого уровня безразлично, каким образом будут транслироваться написанные на нем программы. Тем более, что интерпретаторы Си тоже существуют.
Не получится одновременно писать и на низкоуровневом языке Си, и на высокоуровневом - это оксюморон. Печальные последствия такого программирования мы наблюдаем постоянно. Именно это, а не что-то другое, и явилось причиной, почему мы хотим, чтобы Си умер.
Как багом может являться документированное поведение компилятора? Ни один из указанных автором поста флагов не гарантирует, что компилятор не будет вставлять вызовы memcpy в код. Даже упомянутый "-fno-tree-loop-distribute-patterns" не позволяет утверждать, что компилятор не добавит вызов memcpy куда-нибудь еще, так как этот флаг лишь отключает конкретную оптимизацию циклов.
Проблема - это программа, которая не работает. А не работает она в том числе потому, что комитет стандартизаторов посчитал возможном полностью изменить семантику языка, который к тому моменту существовал уже 20 лет.
Как я указал выше - даже с помощью флагов gcc (как минимум тех, которые указаны на багзилле) невозможно гарантированно реализовать memcpy так, чтобы в нем не было вызовов функции memcpy. Это может сработать для одной версии компилятора, а для другой - нет.
Данный пример нужен для того, чтобы показать, что человек не в состоянии предсказать, какую оптимизацию совершит компилятор. Пытаться думать как он - это бесполезная трата времени. Поэтому чрезвычайно важно не допускать ошибки и не ждать того момента, когда они вылезут наружу.
Был старый код, а стал неработающий код. Если у комитета была задача сломать как можно больше кода (и старого, и нового), то они ее достигли с большим успехом.
Ну правильно, давайте бесконечно дорабатывать работающие программы - делать нам что ли больше нечего?!
Я не говорил. Я осуждаю комитет не за то, что они не сделали, а за то, что они сделали - абсолютно новый Си, который выдает себя за старый Си.
Си, созданный Ритчи, был языком для конкретной машины. Как только он вылез за рамки PDP-11, начались проблемы. Компиляторы Си под новые архитектуры постоянно ломали старый код. А стандарт еще больше подлил масла в огонь - внешне язык практически не изменился, но при этом связь с PDP-11 перестал иметь вообще. Но им было этого мало, и чтобы залатать часть неработающих оптимизаций, они добавили ключевое слово volatile. Без него компиляторы не могли понять, какие обращения в память можно оптимизировать, а какие - нет:
https://groups.google.com/g/comp.std.c/c/tHvQhiKFtD4/m/zfIgJhbkCXcJ
Но спустя 30 лет и этот костыль стал практически бесполезен и даже опасен из-за своей крайне туманной семантики:
https://www.kernel.org/doc/Documentation/process/volatile-considered-harmful.rst
Разница между тем, что написано тут, и что в стандарте очевидна. Программирование на Си до C89 предполагало понимание того, какой код генерирует компилятор под целевую архитектуру. Это было хоть как-то возможно потому, что сами компиляторы были гораздо проще, да и портировать нужно было намного меньше. Но бесконечно так продолжаться просто не могло. Для Си того времени просто немыслима возможность наличия двух указателей с одинаковым содержимым, проверка на равенство для которых возвращает ложь. Разумеется речь идет про плоскую память. А вот для стандартного Си - это вполне нормально, так как подобное сравнение - это неопределенное поведение. И на самом деле еще хорошо, что такое сравнение ложь возвращает, а ведь может еще, если верить стандарту, вам исходный код удалить или монитор сжечь.
Вообще это была ирония, потому что по сути обмануть компилятор - это тоже решение. Только далеко не самое адекватное по очевидным причинам.
Это не баг, -nostdlib работает так, как написано в документации gcc:
Так или иначе вопрос не в том, как заставить компилятор не делать какие-то оптимизации, а в том, валидны ли эти оптимизации с точки зрения языка. И да, они абсолютно валидны. Единственный момент тут в том, что если вы сообщите компилятору, что у вас freestanding окружение (для gcc нужен флаг -ffreestanding), то тогда все, что связано со стандартной библиотекой, будет зависеть от реализации компилятора:
Но это совсем не означает, что в таком случае компилятор не может самостоятельно добавлять в код вызовы функций стандартной библиотеки. Однако implementation-defined behavior разработчики компилятора уже должны документировать, что они и сделали для упомянутого флага -nostdlib.
Ну так и сделали бы новый, прекрасный, переносимый, легко оптимизируемый язык. Зачем было брать прибитый ржавыми гвоздями к архитектуре PDP-11 Си и вводить в заблуждение кучу людей?
Любой, в документации/спецификации/стандарте которого написано, что он на такое способен. И да, скорее всего это будет только ассемблер, но это не такой уж и плохой вариант. Никто не мешает вам линковаться с объектными файлами, написанными на асме. Решение с флагами компилятора тоже неплохое, но только при условии, что вы действительно уверены, что оно сработает как нужно. Но в рамках самого языка Си решить эту проблему невозможно - в этом и был смысл примера, который мы дали в тексте.
Да, стандарт действительно не устанавливает верхнюю планку для размера char, однако минимальный размер - это всегда 8 (2.2.4.2 Numerical limits):
Так что использование явной константы 8 в качестве размера для битового сдвига достаточно, чтобы вызвать UB в большинстве случаев, в том числе и при компиляции примера gcc для x86 (что и было продемонстрировано).
Формально вы, конечно, правы, и чтобы словить UB во всех возможных ситуациях, нужно использовать макрос, но идея тут была в другом. На самом деле пример со сдвигом нужен, чтобы показать, что даже для той архитектуры, для которой известны размер минимально адресуемой ячейки памяти (8 бит) и поведение инструкции битового сдвига, нет гарантии, что будет сгенерирован тот код, который ожидает программист.
Спасибо за отличную ссылку, вот этот комментарий порадовал особенно:
Т.е. реализация memcpy должна быть настолько сложной, чтобы компилятор не смог оптимизировать ее исходный код - прекрасно!
Почему вы считаете, что это баг компилятора? Компилятор ведет себя в соответствии со стандартом (4.1.2 Standard headers):
Да и на самом деле мы ругаем не вакуумный язык Си, а то, что его концепция и история вводит людей в заблуждение о его сущности. И это заблуждение поддерживалось в том числе комитетом стандартизаторов - для чего было вводить столько UB, перекладывая ответственность на создателей компиляторов, и делать вид, что с Си ничего не произошло? Си, который создал Ритчи, и тот Си, который определен в стандарте - это абсолютно разные языки, только внешне они практически неотличимы. Первый - это язык для конкретной машины PDP-11, второй - абстрагированный язык высокого уровня.
Мы упомянули о том, что пароль (или его часть) может остаться в регистре. На самом деле ответ прост - в рамках самого языка Си эта проблема неразрешима. В тексте стандарта даже, очевидно, слово "stack" не упоминается - об очищении чего тогда вообще может идти речь? Локальные переменные могут находиться где-угодно, и исключительно средствами Си это проконтролировать невозможно. Подробнее об этом написано тут (ссылка также есть в статье):
http://www.daemonology.net/blog/2014-09-06-zeroing-buffers-is-insufficient.html
Насчет хэшей - мы использовали пароль, чтобы не вдаваться лишний раз в подробности. Это простой пример, смысл которого в том, что порой бывает важно очищать все места хранения потенциально опасных данных, и что Си в этом вопросе никак помочь не может.
В тексте речь шла про оптимизации компилятора gcc. Второй ваш результат как раз и совпадает с тем, что я сказал выше - оба сравнения возвращают ложь, хотя как минимум два указателя имеют абсолютно одинаковое содержимое.
Если бы в стандарте языка было указано, что вся целочисленная арифметика явным образом использует дополнительный код, то для тех архитектур, которые ее не поддерживают, пришлось бы имитировать такое поведение. Впрочем, тут нет ничего чрезвычайно страшного - компиляторам языка Си и без этого приходится программно реализовывать те операции, которые целевые архитектуры также не поддерживают. Так, например, для тех машин, которые не содержат инструкций для целочисленного деления и не только, gcc использует специальную библиотеку libgcc. Подробнее можно почитать здесь:
https://gcc.gnu.org/onlinedocs/gccint/Libgcc.html
Я этого не говорил да и полагаю, что они оба просто высокоуровневые. Ни один из них не более или не менее высокоуровневый, чем другой, так как нормальных критериев я придумать не могу. Да и какой смысл? Вообще я имел в виду немного другое - большое количество программистов предполагают, что операции в Си будут имеют ровно такое же поведение, как и соответствующие им инструкции на целевой архитектуре. Т.е. если вы скомпилировали код под x86, то битовый сдвиг на размер переменной должен оставить ее нетронутой. В теории и на практике это так не работает. Поэтому Си - это не язык низкого уровня. То, как в стандарте языка определяется битовый сдвиг или знаковое переполнение не влияет на то, станет он от этого более высокого или низкого уровня (если только прямо не сказано, какую инструкцию должен использовать компилятор при трансляции). Да, неопределенное поведение позволяет совершать более агрессивные оптимизации, но всему есть предел. Си просто переполнен UB, приличная часть которого вообще имеет мало смысла. Конкретно проверять знаковое переполнение перед каждой потенциально опасной операцией гораздо неудобнее, чем это делать после.
Спасибо большое за ссылки и комментарий! К сожалению, в текст одной статьи невозможно уместить все сразу, тем более, что бездна UB неисчерпаема. Идея была в том, чтобы провести неискушенного в тонкостях стандарта читателя от достаточно тривиальных ошибок до гораздо более изощренных и опасных случаев неопределенного поведения. Безусловно, говоря о том, что Си должен умереть, я сам не надеюсь на то, что это произойдет. Смысл в том, что вокруг этого языка существует огромное количество серьезных заблуждений, и вызваны они отнюдь не только непрофессионализмом тех, кто на нем пишет. И учитывая историю Си, и то, как написан стандарт, неудивительно, что ошибки неопределенного поведения стали обыденностью. Я сам долгое время считал, что Си - это своего рода кроссплатформенный ассемблер. Потому я полагаю, что концепция языка, который создает столь опасную иллюзию , является по меньшей мере неудачной. И в том случае, если Си остается с нами надолго (а у меня нет оснований считать иначе), очень важно, чтобы программисты четко осознавали то, какой именно инструмент они используют.
Да, по сути верно. Единственная загвоздка тут в том, что знаковое переполнение, ровно как и многие другие случаи неопределенного поведения, большинством программистов считалось правомерным. В этой позиции есть смысл, если думать о Си как языке низкого уровня. В действительности это не так. Отсюда и огромное количество сломанного кода, и требование откатить изменения компилятора.
На самом деле можно использовать оба варианта, и вне зависимости от реального расположения переменных на стеке обе проверки на равенство вернут ложь.
Да, вы совершенно верно отметили, это опечатка. Имелся в виду real_pwd. Спасибо большое, исправлено!