LINUX.ORG.RU

Вызов никогда не вызываемой функции

 ,


3

5

Ваши ставки, господа: насколько безопасно на своём компьютере запускать такую программу? Не сотрет ли она вам корень?

Люблю C++.

#include <cstdlib>

typedef int (*Function)();

static Function Do;

static int EraseAll() {
  return system("yes");
}

void NeverCalled() {
  Do = EraseAll;  
}

int main() {
  return Do();
}
★★★★☆

Последнее исправление: beastie (всего исправлений: 1)

Ответ на: комментарий от anonymous

Есть возражения на то что компилятор ничего не нарушил?

Найдите в том комментарии, на что отвечали, слово компилятор

vodz ★★★★★
()
Ответ на: комментарий от anonymous

новая работа связана с ковырянием потрохов этой песочницы, что неминуемо ведет к изучению c++ кода (на котором она собственно и написана). Зачем это нужно? Да просто у меня пердак горит от c++ ужасов, прочитал немножко кода - и как на войне побывал. Ты не можешь быть уверен ни в чём, никогда, а крупицы уверенности нужно отвоёвывать с боем. Но вариантов слезть с него нет никаких, легаси 20-летней давности никто не перепишет, можно только написать джаву на джаве (см. graalvm), но в самом низу пирамиды будет всё то же самое легаси

stevejobs ★★★★☆
() автор топика
Ответ на: комментарий от stevejobs

Ты не можешь быть уверен ни в чём, никогда, а крупицы уверенности нужно отвоёвывать с боем.

Пример какой-нибудь будет? Или вам приходится иметь дело с C++ным кодом, в котором идут вызовы через неинициализированные указатели?

eao197 ★★★★★
()
Ответ на: комментарий от annulen

Тогда main логично изменить на

int main()
{
  if (Do)
    return EraseAll();
  else
    return ((void*)0)();
}

Ну да, логично.

вместо генерации ветки, недопустимой по стандарту, компилятор от нее избавился.

Ok. Избавимся от ветки, недопустимой по стандарту, и получим:

int main()
{
  if (Do)
    return EraseAll();
  //else
    //return ((void*)0)(); От этой ветки избавились.
}

Но почему ещё и от условия if(Do) избавились? Оно-то чем не угодило?

aureliano15 ★★
()
Ответ на: комментарий от numas13

Even assignment, comparison with a null pointer constant, or comparison with itself, might on some systems result in an exception.

Ну и выбрасывали бы исключение вместо вызова того, что вызывать не нужно.

Хотя то, что даже сравнение 0 с константой может вызывать исключение на некоторых системах, довольно странно. Код типа

if(ptr==NULL)
вполне обыденный (даже в си++, особенно в ранних его версиях, когда new в случае неудачи не генерило исключение, а возвращало NULL). Поэтому на таких платформах просто не будет работать 90% кода.

aureliano15 ★★
()
Ответ на: комментарий от stevejobs

можно только написать джаву на джаве

При всём уважении к яве, это будет ужасно: тормоза и утечки памяти в квадрате.

aureliano15 ★★
()
Ответ на: комментарий от aureliano15

Но почему ещё и от условия if(Do) избавились? Оно-то чем не угодило?

Очевидно, что избавиться от else можно только приняв, что условие в if всегда истинно

annulen ★★★★★
()
Ответ на: комментарий от ckotinko

с какого бодуна компилятор решил что там будет адрес функции?

Функция вызывается по указателю — значит указатель не может быть равным нулю.

anonymous
()
Ответ на: комментарий от anonymous

Ябыстёр.

А мне кажется, что тымедленен.

anonymous
()
Ответ на: комментарий от aureliano15

Ну и выбрасывали бы исключение вместо вызова того, что вызывать не нужно.

Это стандарт С.

Хотя то, что даже сравнение 0 с константой может вызывать исключение на некоторых системах, довольно странно. Код типа

if(ptr==NULL)

вполне обыденный (даже в си++, особенно в ранних его версиях, когда new в случае неудачи не генерило исключение, а возвращало NULL). Поэтому на таких платформах просто не будет работать 90% кода.

Там сказано, что есть valid указатели на функцию и на объект, специальный null-указатель, а так же invalid указатели. Т. к. в стандарте ничего не сказано про тип/размер данных по адресу null указателя, то использование данных по этому адресу аналогично использованию invalid указателя. В тоже время, адресс null указателя нам известен, по-этому его можно использовать в операциях присваивания и сравнения.

numas13
()
Ответ на: комментарий от aureliano15

Там написано, что сравнение invalid pointer с константой может вызвать исключение. А не сравнение null pointer с константой. Лол.

utf8nowhere ★★★
()
Ответ на: комментарий от aureliano15

При всём уважении к яве, это будет ужасно: тормоза и утечки памяти в квадрате.

текущий прототип как минимум не медленней, чем нативная джава. А в некоторых случаях - сильно быстрее. В том числе из-за убер-агрессивных оптимизаций, недоступных в классической реализации.

stevejobs ★★★★☆
() автор топика
Ответ на: комментарий от utf8nowhere

Там написано, что сравнение invalid pointer с константой может вызвать исключение. А не сравнение null pointer с константой.

Сорри, невнимательно прочитал. Но тогда numas13 вообще непонятно к чему это процитировал, явно не в рамках данного треда.

aureliano15 ★★
()
Ответ на: комментарий от stevejobs

При всём уважении к яве, это будет ужасно: тормоза и утечки памяти в квадрате.

текущий прототип как минимум не медленней, чем нативная джава. А в некоторых случаях - сильно быстрее. В том числе из-за убер-агрессивных оптимизаций, недоступных в классической реализации.

Т. е. у вас какая-то своя собственная реализация явы? А выкладывать/продавать её вы планируете или может даже уже? Или только для внутреннего использования?

Сам я с явой имел дело недолго и очень давно: тогда ещё самой свежей была ява2. Впечатления двойственные: насколько красивым мне покался язык и библиотеки, настолько ужасной была реализация, что проявлялось в скорости работы и в утечках памяти в т. ч. Netbeans IDE, написанной на яве (тоже нативной).

После этого с явой дел не имел, но недавно общался с одним дипломником, который подтвердил, что и сейчас ява дико тормозит.

aureliano15 ★★
()
Ответ на: комментарий от aureliano15

всё опенсорцнуто, можно гонять тесты уже сейчас: https://github.com/graalvm/graal

Netbeans IDE

я когда-то в ней не смог открыть ни один проект, кроме хэлловорлдов, ибо чудовище распухало и висло. Пришлось выбросить на помойку. Говорят, многие годы спустя его всё-таки починили, и оно даже чем-то славно (типа хорошей интеграцией с Maven), но репутация безнадежно испорчена

stevejobs ★★★★☆
() автор топика
Ответ на: комментарий от numas13

Ну и выбрасывали бы исключение вместо вызова того, что вызывать не нужно.

Это стандарт С.

В си тоже есть исключения setjmp/longjmp.

Там сказано, что есть valid указатели на функцию и на объект, специальный null-указатель, а так же invalid указатели. Т. к. в стандарте ничего не сказано про тип/размер данных по адресу null указателя, то использование данных по этому адресу аналогично использованию invalid указателя. В тоже время, адресс null указателя нам известен, по-этому его можно использовать в операциях присваивания и сравнения.

Имхо, это уже какая-то собственная вольная трактовка стандарта.

aureliano15 ★★
()
Ответ на: комментарий от annulen

Но почему ещё и от условия if(Do) избавились? Оно-то чем не угодило?

Очевидно, что избавиться от else можно только приняв, что условие в if всегда истинно

Но, как мы увидели, оно не истинно. А значит это баг компилятора, а не фича стандарта.

aureliano15 ★★
()
Ответ на: комментарий от aureliano15

В коде UB, другими словами баг. Компилируя с оптимизациями автор подписывается под фактом, что любые ошибки в коде могут привести к непредсказуемым последствиям. И неважно был ли это вызов нулевой функции, либо непроинициализированная переменная tmp_dir: system((string("rm -rf ") + tmp_dir + "/").c_str()). В последнем случае UB нет, хотя результат тот же, но причина в обоих баг в коде, который обязан быть исправлен автором.

Как уже сказали, компилятор выбросил условие if(Do), потому что при нулевом указателе это UB, а значит корректно должна работать только положительная ветка. Условие не может выполниться отрицательно, это полностью на ответственности автора программы.

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

Dendy ★★★★★
()
Ответ на: комментарий от utf8nowhere

Верный. По правилу Modus tollens.

Да плевать, по какому правилу. Если я говорю, что крокодилы летают, то какие бы умные правила и логичные рассуждения я ни использовал, — это неверно, потому что не соответствует действительности.

aureliano15 ★★
()
Ответ на: комментарий от Dendy

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

Почти полностью согласен. Подправлю только самую малость:

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

aureliano15 ★★
()
Ответ на: комментарий от stevejobs

всё опенсорцнуто, можно гонять тесты уже сейчас: https://github.com/graalvm/graal

Но в сорцах я не вижу си/си++ кода, только яву. Или это уже ява, переписанная на яве? Тогда о каком си++, используемом в проекте, шла речь?

И, если не секрет, почему GPL2? Ведь GPL3 уже давно есть. Мне-то без разницы, просто интересно, чем 3-я версия не устроила.

aureliano15 ★★
()
Ответ на: комментарий от anonymous

Garbage in, garbage out. А алгоритм вывода верный.

Компилятор должен не просто генерить мусор, а ещё и указывать программисту на его ошибки по возможности, а не скрывать их от него, усугубляя тем самым ситуацию.

А если я вместо printf случайно напишу prinf, компилятор тоже вместо сообщения об ошибке должен подставить рандомную функцию, по принципу garbage in, garbage out?

aureliano15 ★★
()
Ответ на: комментарий от aureliano15

Чтобы запуститься, граалю нужна классическая жаба. Он использует оттуда базовые вещи типа ядра виртуальной машины, низкоуровневой части jit-компилятора, сборщик мусора. А класическая жаба написана на C++.

(Сразу предупреждая гнусные инсинуации: пока коммитов от меня нет, там только чтобы понять что происходит в каком-то конкретном фронте работ минимум полгода надо (но можно невозбранно писать документацию).

почему GPL2?

скорей всего потому, что классическая жаба написана на gplv2 и релицензировать ее уже нельзя, по той же причине, что и ведро линукса

stevejobs ★★★★☆
() автор топика
Ответ на: комментарий от aureliano15

компилятор должен не просто генерить мусор, а ещё и указывать программисту на его ошибки по возможности

для таких случаев в стандарте есть специальный термин: ill-formed. Забавно, что даже тут «компания угондошена» (в терминологии Платова): существуют ill-formed, которым в явном виде разрешено быть не диагностируемыми компилятором (конкретные примеры не помню, но они есть).

по-моему вам уже стоит перестать терять время, и перейти от рассуждений к прямым оскорблениям и нанесению тяжких телесных авторам стандарта

stevejobs ★★★★☆
() автор топика
Ответ на: комментарий от stevejobs

конкретные примеры не помню

inline-функция, определённая по-разному в разных translation units.

anonymous
()
Ответ на: комментарий от stevejobs

и нанесению тяжких телесных авторам стандарта

Да всё в порядке со стандартом. Нужно просто, чтобы по-умолчанию генерировались ошибки при детекте UB на этапе компиляции. У меня долгие годы стойкое ощущение, что я чего-то недопонимаю в компиляторах. Они должны воспринимать подобные «оптимизации» с применением UB как ошибку и ругаться матом.

Вон в той же Java UB исключили как термин из языка. И всем счастие, везде одни исключения и ругательства компилятора.

Dendy ★★★★★
()
Ответ на: комментарий от aureliano15

Имхо, это уже какая-то собственная вольная трактовка стандарта.

[url=]http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1548.pdf

6.5.3.2 Address and indirection operators

...

If an invalid value has been assigned to the pointer, the behavior of the unary * operator is undefined. 102)

102) ...

Among the invalid values for dereferencing a pointer by the unary * operator are a null pointer, ...

numas13
()
Ответ на: комментарий от aureliano15

Компилятор должен не просто генерить мусор, а ещё и указывать программисту на его ошибки по возможности, а не скрывать их от него

По возможности он это и так делает, ты же не думаешь что на это специально забивают? Это важнейшее конкурентное преимущество.

Но компиляция, а особенно оптимизация - это трэш и угар. Отследить UB и исключить при этом false positive часто просто нереально. Поэтому и появился рантаймовый UB sanitizer.

anonymous
()
Ответ на: комментарий от Dendy

Но в этом коде нет UB на этапе компиляции. Поэтому что хотят всякие анскильные лалки от компилятора — непонятно.

anonymous
()
Ответ на: комментарий от tim239

кроме следования стандартам здорово когда компилятор подсказывает программисту его факапы

Мне кажется, что здесь и была произведена такая попытка.

Deleted
()
Ответ на: комментарий от stevejobs

Ты не можешь быть уверен ни в чём, никогда

Если не знаешь стандарт, то да, пишешь что-то и надеешься, что это работает. Ну или просто изучаешь инструмент нормально, и не полагаешься на UB. И не обязательно учить _весь_ стандарт наизучть, там половина для language lawyers, об этом сам Бьёрн пишет.

maverik ★★
()
Ответ на: комментарий от numas13

If an invalid value has been assigned to the pointer, the behavior of the unary * operator is undefined

undefined - это значит, что оператор может сотону скастовать, сегфолтнуться, током ударить, или еще что-то сделать. По русски это должно переводиться как «за последствия не отвечаем». А считать undefined = не может быть никогда неверно. undefined - после этой точки может произойти что угодно. в x86 полно undefined - например вот этот бит eflags после такой-то инструкции undefined означает, что там может хранить что угодно, а не то что данная инструкция взорвёт процессор

ckotinko ☆☆☆
()
Ответ на: комментарий от aureliano15

Компилятор должен не просто генерить мусор, а ещё и указывать программисту на его ошибки по возможности, а не скрывать их от него, усугубляя тем самым ситуацию.

Он это и делает, по возможности. Например,

int x;
int y = x; // UB

такой код, обычно, вызывает warning, хотя типичное UB.

А если я вместо printf случайно напишу prinf, компилятор тоже вместо сообщения об ошибке должен подставить рандомную функцию, по принципу garbage in, garbage out?

А это не UB (предполагается, что такой функции нет в области видимости), поэтому не должен.

maverik ★★
()
Ответ на: комментарий от aureliano15

в нашем конкретном случае компилятор вообще окуел

у нас Do = null или &EraseAll в зависимости от внешнего условия.

оба значение определены стандартом, никакого undefined значения там нету. компилятор не имеет права, не зная точно что «внешнее условие» равно true или false, выбрать одно из возможных значений как единственно возможное. он должен считать значение Do неизвестным на момент компиляции.

во-вторых, undefined это означает «за последствия не отвечаем». например будет сегфолт, исключение #PF, обычное обращение к оперативке. undefined позволяет ряд оптимизаций, но оптимизации не должны нарушать логику программы. но undefined почему-то положили оправданием для его расползания на соседние операции. то есть если мы обратились на строке 7500 к такой переменной, то компилятор имеет право колбасить везде где хочет. Наконец, UB не должно проходить сквозь контрольные точки: вход и выход из функции, точки ветвления и т.д. машинный код в этом случае полностью прекращает отражать написанное в программе.

ckotinko ☆☆☆
()
Ответ на: комментарий от anonymous

не выводится. из Do() от неконстантного адреса должен выводиться косвенный вызов функции. и всё. вы утверждаете, что undefined = can never happen. нет. undefined здорового человека = последствия не определены. последствия. у курильщика конечно да, еще и предшествующие события могут быть неопределены. курение - зло.

ckotinko ☆☆☆
()
Ответ на: комментарий от anonymous

значение переменной не константа. ты не можешь полагать его константой. его надо считать и перейти по адресу. всё.

0 - допустимый адрес. nullptr != 0. можно использовать ~0u как nullptr, он отлично будет крахаться.

ckotinko ☆☆☆
()
Ответ на: комментарий от ckotinko

Мне кажется, кто-то ничего не понимает ни про то, как работают совеременные компиляторы, ни про UB.

Компилятор может оптимизировать программу не заботясь о том, насколько логичной она будет в случае UB. Поэтому компилятор думает так: в случае UB я могу делать все, что хочу (в т.ч. могу вызвать любую функцию), в противном случае я должен делать то, что указано. Компилятор явно видит, что Do инициализируется только в одном месте, поэтому выдает такоей код:

EraseAll();

и это нормально, т.к. если Do инициализирован, то только EraseAll() (других вариантов нет), если неинициализирован, то можно делать все, что угодно, в .т.ч. вызвать эту функцию.

В итоге в любом случае, код соответствует стандарту.

maverik ★★
()
Последнее исправление: maverik (всего исправлений: 1)
Ответ на: комментарий от aureliano15

К тому же лучший оптимизатор, как известно, сам программист. Что мешает ему явно вызвать нужную функцию?

Мне кажется, что именно здесь кроется самое важное. В любой книге по си говорится, что программист сам отвечает за корректность программы. А то, на что он может рассчитывать — описано в стандарте. В данном случае разработчики компилятора решили, что такая ошибка программиста может быть случайной и исправили её как могли.

Deleted
()
Ответ на: комментарий от aureliano15

А значит это баг компилятора, а не фича стандарта.

Так как вызов (void*)0 это UB, то можно натянуть сову на глобус и сказать что else тоже вызывает EraseAll, хотя конечно дружественнее было бы вставить туда builtin_trap

annulen ★★★★★
()
Ответ на: комментарий от Deleted

В любой книге по си говорится, что программист сам отвечает за корректность программы

А в других языках кто-то другой отвечает за корректность программы?

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.