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)
Ответ на: комментарий от tim239

Кроме ценности подобной горе-оптимизации

Здесь специально подобранный пример, где эта оптимизация выглядит «горе-„оптимизацией. Думаю, что этот пример сложно привести в соответствие с „ожидаемым“ поведением (выкинуть оптимизацию, анализирующую диапазон принимаемых объектами заначений), не ухудшив скорость работы многих других, вполне корректных программ.

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

Несколько раз на всяких CppCon-ах этот вопрос подымался. clang (фронтенд) почти (или вообще) не занимается оптимизациями, оптимизации происходят на уровне llvm. Проследить, из каких строк кода произошли те или иные инструкции llvm, особенно после того, как отработала дюжина llvm-пассов — задача неподъёмная.

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

такое поведение абсурдно

Нет, в корректной программе только такое поведение и могло быть. А некорректные программы абсурдны сами по себе.

Проблема в том, что все иногда ошибаются, даже разрабы шланга, и не только они. Но когда у меня программа падает из-за обращения к 0 адресу — это нормально. Когда при таком обращении компилятор генерит код, который сообщает об ошибке и корректно завершает программу с ненулевым кодом возврата — это тоже нормально. А когда программа нормально выполняется, но не так, как задумано — это ненормально, потому что такую ошибку очень легко пропустить не только на стадии отладки, но и на стадии тестирования и даже эксплуатации.

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

А что думают по этому поводу разработчики шланга

Вот боюсь они думают «Мы стандарт выучили от корки до корки - и вам придётся».

Чего гадать? Поживём — увидим. :-)

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

этот пример сложно привести в соответствие с „ожидаемым“ поведением [skip], не ухудшив скорость работы многих других, вполне корректных программ.

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

Проследить, из каких строк кода произошли те или иные инструкции llvm, особенно после того, как отработала дюжина llvm-пассов — задача неподъёмная.

Значит, сама архитектура изначально неверна и маст-дай.

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

когда у меня программа падает из-за обращения к 0 адресу — это нормально

Это всё эмоциональные высказывания.

Объективно можно сказать только одно: когда происходит обращение по нулевому указателю — это UB. UB со всеми вытекающими.

А «нормально» это или «не нормально» — это оценки.

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

сама архитектура изначально неверна и маст-дай.

Неверной была бы архитектура, в которой главным требованием закладывали бы непричинение анальных болей анскильным лалкам. А так всё нормально.

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

когда у меня программа падает из-за обращения к 0 адресу — это нормально

Это всё эмоциональные высказывания.

Это не эмоциональные высказывания, а более простая разработка безопасного кода. Эмоции — это когда мне девушка нравится или не нравится. Или шрифт нравится или не нравится. Или обои...

А безопасность — это не эмоции, а очень серьёзно. И не последнюю роль здесь играет компилятор. Поэтому важно, чтобы он вёл себя предсказуемо, даже если код ub и вообще бе-е-е.

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

скажем так. компилятор получает на вход translation unit. и выдает некий черный ящик, характеризуемый внутренним состоянием(часть его лежит в bss, часть в data, чопустимым, по видимому, считать неопределённое значение объединимым с определённым. отсюда и оптимизация в виде инлайнинга - он же невозможен при неконстантности адреса перехода.асть в rodata), и взаимодействиями. в частности это экспортируемые функции (у нас main и nevercalled). но даже переменные можно представить как функции-accessorы. то есть мы можем утверждать, что результатом работы компилятора является finite state machine.

в нашем случае, в Do хранится нечто, что будет перезаписано при вызове nevercalled. по сути, компилятор может сказать лишь то, что

если nevercalled было вызвано раньше main, то do = eraseall, иначе do не определено.

Но! «не определено» не дает права считать что do определено как eraseall. компилятор не живет в вакууме, он генерит код для вполне реальных систем, и переменные, которые он генерит, располагаются в очень даже определённых локациях. В Си как раз определено, что неинициализованный static лежит в bss и будет инициализован нулями.

То есть Си не живет в вакууме, он говорит прямо что код будет скомпилирован в объектники, и будет обладать вполне конкретными свойствами. Т.е. наша finite state machine имеет определённое начальное состояние. К слову, даже неинициализованная переменная в data имеет начальное состояние, просто оно random. На входе всё равно что-то будет.

Компилятор С++ считает допустимым, по видимому, считать неопределённое значение объединимым с определённым. отсюда и оптимизация в виде инлайнинга - он же невозможен при неконстантности адреса перехода. Я считаю такое толкование расширяющим понятие UB, т.к. «неопределённое» значение производной может определяться внешними условиями и в Си оно так и считается.

ckotinko ☆☆☆
()
Последнее исправление: ckotinko (всего исправлений: 1)
Ответ на: комментарий от utf8nowhere

не надо тут рассказывать про неподъёмную задачу.

там есть Module, внутри них есть Function, а в Function есть BasicBlock, и Value, вокруг которых танцует любой плагин.

Кто-то должен был ручками взять решить, что раз эта Value undefined и туда идет одна запись константы, то надо её выполнить всегда, хотя запись вполне себе условная(если вызвали функцию). т.е. условие тоже посчитали всегда выполняющимся. это и есть расширение UB

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

вспоминается ещё QML, объект «справа»- применяет изменение «слева».

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

undefined хватает и без компилятора.

пусть есть каталог dist

в нем есть dist/launcher - выполняемый файл

он линкуется с dist/lib/libcore.so.1

launcher запускает плагины из dist/plugins

плагины линкуются в dist/lib/libcore.so.1 и dist/lib/libwidgets.so.1 и в них указан rpath=$ORIGIN/../lib

все библиотеки подгружаются. казалось бы, в чем же может быть проблема? почему вызов из libplugin1.so в libwidgets.so.1 приводит к переходу по рандомному адресу с вероятностью ~30% хотя я даже launcherу сказал линковаться с libwidgets.so.1? то есть либа точно загружена и я её вижу в info share в gdb. почему так. один поток, гонок нет. адреса библиотек не меняются - проверил в gdb. но 30% вероятности краха потому что .got.plt не заполнен вообще. там мусор лежит. гонка в один поток - с кем?

почему в почему если мы кладем плагины в dist/lib, а в plugins делаем симлинки, все работает как надо?

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

ckotinko ☆☆☆
()
Последнее исправление: ckotinko (всего исправлений: 1)
Ответ на: комментарий от numas13

При том, что в Rust такое тоже работает.

LOL. Впрочем логично, ведь у них компилятор это фронтэнд к LLVM.

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

про бесполезность лютой оптимизации:

http://www.extremetech.com/computing/116561-the-death-of-cpu-scaling-from-one

http://preshing.com/20120208/a-look-back-at-single-threaded-cpu-performance/

http://www.slideshare.net/JosePinilla/eece528-06ilplimitationsjose

http://research.cs.wisc.edu/vertical/papers/2013/isa-power-struggles-tr.pdf

еще кучу ссылок по более старым процам(P2, P3) я тупо протерял. но там тоже IPC~1, конвейер 5х8. у коре7 8х16 и IPC~1.4. гуглите «томассуло» на тему что это такое. Можно было просто на ту же площадь, где сидит один коре7 поместить пяток пней3.

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

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

Ну да, это же мамой клянусь unsafe. В нём можно сделать все теже ошибки, что в C/C++, а компилятор будет молчать.

numas13
()

Всё правильно делают clang-еры. Может, утихнут постепенно мифы про «простую и предсказуемую сишку» и «портабельный ассемблер».

Хотя, вот тут пока до людей не доходит что «баг» не в компиляторе, а в языке.

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

Ну да, это же мамой клянусь unsafe. В нём можно сделать все теже ошибки, что в C/C++, а компилятор будет молчать.

Так в Rust unsafe ехал через unsafe и unsafe-м погонял. Если реально его использовать.

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

это вы так думаете. а в реальности там оптимизация идет теми же плагинами что и в С и С++. шланг из плагинов вообще-то состоит, и эти плагины ничего про раст не знают.

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

это вы так думаете. а в реальности там оптимизация идет теми же плагинами что и в С и С++. шланг из плагинов вообще-то состоит, и эти плагины ничего про раст не знают.

Я не так думаю, так написано. Я же дал ссылку!

Dereference raw pointers

В том числе:

Dereferencing null or dangling pointers

Это UB в C/C++/Rust.

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

в С этот указатель обнулён.

C89, 6.5.7 Initialization.

If an object that has automatic storage duration is not initialized explicitely, its value is indeterminate. If an object that has static storage duration is not initialized explicitely, it is initialized implicitely as if every member that has arithmetic type were assigned 0 and every member that has pointer type were assigned a null pointer constant.

C99, 6.7.8 Initialization.

If an object that has automatic storage duration is not initialized explicitly, its value is indeterminate. If an object that has static storage duration is not initialized explicitly, then: — if it has pointer type, it is initialized to a null pointer; — if it has arithmetic type, it is initialized to (positive or unsigned) zero; — if it is an aggregate, every member is initialized (recursively) according to these rules; — if it is a union, the first named member is initialized (recursively) according to these rules.

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

нет, это не UB. у него есть вполне конкретное значение - ноль. а компилятор утверждает что у него всегда адрес какой-то функции.

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

нет, это не UB. у него есть вполне конкретное значение - ноль. а компилятор утверждает что у него всегда адрес какой-то функции.

В данном случае компилятор статически доказывает, что указатель NULL, а это UB.

numas13
()

Трэд закрыт, скотинки выиграли

3.6.2 Initialization of non-local variables 1. There are two broad classes of named non-local variables: those with static storage duration (3.7.1) and those with thread storage duration (3.7.2). Non-local variables with static storage duration are initialized as a consequence of program initiation. Non-local variables with thread storage duration are initialized as a consequence of thread execution. Within each of these phases of initiation, initialization occurs as follows.

2. Variables with static storage duration (3.7.1) or thread storage duration (3.7.2) shall be zero-initialized (8.5) before any other initialization takes place

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4296.pdf

кто не согласен идет в жёппу.

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

нет, это не UB. у него есть вполне конкретное значение - ноль. а компилятор утверждает что у него всегда адрес какой-то функции.

Значение есть, а вызов может быть чего угодно.

https://imgs.xkcd.com/comics/random_number.png

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

переход по NULL - не UB. это просто переход по нулевому адресу. нулевой адрес не обязан быть недопустимым - он может быть и разрешенным и там код может лежать. это просто адрес.

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

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

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

переход по NULL - не UB. это просто переход по нулевому адресу. нулевой адрес не обязан быть недопустимым - он может быть и разрешенным и там код может лежать. это просто адрес.

Не надо путать NULL и нулевой адрес.

anonymous
()
Ответ на: Трэд закрыт, скотинки выиграли от ckotinko

shall be zero-initialized

okay

У нас Do инициализирована нулем, и может кроме нуля принимать только значение &EraseAll

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

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

Однако в else творится очевидный адъ и израиль, поэтому вместо генерации ветки, недопустимой по стандарту, компилятор от нее избавился.

Вывод - если программист мудак, нечего пенять на компилятор

annulen ★★★★★
()
Последнее исправление: annulen (всего исправлений: 2)
Ответ на: комментарий от ckotinko

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

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

NULL - это по определению нулевой адрес.

Это вы от незнания стандарта так говорите. 0 при касте к указателю не обязательно станет нулевым адресом.

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

int main() return ((void*)0)(); Однако в else творится очевидный адъ и израиль

И что тут удивительно?

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

пфффф.

поэтому вместо генерации ветки

не надо генерировать ветки, алё. там нет веток. нету. в коде нету if.

надо генерировать команду call [addr]. одну единственную команду.

там будет что-то типа

call _i686_pc_stub
call [ebx+offset]

или

call [rip+offset]

какие еще ветвления. вы о чём. это такая анальная деоптимизация что ли?

ckotinko ☆☆☆
()
Последнее исправление: ckotinko (всего исправлений: 2)
Ответ на: комментарий от anonymous

Это я прямо сейчас сверился со стандартом на случай если чего путаю.

Но я чувствую что это путаница в терминах. Есть адрес нулевой ячейки памяти, и есть implementation-defined нулевой адрес. NULL и (void*)0 - это второй случай.

// Но как это связано с топиком?

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

в коде нету if

В коде ясно записано, что указатель на функцию может принимать только одно из двух значений, поэтому вполне логично использовать if + прямой вызов вместо jmp. К тому же, EraseAll отлично инлайнится

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

надо генерировать команду jmp [addr].

Ну вот оптимизатор и увидел, что в addr либо 0, либо eraseall. addr же static, никакого другого значения там быть не может. Правильно ли то что оно не захотело обращаться в явный NULL? Теоретически не правильно, это надо делать опцией, и потому это скорее не баг, а явная лень возиться с опцией.

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

переход по NULL - не UB. это просто переход по нулевому адресу. нулевой адрес не обязан быть недопустимым - он может быть и разрешенным и там код может лежать. это просто адрес.

http://www.open-std.org/JTC1/SC22/WG14/www/docs/C99RationaleV5.10.pdf

Implicit in the Standard is the notion of invalid pointers. In discussing pointers, the Standard typically refers to “a pointer to an object” or “a pointer to a function” or “a null pointer.”

Regardless how an invalid pointer is created, any use of it yields undefined behavior. Even assignment, comparison with a null pointer constant, or comparison with itself, might on some systems result in an exception.

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

call [rip+offset]

А NULL по оффсету вызывать будешь?

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

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

На самом деле нет, опция не нужна, просто надо было сгенерить ветку else с __builtin_trap(), так как кода использующего Do перед этим еще не было.

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

указатель на функцию может принимать только одно из двух значений

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

поэтому вполне логично использовать if

ну вообще там не if, а llvm::Value, которое пишется из нескольких basicblockов с llvm::ConstantNullPointer или llvm::BlockAddress. И никакого ifа там нет стопудово и не может быть добавлено.

Компилятор действительно по-видимому отбрасывает null pointer как невозможный, хотя он вполне возможный. Например мы пишем ядро запускаемое через multiboot. Я тут погуглил, и оказалось что это UB идет аж со времен PDP11, VAXов и прочей адской interdatы. То есть люди тупо тянут legacy из 80х и делают вид что «это для оптимизации». Никакой оптимизации тут ясен пень нету, так как невалидным может быть и ненулевой указатель. А есть тупо демагогия, изза которой условно-возможное значение стало безусловным.

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

и вам придётся

наверное, в команде должен быть хотя бы 1 человек, который действительно знает стандарт от корки до корки. И не только его, а вообще. Чтобы к нему можно было в любой момент обратиться с вопросом или поставить в ревьюеры коммита. А в большой организации этим может заняться выделенная команда, типа «команда поддержки базовой платформы»

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

На самом деле нет, опция не нужна,

Если речь о оригинальном коде, то раз оптимизатор научился переделывать UB из «мы ничего не гарантируем, а делаем что написано и будь что будет» в «мы самые умные и предполагаем, что программист ну явно же не хотел sigseg, а хотел eraseall, ведь вон же всем понятно, что так», то опция как раз нужна. Именно опциями управляется как можно оптимизировать, с какой строгостью всё остальное. Чем это лучше?

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

это не константа и уже поэтому нельзя брать и инлайнить ничего.

В варианте с if можно, так как в каждой ветке точно известно, что вызывается. Правда я не знаю, есть ли в llvm трансформ, который может по факту такую конструкцию сделать, но по-идее оптимизация полезная даже без инлайна, особенно для risc'ов

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

Есть традиция по которой невалидные указатели по умолчанию инициализируют нулем. Есть куча кода, делающего проверки «инициализированности» указателя путем сравнения его с нулем, где компилятор может доказать их ненужность.

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

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

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

интересно, какой пример нужно еще привести дополнительно, чтобы показать багнутость языка? Чем страшнее примеры приводишь, тем крепче вера крестофанатиков :)

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

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

А тебе то это зачем? Сиди молча в своей песочнице со сборщиком мусора, и не вылазь за ее пределы.

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