Хабр Курсы для всех
РЕКЛАМА
Практикум, Хекслет, SkyPro, авторские курсы — собрали всех и попросили скидки. Осталось выбрать!
-Og не вызывает. А с -O2/-O3 вызывает, но если вы не компилируете с -Werror и отлаживаетесь с -Og, то вы можете этого и не заметить.int main() {
int a[4];
int b[4];
a[5] = 'a';
return 0;
}$ cat test.c
int main() {
int a[4];
int b[4];
a[5] = 'a';
return 0;
}
$ gcc -g -fsanitize=address test.c -otest
$ ./test
=================================================================
==7769==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fffefbce234 at pc 0x400803 bp 0x7fffefbce1f0 sp 0x7fffefbce1e8
WRITE of size 4 at 0x7fffefbce234 thread T0
#0 0x400802 in main /tmp/5/test.c:4
#1 0x7fcf7202576c in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2176c)
#2 0x400668 (/tmp/5/test+0x400668)
Address 0x7fffefbce234 is located in stack of thread T0 at offset 52 in frame
#0 0x400767 in main /tmp/5/test.c:1
This frame has 1 object(s):
[32, 48) 'b' <== Memory access at offset 52 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /tmp/5/test.c:4 main
Shadow bytes around the buggy address:
0x10007df71bf0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10007df71c00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10007df71c10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10007df71c20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10007df71c30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x10007df71c40: f1 f1 f1 f1 00 00[f4]f4 f3 f3 f3 f3 00 00 00 00
0x10007df71c50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10007df71c60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10007df71c70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10007df71c80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10007df71c90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Heap right redzone: fb
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack partial redzone: f4
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Contiguous container OOB:fc
ASan internal: fe
==7769==ABORTING
int main() {
int a[0x200];
int b[0x200];
a[0x300] = 'a';
return 0;
}a использовать b — и это вам уже ни один компилятор ни в одном языке не отловит (хотя иногда может отловить статический анализатор). Количество ошибок «проскакивающих» мимо ASAN'а сравнимо с количеством подобных опечаток (и ещё неизвестно — чего больше).HeartBleed был именно такой, что AddressSanitizer был бессилен ))Srsly? Christopher T. Celi (of NIST) confirmed to me on 2014-07-10 that address sanitizer does detect Heartbleed if an attacking query is made against a vulnerable OpenSSL implementation — вам перевести? Выделение там в оригинале, если что.
Речь о том, что был бы Си Джавой, мы бы моментально словили IndexOutOfRangeException.Конечно. Но это не спасло бы нас от кучи других проблем всё равно. А практически AddressSanitizer достигает именно этого в Си.
"abcde"[0] = 5;
например.там где можно 1 специфическую инструкцию — пилят десять общих, ведь так безопасней и слоупочней).
int или регистр, а массив. Ну, скажем, на миллион битов. Извиняюсь за неточность.bt может с таким работать, но написать что-нибудь так, чтобы это оттранслировалось в одну инструкцию bt (плюс проверку или setXX) хотя бы на каком-нибудь компиляторе я не умею.bitmap_bit_is_set и bitmap_is_bit_set. Пока ни того, ни другого мне не удалось придумать как сделать.Просто инструмент исчерпал себя на каком-то этапе, сейчас это бессмысленно…
Может, если напрячься, такую можно придумать, но всё равно предпочту Qt какой-нибудь.А с каких пор у нас Qt языком стал? То есть Qt как библиотека для совместимости с разными системами — Ok, годится. Но вы на чём писать-то будете? На Javascript'е?
if ((counter += STEP) < 0) counter -= STEP; /* вернём обратно */У вас программа не работает? Из-за того, что содержит UB? О, какой ужас. Ну, мы надеемся, вы её почините. Как хорошо, что это не наша проблема. RESOLVED INVALIDСамая же фишка в том, что он вовсе не всегда эти проверки будет вырезать. Он сможет этот if вырезать только в том случае когда выяснит, что counter положителен. А сделать он может это, например, выяснив что стартуете вы с нуля и всюду прибавляете только положительные числа. Я не зря приводил пример в статье — он как раз из той же оперы и очень-очень показателен.size_t бывает и ssize_t, и ptrdiff_t и много чего ещё бывает в нашем мире.if (this == NULL) — это как раз C++-специфические грабли), то я рад за вас. К сожалению не все знают что это может кончиться слезьми.Ба-тю-шки, это как такое можно изобрести?
int *q = (int*)realloc(p, sizeof(int));p после вызова reallocа. Простейший способ это использовать для оптимизаций — это «раздвоить» переменные. То есть, с точки зрения компилятора, переменная p₁ «умрёт» в месте вызова realloc'а и после него «родится» новая, никак со старой переменной не связанная, переменная p₂. Соответственно первую переменную можно будет положить на регистр, который «умрёт» во время вызова reallocа, а вторая — будет жить уже в другом регистре (а пусть даже и в том же!), но с первой никак не будет связана.p.p после вызова reallocа даёт право компилятору отформатировать винчестер и запустить ядерную войну.return(0); в конце не нужен.NULL — не UB. Вот обращение через NULL — это UB.main — это тоже нормально, так что непонятно — чем вы недовольны.void-то не было! Все функции «возвращали» int если не указывалось ничего другого. Но функции, которые ничего не возвращали — были (какая-нибудь free). И поскольку каждый байт был на счету там не было returnов. Потому этот вариант пришлось объявить законным. И законным он является до сих пор — даже в C11.return не было, то попадало бог знает что. Мусор какой-нибудь. Пока его не читали — на поведение программы это не влияло. И именно такое поведение кодифицировано во всех стандартах Си — вплоть до C11. В языке C++ ситуация иная, это правда.The value of a pointer that refers to space deallocated by a call to the free or realloc function is used (7.22.3)
Значение по указателю, который указывает на участок памяти деаллоцированный с помощью вызова free или использования функции realloc
The content of the memory block is preserved up to the lesser of the new and old sizes, even if the block is moved to a new location
realloc должен вернуть NULL.realloc вернёт NULL, то будет обращение к нулевому указателю — но компилятор имеет право считать что этого никогда не произойдёт из-за строчки *p = 1.realloc не вернёт NULL, то будет обращение к деаллоцированному объекту (ну а дальше — как описано в статье).Блок памяти может не быть деаллоцирован как уже было замечено только если не хватило памяти, а тогда realloc должен вернуть NULL
2. Если realloc не вернёт NULL, то будет обращение к деаллоцированному объекту (ну а дальше — как описано в статье).
Если адрес начала блока не перемещается, то я считаю, что старый блок не был деаллоцирован.Стандарт говорит, что возможны ровно два варианта:
NULL.NULL.В смысле мне кажется таким перевод стандарта.Как вы можете по этому переводу что-то решать? Это просто описание одного из десятков UB. Как ведёт себя
realloc описано в другом месте стандарта:realloc function deallocates the old object pointed to by ptr and returns a pointer to a new object that has the size specified by size. The contents of the new object shall be the same as that of the old object prior to deallocation, up to the lesser of the new and old sizes. Any bytes in the new object beyond the size of the old object have indeterminate values.Почему здесь будет обращение к деаллоцированному объекту, если блок памяти не был перемещен?Потому что старый объект может быть не деаллоцированным только в случае нехватки памяти и в этом случае
realloc возвращает NULL. Если она вернула не NULL (а мы знаем, что она вернула не NULL из-за строчки *q = 2), то это — указатель на другой, новый, свежеаллоцированный объект. Хотя, как было явно замечено, it may have the same value as a pointer to the old object.The realloc() function tries to change the size of the allocation pointed to by ptr
to size, and returns ptr. If there is not enough room to enlarge the memory alloca-
tion pointed to by ptr, realloc() creates a new allocation, copies as much of the old
data pointed to by ptr as will fit to the new allocation, frees the old allocation,
and returns a pointer to the allocated memory.
The realloc() function tries to change the size of the allocation pointed to by ptr to size, and returns ptr.А из какого стандарта вы это вытащили? Никаких-таких «tries to change the size» я ни в одном из них не видел, однако.
вот вывод «1 2» это вопрос скорей именно к компилятору clang,С чего вдруг? Программа вызвала UB и имела право сделать что угодно. Программист должен это исправить и всё. Есть много способов это сделать: например можно перенести проверку
if (p == q) { наверх и делать *p = 1; *q = 2; только после этого. Или вообще использовать только q. Но программа в том виде как она есть — гарантированно вызывает UB и, соответственно, должна быть исправлена.А из какого стандарта вы это вытащили?
The realloc() function returns a pointer to the newly allocated memory, which is
suitably aligned for any kind of variable and may be different from ptr
гарантированно вызывает UB
# rm -f a.out; clang -O a.c && ./a.out
1 2
# rm -f a.out; gcc a.c && ./a.out
2 2
man 3 realloc к сожалению или к счастью стандартом не является, это всего лишь неформальное описание, не более того.не на всех компиляторахUB происходит на всех компиляторах. Это определяется только и исключительно стандартом языка и ходом исполнения программы (попадёте вы на ветку с вызовом UB или нет). Вот что конкретно программа выдаст — да, это зависит от компилятора. От его версии, опций компиляции и прочего. Если ваша программа «не взорвалась» от UB, то это не ваше счастье, а ваша беда: значит она взорвётся завтра. И не у вас, а у вашего заказчика. Вам от этого легче будет?
man никак не может быть руководством к написанию переносимого кода, так как по определению описывает ровно одну систему.вы хотите сказать что man realloc(3) для каждой системы описывает системо-зависимые реализации realloc?
man realloc(3) описывает функцию стандартной библиотеки с названием realloc. Стандарт языка описывает поведение языка в части работы с определёнными функциями.memset, memcpy, calloc/malloc/realloc/free — они обрабатываются особо. Фактически это не совсем функции — скорее это некоторые конструкции языка, которые по историческим причинам также являются функциями.manе — это, конечно, хорошо, но недостаточно.Си-компилятор можно написать почти что для чего угодно. Вплоть до систем с троичной логикой.
E1 × 2E2 и E1 / 2E2 соответственно, что вполне можно реализовать и в системе с троичной логикой. То же касается всех остальных операций. Хотя эффективность будет, конечно, ни к чёрту, но портировать можно.Где это в Си вы обнаружили типы бит и байт, я извиняюсь?
С11 3.5: bit — unit of data storage in the execution environment large enough to hold an object that may
have one of two values
С11 3.6: byte — addressable unit of data storage large enough to hold any member of the basic character
set of the execution environment (NOTE: A byte is composed of a contiguous sequence of bits)
Сдвиги там определены как E1 * 2^E2 и E1 / 2^E2 соответственно
The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is E1 × 2^E2, reduced modulo one more than the maximum value representable in the result type.
Но, минуточку, если вы таки пойдёте по ссылке в Википедию, то обнаружите, что UNIX — она не только многозадачная и многопользовательская, но она ещё и переносимая
У вас программа не работает? Из-за того, что содержит UB? О, какой ужас. Ну, мы надеемся, вы её почините. Как хорошо, что это не наша проблема.Вот весь смысл — в последнем предложении.
if (a + 10 > b + 10) превращается в if (a > b), то вы только радуетесь и говорите «у, какой умный компилятор, надо же», а когда, следуя той же логике, он превращает if (a + 10 < 0) (проверка на переполнение intа) в if (false) (с последующим выкидыванием «мёртвого» кода) то вы возмущаетесь и начинаете «метать громы и молнии».Или вы действительно хотите видеть предупреждение каждый раз, когда компилятор сокращает константы? Чегой-то я очень сильно в этом сомневаюсь.
Я действительно не хочу, чтобы компилятор «оптимизировал» блоки кода по принципу «это ж андефайнд, значит будем, считать, что можно делать что угодно».Но в этом весь смысл UB! И его единственное отличие от IB!
STL отдельный разговор, могла бы быть исключением. Сотни предупреждений — результат принципа реализации стандартной бибилиотеки как набора шаблонов, макросов. Поскольку это _стандартная_ библиотека, вполне могла бы обрабатываться особо.STL просто используется [почти] всеми программами. Другие C++ библиотеки тоже часто написаны в таком же стиле. Тот же Boost.
#ifdef TRACE_ENABLED
#define LOG printf
#else
#define LOG if (false) printf
#endifif (false) printf(...); удалён из программы как недостижимый».Вы этого хотите?
Другие C++ библиотеки тоже часто написаны в таком же стиле
STL отдельный разговор, могла бы быть исключением. Сотни предупреждений — результат принципа реализации стандартной бибилиотеки как набора шаблонов, макросов. Поскольку это _стандартная_ библиотека, вполне могла бы обрабатываться особо.Вот это нехорошо. Если я использую стандартную библиотеку, то у меня всё клёво, мусорных сообщений нет. Но стоит мне сделать шаг влево и самому написать нужную мне шаблонную магию в обход STL/boost — и я тут же получаю стену бессмысленных предупреждений.
[[nowarn(unsigned::addition::overflow)]], чтобы размечать свой код, затыкая рот компилятору с его предупреждениями…Это вполне может быть настраиваемым. Уровни предупреждений.Компиляторы и так могут выдавать отладочную информацию о том, что они делают на каждом проходе. Обширную. Вперёд писать парсер для неё. Только это называется статическим анализатором. Для примера из поста, после SSE-преобразований очевидно, что первый аргумент realloc() используется после вызова, чего делать нельзя.
Но компилятор не знает, как там realloc() реализована.Не только компилятор не знает. Программист тоже не знает. Например можно вполне себе представить реализацию, которая вызов
reallocа даже с тем же самым размером, который был аллоцирован изначально использует как повод для компактификации — тогда данные могут быть передвинуты даже в этом случае. Это — абсолютно законный вариант реализации reallocа. Вот если вы проверите равенство p и q — можете использовать любой из них. Тогда и компилятор перестанет чудить.int a(int* const p)
{
printf("%d",*p);
// ... do something
if(p==NULL)
return 1;
return 0;
}
Нельзя так писать. Просто нельзя.Нельзя так писать. Просто нельзя
p проверяется заранее, то проблем не будет. Ну выкинется проверка на NULL, кому от этого хуже будет?Вот это нехорошо. Если я использую стандартную библиотеку, то у меня всё клёво, мусорных сообщений нет. Но стоит мне сделать шаг влево и самому написать нужную мне шаблонную магию в обход STL/boost — и я тут же получаю стену бессмысленных предупреждений.
омпилятор тоже вполне может ругаться на это, но заставлять его делать — это отвлечение от его настоящей работы.
Никакого такого мониторинга у компилятора нету.
Когда компилятор заведомо установил наличие гарантированного UB, он выводит предупреждение.
часто UB вскрывается только после нескольких проходов оптимизатора, когда компилятор уже понятия не имеет, что именно в исходной программе вызвает возможность получить неопределённое поведение
Во-первых, он в таком случае должен выводить не предупреждение, а ошибку.Нет, нет и нет. 100500 раз нет. Ситуация когда компилятор может обнаружить такую ошибку, которая вызывает UB всегда, при любом запуске программы — в природе почти не встречаются. За исключением крошечных, игрушечных, специально для этого сделанных программ.
В таком случае отказ от компиляции правомерен, понуждая программиста следовать этому правилу.Это, собственно, неплохая идея, но её реальзация создаст совсем другой язык. Rust к примеру, пытается так делать. Подход же Си другой: в программе могут быть участки, которые потенциально вызывают UB (да, чёрт побери, без этого вы вообще нифига написать не сможете ибо у вас простейший цикл от «a» до «a + b» может теоретически вызвать UB...), она может даже на 99% состоять из них. Но если они реально во время работы программы не происходят (например потому что соответствующие функции не вызываются) — вы в шоколаде.
Во-вторых, были ли хотя бы предупреждения компилятора при компиляции программы из того примера, что был приведен в статье?Нет — и совершенно понятно почему.
В-третьих, ваше утверждение:На статьях, ссылки на которые были ровно под вашим сообщением. Нет, я понимаю, что вы любитель «спорить о вкусе устриц с теми, кто их ел», но большинство читателей обычно соглашаются с тем доводом, что разработчики компиляторов знают о том, чего они могут сделать, а чего нет чуть лучше, чем кто-то другой. Если вы считаете, что они все идиоты и можете написать компилятор, который будет быстрее и не будет иметь проблем с UB — флаг вам в руки, скорее всего сможете на нём много денег заработать.
часто UB вскрывается только после нескольких проходов оптимизатора, когда компилятор уже понятия не имеет, что именно в исходной программе вызвает возможность получить неопределённое поведение
оно чем обосновано?
Между тем, компилятор от Microsoft считается чуть ли не самым быстрым на рынке.В самом деле? В наших экспериментах он оказывается почти всегда самым медленным. Зачастую с многократным отрывом.
Так что нельзя говорить и о «серьезном замедлении времени компиляции».Ах, вы про это. Вообще-то для большинства людей важна не скорость работы компилятора (всегда можно прикупить ещё пару стоек машин), а скорость работы скомпилированного кода. Если вас это не волнует — компилируйте с "-O0" и можете забыть про UB.
Гораздо более оптимально, и по скорости, и по размеру программы, просто поставить в этом месте вызов abort(). Это ведь тоже вписывается в разрешенные последствия UB.Вы правы не на 100%, а на все 200%. Когда clang может доказать, что где-то обязательно вызывается UB — он так и делает. Только он вставляет не вызов
abort, а инструкцию ud2 — так дешевле. Мог бы и вообще ничего не вставлять, но эксперименты показали, что в этом случае программисты за ножи хвататься начинают.Но почему тогда оптимизация не пошла дальше? Почему она вообще не прекратила генерацию кода в месте, где было обнаружено UB и далее по потоку управления?А это — уже другой вопрос. Нужно разбираться. «Ну не шмогла я, не шмогла». Потому и предупреждения не было, кстати.
a + b > b + c"? Раз мы знаем, что UB не происходит — можем превратить в "a > c". Написано "*p = 1;"? Значит мы знаем, что p у нас в этой точке — не NULL — можем выкинуть лишние проверки. И т.д. и т.п. Иногда после всех этих оптимизаций у нас не остаётся ничего — ну тогда можно и пожаловаться вполголоса. А если что-то осталось — то вот это и будет нашей программой. И если программист «хороший» и «за свои слова отвечает» — то это будет даже правильной программой.Ситуация когда компилятор может обнаружить такую ошибку, которая вызывает UB всегда, при любом запуске программы — в природе почти не встречаются.
Но если они никогда при работе не вызываются — то программа корректна и потому должна скомпилироваться и заработать. Компилятор, разумеется, может эти участки кода выкинуть за ненадобностью — они же никогда не должны вызываться!
(да, чёрт побери, без этого вы вообще нифига написать не сможете ибо у вас простейший цикл от «a» до «a + b» может теоретически вызвать UB...) Но если они реально во время работы программы не происходят (например потому что соответствующие функции не вызываются) — вы в шоколаде.
— были ли хотя бы предупреждения компилятора при компиляции программы из того примера, что был приведен в статье?
Нет — и совершенно понятно почему.
На статьях, ссылки на которые были ровно под вашим сообщением.
Нет, я понимаю, что вы любитель «спорить о вкусе устриц с теми, кто их ел», но большинство читателей обычно соглашаются с тем доводом, что разработчики компиляторов знают о том, чего они могут сделать, а чего нет чуть лучше, чем кто-то другой.
Если вас это не волнует — компилируйте с "-O0" и можете забыть про UB.
Когда clang может доказать, что где-то обязательно вызывается UB — он так и делает.
А это — уже другой вопрос. Нужно разбираться. «Ну не шмогла я, не шмогла»
Компилятор не занимается поиском случаев в которых всегда происходит UB
Написано "*p = 1;"? Значит мы знаем, что p у нас в этой точке — не NULL — можем выкинуть лишние проверки
И если программист «хороший» и «за свои слова отвечает»
Но в таких ситуациях компиляторы вроде clang как раз и не проводят «оптимизацию». «Оптимизация», искажающая семантику программы, которую вы так приветствуете, происходит только в случаях, когда наличие UB может быть установлено компилятором.Да ну? Вы что — последователь Аристотеля и считаете, что можно узнать как работает мир глядя только на собственный пупок?
#include <stdio.h>
int test(int i, int j) {
return (i + 10 > j + 10);
}
int main(){
printf("%d\n", test(1073741820, 2147483640));
}
$ clang -O0 test.c -o test0
$ clang -O2 test.c -o test2
$ ./test0
1
$ ./test2
0
Получите и распишитесь.Но раз уж компилятор взялся за тяжелую задачу обнаружения UB.Где брался? Кто брался? Когда? Компилятор — не брался.
не логично ли результаты решения этой задачи обратить во благо (диагностика ошибок), а не во зло (усугубление последствий ошибок)?Компилятор за эту задачу не брался и её не решил. Как можно использовать результаты, которых нет — науке неведомо.
А вы разработчик компиляторов?Сейчас — нет. Ушёл из команды, разрабатывавшей один порт GCC пару лет назад. Сейчас работаю в команде, которая пилит JIT, что, конечно, не совсем то же самое. А что? Это что-то меняет? Конкретно в LLVM/Clang'е я не засветился, если что.
Хотите задавить авторитетом вместо того, чтобы приводить логические доводы?То есть когда некий Вася говорит, что он, Вася, не может сделать того-то и сего-то, то это уже называется «задавить авторитетом»? Кто, кроме Васи, может это сказать?
People often ask why the compiler doesn't produce warnings when it is taking advantage of undefined behavior to do an optimization, since any such case might actually be a bug in the user code. The challenges with this approach are that it is 1) likely to generate far too many warnings to be useful — because these optimizations kick in all the time when there is no bug, 2) it is really tricky to generate these warnings only when people want them, and 3) we have no good way to express (to the user) how a series of optimizations combined to expose the opportunity being optimized.Если английского не знаете — сходите в Google Translate или переводчика попросите знакомого. А то ведь обвините меня в том, что я вас обманываю и «давлю авторитетом».
Если компилятор видит, что проверки «лишние» — должен быть напечатан варнинг, проверки оставлены.Попробуйте создать такой компилятор — я посмотрю кто сможет им пользоваться. Бессмысленных проверок в любой программе сколько-нибудь нетривиального объёма — чуть более, чем дофига.
Программа будет работать чуть медленнее, чем если бы проверок не было, однако, раз проверка есть в исходном коде — значит программист ее желал и был готов к соответствующему падению скорости исполнения.Вы действительно в это верите? Не придуриватесь? Неспособен на это программист. Причём неспособен от слова «совсем». Когда у вас функции inlineаться, то у вас возникают просто тысячи ненужных проверок. Весь STL заточен под то, что компилятор их выкинет! И Boost! В библиотеках Си это чуть менее критично, но всё равно — отказ от выкидывания лишнего кода приводит к замедлению не на проценты, а в разы!
Уж проверили бы, что ли:
…
Получите и распишитесь.
Где брался? Кто брался? Когда? Компилятор — не брался
Как можно использовать результаты, которых нет — науке неведомо.
Вы не верите, что они это говорили или не можете найти?
Программа будет работать чуть медленнее, чем если бы проверок не было, однако, раз проверка есть в исходном коде — значит программист ее желал и был готов к соответствующему падению скорости исполнения.
Вы действительно в это верите? Не придуриватесь? Неспособен на это программист. Причём неспособен от слова «совсем».
Когда у вас функции inlineаться, то у вас возникают просто тысячи ненужных проверок. Весь STL заточен под то, что компилятор их выкинет!
int a(int* const p)
{
if(p==NULL)
return 1;
// ... do something ...
if(p==NULL)
return 1;
return 0;
}int a(int* const p)
{
printf("%d",*p);
// ... do something
if(p==NULL)
return 1;
return 0;
}Но ради бога, не нужно в чужой монастырь со своим уставом ходить и объяснять разработчикам Си как тот язык, который они разработали, должен быть устроен.
Вы что — последователь Аристотеля и считаете, что можно узнать как работает мир глядя только на собственный пупок?
Или вы из тех, кому мало положить в рот и пожевать, нужно ещё ударить по голове пару раз бейсбольной битой, чтобы пища в глотку пролезла?
Если английского не знаете — сходите в Google Translate
И что доказывает этот пример?Что вы «Фома Неверующий», очевидно.
То, что компилятор обнаружил UB?То что компилятор проигнорировал UB. Как и должен был.
Или наоборот, не обнаружил? Какой был сгенерирован код в обоих случаях?Проверить не судьба?
$ clang test.c -S -o-
...
.type test,@function
test: # @test
.cfi_startproc
# BB#0: # %entry
pushq %rbp
.Ltmp0:
.cfi_def_cfa_offset 16
.Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
.Ltmp2:
.cfi_def_cfa_register %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %esi
addl $10, %esi
movl -8(%rbp), %edi
addl $10, %edi
cmpl %edi, %esi
setg %al
andb $1, %al
movzbl %al, %eax
popq %rbp
retq
...
$ clang -O2 test.c -S -o-
...
.type test,@function
test: # @test
.cfi_startproc
# BB#0: # %entry
cmpl %esi, %edi
setg %al
movzbl %al, %eax
retq
...
$ cat test2.c
#include <stdio.h>
int test(unsigned i, unsigned j) {
return (i + 10 > j + 10);
}
int main(){
printf("%d\n", test(1073741820, 4294967290));
}
$ clang -O2 test2.c -S -o-
...
.type test,@function
test: # @test
.cfi_startproc
# BB#0: # %entry
addl $10, %edi
addl $10, %esi
cmpl %edi, %esi
sbbl %eax, %eax
andl $1, %eax
retq
$ clang -O2 test2.c -o test2
$ ./test2
1
Если бы не брался — то не было бы и самого понятия «оптимизации, основанной на UB». Раз такая оптимизация есть, раз она применяется только в случаях обнаружения UB — значит компилятор обнаруживает UB, когда применяет такую оптимизацию.Ok. Покажите мне конкретно где в примере показанном выше компилятор «обнаружил UB».
Но по-моему задача выкидывания ненужных проверок значительно легче, чем отслеживание всех случаев UB.Конечно. Именно поэтому первую, механическую, работу выполняет компилятор, а вторая, творческая, возложена на программиста. Как и следовало ожидать.
И я не вижу принципиальных причин, почему компилятор не сможет отличить один случай от другого.Сделаете такой компилятор — придёте, покажете, расскажете.
a+c > b+ca > b, свидетельствует о некоторой небрежности программиста, отсутствии с его стороны длительных размышлений над этим местом программы. Не знаю, как другие программисты, но я обычно не полагаюсь на способности компилятора без необходимости и оптимизирую выражения сам. И если из-под моей клавиатуры появится выражение вида a+c > b+c — то исключительно по небрежности и недосмотру.a+c > b+cэквивалентно a > b только при отсутствии переполнения, причем даже неважно, знаковые числа или нет. То, что компилятор не стал оптимизировать выражение для беззнаковых чисел — это следствие того, что в стандарте беззнаковое переполнение не является UB. Если бы являлось — можно было бы оптимизировать и тут. Стандарт в данной части имеет некоторую асимметрию, а совершенство обычно обладает симметрией, поэтому давайте подумаем, какой вариант симметрии был бы предпочтительнее?И если из-под моей клавиатуры появится выражение вида a+c > b+c — то исключительно по небрежности и недосмотру.
(current_allocated + ALLOC_MARGIN > sizeof(PacketHeader) + incoming_packet_payload).ALLOC_MARGIN или sizeof(PacketHeader), и переписывать условие в виде(current_allocated + (ALLOC_MARGIN-sizeof(PacketHeader)) > incoming_packet_payload), если первая константа больше?Почему же все-таки, с вашей точки зрения, в стандарте принята асимметрияЭто очевидно для любого, знакомого с Computer Science. Есть множество эффективных алгоритмов (в криптографии, теории чисел), работающих в кольце ZP, P=2N, N — размер машинного слова. Для чисел со знаком нет абстракции, хорошо укладывающейся в переполнение, поэтому ничего не потеряем, если объявим знаковое переполнение UB.
Не так всё однозначно. Сюда же, под оптимизацию, попадает выражение видаНе попадает. Тут ничего прооптимизировать нельзя, так как у вас используется
(current_allocated + ALLOC_MARGIN > sizeof(PacketHeader) + incoming_packet_payload).
sizeof и, стало быть, вся конструкция — считается в беззнаковой арифметике. Если бы считалась в знаковой, то это выражение можно было бы написать какcurrent_allocated > incoming_packet_payload + sizeof(PacketHeader) - ALLOC_MARGINи тут уж оптимизатор смог бы всё посчитать без учёта каких-либо переполнений.Это очевидно для любого, знакомого с Computer Science. Есть множество эффективных алгоритмов (в криптографии, теории чисел), работающих в кольце ZP, P=2N, N — размер машинного слова. Для чисел со знаком нет абстракции, хорошо укладывающейся в переполнение, поэтому ничего не потеряем, если объявим знаковое переполнение UB.Если бы всё было так очевидно, то вряд ли бы создатели GCC и Clang'а добавили бы в свои компиляторы опцию -fwrapv. Нет, я думаю создатели стандарта скорее ориентировались на то, что бывают процессоры дополнительным кодом и с обратным кодом, а потому в переносимой программе переполнение ни для чего полезного использовать нельзя. Ну а дальше уже разработчики компиляторов подсуетились и решили «с паршивой овцы урвать хоть шерсти клок» и использовали этот запрет для своих оптимизаций.
Рассмотрим ваш пример. Сам факт того, что в нем присутствовало выражение:А почему это не может свидетельствовать о том, что в исходном тексте было написано
которое, при отсутствии UB, эквивалентно менее затратномуa+c > b+c
, свидетельствует о некоторой небрежности программиста, отсутствии с его стороны длительных размышлений над этим местом программы.a > b
a+c > b+d, но, после троекратной подстановки inline-функций друг в друга выяснилось, что c == d? Это куда как более реалистичный случай.И если из-под моей клавиатуры появится выражение видаА выражение типа— то исключительно по небрежности и недосмотру.a+c > b+c
AccumulatedLen1 + ElementLen2 > AccumulatedLen2 + ElementLen2 из-под вашей клавиатуры тоже не может? А использовать функцию с этим выражением в дальнейшем для варианта с фиксированной длиной элемента вам религия не велит?Тем более что отследить отсутствие UB — более сложная задача, чем оптимизация, и она требует высокой концентрации внимания?Ну вот компилятор и освобождает вам ресурсы: оставьте микрооптимизацию компилятору и проследите за тем, за чем компилятор проследить не сможет. Вам это сделать проще, чем компилятору, потому что у вас есть информация о семантике кода. В частности, вы знаете (или, по крайней мере, должны знать) о том, откуда в его программе появились те или иные величины, вы знаете (или, по крайней мере, должны знать) какие указатели указывают на «живые» объекты, а какие — на «неживые» и т.д. и т.п. Как вы код будете писать, если и понятия не имеете о том, что этот код делает?
Почему же все-таки, с вашей точки зрения, в стандарте принята асимметрия, а не вариант 1)?Потому что в природе существуют процессоры, которые ведут себя по разному в случаях знакового переполнения (и, стало быть, использовать знаковое переполнение в программах нельзя) и не существуют процессоров, ведущих себя по разному в случае беззнакового переполнения. Всё.
Давайте тогда на атомных электростанциях введем алгоритмы, выводящие стержни управления реактором из активной зоны в случае, если оператор совершит какое-нибудь запрещенное инструкциями действие, хотя бы прямо и не приводящее к таким последствиям.
все люди, пытавшиеся об этом писать пропускали важный пункт: по-моему никто внятно так и не удосужился объяснить — откуда это понятие в языке, собственно, появилось, и, главное, кому оно адресовано
#line 76 "main.c"
}
Вообще же, я упустил, что вы понимаете под «переносимым» языком?Самый простой (без учета знака, вероятно), чем отдавать это на откуп компилятору.Вы статью-то читали? Это отдано на откуп не компилятору, а процессору. Потому что «самый простой» ARM'овский сдвиг — это восемь инструкций на x86, к примеру (семь если не нужен carry флаг). Вы этого хотите? Чтобы у вас вместо одной инструкции восемь генерировалось? Тогда вам нужна Java, C# и т.п., а не C/C++.
И я подозреваю, что для большого количества вещей, которые сейчас implementation-defined можно было бы дать четкое описание.Нельзя. Процессоры разные и ведут себя по разному. Вот вместо тех мест где сейчас написан undefined-behavior можно было бы написать implementation-defined-behavior. Но для переносимых программ это ситуацию бы ухудшило, так как undefined behavior выгоднее для написания оптимизаторов.
Пардон, разве shl в x86 — это восемь инструкций?Если вы хотите иметь такую же семантику как у ARMа — то да. А если вы потребуете такую же семантику на ARM'е, как на x86, то во многих случаях получите одну-две инструкции вместо нуля (сдвиг во многих ARM'овские инструкции можно получить «забесплатно» — но это будет ARM'овский сдвиг!). Что уже получше, но тоже не очень хорошо.
Потому что некоторые из них уже ведут себя одинаково!Не некоторые, а «вполне определённые» — те, которые на разных процессорах ведут себя одинаково!
Процессоры разные, но выполнять они могут любые вычисления. Да, где-то сдвиг будет дороже чем умножение, где-то байт будет дороже чем четыре байта.Ещё раз: если вам нужна Java — вы знаете где её найти. Но Си устроен по другому: те операции, которые хорошо ложатся на все процессоры — в них прямо и отображаются. А операций, которые где-то лягут хорошо, а где-то — нет в программе быть просто не должно. Вот и всё.
Чем сдвиг хуже?Именно тем, что он у процессоров есть — но разный. Если чего-то у процессора просто нет, то вам всё равно — эмулировать «правильное» поведение в библиотеке или в языке. А вот если у процессора что-то есть — то хочется использовать именно то, что есть, а не какие-то эмуляции. А что при этом придётся запретить сдвиг на «неудобные» величины — ну так «программист привычный, он поймёт». Но многие почему-то категорически отказываются понимать.
Именно тем, что он у процессоров есть — но разный.
Не некоторые, а «вполне определённые» — те, которые на разных процессорах ведут себя одинаково!
Особенно когда разработчики компиляторов делают следующий шаг и решают, что «раз мы программисту что-то делать запретили, то теперь можем опираться на то, что он этого никогда не делает в компиляторе».
Деления у некоторых процессоров нет вообще! Почему оно в Си ведет себя одинаково?Потому что у всех процессоров, у которых оно есть оно ведёт себя одинаково. Согласитесь что было бы глупо на тех процессорах, на которых его нет его каким-то другим способом?
Плавающей арифметики иногда нет вообще.Опять-таки: что это меняет? Если у вас чего-то нету и это нужно эмулировать, то вам, в общем, всё равно — что именно эмулировать. Вопросы возникают как раз когда чего-то есть.
Я не понимаю, чем деление принципиально отличается от сдвига; почему деление обязано всегда выполнятся одинаково, а сдвиг — нет.Потому что процессоров, на которых деление выполнялось бы не так, как на других — нету, а процессоры, на которых сдвиг ведёт себя по разному — есть.
Веселья добавляют и просто исторически-сложившиеся вещи, вроде того, что sizeof('a') == sizeof(int). Я очень сильно сомневаюсь, что у этого есть какая-то причина, помимо «так сложилось».Ну это само собой. Выбор между UB и IB часто тоже достаточно произволен.
Либо реализовать работу с 1байтовыми char'ами на неподходящих платформах можно с меньшими ресурсными издержками железа, чем сдвиги.Не нужно организовывать никакую работу с 1 байтовыми
char'ами! На Cray и некоторых DSP CHAR_BIT==32, а sizeof(char) == sizeof(short) == sizeof(int) == 1. Язык Си это вполне допускает.Я честно старался, но никакого полезного применения для нее так и не придумал.Да вы что? А как у вас препроцессор будет работать без неё?
void *my_realloc(void *ptr, size_t size)
{
return realloc(ptr, size);
}
int *p = (int*)malloc(sizeof(int));
int *q = (int*)realloc(p, sizeof(int));
*p = 1;
*q = 2;void init(int&){}
int main() {
int i;
init(i);
printf("%d",i);
}
Так в том то все и дело гарантированно доказать UB невозможно, на то оно и UB.
Но в рассматриваемых примерах компилятором как раз доказывалось наличие UB, чем он и пользовался.Вы так ничего и не поняли. Не было там такого! Если бы было — было бы предупреждение при компиляции.
a = b + c", то компилятор вправе полагать, что "b + с" не выходят за интервал INT_MIN..INT_MAX. А если написано "*q = 2;" — то, следуя той же логике, он заключает, что у вас в этой точке q — не NULL. А раз q — не NULL, то realloc смогла-таки создать новый объект (по стандарту она либо выделяет память и возвращает не NULL, либо, при нехватке памяти, возвращает NULL; чтобы NULL гарантированно не вернулся нужно, чтобы размер «нового» объекта был не меньше размера «старого»). И так далее.Если бы UB не было доказано — то сгенерированный код был бы другим.Как раз если бы UB было доказано, то код бы был другим: он не содержал бы ни
printf'а, ни проверок, ни чего либо ещё. Скорее всего там бы ud2 вместо всего этого было бы вставлено.Очень характерный пример с clang в статье.Угу. Но это как раз пример случая, когда ни clang не смог доказать наличие UB!
В комментариях же и примеры приводили, как обмануть clang, чтобы он не «оптимизировал» доказанное им UB.Где? Кто? В комментариях приводили примеры того, как исправить программу, чтобы она не вызывала UB. После чего она, разумеется, начинала работать.
Вы так ничего и не поняли. Не было там такого!
Где? Кто? В комментариях приводили примеры того, как исправить программу,
clang поступил бы, если бы действительно «просёк фишку» и смог определить, что UB там возникает всегда. Если вы хотели сказать об этом в саркастическом тоне, то попали мимо цели: clang действительно так и делает. Вернее вместо всего этого после вызова reallocа он бы поставил ud2, а так правильно.А еще, математика прямо говорит, что априорно определить поведение любого алгоритма, имея лишь сам алгоритм в общем случае — невозможно.Если это про проблему останова, то там идёт речь о невозможности решения этой задачи другим алгоритмом, отнюдь не о нерешаемости вообще.
На Геделя я ссылаюсь потому, что из его теоремы следует
Нам нужна теорема Черча — Тьюринга.
Пока же компьютер работает с арифметикой без переполнения, он попадает и в условия теоремы Геделя, и в условия теоремы Черча-Тьюринга.
С точки зрения теории обычный компьютер — конечный автомат и с ним вопрос закрыт.Это с головой у вас вопрос закрыт.
Строим полный граф состояний и убеждаемсячто граф с 24'294'967'296состояний не лезет не то, что в тот же самый компьютер, он не влезет и во всю видимую часть вселенной. То есть его никто и никогда не сможет ни построить, ни исследовать.
Ранее khim показал, что удивительные вещи случаются и при написании программ на более дружественных к программисту языках.Вот только не нужно мне приписывать то, чего я не говорил. Языки Javascript и PHP — гораздо менее дружественны, чем Си. Это было непросто, но их разработчики всё-таки смогли придумать языки, работать с которыми сложнее, чем с Си. Недаром появилась куча «обёрток» вокруг JavaScript'а, призванных решить проблему его недружественности, ох не зря (обёрток вокруг PHP гораздо меньше просто потому, что в его случае есть более простое решение: просто не использовать PHP). Более дружественные языки — это python, lisp, может быть, но никак не Javascript и/или PHP.
Это говорит о том, что UB — не прерогатива стандарта языка (в отдельно стоящем от программы языке UB не возникает). UB возникает в системе входные данные — программа.Вообще-то UB — это свойство именно языка. В частности маразмы Javascript'а и PHP никакого отношения к UB не имеют. Сколько бы раз вы не запускали программу и на каких бы реализациях вы этого ни делали результат будет один. Идотский, да, но один и тот же.
Тогда в моем сообщении следует заменить UB — на «непредусмотренное программистом поведение программы», тогда UB войдет в это понятие как частный случай.Это — довольно-таки неравноценная замена: что такое UB — понятно, описано в соответствующем стандарте, можно обсуждать есть оно или нет глядя только на программу. Что такое «непредусмотренное программистом поведение программы» — непонятно, только сам программист может сказать чего он, собственно, имел в виду — и то, если не забыл.
Ещё раз о неопределённом поведении или «почему не стоит забивать гвозди бензопилой»