Предлагаем вашему вниманию цикл статей, посвященных рекомендациям по написанию качественного кода на примере ошибок, найденных в проекте Chromium. Это шестая часть, которая будет посвящена функции malloc. Вернее, тому, почему следует обязательно проверять указатель, возвращаемый этой функцией. Скорее всего, вы не догадываетесь, какой подвох связан с malloc, потому рекомендуем познакомиться с этой статьей.
Примечание. В статье под функцией malloc часто будет подразумеваться, что речь идёт не только именно об этой функции, но и о calloc, realloc, _aligned_malloc, _recalloc, strdup и так далее. Не хочется загромождать текст статьи, постоянно повторяя названия всех этих функций. Общее у них то, что они могут вернуть нулевой указатель.
malloc
Если функция malloc не смогла выделить буфер памяти, то она возвращает NULL. Любая нормальная программа должна проверять указатели, которые возвращает функция malloc, и соответствующим образом обрабатывать ситуацию, когда память выделить не получилось.
К сожалению, многие программисты небрежно относятся к проверке указателей, а иногда сознательно не проверяют, удалось ли выделить память или нет. Их логика следующая:
Если функция malloc не смогла выделить память, то вряд ли моя программа продолжит функционировать должным образом. Скорее всего, памяти будет не хватать и для других операций, поэтому можно вообще не заморачиваться об ошибках выделения памяти. Первое же обращение к памяти по нулевому указателю приведёт к генерации Structured Exception в Windows, или процесс получит сигнал SIGSEGV, если речь идёт о Unix-подобных системах. В результате программа упадёт, что меня устраивает. Раз нет памяти, то и нечего мучаться. Как вариант, можно перехватить структурное исключение/сигнал и обрабатывать разыменовывания нулевого указателя более централизовано. Это удобнее, чем писать тысячи проверок. |
Кстати, существует ещё одно оправдание разработчиков, почему они не проверяют, что вернула функция malloc. Функция malloc только резервирует память, но вовсе нет гарантии, что хватит физической памяти, когда мы начнём использовать выделенный буфер памяти. Поэтому, раз всё равно гарантии нет, то и проверять не надо. Например, именно так Carsten Haitzler, являющийся одним из разработчиков библиотеки EFL Core, объяснял, почему я насчитал более 500 мест в коде библиотеки, где отсутствуют проверки. Вот его комментарий к статье:
OK so this is a general acceptance that at least on Linux which was always our primary focus and for a long time was our only target, returns from malloc/calloc/realloc can't be trusted especially for small amounts. Linux overcommits memory by default. That means you get new memory but the kernel has not actually assigned real physical memory pages to it yet. Only virtual space. Not until you touch it. If the kernel cannot service this request your program crashes anyway trying to access memory in what looks like a valid pointer. So all in all the value of checking returns of allocs that are small at least on Linux is low. Sometimes we do it… sometimes not. But the returns cannot be trusted in general UNLESS its for very large amounts of memory and your alloc is never going to be serviced — e.g. your alloc cannot fit in virtual address space at all (happens sometimes on 32bit). Yes overcommit can be tuned but it comes at a cost that most people never want to pay or no one even knows they can tune. Secondly, fi an alloc fails for a small chunk of memory — e.g. a linked list node… realistically if NULL is returned… crashing is about as good as anything you can do. Your memory is so low that you can crash, call abort() like glib does with g_malloc because if you can't allocate 20-40 bytes… your system is going to fall over anyway as you have no working memory left anyway. I'm not talking about tiny embedded systems here, but large machines with virtual memory and a few megabytes of memory etc. which has been our target. I can see why PVS-Studio doesn't like this. Strictly it is actually correct, but in reality code spent on handling this stuff is kind of a waste of code given the reality of the situation. I'll get more into that later. |
Chromium
Chromium здесь при том, что в используемых в нём библиотеках имеется не менее 70 ошибок, связанных с отсутствием проверки после вызова таких функций, как malloc, calloc, realloc. Да, в самом Chromium эти функции почти нигде не используются. В Chromium применяются только контейнеры или operator new. Однако, раз ошибки есть в используемых библиотеках, то значит, можно сказать, что они есть и в Chromium. Конечно, какие-то части библиотек могут не использоваться при работе Chromium, но определять это сложно и ненужно. Всё равно надо править все ошибки.
Я не буду приводить в статье множество фрагментов кода с ошибками, так как они однотипны. Приведу для примера только одну ошибку, обнаруженную в библиотеке Yasm:
static SubStr *
SubStr_new_u(unsigned char *s, unsigned int l)
{
SubStr *r = malloc(sizeof(SubStr));
r->str = (char*)s;
r->len = l;
return r;
}
Предупреждение PVS-Studio: V522 CWE-690 There might be dereferencing of a potential null pointer 'r'. Check lines: 52, 51. substr.h 52
В коде нет никакой защиты от нулевого указателя. Другие подобные ошибки из Chromium и используемых библиотек я собрал вместе в файл и выложил их здесь: chromium_malloc.txt. В файле упоминаются 72 ошибки, но на самом деле их может быть больше. Как я писал в вводной статье, я просматривал отчёт только поверхностно.
Согласно Common Weakness Enumeration обнаруженные ошибки PVS-Studio классифицирует как:
- CWE-690: Unchecked Return Value to NULL Pointer Dereference.
- CWE-628: Function Call with Incorrectly Specified Arguments.
- CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer
Как видите, даже в таком высококачественном проекте как Chromium, можно заметить массу дефектов, связанных с отсутствием проверок. Теперь я перехожу к самому интересному и расскажу, почему проверки обязательно нужны.
Почему обязательно нужна проверка
Есть сразу 4 причины, каждой из которых достаточно, чтобы обязательно делать проверки после вызова функции malloc. Если кто-то в команде не пишет проверки, то обязательно заставьте его прочитать эту статью.
Прежде чем начать, небольшая теоретическая справка, почему возникают структурные исключения или сигналы, если происходит разыменовывание нулевого указателя. Это будет важно для дальнейшего повествования.
В начале адресного пространства одна или несколько страниц памяти защищены операционной системой от записи. Это позволяет выявить ошибки обращения к памяти по нулевому указателю, или указателю, значение которого близко к 0.
В разных операционных системах для этих целей резервируется разное количество памяти. При этом в некоторых ОС это значение можно настраивать. Поэтому нет смысла называть какое-то конкретное число зарезервированных байт памяти. Но чтобы как-то сориентировать читателя, скажу, что в Linux системах типовым значением является 64 Кбайт.
Важно, что, прибавив к нулевому указателю какое-то достаточно большое число, можно «промазать» мимо контрольных страниц памяти и случайно попасть в какие-то незащищенные от записи страницы. Таким образом можно испортить где-то какие-то данные, но операционная система этого не заметит и никакого сигнала / исключения она не сгенерирует.
Заваривайте кофе, мы начинаем!
Разыменовывание нулевого указателя — это неопределённое поведение
С точки зрения языка C и C++ разыменовывание нулевого указателя приводит к неопределенному поведению. Неопределённое поведение — это что угодно. Не думайте, что вы знаете, как будет вести себя программа, если произойдёт разыменовывание nullptr. Современные компиляторы занимаются серьезными оптимизациями, в результате чего бывает невозможно предсказать, как проявит себя та или иная ошибка в коде.
Неопределённое поведение программы — это очень плохо. Вы не должны допускать его в своём коде.
Не думайте, что сможете совладать с разыменовыванием нулевого указателя, используя обработчики структурных исключений (SEH в Windows) или сигналы (в UNIX-like системах). Раз разыменовывание нулевого указателя было, то работа программы уже нарушена, и может произойти что угодно. Давайте рассмотрим абстрактный пример, почему нельзя полагаться на SEH-обработчики и т.п.
size_t *ptr = (size_t *)malloc(sizeof(size_t) * N * 2);
for (size_t i = 0; i != N; ++i)
{
ptr[i] = i;
ptr[N * 2 - i - 1] = i;
}
Этот код заполняет массив от краёв к центру. К центру значения элементов увеличиваются. Это придуманный за 1 минуту пример, поэтому не гадайте, зачем такой массив кому-то нужен. Я и сам не знаю. Мне было важно, чтобы в соседних строках программы происходила запись в начало массива и куда-то в его конец. Такое иногда бывает нужно и в практических задачах, и мы рассмотрим реальный код, когда доберёмся до 4-ой причины.
Ещё раз внимательно посмотрим на эти две строки:
ptr[i] = i;
ptr[N * 2 - i - 1] = i;
С точки зрения программиста, в начале цикла произойдёт запись в элемент ptr[0], и возникнет структурное исключение/сигнал. Оно будет обработано, и всё будет хорошо.
Однако компилятор в каких-то целях оптимизации может переставить присваивания местами. Он имеет на это полное право. С точки зрения компилятора, если указатель разыменовывается, то он не может быть равен nullptr. Если указатель нулевой, то это неопределённое поведение, и компилятор не обязан думать о последствиях оптимизации.
Так вот, компилятор может решить, что в целях оптимизации выгоднее выполнить присваивания так:
ptr[N * 2 - i - 1] = i;
ptr[i] = i;
В результате в начале произойдет запись по адресу ((size_t *)nullptr)[N * 2 — 0 — 1]. Если значение N достаточно велико, то страница защиты в начале памяти будет «перепрыгнута» и значение переменной i может быть записано в какую-то ячейку, доступную для записи. В общем, произойдёт порча каких-то данных.
И только после этого будет выполнено присваивание по адресу ((size_t *)nullptr)[0]. Операционная система заметит попытку записи в контролируемую ею область и сгенерирует сигнал/исключение.
Программа может обработать это структурное исключение/сигнал. Но уже поздно. Где-то в памяти есть испорченные данные. Причем непонятно, какие данные испорчены и к каким последствиям это может привести!
Виноват ли компилятор, что поменял операции присваивания местами? Нет. Программист допустил разыменовывание нулевого указателя и тем самым ввёл программу в состояние неопределённого поведения. В данном конкретном случае неопределённое поведение программы будет заключаться в том, что где-то в памяти испорчены данные.
Вывод
Исходите из аксиомы: любое разыменовывание нулевого указателя — это неопределённое поведение программы. Не бывает «безобидного» неопределённого поведения. Любое неопределённое поведение недопустимо.
Не допускайте разыменовывания указателей, которые вернула функция malloc и её аналоги, без их предварительной проверки. Не полагайтесь на какие-то другие способы перехвата разыменовывания нулевого указателя. Следует использовать только старый добрый оператор if.
Разыменовывание нулевого указателя — это уязвимость
То, что некоторые разработчики вообще не считают за ошибку, другие воспринимают как уязвимость. Именно так обстоит дело с разыменовыванием нулевого указателя.
Одним нормально, если программа из-за разыменовывания нулевого указателя упадёт или если ошибка будет обработана каким-то общим способом с помощью перехвата сигнала/структурного исключения.
Другие считают, что разыменовывание нулевого указателя приводят к ошибке «отказ в обслуживании» и является уязвимостью. Вместо того, чтобы штатно обработать нехватку памяти, программа, или одна из нитей программы, завершает свою работу. Это может приводить к потере данных, нарушению целостности данных и так далее. Другими словами, CAD система тупо закроется, если не сможет выделить память для какой-то сложной операции, не предложив пользователю даже сохранить результат своей работы.
Не буду голословным. Есть, такая программа как Ytnef, предназначенная для декодирования TNEF потоков, например, созданных в Outlook. Так вот, разработчики приложения считают отсутствие проверки после вызова calloc уязвимостью CVE-2017-6298.
Все исправленные места, в которых могло произойти разыменовывание нулевого указателя, имели приблизительно один и тот же вид:
vl->data = calloc(vl->size, sizeof(WORD));
temp_word = SwapWord((BYTE*)d, sizeof(WORD));
memcpy(vl->data, &temp_word, vl->size);
Выводы
Если вы разрабатываете безответственное приложение, для которого упасть в процессе работы не является бедой, то да, писать проверки необязательно.
Однако, если вы разрабатываете какую-то библиотеку, то отсутствие проверок недопустимо! Вашей библиотекой могут пользоваться не только ленивые программисты, создающие безответственные приложения, типа игры Тетрис. Надо заботиться и о нормальных программистах, и нормальных программах.
Поэтому я идеологически не согласен, например, с Carsten Haitzler, что в библиотеке EFL Core нет проверок (подробности в статье). Это не позволяет построить на основе таких библиотек надёжные приложения.
В общем, если вы создаёте библиотеку, то помните, что в некоторых приложениях разыменовывание нулевого указателя — это уязвимость. Необходимо обрабатывать ошибки выделения памяти и штатно возвращать информацию об неудаче.
Где гарантии, что будет разыменовывание именно нулевого указателя?
Те, кто ленится писать проверки, почему-то думают, что разыменование затрагивает именно нулевые указатели. Да, часто именно так и бывает. Но может ли поручиться программист за код всего приложения? Уверен, что нет.
Сейчас я на практических примерах покажу, что я имею в виду. Возьмём, например, код из библиотеки LLVM-subzero, которая используется в Chromium. Если честно, я теряюсь в догадках, какая связь между проектом Chromium и LLVM, но она есть.
void StringMapImpl::init(unsigned InitSize) {
assert((InitSize & (InitSize-1)) == 0 &&
"Init Size must be a power of 2 or zero!");
NumBuckets = InitSize ? InitSize : 16;
NumItems = 0;
NumTombstones = 0;
TheTable = (StringMapEntryBase **)
calloc(NumBuckets+1,
sizeof(StringMapEntryBase **) +
sizeof(unsigned));
// Allocate one extra bucket, set it to look filled
// so the iterators stop at end.
TheTable[NumBuckets] = (StringMapEntryBase*)2;
}
Предупреждение PVS-Studio: V522 CWE-690 There might be dereferencing of a potential null pointer 'TheTable'. Check lines: 65, 59. stringmap.cpp 65
Сразу после выделения буфера памяти происходит запись в ячейку TheTable[NumBuckets]. Если значение переменной NumBuckets достаточно большое, то мы испортим какие-то данные с непредсказуемыми последствиями. После такой порчи вообще нет смысла рассуждать, как будет работать программа. Могут последовать самые неожиданнейшие последствия.
Аналогичные опасные присваивания я вижу ещё в двух местах этого проекта:
- V522 CWE-690 There might be dereferencing of a potential null pointer 'Buckets'. Check lines: 219, 217. foldingset.cpp 219
- V769 CWE-119 The 'NewTableArray' pointer in the 'NewTableArray + NewSize' expression could be nullptr. In such case, resulting value will be senseless and it should not be used. Check lines: 218, 216. stringmap.cpp 218
Так что это не уникальный случай, а вполне типовая ситуация, когда данные записываются не точно по нулевому указателю, а по какому-то произвольному смещению.
Продолжу заочную дискуссию с Carsten Haitzler. Он утверждает, что они понимают, что делают, когда не проверяют результат вызова функции malloc. Нет, не понимают. Давайте взглянем, например, на вот такой фрагмент кода из библиотеки EFL:
static void
st_collections_group_parts_part_description_filter_data(void)
{
....
filter->data_count++;
array = realloc(filter->data,
sizeof(Edje_Part_Description_Spec_Filter_Data) *
filter->data_count);
array[filter->data_count - 1].name = name;
array[filter->data_count - 1].value = value;
filter->data = array;
}
Предупреждение PVS-Studio: V522 There might be dereferencing of a potential null pointer 'array'. edje_cc_handlers.c 14249
Примечание. Я использую старые исходники EFL Core Libraries, которые остались у меня со времён написания статьи про эту библиотеку. Поэтому код или номера строк могут уже не соответствовать тому, что есть сейчас. Однако это не важно для повествования.
Перед нами типовая ситуация: в буфере не хватает свободного места для хранения данных, и его следует увеличить. Для увеличения размера буфера используется функция realloc, которая может вернуть NULL.
Если это произойдёт, то вовсе не обязательно возникнет структурное исключение/сигнал из-за разыменовывания нулевого указателя. Взглянем вот на эти строчки:
array[filter->data_count - 1].name = name;
array[filter->data_count - 1].value = value;
Если значение переменной filter->data_count достаточно большое, то значения будут записаны по какому-то непонятному адресу.
В памяти будут испорчены какие-то данные, а программа продолжит своё выполнение. Последствия вновь непредсказуемые, но ничего хорошего точно не получится.
Я внимательно не стал изучать старый отчёт, касающийся EFL Core Libraries, но это точно не единственная подобная ошибка. Я заметил как минимум ещё два похожих места, где после realloc данные дописываются по какому-то индексу.
Вывод
Я вновь задаю вопрос: «где гарантии, что будет разыменовывание именно нулевого указателя?». Нет такой гарантий. Невозможно, разрабатывая или модифицируя код, помнить про только что рассмотренный нюанс. Запросто можно что-то испортить в памяти, при этом программа продолжит выполняться как ни в чём не бывало.
Единственный способ написать надёжный и правильный код — это всегда проверять результат, который вернула функция malloc. Проверь и живи спокойно.
Где гарантии, что memset заполняет память в прямом порядке?
Найдётся кто-то, кто скажет что-то подобное:
Я отлично понимаю про realloc и всё остальное, что написано в статье. Но я профессионал и, выделяя память, сразу заполняю её нулями с помощью memset. Там, где действительно необходимо, я использую проверки. Но лишние проверки после каждого malloc я писать не буду. |
int Resampler::Reset(int inFreq, int outFreq, size_t num_channels) {
....
state1_ = malloc(8 * sizeof(int32_t));
memset(state1_, 0, 8 * sizeof(int32_t));
....
}
Выделяется память, затем буфер заполняется нулями. Очень частая практика, хотя, на самом деле, две строчки можно сократить до одной, используя calloc. Но всё это не важно.
Главное, что даже подобный код не безопасен! Функция memset не обязана начать заполнять память с начала и тем самым вызывать разыменовывание нулевого указателя.
Функция memset имеет право начать заполнять буфер с конца. И, если выделялся большой буфер, то могут быть затёрты какие-то полезные данные. Да, заполняя память, функция memset рано или поздно достигнет страницы, защищённой от записи, и операционная система сгенерирует структурное исключение/сигнал. Однако обрабатывать их уже не имеет смысла. К этому моменту будет испорчен большой фрагмент памяти, и дальнейшая работа программы будет непредсказуема.
Читатель может возразить, что всё это носит исключительно теоретический характер. Да, функция memset теоретически может заполнять буфер начиная с конца буфера, но на практике никто не будет так реализовывать эту функцию.
Соглашусь, что подобная реализация memset действительно экзотика, и я даже задавал вопрос на StackOverflow на эту тему. В ответе говорится:
The Linux kernel's memset for the SuperH architecture has this property: link.
К сожалению, это код на незнакомой мне разновидности ассемблера, поэтому я не берусь рассуждать о нём. Зато ещё есть вот такая интересная реализация на языке Си. Приведу начало это функции:
void *memset(void *dest, int c, size_t n)
{
unsigned char *s = dest;
size_t k;
if (!n) return dest;
s[0] = c;
s[n-1] = c;
....
}
Обратите внимание на:
s[0] = c;
s[n-1] = c;
Здесь мы возвращаемся к причине N1 «Разыменовывание нулевого указателя — это неопределённое поведение». Нет гарантии, что компилятор в целях оптимизации не поменяет присваивания местами. Если компилятор это сделает, и аргумент n будет иметь большое значение, то вначале будет испорчен какой-то байт памяти. И только потом произойдёт разыменовывание нулевого указателя.
Опять не убедительно? Хорошо, а как вам вот такая реализация:
void *memset(void *dest, int c, size_t n)
{
size_t k;
if (!n) return dest;
s[0] = s[n-1] = c;
if (n <= 2) return dest;
....
}
Вывод
Нельзя доверять даже функции memset. Да, это во многом искусственная и надуманная проблема. Я просто хотел показать, как много существует нюансов, если не проверять значение указателя. Просто невозможно всё это учесть. Поэтому не надо выпендриваться, а следует аккуратно проверять каждый указатель, который вернула функция malloc и аналогичные ей. И вот именно тогда вы станете профессионалом.
Заключение
Всегда сразу проверяйте указатель, который вернула функция malloc или аналогичная ей.
Как видите, анализатор PVS-Studio совсем не зря предупреждает о том, что нет проверки указателя после вызова malloc. Невозможно написать надёжный код, не делая проверки. Особенно это важно и актуально для разработчиков библиотек.
Надеюсь, теперь вы по-новому взглянете на функцию malloc, проверки указателей в коде и предупреждения анализатора PVS-Studio. Не забудьте показать эту статью своим коллегам и начать использовать PVS-Studio. Желаю всем поменьше багов.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Why it is important to check what the malloc function returned.
Комментарии (200)
akadone
01.02.2018 18:16Очень много воды и ни одного довода по существу. Если malloc/new вернула null, то лучше падать сразу, чем писать дикие ветвления кода и всё равно падать в конце концов, когда не смогли отследить не корректность данных, так как проверить как поведёт себя вся прога во всех случаях при new==null просто не возможно.
P.S. Для надёжности в серьёзных проектах делают свой new/malloс/..., что бы он сразу валил прогу во внешний try/catch, и весь головняк этой проверки снимается. Странно если в хроме этого нет. Точно нет (не смотрел)?dmitryredkin
01.02.2018 19:30+3Вы неправы. Так делать неправильно.
Простейший пример: из-за ошибки программиста программа просить выделить ОЧЕНЬ большой буфер, которого нет.
При правильной обработке ошибок мы просто выйдем с ошибкой из функции, показав пользователю понятное сообщение и дав ему сохранить имеющуюся работу. А возможно и продолжить работу нормально, просто какая-то часть функционала будет недоступна.
Пример простой, но если проверять КАЖДОЕ выделение памяти (и освоождать все выделенное даже при пробросе исключения), то программа станет гораздо надежнее.akadone
01.02.2018 19:59Так для этого случая как раз и нужен try/catch. Если какое-то большое действие внутри завершается неудачей, программа работает дальше, если может. При правильной архитектуре в следующем try/catch даже память выделенную внутри этого действия пытается чистить за собой. А потом и выводит пользователю какое-то сообщение если может/надо.
Весь код надо тестировать. И если раздувать его подобными ветвлениями, которые ни когда не должны исполняться — это усложняет всё очень сильно там, где ни какого смысла усложнять нет.cuwHuk
02.02.2018 18:35+11. try/catch в С нет
2. использовать malloc в C++ довольно странная затея
3. стандартный new (насколько мне известно) никогда nullptr не возвращает.Kobalt_x
02.02.2018 18:56В C есть для этого goto и setjmp/longjmp
2 Да странно, но ручки у malloc которая затронет все приложение есть (mallopt) В C++ такои случае нужно либо везде пихать свои аллокаторы либо дергать тот же mallopt надеясь что все юзают стандартные аллокаторы с new который свалится в тот же malloc. А так да malloc в C++ выглядит чужеродно
3) Возвращал но до офф версии стандарта 98 года
kogemrka
01.02.2018 22:22Очень много воды и ни одного довода по существу. Если malloc/new вернула null, то лучше падать сразу,
Так против этого автор статьи ничего и не говорит. Нужно падать сразу. Но явно, сделав проверку на Null.
во всех случаях при new==null
Здесь чистый косяк компилятора — new (без опции std::nothrow) обязан либо вернуть не null, либо выбросить исключение.russell
02.02.2018 10:12Это не косяк компилятора, просто в статье описывается именно поведение malloc. Насколько я понимаю, при выделении памяти оператором new такой проблемы не возникнет
sav6622
02.02.2018 02:06Да, прекрасное решение, особенно для встраиваемых непрерывных систем. Хотели бы Вы полетать на самолете запрограммированном таким образом? или лифту хотя бы поездить?
a-tk
02.02.2018 10:24А не надо для встраиваемых систем динамически выделять память. Только преаллокация при компиляции.
AFakeman
03.02.2018 10:00Слышал интересное утверждение (там, кажется, ссылались конкретно на правила Сименса), что у самолетов в бортовом оборудовании вся память должна быть выделена до взлета.
a-tk
03.02.2018 13:53Так и есть. То же самое относится и к космической технике. И вообще к любой встраиваемой.
alexxisr
02.02.2018 10:43поэтому для встраиваемых систем вообще malloc использовать не рекомендуется.
чего там лифту выделять динамически? можно эти 3 байта статически глобально объявить.sav6622
02.02.2018 10:46Подскажите стандарт на счет использовать не рекомендуется, мне на ум только Misra приходит. Тут возникает вопрос в другом, где взять программиста который будет делать без malloc в другом стиле мышления, когда все кругом привыкли писать уже и с автоматическим сборщиком мусора.
alexxisr
02.02.2018 11:47я мисру и имел в виду
предполагается, что во встраиваемых системах не будут поступать неизвестные объемы данных, а место под известные объемы можно выделить вручную заранее.
для самолета подход вряд ли прокатит — там скорее всего используется куча готовых библиотек, а для лифта — запросто, и программист скорее всего даже не задумываясь не станет вызывать malloc для хранения массива нажатых кнопок итп, а просто объявит его с известным размером.sav6622
02.02.2018 11:59у нас в программе при старте используется malloc для создания обьектов (потому что удобнее это делать в унифицированном виде, к примеру, массива обьектов разнообразных фильтров разной длинны), но созданные обьекты НИКОГДА до перезагрузки не уничтожаются…
khim
02.02.2018 13:25Поздравляю: вы изобрели JavaCard — великую версию Java без GC.
У них, правда, круче: GC нет, память можно только выделить, но не освободить… и никакой перезагрузки тоже нет — если хотите память освободить извольте приложение (вернее «кардлет») снести, а потом поставить заново.
Зато Java…sav6622
02.02.2018 13:55Такого редкого гуся в нашем процессоре не задействовать, ни одного компилятора стороннего нет, только проприетарный от производителя. Поэтому вертимся как можем.
LampTester
02.02.2018 14:24+2Тут возникает вопрос в другом, где взять программиста который будет делать без malloc в другом стиле мышления
Все просто — прошивку для железа должен писать разработчик этого самого железа, имеющий соответствующую квалификацию, а не программист в чистом смысле этого слова.
Именно по этой причине я, в числе многих других людей, при случае всегда разъясняю, чем отличаются игры с Ардуино от настоящей разработки устройств, которые планируется производить серийно или использовать в каких-то достаточно критичных применениях.
Попытки сэкономить, наняв для разработки железа схемотехника без навыков разработки ПО и потом для разработки прошивки программиста без знания схемотехники обычно кончаются печально.
Если устройство имеет очень серьезную программную часть (насыщенный интерфейс пользователя, содержательные вычисления и т.п.), то обычно разработчик(и) железа оборачивают взаимодействие с аппаратурой в API, учитывающий все нюансы, и уже его отдают программистам.
blanabrother
01.02.2018 18:29+3Получается причина одна, просто приведены разные сценарии ее проявления. Но в любом случае Вы правы.
Если честно, я теряюсь в догадках, какая связь между проектом Chromium и LLVM, но она есть
Наверное генерят нативный код для исполнения JavaScript виртуальной машиной.
madfly
01.02.2018 19:23Возможно я отстал от жизни и что-то изменилось (прошло уже лет 10, как я проверял это поведение, но поскольку на тот момент это была "не бага, а фича", то...), но чтобы получить NULL от malloc под Линуксом, нужно еще постараться — скорее система сама прибьёт процесс при попытке выделить память. Вроде можно повозиться с настройкой каких-то лимитов (я виндузятник, под линуксом только время от времени), но с настройками по умолчанию по исчерпанию памяти система просто прибивала процесс. Т.е. до проверки на NULL дело не доходило, хотя она и была.
OlvinKSA
01.02.2018 21:34… но чтобы получить NULL от malloc под Линуксом, нужно еще постараться — скорее система сама прибьёт процесс при попытке выделить память
Сейчас, допустим, трудно «получить NULL от malloc под Линуксом». Где гарантия, что через год будет также? В статье как раз и говорится, что это неопределённое поведение и не нужно до него доводить.madfly
01.02.2018 22:06Я про то, что можно городить хитрые проверки, загромождая и запутывая код, а это все будет совершенно без толку, потому что на клиентской машине настройки по умолчанию и проверки просто не сработают. Кстати, вспомнилось, что линукс процесс убьет даже не при выделении памяти, а только при попытке обращения к ней. Привет проверкам! :)
Проверять результат malloc стоит, когда выделяются существенные объемы памяти под какие-то нужды программы. Тогда, получив NULL (что не факт — см. линукс), есть шанс отработать ошибку. В остальных случаях может оказаться, что даже функции вывода сообщения на экран может потребоваться память для форматирования сообщения, которую так же не удастся выделить. В таких случаях сам факт исчерпания памяти — уже неопределенное поведение. А вот множество проверок на результат выделения памяти так засорят код, что врагу не пожелаешь. Так что проверки — только в ключевых местах, где происходит выделение больших регионов памяти.
Речь, конечно, не идет и микроконтроллерах и т.п. системах с жесткими ограничениями на размер памяти.
kogemrka
01.02.2018 22:32Я про то, что можно городить хитрые проверки, загромождая и запутывая код,
В статье речь про ровно одну проверку. Которую можно убрать в свою обёртку на malloc'ами.
Без этой проверки — неприятное UB, за которое цепляется глаз при чтении кода.
Кстати, вспомнилось, что линукс процесс убьет даже не при выделении памяти, а только при попытке обращения к ней. Привет проверкам! :)
Убьёт — так убьёт. Нам в общем-то от проверки это и нужно — проверить на null и умереть. Не побив по дороге данные.
Пусть и слегка гипотетический пример автора как раз о том, что мы хотим от программы умирания, а получаем побитые данные и хрен знает что.
В таких случаях сам факт исчерпания памяти — уже неопределенное поведение.
На всякий случай — словосочетание «неопределённое поведение» в статье автора и словосочетание «неопределённое поведение» в вашем комментарии обозначают разные вещи.
В частности, ваше «неопределённое поведение» не является UB. Оно вообще в рантайме происходит. В статье же — речь об UB, наличие которого влияет на то, какой код из вашего исходника выдаст компилятор.
Так что проверки — только в ключевых местах, где происходит выделение больших регионов памяти.
Premature optimization.
К слову, допустим, эта самая оптимизация вам прямо-таки нужна.
Ну то есть, давайте я возьму и поверю, что убирание одной проверочки на null в вызовах malloc'а в местах, где большие регионы памяти не выделяются, делает вам погоду. Что ж, время задуматься о применении (или даже написании) кастомного алокатора.
Речь, конечно, не идет и микроконтроллерах и т.п. системах с жесткими ограничениями на размер памяти.
Речь идёт о коде кроссплатформенных библиотек. Сегодня Вася её в код браузера впихивает под какой-нибудь десктопный линукс или винду, а завтра та же самая библиотека будет пересобрана под minix в Intel ME на ограниченном количестве памяти хрен знает как замапаном. Это нормальное использование Open Source библиотек. И это нормально такие проверочки делать.madfly
02.02.2018 11:42Под линуксом (без специальных настроек системы) malloc вернет не NULL, даже если память закончилась. И программа упадет уже при попытке обращения к этой памяти. Т.е. проверка на NULL не имеет смысла. Ну да не линуксом единым...
Под виндой ситуация лучше — malloc вернет NULL, если память выделить не удалось. Но что с этим знанием делать? Если просто молча аварийно завершить выполнение, то разыменование нулевого указателя сделает это за вас (да, иногда адресная арифметика может преподнести сюрпризы, но когда речь идет о достаточно больших регионах памяти, проверка результата malloc даже под линуксом не повредит). Если же пытаться как-то обрабатывать эту ситуацию, то обработка ошибки выделения памяти тоже может потребовать немало памяти. Это в эпоху MSDOS можно было вывести строку на экран, вызвав прерывание BIOS, до байта понимая, сколько памяти потребуется и как ее зарезервировать. В современных системах это уже гораздо сложнее. А привести к отказу выделения памяти может запрос даже одного байта.
С библиотеками ситуация немного проще — им обычно не требуется обработка ошибки. Достаточно просто установить как-то ее признак и завершить выполнение текущей библиотечной функции (тоже может оказаться нетривиально).
Что касается обертки вокруг malloc… Если бы это был выход, библиотечный malloc вызывал бы exit вместо возвращения NULL. Если обертка вокруг malloc будет всегда убивать процесс при получении любого нулевого указателя, вы лишите программу возможности обрабатывать ситуации, когда есть шанс обработать эту ошибку более корректно (выдав пользователю сообщение, что, к сожалению, его гигабайтный файл не удалось загрузить в память). Если не всегда, то обертка теряет смысл.
Таким образом, если перед вами стоят нетривиальные задачи, требующие больших объемов памяти, и очень критичные к ошибкам работы с памятью (например, база данных), нужно использовать специализированные библиотеки для работы с памятью или писать свой malloc с блэкджеком и шлюхами. В остальных случаях разумнее вставлять проверки только в ключевых местах, там, где мы наверняка можем обработать ситуацию (выделение большого объема памяти под чтение файла и т.п.).
В общем, я полностью согласен с автором, что ошибка выделения памяти — очень серьезная ошибка. Одна из самых серьезных. Но вот только корректных способов ее обработки нет, за редким исключением.
kogemrka
02.02.2018 12:02+1Если же пытаться как-то обрабатывать эту ситуацию, то обработка ошибки выделения памяти тоже может потребовать немало памяти.
. Это в эпоху MSDOS можно было вывести строку на экран, вызвав прерывание BIOS, до байта понимая, сколько памяти потребуется и как ее зарезервировать. В современных системах это уже гораздо сложнее. А привести к отказу выделения памяти может запрос даже одного байта.
Позвольте полюбопытствовать.
Вот вы видите, что я написал следующее:
Убьёт — так убьёт. Нам в общем-то от проверки это и нужно — проверить на null и умереть. Не побив по дороге данные.
Так же вы видите слова автора статьи в соседней ветке:
вызывает malloc() с тем же параметром, если вернулся NULL — вызывает abort()
Ради бога. Это как раз и есть та самая проверка, на которой я настаиваю и реакция на неё.
Зачем же вы (и другие комментаторы) продолжаете писать большие и длинные аргументы про то, что содержательную обработку такой ошибки сделать не получится, если очевидно, что абсолютно никто о ней не говорил ни в посте, ни в комментариях?
Вода — мокрая.
Содержательная обработка в такой ситуации — не нужна.
Ни с этим, ни с другим вроде бы никто не спорит, не?
Под линуксом (без специальных настроек системы) malloc вернет не NULL, даже если память закончилась. И программа упадет уже при попытке обращения к этой памяти. Т.е. проверка на NULL не имеет смысла. Ну да не линуксом единым...
Да, под линуксом при нехватки памяти malloc скорее всего не вернёт null — это истинное утверждение.
Под линуксом (без специальных настроек системы) malloc вернет не NULL
Нет, ситуации, когда malloc вернёт null под линуксом возможны.
Опять же, причём здесь линукс? Речь идёт о библиотеке вроде как.
Если не всегда, то обертка теряет смысл.
Сформулировали проблему — проверка на null раздражает вас тем, что часто приходится смотреть на аж целую строчку проверки.
Обёртка malloc_or_die решает эту проблему — не придётся.
По мне проблема — дурацкая.madfly
02.02.2018 12:16Видимо, у меня есть сомнения в том, что прибивать процесс при любом неудачном вызове malloc является хорошей стратегией.
И да, у меня, к сожалению, нет столько свободного времени, чтобы читать все комментарии к статье. Подозреваю, что многие другие комментаторы этим тоже грешат.
kogemrka
02.02.2018 12:28+2Видимо, у меня есть сомнения в том, что прибивать процесс при любом неудачном вызове malloc является хорошей стратегией.
Тогда не прибивайте.
Суть статьи: «Смотрите, вот в этом месте люди надеются, что приложение прибьётся при обращении по нулевому указателю, а на самом деле здесь UB. Вот такое бывает, да. Оно скорее всего прибьётся, но по дороге могут побочные эффекты возникнуть. А если звёзды крайне удачно сложатся, то вот это UB в какой-нибудь левой библиотеке вдруг возьмёт и выстрелит как уязвимость в вашем приложении, но это маловероятно.»
Я решительно не понимаю, с каким тезисом вы спорите и что вы пытаетесь кому-то доказать.
Вот вы сначала говорите «Ну так пусть приложение возьмёт и умрёт! Не надо обрабатывать ошибку! У вас и не получится её обработать нормально — памяти-то нет!»
Но с этим никто и не спорит. Пусть приложение возьмём и умрёт, если в этой ситуации это логично. Почему бы и нет. Посыл статьи как раз о том, что «Если вы хотите, чтобы приложение в этой ситуации умерло — возьмите его и прибейте, всё лучше, когда умирает явно, а не „здесь UB и побочные эффекты которые попортят структуры данных а после этого может быть приложение прибьётся“.
Видимо, у меня есть сомнения в том, что прибивать процесс при любом неудачном вызове malloc является хорошей стратегией.
Теперь вы говорите нечто противоположное.
Но, опять же, и с этим тоже никто не спорит — это не всегда рационально, но никто не мешает взять и написать код обработки ошибок без аллокаций, который как-то мягонько потушит приложение. Это всё берётся и прекрасно пишется, если ситуация того требуется. И в этом случае (когда вы всё-таки хотите уйти в умный обработчик).
И да, у меня, к сожалению, нет столько свободного времени, чтобы читать все комментарии к статье. Подозреваю, что многие другие комментаторы этим тоже грешат.
Стоит читать хотя бы те комментарии, на которые вы отвечаете.kogemrka
02.02.2018 12:37пардон, отвлёкся и не успел поправить комментарий в установленный срок, чтобы это считалось редактированием.
И в этом случае (когда вы всё-таки хотите уйти в умный обработчик)
Имелось в виду:
И в этом случае (когда вы всё-таки хотите уйти в умный обработчик), проверка на null вам тем более не будет лишней. Вам, более того, скорее всего даже захочется ещё дополнительно проверить и убедиться на месте, что система память сможет выделить. (При помощи memset_s'а, к примеру).
splav_asv
01.02.2018 21:46+1Сейчас да — происходит page fault, памяти для страницы физической нет -> процесс умирает. А указатель malloc чаще всего выдаёт нормальный.
Это всё при включённом overcommit. Если порог понизить или выключить совсем — можно получить NULL.
Итого мораль простая — если хочется, чтобы программа корректно без сюрпризов работала и сегодня при любых настройках и в будущем — проверка нужна. Если целевая платформа это Linux с настройками по умолчанию: в принципе не то чтобы и нужна.Andrey2008 Автор
01.02.2018 21:54Если целевая платформа это Linux с настройками по умолчанию: в принципе не то чтобы и нужна.
Хочу заметить, что такое недопустимо для библиотек. Библиотека не знает, где и как её будут использовать. Например, тот-же Chromium, использующий кучу библиотек, работает в Windows, где malloc/realloc вполне может дать нулевой указатель, если запросили много или если память приложения сильно фрагментирована.splav_asv
01.02.2018 22:06Согласен. От библиотек всегда хочется стабильности и предсказуемости в самых разных ситуациях, часть из которых даже в голову разработчиками не могла прийти на момент написания кода.
saterenko
02.02.2018 10:24Нет ни чего сложного. У меня несколько раз получалось, когда из-за ошибки я просил выделить слишком много памяти, в результате malloc возвращал NULL. Я так как я проверяю, что вернула функция, то я просто получал в логе ошибку, что не удалось выделить память…
madfly
02.02.2018 11:06Есть ситуации, когда runtime сам может понять, что запрошено слишком много памяти и нет шансов разместить данный блок в куче. Тогда до обращения с запросом памяти к системе дело и не дойдет, можно сразу вернуть NULL. А вот вы попробуйте в цикле небольшими блоками выделять память, пока она не закончится...
saterenko
02.02.2018 11:16Зачем? Я просто написал, что это сделать не так сложно… По моему опыту ошибки с выделением памяти были связаны только с ошибками в коде, мне ни когда не требовалось очень много памяти… А ошибку я вспомнил, она была простая: увеличиваем пул памяти в полтора раза когда не хватает, при этом забываем увеличить счётчик выделенной памяти и очень быстро получаем ошибку :)
4eyes
02.02.2018 20:42На 32-битной системе (любой) получилить очень просто — размер адресного пространства 3 Gb, в которые еще и спроэцирована куча библиотек и которое еще и фрагментировано. 2-2.5 Gb выделили — и
всёnullptr.
MagicGTS
01.02.2018 21:39+1Самый очевидный пример: базы данных. Неожиданное падение на транзакции, да еще с порчей неизвестных данных в разделяемой памяти. Пользователь будет рад, что в его базе появилась крутая библиотечка, которая круто парсит json или еще чего.
Andrey2008 Автор
01.02.2018 21:42Кому-то очевидно, что падать при первом поводе это плохая идея, а кому-то нет. :) Вот на реддите, например, статью и заминусовали и закритиковали.
AllexIn
02.02.2018 11:41FailFast — прекрасный паттерн. Но это не значит, что его нужно применять бездумно.
Если падение в определенный момент приводит к порче данных — от этого нужно защищаться, это очевидно и это не противоречит Fail Fast.
eao197
02.02.2018 13:58По опыту обсуждения подобной темы складывается ощущение, что в Интернетах есть две секты. Приверженцы первой свято уверены в том, что под Linux-ом malloc никогда не возвращает NULL. Приверженцы второй свято уверены в том, что если память в программе выделить не удалось, но ничего уже в принципе сделать нельзя, нужно только падать.
Переубедить их никак нельзя. Особенно когда эти две секты пересекаются. Можно только принять это как данность. Причем не суть важно, reddit это, Хабр, LOR или еще какой профильный ресурс.
KodyWiremane
02.02.2018 00:15Нарисовали бы единорога с циркуляркой на КДПВ, выбивается из фирменного стиля )
Можно не минусить, проверки на NULL я делаю
maaGames
02.02.2018 06:49Если не ошибаюсь, то обход массива от последнего к первому чуточку эффективнее (лет 10 назад пытался изучать ассемблер, декремент указателя эффективнее работает, кажется), так что проблема весьма реальна. Не мемсетом едины.)
Deosis
02.02.2018 07:45Основная проблема в скорости памяти.
Когда процессоры были маленькими, выигрыш от Loop был заметен.
Сейчас потеряется больше времени на загрузке данных.
А при прямом чтении она выполнится спекулятивно.
mkm565
02.02.2018 08:31+1Проверять результат malloc, равно как и проверять результат любой другой функции, а внутри самой функции — проверять параметры (не засунули ли в функцию нулевую ссылку, не является ли переданное число элементов массива отрицательным, имеют ли смысл значения физических величин) — это правила поведения в приличном обществе. Типа как руки после туалета мыть. Конечно, можно сказать, что мыть не надо, поскольку я иду дальше в огород копать навоз. Но это неприлично, во-первых. А во-вторых, когда-нибудь, где-нибудь — грязные руки сработают.
Также и программах — проверять, чтобы тем, кто придет после вас, не копаться в в вашем хламе. Ну и ракеты будут меньше падать из-за ошибок в программахmultiprogramm
02.02.2018 14:20О том же подумал, когда статью читал. Всё надо либо проверять, либо заранее «договариваться» о том, чего быть «не должно», потому что это уже было обязательно проверено выше.
nckma
02.02.2018 15:26-1С другой стороны представьте себе объект, который выделяет память. Тут можно проверить результат — а выделилась ли память на самом деле. Действительный указатель на выделенную память запоминает в переменной, члене класса объекта. Далее объект вызывает другие функции других классов и передает им параметром этот указатель.
Нужно ли во всех функциях принимающих параметр указатель проверять его на ноль?
Не потеряется ли эффективность программы если по сто раз проверять, то что один раз должно быть проверенно?
Или вот, например:
void draw_pixel(char* framebuffer, int x, int y)
{
//SHOULD I DO THAT?
if(framebuffer==nullptr) return;
//draw pixel actually
..................
}
void draw_line(char* framebuffer, int x1, int y1, int x2, int y2)
{
//SHOULD I DO THAT?
if(framebuffer==nullptr) return;
for( ..N.. ) {
draw_pixel(framebuffer,x,y);
}
}
main()
{
char* screen = new char[width*height];
//actual check for null
if(screen==nullptr) { cout << OOPS; return -1; }
draw_line(screen, 100,50,500,800);
}
kogemrka
02.02.2018 15:35+1char* screen = new char[width*height];
//actual check for null
if(screen==nullptr) { cout << OOPS; return -1; }
new не вернёт null.
А оптимизатор. к слову, все проверки на null в этом примере, вырежет (хотя закладываться на это не стоит и писать их в данной ситуации как минимум странно).nckma
02.02.2018 15:37Ну окей, ошибся, быстро писал.
Например, там malloc.
Вопрос по существу — нужно ли проверять везде на nullptr или нет.kogemrka
02.02.2018 15:44Например, там malloc.
Вопрос по существу — нужно ли проверять везде на nullptr или нет.
Теперь представьте, что в проверках этих внутри методов не «return;», а какая-то содержательная фигня. В логи написать «ФРЕЙМБУФЕР УПАЛ», например. Байтик переслать на отладочный девайс. Ещё что-то.
А ещё представьте, что «if(screen==nullptr) { cout << OOPS; return -1; }» в main'е вы не написали.
И перед вызовом draw_line умудрились разыменовывать screen и не упасть.
Отправится ли теперь заветный байтик? Найдём ли мы теперь «ФРЕЙМБУФЕР УПАЛ» в логе? Теперь это загадка.
Вопрос по существу — нужно ли проверять везде на nullptr или нет
Если нужно — проверяйте, не нужно — не проверяйте. Вопрос проектирования API и оптимизации бутылочных горлышек в вашем коде.
Может быть эти проверки погоды вам не делают.
А может быть половину этих методов надо заменить на ассемблерные вставки с огромным комментарием «НА ВХОД ОЖИДАЕТСЯ ВОТ ЭТО И ВОТ ТО — КОД НЕ ТРОГАТЬ!1111».
Если эти методы — не нечто торчащее наружу — зачем вам вообще чистый указатель, собственно, принимать?
Если эти методы — нечто торчащее наружу, очевидно, что стоит написать в документации к API, чего вы ждёте и действовать соответственно.nckma
02.02.2018 15:48В принципе, это я и ожидал прочитать в ответ.
Иными словами, не существует обязательного правила всегда проверять указатель на ноль. Иногда такая проверка не лишнее, а иногда очень даже и вредная.
Все по обстоятельствам.kogemrka
02.02.2018 16:05не существует обязательного правила
Если очень хочется, можно даже на ноль делить. Только т-ссс, никому!
mkm565
03.02.2018 00:39Закон Мерфи. Если программа может вылететь из-за того, что в какую-то функцию на 115м уровне вложенности, какой-то Вася Пупкин передал нулевую ссылку, то она обязательно вылетит.
Следствие: Вылетит она в тот момент, когда программа будет управлять взлетом ракеты с комсмодрома Восточный
Следствие: Если суммарная экономия на том, что не надо выполнять проверку на NULL, составляет 3 секунды процессорного времени за 10 лет непрерывной работы программы, то стоимость поиска ошибки будет астрономической
Следствие: Если программа полагается на системные средства Линух (например), чтобы генерировать сообщения об ошибке, то код этой программы обязательно будет перенесен в Виндоуз/Мак/Ардуино, где эта фича работает по другому и программа навернется
Вывод: Лучше ВСЕГДА перебздеть, чем недобздеть.
nckma
03.02.2018 02:47Странно, что когда я привожу конкретный пример про рисование линии с помощью функции рисования пикселей мне приводят контр доводы про полет спутника.
Несопоставимые вещи.
В конкретно моем примере проверка на ноль в функции рисования пикселя может значительно сказаться на быстродействии программы, так как эта проверка будет вызываться миллионы раз в графической программе, а может быть вызвана всего один раз при инициализации графической системы.
Гораздо разумнее задокументировать АПИ и описать требования к параметрам, чем проверять «везде» и «всегда».
Давайте я другой пример приведу.
Представьте, что вы пишите программу майнинга криптовалюты и ваш код на OpenCL будет работать на тысяче процессоров в видеокарте. Вы передаете в процедуру параметры и указатели на блоки памяти. Будете всегда проверять указатели на ноль? Или все же не станете этого делать? Ведь от быстродействия кода зависит прибыль майнинга…mayorovp
03.02.2018 09:29+1Вот именно поэтому и надо проверять в том месте где память выделяется, а не там где она используется.
Disasm
02.02.2018 08:40Вы как-то мелко придираетесь. Давайте уж сразу проверять в каждой функции, хватит ли ей оставшегося места на стеке (с учётом всех дочерних вызовов). А иначе программа аварийно завершится (и вы даже обработать это не факт, что сможете) и очень важные данные, которые в этот момент сохранял CAD, потеряются навсегда. Полнейшая безответственность!
Уже трижды сталкивался с нехваткой места в стеке (а его обычно аж целый 1Мб). Один раз даже такая программа привела к глюкам ядра: система, вроде бы, и продолжала работать, но любой запрос списка процессов приводил к зависанию запрашивающей программы с невозможностью даже её убить.
eurol
02.02.2018 09:42Лично меня больше задела мысль о том, что программа может обратиться к чужой памяти и испортить ее. Когда-то давно, когда я прочитал про 386 процессор и его защищенный режим, я понял, что нормально написанная ОС не дает процессу возможности записи туда, куда не положено.
Здесь же утверждается, что ОС с ядром Линукс защищают от порчи данных только первые 64Кб некоторого адресного пространства. Но если это так, не значит ли это, что в этих ОС большие проблемы с безопасностью?
И если это — общеизвестный факт, то почему с этим ничего не делают разработчики ядра?
Подскажите, пожалуйста, те, кто разбираются, неужели это правда?
Становится страшно жить.Andrey2008 Автор
02.02.2018 09:42Да, данные сторонних приложений в современных ОС так просто не испортить. Однако, программе достаточно испортить собственные данные, чтобы возникли непредсказуемые эффекты. С начала испортили, потом перехватили исключение/сигнал и продолжили работать. Начали, например, аварийно данные сохранять. А ведь где-то что-то испорчено.
Или испортили что-то в данных собственной параллельной нити. И пока программа из-за нехватки памяти готовилась к завершению работы (в лог про ошибку писала), эта параллельная нить с неверными данными способна ужас разный наделать.a-tk
02.02.2018 10:26А ещё собственные данные могут обрабатываться для кого-то. Пример — те же базы данных (выше в комментариях было).
eurol
02.02.2018 11:53Камень с души упал. Значит, я просто неверно понял суть проблемы.
А почему бы не убрать в ОС эту проверку на обращение к нулевому адресу, чтобы дисциплинировать программистов? :)
grmood
02.02.2018 14:19Наверняка этим же методом можно что-то еще подпортить в шаренной между процессами памяти, например.
linuxover
02.02.2018 10:41Не думайте, что вы знаете, как будет вести себя программа, если произойдёт разыменовывание nullptr. Современные компиляторы занимаются серьезными оптимизациями, в результате чего бывает невозможно предсказать, как проявит себя та или иная ошибка в коде.
а поподробнее? код же не морфирует от значения указателя, чем тут null особенный?
в чем разница
1234->member
иnull->member
?
В общем, произойдёт порча каких-то данных.
порча да, но только данных этого приложения же
Andrey2008 Автор
02.02.2018 11:00в чем разница: 1234->member и null->member?
Прошу уточнить вопрос.
порча да, но только данных этого приложения же
Этого мало? Уточню: надо думать не в контексте порчи памяти в игре тетрис :).a-tk
02.02.2018 11:05Было бы интересно в тетрисе найти уязвимость, которая при определённой конфигурации фигурок приводила бы к увеличению счёта до достаточно большой величины :)
linuxover
02.02.2018 11:08+1Этого мало? Уточню: надо думать не в контексте порчи памяти в игре тетрис :).
этого мало, поскольку вредит только себе и не отменяет возникающего ексепшена.
навредить другому приложению нет возможности.
я полностью согласен с тезисом: если Вы пишете библиотеку, то проверка обязательна. Библиотека может быть встроена в разные приложения у одного (большинства) невыделение памяти фатально, у другого (очень редкие приложения) невыделение памяти позволит продолжить работу.
Но мы говорим об общем подходе.
Я говорю:
- большинство приложений при неуспехе выделения памяти не предусматривают нормальную дальнейшую работу
- множество языков, например C не поддерживают исключения
поэтому подход "оно грохнется на системном исключении" вполне работоспособен
PS: вот Вы выделили память, а ее нет. Что может сделать приложение? Освободить какую-то память и повторить попытку выделения. Других вариантов у него нет.
Что это значит? это значит что приложение содержит (по сути!) свой менеджер памяти и внутри себя для выделения использует не[cm]alloc
, а надстройку.
таких по пальцам перечислить: базы данных итпAndrey2008 Автор
02.02.2018 11:18В целом согласен, но хочу отметить:
навредить другому приложению нет возможности.
Достаточно навредить себе! Особенно если это большое, сложное параллельное приложение, где malloc вернул 0 в одном из потоков и поток был прибит, а приложение продолжит работать. Вот только, где-то что-то могло быть испорчено.linuxover
02.02.2018 11:21-1malloc вернул 0 в одном из потоков и поток был прибит
опять не могу представить такой ситуации, извините.
это о какой операционной системе речь в таком случае?
я смотрел множество core-dump'ов и в т.ч многопоточных приложений. Ни разу не встречал ситуации чтобы был прибит именно поток, прибивается именно процесс обычно. Возможно можно сделать приложение чтобы прибивался именно поток, но для этого какие-то нетривиальные спецусилия требуются от разработчика.
VolCh
02.02.2018 18:50+1вот Вы выделили память, а ее нет. Что может сделать приложение? Освободить какую-то память и повторить попытку выделения. Других вариантов у него нет.
Есть. Вывести сообщение пользователю или в лог. Подождать. Отменить текущую команду. Убить текущую нить. Умереть. Комбианции всего этого в разных варинатах.
khim
02.02.2018 19:03+2PS: вот Вы выделили память, а ее нет. Что может сделать приложение? Освободить какую-то память и повторить попытку выделения. Других вариантов у него нет.
Почему нет? Есть. Самый простой вариант: выделить память из резервного буффера. Так работал Turbo Pascal 6.0 четверть века назад.
Кажется бредом — мы просто исчерпаем и резервный буфер тоже… но на самом деле — это способ делать простые и надёжные приложения. Ибо переключение на резервный буффер просто заметить, а значит — заставить работать программу по другому.
Если нас просят открыть какое-нибудь окно, то проверяем — не перешли ли мы на использование резервного буффера и если да — сообщаем об ошике. Создаём окно (так как у нас есть резервный буффер, то проблем с выделением памяти не возникнет), после чего ещё раз проверяем — не начали ли мы использовать «резерв». Если начали — закрываем окно, сообщаем об ошибке.
Просто, надёжно — и никаких проблем с «exception safety» и прочими разными NPE. Очень жаль, что за прошедшие с тех пор годы индустрия почти разучилась писать надёжные программы. «И так сойдёт» — девиз современного IT. Вот это вот — всего лишь вишенка на тортике…
linuxover
02.02.2018 11:18Прошу уточнить вопрос.
1234 — случайный неправильный адрес
null — просто неправильный адрес
операционные системы дааавно уже не делают "спецобласть ексепшенов около нулевого адреса", а проверяют "принадлежит ли адрес X процессу или нет". При этом X==0 или X==1234 проверяются одинаково.
соответственно:
- все рассуждения "N достаточно большой и поэтому проверку пройдем" — становятся некорректными
- если проверка пройдена, то меняются только данные процесса и вредит только сам себе
я об этом
linuxover
02.02.2018 10:53-1Если значение переменной filter->data_count достаточно большое, то значения будут записаны по какому-то непонятному адресу.
ну не так же это все. ну по крайней мере не так фатально как представляется в статье.
вот исследуем операционную систему. Произвольное приложение:
cat /proc/self/maps 565295609000-565295611000 r-xp 00000000 fe:00 1437072 /bin/cat 565295810000-565295811000 r--p 00007000 fe:00 1437072 /bin/cat 565295811000-565295812000 rw-p 00008000 fe:00 1437072 /bin/cat 56529758a000-5652975ab000 rw-p 00000000 00:00 0 [heap] 7f2614e51000-7f2614fe6000 r-xp 00000000 fe:00 783458 /lib/x86_64-linux-gnu/libc-2.24.so 7f2614fe6000-7f26151e6000 ---p 00195000 fe:00 783458 /lib/x86_64-linux-gnu/libc-2.24.so 7f26151e6000-7f26151ea000 r--p 00195000 fe:00 783458 /lib/x86_64-linux-gnu/libc-2.24.so 7f26151ea000-7f26151ec000 rw-p 00199000 fe:00 783458 /lib/x86_64-linux-gnu/libc-2.24.so 7f26151ec000-7f26151f0000 rw-p 00000000 00:00 0 7f26151f0000-7f2615213000 r-xp 00000000 fe:00 783441 /lib/x86_64-linux-gnu/ld-2.24.so 7f2615244000-7f2615266000 rw-p 00000000 00:00 0 7f2615266000-7f26152b7000 r--p 00000000 fe:00 653107 /usr/lib/locale/aa_DJ.utf8/LC_CTYPE 7f26152b7000-7f26153e7000 r--p 00000000 fe:00 653503 /usr/lib/locale/be_BY.utf8/LC_COLLATE 7f26153e7000-7f26153e9000 rw-p 00000000 00:00 0 7f26153ff000-7f2615400000 r--p 00000000 fe:00 4407 /usr/lib/locale/ce_RU/LC_NUMERIC 7f2615400000-7f2615401000 r--p 00000000 fe:00 9839 /usr/lib/locale/ru_RU.utf8/LC_TIME 7f2615401000-7f2615402000 r--p 00000000 fe:00 4406 /usr/lib/locale/ce_RU/LC_MONETARY 7f2615402000-7f2615403000 r--p 00000000 fe:00 9838 /usr/lib/locale/ru_RU.utf8/LC_MESSAGES/SYS_LC_MESSAGES 7f2615403000-7f2615404000 r--p 00000000 fe:00 653114 /usr/lib/locale/aa_DJ.utf8/LC_PAPER 7f2615404000-7f2615405000 r--p 00000000 fe:00 1700745 /usr/lib/locale/ak_GH/LC_NAME 7f2615405000-7f2615406000 r--p 00000000 fe:00 9836 /usr/lib/locale/ru_RU.utf8/LC_ADDRESS 7f2615406000-7f2615407000 r--p 00000000 fe:00 4408 /usr/lib/locale/ce_RU/LC_TELEPHONE 7f2615407000-7f2615408000 r--p 00000000 fe:00 653109 /usr/lib/locale/aa_DJ.utf8/LC_MEASUREMENT 7f2615408000-7f261540f000 r--s 00000000 fe:00 1335646 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache 7f261540f000-7f2615410000 r--p 00000000 fe:00 9837 /usr/lib/locale/ru_RU.utf8/LC_IDENTIFICATION 7f2615410000-7f2615413000 rw-p 00000000 00:00 0 7f2615413000-7f2615414000 r--p 00023000 fe:00 783441 /lib/x86_64-linux-gnu/ld-2.24.so 7f2615414000-7f2615415000 rw-p 00024000 fe:00 783441 /lib/x86_64-linux-gnu/ld-2.24.so 7f2615415000-7f2615416000 rw-p 00000000 00:00 0 7ffccf27a000-7ffccf29b000 rw-p 00000000 00:00 0 [stack] 7ffccf3d5000-7ffccf3d7000 r--p 00000000 00:00 0 [vvar] 7ffccf3d7000-7ffccf3d9000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
вот перечень всех областей памяти куда оно может обратиться без ексепшена на чтение ну и в зависимости от аттрибута — на запись. Обратите внимание, что область с адресами около нуля в этом списке и не присутствует как "особая".
то есть:
- когда Вы получаете segfault от nullptr, то его генерит отнюдь не "спецобласть около NULL"
- Вы никогда не сможете затереть какую-то память, Вашему процессу не принадлежащую (то есть навредить кому-то кроме себя)
Я вновь задаю вопрос: «где гарантии, что будет разыменовывание именно нулевого указателя?». Нет такой гарантий.
а не требуются эти гарантии: смотри дамп маппингов произвольного приложения что я привел
и заодно: когда Вы пишете о "N достаточно большой" посмотрите на первый адрес в этом дампе
onyxmaster
02.02.2018 11:39+1Это спор деонтологиста с консеквентиалистом, просто разные точки зрения.
Я (как представитель первого типа) бы не хотел пользоваться, БД, которая использует mmap или ведёт журнал в памяти и который, при отсутствии возможности выделить память, «портится». По поводу адресов и overcommit — это может быть другая платформа (в т.ч. embedded), другая ОС, что угодно может отличаться от того, что «обычно бывает».
Самое отвратительное, что это может ухудшить обработку ошибок, допустим, приведя к записи на диск из recovery path повреждённых данных, причём выявить такое «обычным» тестированием может быть весьма проблематично.
Отдельно отмечу, что я знаком с мнением, что обработка ошибок вторична, а первична основная функциональность, но не согласен с ним, в любой системе с требованиями к надёжности, отличными от «всё равно что получится», политика обработки ошибок и восстановления после них как минимум не менее важна чем эта основная функциональность (как пример приведу ту же самую БД, я не думаю что было бы много желающих пользоваться БД, которая почти всегда работает нормально, но если на ФС заканчивается место или возникает ошибка записи на диск, то все данные повреждаются). Дам мою любимую ссылку: www.usenix.org/system/files/conference/osdi14/osdi14-paper-yuan.pdflinuxover
02.02.2018 12:25Это спор деонтологиста с консеквентиалистом
пойду гуглить что эти страшные слова значат :)
linuxover
02.02.2018 12:29Это спор деонтологиста с консеквентиалистом, просто разные точки зрения.
Я (как представитель первого типа) бы не хотел пользоваться, БД, которая использует mmapмне кажется это спор перфекциониста с человеком который стоит на позиции разумной достаточности.
перфекционист говорит: "надо всегда выполнять проверки/реакцию"
а человек стоящий на позиции разумной достаточности говорит: "в большинстве случаев сигнала достаточно".onyxmaster
02.02.2018 12:43$ cat /proc/self/maps 00400000-0040c000 r-xp 00000000 08:01 3932328 /bin/cat 0060b000-0060c000 r--p 0000b000 08:01 3932328 /bin/cat 0060c000-0060d000 rw-p 0000c000 08:01 3932328 /bin/cat 01bb9000-01bda000 rw-p 00000000 00:00 0 [heap] ... $ uname -a Linux 4.10.0-42-generic #46~16.04.1-Ubuntu SMP Mon Dec 4 15:57:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
Уже не так невозможно выглядит, правда?linuxover
02.02.2018 13:15> Уже не так невозможно выглядит, правда?
в соседнем посте я точ такой cat показывал.
этот cat показывает как раз то, что операционная система не проверяет какую-то «загадочную область около null и при больших N можно эту проверку пройти», операционная система выполняет проверку «принадлежит ли адрес X процессу?»
в этом смысле 1234 и 0 полностью эквивалентны. Далее, даже если довериться автору статьи, то 00400000 — это 4 мегабайта в Вашем случае и 85 терабайт в моем.
если обсуждать даже Ваш случай, то вероятность попасть на данные все равно очень небольшая: очень много обстоятельств
1. и выделение больше 4 мб
2. и заполнение с конца
3. и попасть надо в интервал 4мб — 4.04мб
в общем и проблема выглядит надуманной и способ ее решения избыточным.
я (ну и оппоненты автора) говорю, что надо стоять на позиции «разумной достаточности». Проверки надо делать не «всегда», а «когда это действительно необходимо».
kogemrka
02.02.2018 12:51перфекционист говорит: «надо всегда выполнять проверки/реакцию»
Перфекционист в этой ситуации скорее хочет, чтобы код соответствовал, во-первых, его пониманию того, как он будет скомпилирован (и как будет работать), во вторых — пониманию вот тех десяти джунов, которых наймут завтра, и которые этот код чёрт пойми куда будут интегрировать.
И ситуации с UB для перфекциониста неприятны.
Вот перфекционист хочет, чтобы приложение просто взяло и упало, если malloc вернул null.
Можно написать явно «если null — возьми и упади».
А можно положиться на знания того, что вот оно в этой конкретной ситуации упадёт сразу, вот в той конкретной ситуации, которая возникает в сто раз реже, упадёт предварительно непредсказуемым образом попортив свои же данные, но для того множества приложений в которые мой код попадёт это не критично, всё равно ведь упадёт, вот в этой ситуации, которая происходит один раз на миллион, создаст вполне себе уязвимость, но такая ситуация не случится (наверное. Правда ведь не случится?), а послезавтра вообще появится необходимость эту библиотеку собрать под телевизор или IoT устройство и появится ещё две-три новых ситуаций.
И вот как бы на кой чёрт этому самому перфекционисту держать в своей голове все эти инварианты и объяснения, почему оно всё-равно-упадёт-как-надо, если можно взять и написать явно. Тем более, цена такого решения — нулевая.linuxover
02.02.2018 13:26Тем более, цена такого решения — нулевая
центральный вопрос тут как раз в том что цена не нулевая.
то есть в hello world она нулевая, а в реальных приложениях вполне будет ненулевой.
потому что помимо вопроса с malloc будет вопрос с open, перечнем errno, итп итд.
можно писать приложение выполняющее строго работу, а если что пойдет не так, пусть валится/разбираются, компилируют с включенной отладкой итп.
а можно писать приложение, выполняющее все проверки-перфекции, но которое возможно будет невостребовано из за сроков написания или из за того что "очень много потратили на эти проверки, а главный алгоритм и не додумали".
просто если исходить из перфекций, то например вот такой код иногда сегфолтит:
int foo(...) { char buffer[BUFFER_SIZE]; // тут вычисления использующие буфер return result; }
в коде нет ни выходов за пределы буфера, ни других "видимых" проблем, а код сегфолтит в зависимости от стратегии заполнения (от начала к концу или от конца к началу) этого самого buffer и его размера.
перфекционист скажет "возьмите за правило всегда использовать new/malloc в таком коде". Однако разумно будет просто понимать что такая проблема есть и использовать new/malloc только там где она может доставить неприятности. как-то так.
kogemrka
02.02.2018 13:45то есть в hello world она нулевая, а в реальных приложениях вполне будет ненулевой.
потому что помимо вопроса с malloc будет вопрос с open, перечнем errno, итп итд.
Ага. Или с fork.
и использовать new/malloc только там где она может доставить неприятности. как-то так.
Почему вы таки смешиваете new и malloc? Конкретная проблема, которая обсуждается в этой статье, касается только с malloc (ну или nothrow-вариации оператора new разве что).linuxover
02.02.2018 13:50Почему вы таки смешиваете new и malloc?
для той проблемы что я описал в посте на который Вы отвечаете new/malloc является одинаковым решением.
Конкретная проблема, которая обсуждается в этой статье, касается только с malloc
а я говорю что это — перфекция и привел пример другой перфекции: Выделяется буффер на стеке функции и используется.
в зависимости от
- размера буфера
- способа его заполнения (от начала к концу или наоборот)
- глубины стеков вызова функции
могут возникать сегфолты.
я говорю: перфекционист скажет "всегда используйте new/malloc в таком кейзе"
а я (и люди стоящие на позициях разумной достаточности средств) скажут "достаточно знать о проблеме и применять превентивные меры только там где они необходимы"kogemrka
02.02.2018 14:33+1скажут «достаточно знать о проблеме и применять превентивные меры только там где они необходимы»
В случае с сегфолтами — возможные варианты возникают в рантайме.
В случае с UB, о котором статья — возможные варианты и неопределённость возникают на стадии компиляции.
«достаточно знать о проблеме и применять превентивные меры только там где они необходимы»
Ну, да. Так в общем-то и предлагается делать.
Есть проблема (которую описал автор статьи). Есть превентивная мера, которую предлагает автор статьи — писать без UB.
Вот в рантайме сплошная неопределённость, сами приводите пример с ней, зачем нам дополнительная неопределённость на этапе компиляции — чтобы ещё больше усложнить отладку?linuxover
02.02.2018 14:35В случае с UB, о котором статья — возможные варианты и неопределённость возникают на стадии компиляции.
вот тут поподробнее.
какие проблемы на стадии компиляции от не проверки результата malloc?kogemrka
02.02.2018 15:00какие проблемы на стадии компиляции от не проверки результата malloc?
Ну, гипотетический пример:
Выделили буфер в 100мб, malloc вернул null, не проверили сразу -> начали разыменовывать (например, положили в конец буфера что-то и попали на свою память).
Передали указатель дальше.
Дальше передали указатель туда-сюда и отправили и имеем код вида
if (p == null)
{
какая-то умная обработка ошибки
}
else
{
начинаем заполнять p целиком
}.
Оп-оп, оптимизатор убивает целиком первую ветку и проверку условия (самое смешное, если это происходит не сразу, а, допустим, при смене версии компилятора или при смены какого-то дополнительного условия, из-за которого раньше эта ветка не была избыточной).
Вы, конечно, молодец, и так не напишите, потому что знаете, что оптимизатор может так поступить, и убедитесь перед компиляцией, что проверки на null произойдут позже разыменовывания указателя, но завтра ваш код будет редактировать джуниор, который не знает, что такое UB.
И отлаживая джун тоже будет мучаться — ну как же, ведь если бы в этом месте указатель был равен нулю, сработала бы его обработка!
Ну как же, вот в дампе структура данных повреждённая (мы же раньше записали что-то) — наверное где-то там ошибка!
Ну вы то умный, через пару дней мучения джуна покажете ему что такое UB и как оптимизатор работает, но зачем оно вам нужно, если с самого начала можно было писать не допуская UB бесплатно и без смс?linuxover
02.02.2018 15:05по моему что-то очень надуманное.
kogemrka
02.02.2018 15:31по моему что-то очень надуманное.
Надуманный сценарий, при котором данные инициализируются в таком порядке или надуманное отрезание ветки с проверкой на null?linuxover
02.02.2018 15:37Надуманный сценарий
ага: что первично: курица или яйцо?
что первично в Вашей программе? данные которые при падении могут попориться или функция которую она выполняет?
если это калькулятор в котором пользователь вводит циферки или даже браузер и он валится в условиях недостатка памяти (как и половина приложений системы), то пользователя как правило это устроит: ему важно что этот калькулятор/браузер бесплатны. Ну а с памятью он разберется.
если это БД которая хранит Ваши Важные Данные, то тут несомненно, нужны все проверки и еще хорошо бы набор автоматических тестов итп итд.
я говорю: Ваш сценарий появляется смысл обсуждать в одном случае и нет смысла о нем говорить в другом.
a-tk
02.02.2018 16:13При всём при этом крайне желательно, чтобы браузер хотя бы сохранил список открытых закладок и настройки, если те были изменены.
linuxover
02.02.2018 16:18вот идем на любой игровой форум:
пользователь пишет "игра вылетает!"
ему пишут "нужно больше памяти"
ну и ок, добавляет и играет.
пожелания "хорошо бы оно шло на спектрумах" так и остается пожеланием. Его теоретически можно выполнить, но как правило за такую цену, что сам пользователь скажет "да я лучше памяти добавлю!"a-tk
03.02.2018 00:36Для таких вещей есть понятие «Системные требования». Но когда в разделе «Системные требования» будет стоять строка «Объём оперативной памяти: 128 Петабайт» (и в скобках — залобались выделять больше и убили процесс), то это немножко странно, не находите?
AllexIn
02.02.2018 11:43Программа может обработать это структурное исключение/сигнал. Но уже поздно. Где-то в памяти есть испорченные данные. Причем непонятно, какие данные испорчены и к каким последствиям это может привести!
А не всё равно какие данные мы сломали? Данные в адресном пространстве нашего приложения. Наше приложение упало. Все данные потеряны и не важно — плохие там данные были или нет.
Поэтому в этой ситуации и надеятся на FailFast, прикручивая проверки только там, где это действительно имеет значение и нельзя просто упасть.
Ну и конечно, нельзя игнорировать потенциальный нулевой указатель, если у вас на уровень выше сидит тупой обработчик исключений, который молча погасит исключение возникшее в результате.madfly
02.02.2018 11:49Главное, не успеть записать эти испорченные данные в базу/файл. Но там, где это действительно важно, можно использовать журналирование.
AllexIn
02.02.2018 11:53Есть много случаев, когда UB связанное с нулевым указателем может привести к беде. И эти случаи надо знать. Но это сводится к «понимай как работает твой код, если случится nullptr», а не к «везде вставляй проверки».
Вообще вся статья относитсяисключительно к С. В С++ просто не надо использовать malloc и всё.
khdavid
02.02.2018 12:15Спасибо за статью. Правильно я понимаю, что с оператором new совсем другая история? При ошибке, он кидает исключение. Поэтому можно спокойно разыменовывать указатель без проверки, и нету никакого неопределенного поведения. Я прав?
Andrey2008 Автор
02.02.2018 12:16+1Да.
P.S. Есть std::nothrow вариант оператора new, но это другая история.
Strawb
02.02.2018 13:23Ни в одном комментарии про Linux OOM killer.
Автор, если сможешь получить в Linux от malloc NULL, напиши пожалуйста статью, с интересом прочитаю. С этого наверное и следовало начать.linuxover
02.02.2018 13:30Вместе с Вашим я насчитал штуки три отсылки к нему :)
Strawb
02.02.2018 13:36ок. я просто поиском по странице пытался
тем не менее. буду признателен за рабочий пример получения NULL от malloclinuxover
02.02.2018 13:42проблемы статьи не только в NULL а и в том что оперирует какими-то сильно устаревшими понятиями вроде "защитная область около NULL". Когда-то в 90-е годы я о таком читал, но чтобы сейчас такое специально где-то было — я даже не могу представить где.
- есть понятие "адрес X принадлежит процессу" где разницы между X==0 и X==1234 нет никакой.
- и есть понятие "некоторые библиотечные функции при ошибке возвращают NULL"
но смешения этих понятий и действий в ОС специально по второму кейзу давно нет.
onyxmaster
02.02.2018 13:45Слегка модифицированный вариант voices.canonical.com/jussi.pakkanen/2013/05/24/malloc-and-linux:
#include<stdio.h> #include<malloc.h> int main(int argc, char **argv) { long size=1; while(1) { char *x = malloc(size*1024); printf("Tried to alloc: %ldk.\n", size); if(!x) { printf("Malloc returned null.\n"); return 0; } *x = 0; free(x); size *= 2; } return 1; }
$ gcc q.c ; ./a.out Tried to alloc: 1k. Tried to alloc: 2k. Tried to alloc: 4k. Tried to alloc: 8k. Tried to alloc: 16k. Tried to alloc: 32k. Tried to alloc: 64k. Tried to alloc: 128k. Tried to alloc: 256k. Tried to alloc: 512k. Tried to alloc: 1024k. Tried to alloc: 2048k. Tried to alloc: 4096k. Tried to alloc: 8192k. Tried to alloc: 16384k. Tried to alloc: 32768k. Tried to alloc: 65536k. Tried to alloc: 131072k. Tried to alloc: 262144k. Tried to alloc: 524288k. Tried to alloc: 1048576k. Tried to alloc: 2097152k. Tried to alloc: 4194304k. Tried to alloc: 8388608k. Tried to alloc: 16777216k. Tried to alloc: 33554432k. Tried to alloc: 67108864k. Malloc returned null. $ uname -a Linux 4.10.0-42-generic #46~16.04.1-Ubuntu SMP Mon Dec 4 15:57:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux $ free total used free shared buff/cache available Mem: 82479492 39326248 1789876 854504 41363368 41316992 Swap: 16618364 1884660 14733704
Strawb
02.02.2018 13:5864г одним куском. еще варианты? ради этого писать не демона (к примеру), а звездолет?
kogemrka
02.02.2018 14:10+2Много кусков по 100 мегабайт тоже прокатывают.
ради этого писать не демона (к примеру), а звездолет?
Один if превращает программу в звездолёт?
onyxmaster
02.02.2018 16:44+4Вы написали «буду признателен за рабочий пример получения NULL от malloc»? Вот вам рабочий пример, разве нет?
Вы не допускаете ошибки в расчёте требуемого объёма памяти, которая может привести к тому, что допустим обычно пытаются выделить 40МБ, но если выбран флаг, который «никто не выбирает», то пытаются выделить 64ГБ? И из-за этого, при попытке записи по смещению 15МБ в выбранный массив будет перезаписываться heap например. Это _абсолютно_ нереалистичный сценарий? А если смещение не 15МБ?
Вероятно инженеры, проектировавшие процессоры тоже думали что временнАя атака на модуль предсказания перехода или спекулятивного исполнения, что это нереалистичный сценарий =)
Последствия такого мышления в виде ~20% увеличения времени исполнения при той же нагрузке я вот как раз наблюдаю у себя в продакшне после патчей против Meltdown =)
creker
02.02.2018 13:45Что может быть лучше, чем код, который надеется не только на ОС, но еще на какой-то специфический ее компонент. А если в ядре ваш код, а если в UEFI? Во встраиваемой системе? Для библиотеки общего назначения это раз плюнуть.
a-tk
02.02.2018 14:06+1Лехко.
Скрытый текстAllocation 0: 0xb34ae008
Allocation 1: 0xaf4ad008
Allocation 2: 0xab4ac008
Allocation 3: 0xa74ab008
Allocation 4: 0xa34aa008
Allocation 5: 0x9f4a9008
Allocation 6: 0x9b4a8008
Allocation 7: 0x974a7008
Allocation 8: 0x934a6008
Allocation 9: 0x8f4a5008
Allocation 10: 0x8b4a4008
Allocation 11: 0x874a3008
Allocation 12: 0x834a2008
Allocation 13: 0x7f4a1008
Allocation 14: 0x7b4a0008
Allocation 15: 0x7749f008
Allocation 16: 0x7349e008
Allocation 17: 0x6f49d008
Allocation 18: 0x6b49c008
Allocation 19: 0x6749b008
Allocation 20: 0x6349a008
Allocation 21: 0x5f499008
Allocation 22: 0x5b498008
Allocation 23: 0x57497008
Allocation 24: 0x53496008
Allocation 25: 0x4f495008
Allocation 26: 0x4b494008
Allocation 27: 0x47493008
Allocation 28: 0x43492008
Allocation 29: 0x3f491008
Allocation 30: 0x3b490008
Allocation 31: 0x3748f008
Allocation 32: 0x3348e008
Allocation 33: 0x2f48d008
Allocation 34: 0x2b48c008
Allocation 35: 0x2748b008
Allocation 36: 0x2348a008
Allocation 37: 0x1f489008
Allocation 38: 0x1b488008
Allocation 39: 0x17487008
Allocation 40: 0x13486008
Allocation 41: 0xf485008
Allocation 42: 0xb484008
Allocation 43: 0x4047008
Allocation 44: 0x46008
Allocation 45: 0xb77ed008
Allocation 46: 0xbb7ee008
Allocation 47: 0
Cleanupa-tk
02.02.2018 14:14Кстати:
Linux ***** 3.16.0-4-586 #1 Debian 3.16.43-2+deb8u2 (2017-06-26) i686 GNU/Linux
Linux ***** 3.4.75-sun7i #35 SMP PREEMPT Sat Feb 8 02:10:31 CST 2014 armv7l GNU/Linux
creker
02.02.2018 13:43Отказ в обслуживании это еще ерунда. Отсутствие проверки malloc на NULL вполне и к RCE может привести.
beeruser
02.02.2018 13:54The Linux kernel's memset for the SuperH architecture has this property: link.
К сожалению, это код на незнакомой мне разновидности ассемблера, поэтому я не берусь рассуждать о нём.
Там всё просто. Память заливается вот таким циклом.
dt r0 ;декремент счётчика bf/s 1b ;продолжаем пока r0 != 0 mov.b r5,@-r4 ;запись r5 => [r4] с преддекрементом
SH — такой ранний RISC, имеющий delay slot как MIPS или Sparc. Команда mov выполняется параллельно с инструкций перехода.
Почему бы не поставить постинкремент вместо преддекремента, как на 68k или ARM?
Не получится — ведь такой инструкции нет (вернее есть, но не у всех процессоров SH)
www.shared-ptr.com/sh_insns.html
SH2A mov.b R0,@Rn+
Для остальных чипов хитрые японцы сделали лишь команды чтения с постинкрементом, и записи с преддекрементом (очевидно для организации стека)
F376
02.02.2018 14:19Заполнять буфер с конца, также как и писать другие алгоритмы «трогающие» адреса памяти с конца на современных архитектурах не следует, потому что современное hardware в основном работает по принципу look-ahead, т.е. кэширует то, что впереди.
a-tk
02.02.2018 14:24А можно распараллелить запись, начав двигаться с начала и с середины, но на половину длины.
F376
02.02.2018 14:28+1Рекомендация загромождать код проверкой воспринимается мной как анти-паттерн, вредная рекомендация.
Обоснование. Одной только проверки недостаточно, если уж проверять, кроме проверки придется еще и в каждом случае решать что делать дальше, а это означает что придется писать дополнительный код, принимать решения, которые не всегда могут оказаться удачными или безошибочными. Это отвлекает, это время и деньги и дополнительные баги/проблемы. Кроме того, этот код будет кодом решающим системные задачи и по смыслу не имеющим отношения к непосредственному алгоритму/ решаемой задаче, вот почему его быть не должно.
Проблема тут в самих системных *nix функциях *alloc(). Они слишком старые, слишком «системные», и предназначались для системных реалий 70х-80х годов.
Как выглядит более корректное решение?
На мой взгляд лучше не использовать эти системные вызовы напрямую. Использовать или другие библиотечные функции, или написать свои обертки и использовать их. Основная задача — оставить в коде только семантику работы с памятью, не загромождая его ничем посторонним.F376
02.02.2018 14:35Это можно обосновать другими словами тем, что хорошо построенные системы разбиваются на слои. Системному слою абстракции, которому принадлежат функции *alloc() — системную логику и системную «ответственность». Пользовательскому слою абстракции — соответственно, свою упрощенную ответственность.
kogemrka
02.02.2018 14:41+1принимать решения, которые не всегда могут оказаться удачными или безошибочными
Вы в любом случае принимаете решение. Просто если не писать проверку — ваше решение приводит к результату вида «Ну на вот этих платформах упадёт, на вот этих упадёт, попортив данные по этим адресам, на вот этих — попортив данные по тем адресам, а в одном на миллион случаев породит уязвимость, но я надеюсь это не произойдёт».
Это ничуть не лучше решения «Пусть падает», реализация которого занимает несколько секунд.
Отличаются ли эти две альтернативы скоростью реализации? Да, секунд на десять. Ну или сколько времени у вас займёт один if.
Отличаются ли последствия принятий этих решений?
Когда всё идёт по плану — нет.
Когда случается баг — да. Обнаруживать UB и отлаживать связанные с ним гейзенбаге в системном коде — адище лютое. Впрочем, случаются они крайне редко и в плане мат.ожидания могут выйти те же самые секунды три, что потратятся на написание if'а.
эти системные вызовы
*alloc — это не системный вызов.
Тем не менее, системные вызовы действительно лучше не использовать напрямую.F376
02.02.2018 14:57Просто если не писать проверку — ваше решение приводит к результату вида «Ну на вот этих платформах упадёт, на вот этих упадёт
Вы не поняли смысл моих слов, видимо воспринимая слова «системные» или «системный код» буквально, system call. А также, можно выдвинуть гипотезу, вы недопонимаете зачем архитектурно выполняется деление между высокоуровневым application-level слоем, и «системным», более низкоуровневым слоем.
Объясняю этот материал «на пальцах».
Высокоуровневый user-level слой ничего не знает ни о каких «платформах». Следовательно, ничего там не может «упасть» и пишется уже «не думая» об этом, не предусматривая «падения». За падения, за ошибки выделения памяти и «платформы» ответственнен именно system-level слой, который и должен по моей прямой рекомендации включать *alloc() функции. Это то, что я порекомендовал выполнить как «обертку».
Соответственно, ваш комментарий хоть и имеет собственную ценность, имеет слабое отношение к сказанному мной и/или не опровергает мой тезис.F376
02.02.2018 15:06Еще раз уточню, во избежание. Деление «application/user-level» и «system-level» не имеет отношения к уровням защиты и os-терминологии. Это лишь указания на распределение ответственности в нашем, доступном коде, это символическое название слоев абстракции, которые вводятся при проектировании.
kogemrka
02.02.2018 15:10Объясняю этот материал «на пальцах».
Высокоуровневый user-level слой ничего не знает ни о каких «платформах». Следовательно, ничего там не может «упасть» и пишется уже «не думая» об этом, не предусматривая «падения».
Разыменовывание нулевого указателя в надежде, что
1. Получится сегфолт
2. Компилятор раскроет UB именно так как вы планируете.
Уже является закладыванием и на платформу (работа со страницами памяти, соглашение линкера о том, что исполняемые файлы мапаются на память так, чтобы не занимать окрестность нуля замапанными страницами) и на компилятор (оптимизатор не порежет некоторые ветки где вы в дальнейшем действительно попытаетесь проверить указатель на ноль).
Я-то как раз и говорю, что у вас высокоуровневый код — и он на платформу и компилятор мог бы и не закладываться. Но закладывается. А зачем, если этого можно избежать одним if'ом?
Абстракции, с которыми работает высокоуровневый код в данном случае хорошо описаны.
Вот дёргает ваш код malloc. В спецификации стандартной библиотеки к malloc'у написано — может вернуть ноль.
Вот берёт ваш код указатель, который согласно документации может быть нулём и разыменовывает — в стандарте к компилятору написано, что оптимизатор может начать творить неожиданную хрень.
Не делать проверку на null -> закладываться на особенности конкретной версии вашего конкретного компилятора и конкретные реализации библиотек.
Собственно, обратите внимание: автор поста говорит именно о языке, стандарте и явлении языка (UB).
Комментирующие оппоненты: рассказывают про то, что такого не может быть в malloc'е в linux'е (конкретные реализации под конкретные платформы).F376
02.02.2018 15:37Я думаю стоит начать с этого:
Разыменовывание нулевого указателя в надежде, что
1. Получится сегфолт
2. Компилятор раскроет UB именно так как вы планируете.
Уже является закладыванием и на платформу
В упомянутом выше «application-level» слое абстракции никогда не может возникнуть такой ситуации из-за того что нижележащий «system-level» слой не должен допускать ситуацию с возвратом не выделенной памяти. (Грубо говоря, null в application-level не может вернуться в принципе, на это можно рассчитывать).
Соответственно, начать надо с того, как у вас вообще тогда может возникнуть разыменование нулевого указателя? Никак, возвращаемая вам память всегда валидна. Поэтому развиваемая вами дальше гипотетическая ситуация хоть и имеет ценность, никогда не должна возникнуть на application level уровне абстракций при правильном построении программной системы.
Все остальное в вашем сообщении верно и имеет самостоятельную ценность. Вы совершенно верно сообщаете что рассчитывать на segfault нельзя и это в целом system dependent.kogemrka
02.02.2018 15:54+2(Грубо говоря, null в application-level не может вернуться в принципе, на это можно рассчитывать).
… и именно поэтому нужно использовать new, а не malloc, если уж пишем на C++.
Кстати, автор об этом упоминал. Мол, в самом коде хромиума такой проблемы не видно, потому что они в основном используют new.
Соответственно, начать надо с того, как у вас вообще тогда может возникнуть разыменование нулевого указателя?
Если вы делаете вовне своего клиентского кода (в аллокатор памяти, например) запрос, который никогда не возвращает нулевой указатель (дёрнув new, или дёрнув malloc и сделав проверку, предлагаемую автором) — действительно, никак.
Если же вы дёргаете malloc — ты вы используете интерфейс malloc'а. В интерфейсе malloc'а написано — «может вернуть null».
Поэтому и имеет смысл избавляться от ситуации, когда в ваш бизнес код приходит UB. Например, сделав все проверки сразу же на месте выделения памяти. Или написав обёртки. Или аллоцируя не через malloc.
mayorovp
02.02.2018 16:21В упомянутом выше «application-level» слое абстракции никогда не может возникнуть такой ситуации из-за того что нижележащий «system-level» слой не должен допускать ситуацию с возвратом не выделенной памяти.
Но он допускает. Тут же обсуждается вполне конкретная функция malloc, которая описана в стандарте...
linuxover
02.02.2018 15:02Просто если не писать проверку — ваше решение приводит к результату вида «Ну на вот этих платформах упадёт, на вот этих упадёт, попортив данные по этим адресам, на вот этих — попортив данные по тем адресам, а в одном на миллион случаев породит уязвимость, но я надеюсь это не произойдёт».
- большинство программ не пишут в расчете работы в условиях недостатка памяти (да можно привести 100500 примеров таких программ, но еще больше обратных примеров)
- если перфекционировать, то надо перфекционировать не над одним паттерном, а над всеми. Паттерн с выделением переменной на стеке я выше вон привел. теоретически дает такой же креш приложения, который перфекционист "мог исправить на стадии компиляции". В таком подходе неизбежно приходим к скриптовым языкам :)
иногда код с хвостовой рекурсией выглядит красиво и проще. перфекционисты как правило ненавидят рекурсию вообще.
я говорю что проверки нужны там где они нужны:
- если мы пишем БД или библиотеку, то проверки несомненно нужны
- если мы пишем скажем калькулятор, которым пользователь будет пользоваться нажимая в нем кнопки и тот калькулятор грохнется при недостатке памяти (как и половина приложений системы), то смысл лепить те проверки какой? Тут на первый план начинает выходить стоимость разработки ПО: может быть тот калькулятор пока Вы его покроете всеми перфекциями станет не нужен уже никому?
PS: чтобы реалистичнее пример. Вот системная утилита rename написанная Ларри была лет этак 15 написана в таком стиле: ARGV[0] — регексп, ARGV[1..] — имена файлов. Ошибки синтаксиса, рантайм итп — со всеми разбирался клиент самостоятельно. Перфекционисты судя по changes изменили это совсем-совсем недавно (в 2013-м году) а с 97-го по 2013-й оно "итак всех устраивало": и вот он вопрос: эта перфекция кому-то нужна?
kogemrka
02.02.2018 15:21+1если мы пишем скажем калькулятор, которым пользователь будет пользоваться нажимая в нем кнопки и тот калькулятор грохнется при недостатке памяти (как и половина приложений системы), то смысл лепить те проверки какой? Тут на первый план начинает выходить стоимость разработки ПО: может быть тот калькулятор пока Вы его покроете всеми перфекциями станет не нужен уже никому?
Напоминаю — автор статьи рассказал про то, какое UB бывает, какие у него могут быть эффекты и как одним простым if'ом его избежать.
Пользоваться этим знанием вас никто не заставляет.
большинство программ не пишут
Большинство программ так же содержит ошибки и уязвимости.
Это нормально.
Я не вижу смысла столкнувшись с указанием на какой-то косяк в программе начинать разглагольствовать про «НУ И ЧТО, В БОЛЬШИНСТВЕ ПРОГРАММ ТАК ЖЕ». Иногда стоит исправить. Иногда разумно проигнорировать или отложить. Информация о наличии косяка, возможных последствиях и способах борьбы и предотвращения возникновения аналогичных ошибок (т.е. ровно то, чем является статья) — ценна для принятия обоснованного решения даже если это решение — забить.
linuxover
02.02.2018 15:28Напоминаю — автор статьи рассказал про то, какое UB бывает, какие у него могут быть эффекты и как одним простым if'ом его избежать.
напомню статья называется "почему важно проверять", а так же в рамках статьи рассказано "в библиотеках имеется не менее 70 ошибок" которые надо бы [по мнению автора] непременно исправить.
Я считаю:
- это избыточным перфекционизмом (иначе покажите обоснование по тем 70 ошибкам: думаю что даже падение всего браузера в условиях недостатка RAM не такой уж и фатал. Мало того в целом рядовое явление)
- разработчикам браузера лучше сосредоточиться скажем на поддержке HiDPI мониторов, чем перфекционировать на malloc. Если браузер начнет работать на моем мониторе (реальная причина почему я его сейчас не использую), но при этом будет падать если ему памяти не хватает — это для меня (пользователя) лучше чем он не падает, но и не работает.
как-то так
eao197
02.02.2018 15:17+1Рекомендация загромождать код проверкой воспринимается мной как анти-паттерн, вредная рекомендация.
Это мощно.
Одной только проверки недостаточно, если уж проверять, кроме проверки придется еще и в каждом случае решать что делать дальше, а это означает что придется писать дополнительный код, принимать решения, которые не всегда могут оказаться удачными или безошибочными.
Так ведь это же программирование. Тут нужно думать, решать, обрабатывать различные варианты и т.д. Работа программиста в этом и состоит.
Или сейчас уже принято писать программы на C или C++ без всей этой отвлекающей настоящего мастера рутины?linuxover
02.02.2018 15:21-1Или сейчас уже принято писать программы на C или C++ без всей этой отвлекающей настоящего мастера рутины?
Допустим Вам надо написать градиентный спуск для решения задачи управления курьерами. Самое последнее в данной задаче что Вас будет волновать — это вопрос "как будет вести себя данная программа/утилита в условиях недостатка памяти?"
И очень редкий заказчик Вам что-то предъявит по этому поводу.
а вот с тем что "программа X находит решение лучше Вашей" — вполне
помните анекдот/диалог?:
-
моя программа работает быстрее Вашей и ошибки проверяет!
-
зато моя программа считает правильноeao197
02.02.2018 15:27+1Самое последнее в данной задаче что Вас будет волновать — это вопрос «как будет вести себя данная программа/утилита в условиях недостатка памяти?»
Было бы очень хорошо, если бы вы не апроксимировали свои заблуждения на весь остальной мир. Нехватка памяти может ничем не отличаться от других ошибок, с которыми сталкивается программа. Например, как невозможность открыть файл.
Или вы в своих C/C++ программах результаты open/read/write так же предпочитаете не проверять?linuxover
02.02.2018 15:32-1Было бы очень хорошо, если бы вы не апроксимировали свои заблуждения на весь остальной мир. Нехватка памяти может ничем не отличаться от других ошибок, с которыми сталкивается программа.
ВОООТ!!!
я об этом и пишу!
я говорю: перфекционируя — надо перфекционировать до конца: начали проверять malloc, проверяем и open (я выше писал об этом несколько раз).
а после перфекций смотрим полученную стоимость программы и удивляемся что вышеприведенные тезисы о том что "это бесплатно" совершенно не соответствуют реальности.
Или вы в своих C/C++ программах результаты open/read/write так же предпочитаете не проверять?
там где это не нужно предпочитаю не проверять. и не тольк в C/C++, но и на других языках.
eao197
02.02.2018 15:38+1я говорю: перфекционируя — надо перфекционировать до конца: начали проверять malloc, проверяем и open (я выше писал об этом несколько раз).
Вообще-то это не перфекционизм, это нормальная, обычная работа программиста: выполнил действие, которое может завершиться неудачно, проверь результат. В зависимости от этого принимай решение о дальнейших действиях.
Другой подход к делу, т.е. программирование в расчете только на благоприятный сценарий, в мире C/С++ возможен только при написании quick-and-dirty прототипов «на выброс». Может быть где-то в мире Erlang-а, Ruby или JavaScript-а по-другому, а здесь вот так.
там где это не нужно предпочитаю не проверять. и не тольк в C/C++, но и на других языках.
Очень надеюсь, что мне не придется иметь дело с написанным вами софтом.linuxover
02.02.2018 15:43Вообще-то это не перфекционизм, это нормальная, обычная работа программиста
давайте еще раз с примером задачи с градиентным спуском поразбираемся? (выше)
Другой подход к делу, т.е. программирование в расчете только на благоприятный сценарий, в мире C/С++ возможен только при написании quick-and-dirty прототипов «на выброс».
- если вероятность неблагоприятного сценария очень мала
- если потерь при неблагоприятном сценарии мало или нет (и они не только в Вашем коде но и во многих других местах)
- если стоимость программирования неблагоприятного сценария относительно (всего кода) велика
то вопрос: нужно ли заморачиваться неблагоприятным сценарием?
eao197
02.02.2018 15:49+1давайте еще раз с примером задачи с градиентным спуском поразбираемся?
Задачи, как таковой, не было. Есть что-то в вашей голове, о чем я не имею ни малейшего понятия. Было бы ТЗ, можно было бы о чем-то говорить. Но даже если вы и сможете выкатить подробное ТЗ, то у меня и без этого есть чем заняться.
то вопрос: нужно ли заморачиваться неблагоприятным сценарием?
Конечно. Я вам даже больше скажу, это вопрос дисциплины. При должной дисциплине стоимость минимальной обработки неудачных результатов совершенно невысока. Особенно, если не брать C, а ограничиваться C++ с возможностью использования исключений.linuxover
02.02.2018 15:54-1Задачи, как таковой, не было.
Ну вот с Вас просят написать утилиту, решающую задачу комивояжера:
- на входе список мест куда и откуда доставить товары
- на выходе оптимальный порядок в котором их (места) объехать
задача как таковая очень сложна в программировании, однако очень многие программисты и бизнесы ее так или иначе программируют.
я говорю Вам, что заказчик, который Вам ее закажет будет
- в первую очередь интересоваться качеством решения самой задачи (насколько оптимален маршрут)
- в последнюю очередь его будет интересовать вопрос "Как ведет себя Ваша утилита в условиях недостатка ресурсов на сервере"
"Градиентный спуск" — это просто один из вариантов решения данной задачи. Один из самых простых, так сказать
eao197
02.02.2018 16:03+2Простите мне мой резкий тон, но мне очень жаль времени, которое я на вас вынужден тратить. Есть сильное ощущение, что к программированию (особенно на C и С++) вы никаким боком не относитесь. Может быть учитесь. Посему вам кажется, что дать ссылку на описание задачи коммивояжера — это дать постановку задачи. Боюсь вас огорчить, но это вовсе не так.
А т.к. вы к программированию имеете отношение постольку-поскольку, то предметно с вами разговаривать вряд ли возможно. Т.к. вы просто не понимаете предмета разговора. Причем как на уровне собственно программирования (тупо if-ы в коде), так и на уровне того, что хочет заказчик и что его интересует.linuxover
02.02.2018 16:10Простите мне мой резкий тон, но мне очень жаль времени, которое я на вас вынужден тратить.
я как бы Вас не заставлял мне оппонировать :)
Есть сильное ощущение, что к программированию (особенно на C и С++) вы никаким боком не относитесь. Может быть учитесь.
ну чтож, ничего не могу поделать с Вашими ощущениями :)
А т.к. вы к программированию имеете отношение постольку-поскольку, то предметно с вами разговаривать вряд ли возможно.
ага, ну Вы тут классически сами сформулировали и сами об это споткнулись.
Причем как на уровне собственно программирования (тупо if-ы в коде), так и на уровне того, что хочет заказчик и что его интересует.
вроде нормально (для уровня форума) сформулирована задача же. На входе список точек, которые объехать, на выходе надо получить маршрут. Плюс отсылка на викпедию где описано.
Вы на уровне форума хотите другой постановки этой задачи?
если что, то она — всего лишь пример задачи в решении которой вопрос "поведения системы, поставленной в граничные условия" будет иметь самый последний приоритет.
мне очень тяжело отнимать Ваше драгоценное время, если Вам не хочется дальше обсуждать — не пишите в ответ ничего :)
linuxover
02.02.2018 16:00Конечно. Я вам даже больше скажу, это вопрос дисциплины.
это вопрос не дисциплины, а затрат.
каждая вещь, в том числе и дисциплина стоят определенных затрат.
на написание программы Вы потратили X
на написание дисциплинированной программы Вы потратили Y
вопрос:
- на сколько Y > X?
- готов ли заказчик платить разницу?
- нужна ли дисциплинированная версия программы или достаточно недисциплинированной?
все это сводит к тому что мы пишем?
kogemrka
02.02.2018 16:00давайте еще раз с примером задачи с градиентным спуском поразбираемся? (выше)
Давайте. Много весёлых моментов испытал с переписыванием кода всяких лингвистических числодробилок, написанными людьми на плюсах из соображений «ну пусть пишется, как пишется, ой, что значит, что у нас при определённых условиях nan'ы повылезали отовсюду?».
Отладить или переписать чужой код нормально, когда проблема возникла, занимает больше времени, чем научить человека один раз как на эту проблему не натыкаться. Добавочное время, требуемое на написание одной-двух проверок или одного-двух преобразований данных в числодробилке — на порядок меньше, чем требуется через годы другому человеку на отладку и починку этого кода.
Энивер, действительно, сценариев, когда нужно быстро прототипировать — много.
Лучше в таких случаях использовать более устойчивые к выстрелам себе в ногу инструменты (т.е. аккуратнее с C++).
И даже если принимается решение говнокодить весело, отважно, бесрассудно — лучше понимать, в каком моменте ты оставил для себя грабли (в виде того же UB, про который рассказал автор), что делает знания, сообщённые автором, безусловно ценными.linuxover
02.02.2018 16:03Много весёлых моментов испытал с переписыванием кода всяких лингвистических числодробилок, написанными людьми на плюсах из соображений «ну пусть пишется, как пишется, ой, что значит, что у нас при определённых условиях nan'ы повылезали отовсюду?».
тут проблема не перфекционизма а отсутствия автоматического тестирования :)
- nan'ы лезут? — в тесты их! фиксить!
- падает при недостатке памяти? — определяться нужно ли над этим работать вообще? да? в тесты это! фиксить! Нет? (99% приложений) — пусть так и будет.
kogemrka
02.02.2018 16:11+1тут проблема не перфекционизма а отсутствия автоматического тестирования :)
Ого! То есть, теперь покрыть код тесткейсами (да ещё и заранее придумать такие кейсы, которые, собственно, выстрелили только через годы непрерывной работы с кучей данных) — это не вопрос, который требует затраты времени. А вот один if поставить, чтобы UB убрать — это ужас-ужас, теряем конкуретное преимущество.
И кто ещё из нас максималист? ;)linuxover
02.02.2018 16:15- автоматические тесты — способ написания ПО (так же как "функциональное программирование", "объектно ориентированное программирование", "визуальное программирование" итп)
- проверка возвращаемых значений — это алгоритмическая, ситуативная часть.
то есть не надо смешивать способ достижения цели с определением цели и все будет хорошо :)
kogemrka
02.02.2018 16:24то есть не надо смешивать способ достижения цели с определением цели и все будет хорошо :)
Избегание UB — такая же часть современной парадигмы разработки на С++, как и использование идиом вроде RAII.
автоматические тесты — способ написания ПО
TDD — это способ написания ПО. А вот просто написание каких-то автоматических тестов — это уже реализация.
Интегрировать TDD куда-либо дороже, чем написание if'а после malloc'а. Я абсолютно серьёзен, хотя тесты искренне уважаю и люблю.
F376
02.02.2018 15:59+1Это мощно.
Каков должен быть ответ на «мощно» — я не знаю.
Уместно ли делать такие комментарии в технической дискуссии?
Выше я привел логические обоснования, см. деление «app»-level уровень абстракции и «системный», закладываемый при проектировании. Я описал как эта проблема должна быть решена наиболее универсальным и наиболее разумным способом.
Тут нужно думать, работа программиста в этом и состоит.
Вот и подумайте, что именно предлагается, если вы, конечно, разработчик.
То что я предложил находится на чуть более высоком уровне, архитектурном. Возможно просто «программист» этой проблемы не видит/не понимает, думая что выходом является поставить везде copy-paste проверки…
Или сейчас уже принято писать программы на C или C++ без всей этой отвлекающей настоящего мастера рутины?
Возможно вас это удивит, но — да. К этому всегда стремятся…eao197
02.02.2018 16:08Я описал как эта проблема должна быть решена наиболее универсальным и наиболее разумным способом.
Могли бы ткнуть пальцем? А то кроме пространных и абстрактных рассуждений о старости системных вызовов в unix-ах вообще и *alloc-ов в частности ничего не было. Никакой конкретики. Ну или я не видел, комментариев здесь много, за всеми не уследить.
Вот и подумайте, что именно предлагается, если вы, конечно, разработчик.
Временами еще разработчик. И вот думаю-думаю, и глубину вашей мысли не постигаю. Могли бы вы объяснить ее на примерах кода, для тех, кто «от сохи» и в абстрактной архитектуре как-то не очень?
Возможно вас это удивит, но — да.
Очень сильно удивит. А вам ведь не составит труда на примерах кода показать, как это оно?F376
02.02.2018 16:49-1Очень сильно удивит.
Если вас удивляет тезис того что программисты постоянно стремятся снизить себе трудоемкость работы — тогда ответьте (самому себе, мне не надо) на вопрос — зачем создаются библиотеки? Зачем добавляется syntactic shugar?
А вам ведь не составит труда на примерах кода показать, как это оно?
А ведь составит. Мне это затратно, тратить время на вас лично.
Изучайте существующие кодовые базы, а не пользуйтесь уловкой «а вот покажите мне пальцем». В простейшем случае изучите поведение/реализацию operator new в языке c++ (на предмет проверки и exeption) или что происходит в managed-языках.
Там все происходит именно так, как это вам описано — пользователь языка генерально доверяет распределению памяти, нет проверок по месту на то что аллокация произошла/не произошла.
Объясняю предельно примитивно.
Предлагается не втыкать везде проверки после malloc(). Это грязь. Это нарушает ряд высокоуровневых принципов программирования/проектирования, когда у вас код, выполняющий некую функцию должен быть определен в одном месте, и не должен повторяться. Кроме того, это чревато другим, тем что вам придется еще и писать код обрабатывающий проверки, (возможно освобождающий уже распределенную память), и отсюда потенциально — возникновение новых ошибок.
Предлагается выполнить эту проверку в одном месте, сразу после вызова malloc(), оформить это как функцию/класс/макро, назначив этому функционалу обязанности «системного слоя» и далее уже писать свой application код в надежде на то что в application-level уровне абстракции память валидна всегда, не может быть ситуации когда память не возвращена, и, соответственно, UB не может возникнуть в принципе.
Вопрос который остался за кадром — это что делать на системном слое сразу после вызова *alloc, если память не вернулась? Все поведение malloc() в оригинале предназначалось для GE-645 с которым работали в Bell Labs и который являлся мультизадачным mainframe с виртуальной памятью. Не исключено что в оригинале можно было подождать :) пока память появится. В данном же случае можно либо завершать процесс, выводить сообщение пользователю и ждать, либо предусмотреть какое-либо другое поведение, но это уже детали.linuxover
02.02.2018 16:59+2А ведь составит. Мне это затратно, тратить время на вас лично.
Вы пишете эту фразу оппоненту, а Ваш оппонент такую же фразу пишет мне.
я вот одного не пойму, если вы все такие занятые, то зачем включаться в дискуссию? (при том что я с Вашей точкой зрения очень близок).
eao197
02.02.2018 17:00Там все происходит именно так, как это вам описано — пользователь языка генерально доверяет распределению памяти, нет проверок по месту на то что аллокация произошла/не произошла.
Хочу вас разочаровать. Когда разработчик имеет дело с исключениями вместо кодов возврата, необходимость думать и принимать решение никуда не исчезает. Там есть свои проблемы, например, позаботиться об exception safety.
Предлагается выполнить эту проверку в одном месте, сразу после вызова malloc(), оформить это как функцию/класс/макро, назначив этому функционалу обязанности «системного слоя» и далее уже писать свой application код в надежде на то что в application-level уровне абстракции память валидна всегда, не может быть ситуации когда память не возвращена, и, соответственно, UB не может возникнуть в принципе.
Уж простите мне мой французский, но какая глубокая мысль. Видимо, мосье теоретик и архитектор-астронавт?
Иначе как объяснить вот эту вот «досадную мелочь»:
В данном же случае можно либо завершать процесс, выводить сообщение пользователю и ждать, либо предусмотреть какое-либо другое поведение, но это уже детали.
Получается в точности как в народной мудрости: гладко было на бумаге, но забыли про овраги. На словах все красиво про уровни абстракции, а как доходит до практики, то «это уже детали».linuxover
02.02.2018 17:16Хочу вас разочаровать. Когда разработчик имеет дело с исключениями вместо кодов возврата, необходимость думать и принимать решение никуда не исчезает.
эта необходимость уменьшается. раньше ему надо было думать на каждом вызове.
теперь можно весь стек вызовов охватить одним try/catch и не париться особо.
ну и плюс имеется на системном уровне как бы заранее сделанный try/catch.
в некоторых случаях вполне допустимо делегировать работу ему.
:)eao197
02.02.2018 17:53+1эта необходимость уменьшается. раньше ему надо было думать на каждом вызове.
Попробуйте сделать тот же std::vector::push_back/emplace_back с обеспечением exception safety. Посмотрим, придется вам париться или нет.
теперь можно весь стек вызовов охватить одним try/catch и не париться особо.
F376
02.02.2018 17:39-1Хочу вас разочаровать. Когда разработчик имеет дело с исключениями вместо кодов возврата, необходимость думать и принимать решение никуда не исчезает. Там есть свои проблемы, например, позаботиться об exception safety.
Вы не правы, исчезает.
В легко достижимом идеале пишется код реализующий только свою задачу, не отвлекаясь на посторонние детали, такие как проверки выделения памяти, реализация exception safety, мультипоточности и проч. Мы не должны смешивать код с посторонними «костылями», выполняющими вспомогательные функции. И этот идеал достижим, именно к нему и следует стремиться, приближаясь по мере возможности. В простейшем случае создается библиотечный набор exception-safe классов, берущий на себя соотв. задачи
Уж простите мне мой французский, но какая глубокая мысль. Видимо, мосье теоретик и архитектор-астронавт?
Зачем вы хамите, переходя на личности? Если у вас отсутствуют аргументы — это не повод к Ad hominem.
Получается в точности как в народной мудрости: гладко было на бумаге, но забыли про овраги. На словах все красиво про уровни абстракции, а как доходит до практики, то «это уже детали».
Вы не правы.
Построить обертку и не использовать malloc() повсеместно (т.е. на application-level уровне) — это именно то, что следует сделать. В чем тут проблема и какие тут «овраги»?
Итого, никаких аргументов против простейшего решения вы не привели.
Более того, вы отчаянно сваливаете техническую дискуссию в фанбойский балаган.eao197
02.02.2018 18:00+2В легко достижимом идеале пишется код реализующий только свою задачу, не отвлекаясь на посторонние детали, такие как проверки выделения памяти, реализация exception safety, мультипоточности и проч.
Простите, а вы точно к разработке на C или C++ имеете отношение?
Зачем вы хамите, переходя на личности? Если у вас отсутствуют аргументы — это не повод к Ad hominem.
А вы начните предметно разговаривать, без воспарений в высокие абстракции на счет каких-то слоев, которые волшебным образом решают проблемы неудачных операций (хоть вызовов malloc, хоть чего другого). Тогда можно будет поговорить о решениях. Пока же есть ощущение, что вы живете не в мире реального программирования, а в какой-то параллельной вселенной, в которой легко достигается абстрагирование от многопоточности, exception safety и пр.
Другими словами: покажите, что с вами есть о чем говорить. Что вы не теоретик и архитектор-астронавт.
Построить обертку и не использовать malloc() повсеместно (т.е. на application-level уровне) — это именно то, что следует сделать. В чем тут проблема и какие тут «овраги»?
Ну так покажите, какую обертку вы предлагаете делать. Тогда можно будет сказать, где эта обертка будет работать, а где нет. Но вместо этого вы говорите про какие-то system-level и application-level. А действительно важные вещи, которые влияют и на то, и на другое, это у вас: «уже детали».F376
02.02.2018 19:11-1Простите, а вы точно к разработке на C или C++ имеете отношение?
Какая вам разница? Почему вы в сугубо технической дискуссии неоднократно переходите на личность человека?
А вы начните предметно разговаривать, без воспарений в высокие абстракции на счет каких-то слоев, которые волшебным образом решают проблемы неудачных операций.
Волшебным?
Сошлитесь на мой фрагмент текста, который для вас непонятен и объясните что там для вас «не предметно» и «волшебно».
Тогда можно будет поговорить о решениях. Пока же есть ощущение
С «ощущениями» и «чувствами» это не ко мне. Я только по технической и архитектурной части.
Ну так покажите, какую обертку вы предлагаете делать. Тогда можно будет сказать, где эта обертка будет работать, а где нет.
Вы троллите? Что тут может быть «где эта обертка будет работать, а где нет»?
// Call it "System level" void* checked_malloc(size_t sz) { void* ptr = malloc(sz); if (!ptr) { exit(YOUR_ERROR_CODE); } return ptr; } // Call it "application level" int main() { void* mymem1 = checked_malloc(YOUR_SIZE1); // Here I fully trust mymem1, no need for checking void* mymem2 = checked_malloc(YOUR_SIZE2); // Here I fully trust mymem2, no need for checking free(mymem2); free(mymem1); return 0; }
— Концепция ясна? Это же сверх-тривиально, как тут что-то может быть неясно, что тут требуется иллюстрировать?
Но вместо этого вы говорите про какие-то system-level и application-level.
Это простейшее разбиение на логические слои. Почему вы не можете мне просто написать: «Простите, я не совсем понимаю что вы имеете в виду. Можете мне объяснить?»
А действительно важные вещи, которые влияют и на то, и на другое, это у вас: «уже детали».
Вы не правы.
Решение принимаемое в каждой конкретной программе в случае нехватки памяти не имеет значения (выше это тривиальный вызов exit() ). Мы обсуждаем только узкий контекст необходимости проверки аллокаций памяти. Всё что вне этого контекста — не информативно.eao197
02.02.2018 19:19+2Какая вам разница?
Огромная. Когда в тему о проблемах использования malloc в C и C++ приходят теоретики, ссылающиеся на опыт управляемых языков, то разговаривать, как правило, можно только о личностях, которые до такого додумываются.
Сошлитесь на мой фрагмент текста, который для вас непонятен
Это вы троллите? Я вам уже сказал, что до сих пор вы вообще не сказали ничего конкретного, что можно было бы предметно обсуждать. Вот только сейчас вы привели пример кода. Из которого уже видна степень вашей экспертизы.
Я только по технической и архитектурной части.
Пока что это ничем не подтверждено.
— Концепция ясна?
Более чем.
Это же сверх-тривиально, как тут что-то может быть неясно, что тут требуется иллюстрировать?
Отличная иллюстрация того, что вы не понимаете сложности и широты темы, о которой идет речь. Если такой checked_malloc будет в какой-нибудь библиотеке для парсинга хитрого формата файлов, то использовать библиотеку с подобным checked_malloc будет ой как стремно, скажем, в многооконном офисном приложении.F376
02.02.2018 20:21-3то разговаривать, как правило, можно только о личностях, которые до такого додумываются.
Я считаю что о вашей деятельности нужно уведомить модераторов. Когда человек переходит на личности нечаянно, это одно дело. Но когда он вбил себе в голову и раздает всем вокруг ярлыки, еще и ОБОСНОВЫВАЯ свое хамство — это совершенно другой случай.
Евгений, вы не правы во всем вашем выступлении.
Моя тривиальнейшая мысль заключалась в том что в коде желательно писать не напрямую библиотечный malloc() с повсеместной проверкой возвращаемого значения, но его собственную замену/обертку — совершенно верна. Это азы. Это тривиальщина.
У этого, кроме озвученных выше есть дополнительные преимущества, например такие что не придется выполняя рефакторинг, бегать по коду и менять все места при желании что-либо сделать с аллокацией памяти, будь то ввод дополнительного memory guard'а, логгинг аллокаций или даже простейший breakpoint, который удобно поставить внутри обертки.
Вы никак мои слова не опровергли. Вот именно от вас ПО СУЩЕСТВУ никто тут ничего интересного не услышал, впустую потеряв время на вычитку ваших бессмысленных текстов.
И вместо этого, будучи изначально неправым вы:
а) Совершаете атаку на мою личность, занимаясь на Habrahabr выяснением кто якобы © «лучше знает XYZ а кто не знает».
— это очень нехорошо.
б) Вы уводите в полнейший офтопик, занимаясь обсуждением совершенно посторонних вещей, безосновательно заявляя что эта тривиальщина уровня рефакторинга должна иметь проблемы с реализацией на практике.
ORLLY? Тривиальный рефакторинг будет иметь проблемы? Чего? Обертка malloc()'а доставит какие-то практические проблемы? Конечно же это не так, и это ложь, бред, потеря реальности.
Я свою задачу выполнил и умываю руки, так как дальнейшее не обогащает меня знанием но является пустой растратой времени. А вот вы можете взять на себя обязательства доказать якобы практическую несостоятельность решения. Дерзайте, занимайтесь опровержением 2+2. Докажите, раз вы в этом так заинтересованы. Но с моей точки зрения это заведомо полнейшая глупость.
Вынужден разорвать бессмысленную дискуссию.eao197
02.02.2018 20:59+1Давайте я вам напомню, как развивались события. Вы написали нечто, с чем сложно согласиться. Это заставило задать вам уточняющий вопрос. Но вместо того, чтобы на него ответить, вы завели шарманку в стиле:
Вот и подумайте, что именно предлагается, если вы, конечно, разработчик.
Далее я вас попросил говорить более предметно (т.к. и в ответах другим участникам дискуссии вы не утруждали себя конкретными примерами). На что вы продолжили играть на той же шарманке:
Мне это затратно, тратить время на вас лично.
И продолжали играть до тех пор, пока из вас таки не удалось выжать хоть какой-то пример кода.
Изучайте существующие кодовые базы, а не пользуйтесь уловкой «а вот покажите мне пальцем». В простейшем случае изучите поведение/реализацию operator new в языке c++ (на предмет проверки и exeption) или что происходит в managed-языках.
И вот когда код появился, с вами и вашими «убеждениями» все стало окончательно понятно. Ваши же рекомендации не проходят простейшей критики. Ну не подходит предложенный вами вариант checked_malloc для ряда случаев. И именно чего-то подобного я и ожидал, как только вы написали вот это:
В данном же случае можно либо завершать процесс, выводить сообщение пользователю и ждать, либо предусмотреть какое-либо другое поведение, но это уже детали.
Нежелание человека разговаривать о значимых деталях наводит на подозрение о том, что человек не является действующим разработчиком (по крайней мере действующим C или C++ разработчиком), об этих-то подозрениях и приходилось вам мягко намекать.F376
02.02.2018 22:52А давайте я взамен тонны вот этого запредельного школьного хамства:
eao197 Это мощно.
eao197 Уж простите мне мой французский, но какая глубокая мысль. Видимо, мосье теоретик и архитектор-астронавт?
eao197 вы не понимаете сложности и широты темы, о которой идет речь
eao197 подозрение о том, что человек не является действующим разработчиком
eao197 покажите, что с вами есть о чем говорить. Что вы не теоретик и архитектор-астронавт.
Отпишу доступным вам языком? Ну-ка брысь курить xmalloc!
Потому что весь этот школьный бред просто не мог бы возникнуть, если бы eao197 хотя бы подозревал о его существовании.
eao197 Ну не подходит предложенный вами вариант checked_malloc для ряда случаев.
Жгите ещще красавчик, я польщен глубиной беседы и вашими «выводами» о иллюстративном коде, набитом за пару минут.
© Ге-ни-аль-но!eao197
02.02.2018 23:12вашими «выводами» о иллюстративном коде, набитом за пару минут.
Так как прикажете воспринимать приведенный вами здесь код?
Как демонстрацию ваших рассуждений о слоях абстракции? Или как не имеющую отношения к теме иллюстрацию, на которую не стоит обращать внимания?
Если это всего лишь иллюстрация, то когда будет хоть какая-то конкретика, которую можно обсуждать предметно? Или обсуждать останется только вашу тонкую душевную организацию?
Я вас уже который комментарий прошу: давайте ближе к теме.F376
03.02.2018 02:23Евгений, вы присмирели, благодарю вас. Таким вы мне нравитесь. :)
Вы мне только что написали:Ну так покажите, какую обертку вы предлагаете делать. Тогда можно будет сказать, где эта обертка будет работать, а где нет.
Я вам объяснил идею словами и привел иллюстративный пример кода. Кто знает? Может у вас реально проблемы с пониманием русского языка, действительно, бывают такие люди. Не проблема, я пошел вам навстречу набросав простейший пример в качестве иллюстрации… эээ… «архитектурным решением» называть это слишком громко, пусть будет «рефакторинга».
На это вы выдали в эфир совершенно пустое рассуждение (по смыслу никак не опровергающее и не подтверждающее мой исходный тезис):Ну не подходит предложенный вами вариант checked_malloc для ряда случаев.
Ну очевидно не подходит, а что должен? Это как-то отменяет саму идею, сказанное в самом первом сообщении, как-то ПРИНЦИПИАЛЬНО запрещает заменять malloc на собственную обертку, или что? В чем проблема-то? Вы это никак не прояснили. И вот вы пишете:Так как прикажете воспринимать приведенный вами здесь код? Как демонстрацию ваших рассуждений о слоях абстракции? Или как не имеющую отношения к теме иллюстрацию, на которую не стоит обращать внимания?
Хахахаха. Я после этой фразы пытаюсь понять, чего вы добиваетесь. Реально, уже думаю о нехорошем. :)
Если это всего лишь иллюстрация, то когда будет хоть какая-то конкретика, которую можно обсуждать предметно? Или обсуждать останется только вашу тонкую душевную организацию? Я вас уже который комментарий прошу: давайте ближе к теме.
Конкретика?
А какая вам «конкретика» тут нужна?
Ну вот вам написали, не используйте malloc, заменяйте враппером, не замусоривайте код проверками, это решение несколько получше, вон даже на *nix ах без проблем существует аналогичный xmalloc. Даже продемонстрировали примерный код. Куда уж конкретней, Евгений? Приведите пример своей «конкретики»?
Ну и давайте все-таки вернемся к исходной посылке, ибо вы не продемонстрировали какого-либо суждения по существу вопроса.
Исходное предложение до абсурдного простое, я и другие люди не видим что там ВООБЩЕ можно обсуждать. Но вы, видимо, усматриваете какие-то концептуальные проблемы. Вот только рассказать не успели, всё больше требуя от меня. Я ваши требования выполнил.
Жду теперь от вас наконец какого-то разумного опровержения или возражения по существу сказанного в исходном сообщении Подчеркиваю, жалобы на якобы «не конкретику» я уже слышал. Код привел, объяснения дал. На xmalloc сослался. Давайте теперь Евгений вашу здравую аргументацию в ответ на исходный пост.
И у меня к Вам будет предложение. А давайте с вашей помощью поставим на хабре рекорд абсурда и войдем в историю? Ну например, я напишу слова «оператор сравнения», а вы возьмете на себя функцию раз десять написать «давайте конкретику»? Я вам уже и код
приведу, а вы всё знай будете требовать «конкретики», да «конкретики»? Давайте, а? Чего уж там,if (x) {}
гулять так гулятьабсурд так абсурд! :)eao197
03.02.2018 09:32+2Опять графоманские потоки вместо обсуждения технических деталей. Сильно не вяжется с приснопамятным:
Я только по технической и архитектурной части.
Скорее я тут вынужден разговаривать не с архитектором-астронавтом, а с теоретиком-графоманом.
Поскольку вы ссылаетесь еще на кого-то («я и другие люди не видим что там ВООБЩЕ можно обсуждать») то придется потратить время дабы объяснить вам и мифическому еще кому-то очевидные вещи. Очевидные для тех, кто не отмахивается посредством «это уже детали», как вы.
Итак, предложенная вами идея оберток вокруг malloc-а, во-первых, работает только тогда, когда обертка имеет возможность прервать выполнение кода после возникновения ошибки тем или иным способом. Вызовом abort/exit/terminate, или выбросом исключения, или эмуляцией исключения на подручных средствах. Или даже goto error, если «предлагаемая» вами обертка реализована в виде макроса.
Проблема здесь в том, что это не всегда работает. Безусловное прерывание программы (abort/exit/terminate) может быть приемлемым для утилит типа grep, wc или xargs, но совершенно не приемлем для библиотек вроде libxml2, zlib или libcurl. Так же это не приемлемо для больного класса приложений, скажем офисных пакетов или СУБД.
Исключения могут быть недоступны в принципе. Скажем, если мы ограничены чистым C. Или же по каким-то веским причинам исключения отключены в C++. С эмуляцией исключений может быть та же самая беда — они могут быть запрещены.
Как должна вести себя обертка вокруг malloc в условиях, когда прервать исполнение кода нельзя (например, внутри libxml2 или libcurl)?
Эта обертка будет вынуждена возвращать результат операции, в том или ином виде. И не суть важно, будет ли этот результат возвращаться в виде голого указателя (что нам придется делать, если мы находимся в чистом C) или в виде какой-то обертки, вроде std::optional или folly::Expected. Все равно мы будем вынуждены этот результат проверять. А раз так, то вопрос «нужно ли проверять коды возврата malloc?» для большого количества прикладных ниш, где применяются C/C++ превратиться в «нужно ли проверять коды возврата обертки над malloc?». Получаем то же самое, вид в профиль.
Ну и, во-вторых, как уже было сказано, наличие оберток вовсе не отменяет того, что:в каждом случае решать что делать дальше, а это означает что придется писать дополнительный код, принимать решения, которые не всегда могут оказаться удачными или безошибочными
F376
03.02.2018 11:23-3eao197, Евгений Олейников, вы занимаетесь пустой демагогией и мне уже просто интересно исследовать психологию, т.е у вас реальные проблемы, или же намеренно уходите от ответа, т.е. намеренно врёте в отсутствие аргументации.
Я же вам написал — давайте теперь по существу поставленного вопроса. А вопрос у нас по существу такой: Автор статьи, Andrey2008 объяснил почему результат вызова malloc() проверять надо, и аргументировал это в общем случае UB в случае отказа функции вернуть память и возврата null pointer. Я на это (пишу раз в десятый) аргументировал тем, что не совсем красиво будет ставить везде проверки, и что это функцию лучше обернуть проверкой, создав условие что null pointer никогда не вернется, также как это делает xmalloc.
Вы начинаете уводить в сторону, вместо обсуждения вопросов Андрея и моего, рассказывать постороннее, что в общем случае нельзя реализовать универсальную обработку случая отказа выделения памяти, о чем вам было неоднократно уже сказано:Вопрос который остался за кадром — это что делать на системном слое сразу после вызова *alloc, если память не вернулась? Все поведение malloc() в оригинале предназначалось для GE-645 с которым работали в Bell Labs и который являлся мультизадачным mainframe с виртуальной памятью. Не исключено что в оригинале можно было подождать :) пока память появится. В данном же случае можно либо завершать процесс, выводить сообщение пользователю и ждать, либо предусмотреть какое-либо другое поведение, но это уже детали.
Конечно же нельзя предусмотреть абсолютно универсальную, общую функцию для абсолютно любых случаев в жизни. Если уж на то пошло, следует реализовать callback на желаемую функцию обработки ошибки и дело в шляпе. Но это не является поставленным вопросом, это офтопик, совершенно посторонняя деталь. Вопрос тут:
а) Тема начатая автором статьи
б) Мое дополнение (см. курсив выше)
Вот их и следует обсуждать, не уводя в сторону на несущественные по смыслу детали.
В связи с чем ваша попытка ответа не засчитывается, Евгений, пытайтесь еще. Ждем ответа от Вас на поставленный вопрос по-существу (выделено выше курсивом), ответ вы пока не дали, отделавшись отпиской.eao197
03.02.2018 11:32-11. Вы не понимаете, о чем мы с вами разговариваем и почему-то ждете ответов от меня, хотя это вам нужно отвечать за свои убеждения и рекомендации.
2. Вы настолько тупы, что не можете скопипастить мое имя и фамилию правильно из профиля. Это полностью объясняет проблемы с п.1.
На якобы хамство с мой стороны лучше пожаловаться сразу в ООН и Спортлотто.F376
03.02.2018 12:33-2Я тут внимательнее вчитался в ваш текст, Евгений eao197, вы выдали себя с потрохами.
Эта обертка будет вынуждена возвращать результат операции, в том или ином виде. И не суть важно, будет ли этот результат возвращаться в виде голого указателя (что нам придется делать, если мы находимся в чистом C) или в виде какой-то обертки, вроде std::optional или folly::Expected. Все равно мы будем вынуждены этот результат проверять.
— Обратим внимание на ваши слова, Евгений, «вынуждена возвращать результат», «будем вынуждены этот результат проверять».
Это, наверное, у Вас как раз тот случай, когда человек понимает мелочи из C++ (std::optional, итд) но не понимает самую важную суть предлагаемого в разговоре, простейшее архитектурное и алгоритмическое решение. Но при этом как надуты щеки! :)
Еще раз поясняю специально для вас Евгений, суть предложения, что и зачем делается. Читайте внимательно!
malloc() заменяется на свою функцию-обертку.
Зачем это делается? Да затем, чтобы результату её вызова доверять в своем коде ВСЕГДА. Почему это можно делать?
Обертка внутри себя проверяет результат вызова malloc(), и в случае возврата malloc()'ом значения null pointer, обертка НИКОГДА не вернет в вызвавший её код null pointer, сразу предприняв какую-то обработку, которую можно задать универсальным callback'ом или прямиком прописав в данном конкретном случае данной конкретной обертки. Она просто не вернется назад. Всё, краш, fail. Памяти нет. Отказ.
А следовательно, вам не надо каждый раз писать в коде проверку результата вызова обертки.
Вам этот код был продемонстрирован выше. Все что вам нужно было сделать — просто проанализировать и понять его выполнение, прочитав комментарии за вызовом обертки.
ПОВТОРЯЮ :)
В чем плюс? В чем смысл всего этого?
В основном в том, что null pointer в вызываемый код не вернется НИКОГДА, и соответственно, результату вызова можно всегда полностью доверять. А это значит, что его НЕ НАДО ПРОВЕРЯТЬ. Это ровно противоположно тому что вы думаете и пишете.
Тем самым проблема постоянной проверки возвращаемого значения (и каких-либо дополнительных действий) уходит. В «клиентский» код (вызывающий обертку) никогда не может вернуться null pointer, и поэтому и проверять не надо и undefined behaviour не возникнет. Всё, проблема решена. Это понятно?
Надеюсь Вы всё поняли, Евгений eao197? Потому что если это не так, это фиаско, братан! Это фиаско! :)
Как и прежде, жду от вас дельных комментариев по-существу поставленного вопроса.khim
03.02.2018 22:43+3Обертка внутри себя проверяет результат вызова malloc(), и в случае возврата malloc()'ом значения null pointer, обертка НИКОГДА не вернет в вызвавший её код null pointer, сразу предприняв какую-то обработку, которую можно задать универсальным callback'ом или прямиком прописав в данном конкретном случае данной конкретной обертки. Она просто не вернется назад. Всё, краш, fail. Памяти нет. Отказ.
Опять 25. Вы хоть понимаете, что библиотека, позволяющая себе подобные вольности будет просто выкинула и замена чем-то другим? Потому что ей просто нельзя пользоваться в условиях, когда памяти не хватает (а когда её заведомо хватает и атак на вашу программу не ожидается, то никакие обёртки, собственно, не нужны — можно просто вызывать malloc и «надається на лучшее»)
Как и прежде, жду от вас дельных комментариев по-существу поставленного вопроса.
Комментарий — всё тот же: приведите же, наконец, хоть какую-нибудь реализацию, которая не обрушивает программу и не вызывает исключений, а потом можно будет уже что-то обсуждать.
В основном, понятно, нужно обсуждать «цену» такой обёртки (дополнительную память, влияние на код и прочее) — но пока примера реализации нет, а есть одно «механик руками» нет, собственно, и предмета для обсуждений.
Надеюсь Вы всё поняли, Евгений eao197? Потому что если это не так, это фиаско, братан! Это фиаско! :)
Да что ж вы опять куда-то в стратосферу устремились-то? Какой выигрыш может быть от подобного подхода — понятно всем. А вот какие могут быть побочные эффекты — нет. А они ведь разные, в зависимости от реализации вашей обёртки. В каких-то случаях они приемлемы, в каких-то — нет. Вот о чём разговор, вот почему от вас требуют конкретики. И не в виде «псевдокода в комментариях» (любимое занятие астронавтов), а виде работающего кода!F376
04.02.2018 20:39-1Опять 25. Вы хоть понимаете, что библиотека, позволяющая себе подобные вольности будет просто выкинула и замена чем-то другим? Потому что ей просто нельзя пользоваться в условиях, когда памяти не хватает (а когда её заведомо хватает и атак на вашу программу не ожидается, то никакие обёртки, собственно, не нужны — можно просто вызывать malloc и «надається на лучшее»)
Я понимаю, полнолуние, но все же, с чего вы взяли что всенепременно следует говорить о библиотеке, и что вам мешает реализовать callback?
В основном, понятно, нужно обсуждать «цену» такой обёртки (дополнительную память, влияние на код и прочее) — но пока примера реализации нет
И не в виде «псевдокода в комментариях» (любимое занятие астронавтов), а виде работающего кода!
Час моей работы стоит $50 и меньше чем на 20 часов я не размениваюсь. Предоплата в столь мелких случаях 100%. Как будете оплачивать?
khim
05.02.2018 00:51+1Я понимаю, полнолуние, но все же, с чего вы взяли что всенепременно следует говорить о библиотеке
Чукча не читатель, чукча — писатель?
Вы статью-то вообще читали, которую мы тут обсуждаем? И именно в этом контексте идёт всё обсуждение.
Всё что здесь обсуждается — это проблемы только и исключительно библиотек.
Цитата:Но прежде надо ответить на вопрос: «а причём здесь Chromium?».
Chromium здесь при том, что в используемых в нём библиотеках имеется не менее 70 ошибок, связанных с отсутствием проверки после вызова таких функций, как malloc, calloc, realloc. Да, в самом Chromium эти функции почти нигде не используются. В Chromium применяются только контейнеры или operator new. Однако, раз ошибки есть в используемых библиотеках, то значит, можно сказать, что они есть и в Chromium. Конечно, какие-то части библиотек могут не использоваться при работе Chromium, но определять это сложно и ненужно. Всё равно надо править все ошибки.
и что вам мешает реализовать callback?
А вот с этого момента обсуждение, собственно, и стоит начать. Покажите пример кода, который предлагает использовать — и давайте его сравним с банальной проверкой ошибок. Не какие-то «сферические принципы в вакууме», а реальный код, который реально может захотеться засунуть в биболиотеку. Не обязательно приводить его здесь — ссылка на реализацию где-нибудь на GitHub'е тоже пойдёт.
Час моей работы стоит $50 и меньше чем на 20 часов я не размениваюсь. Предоплата в столь мелких случаях 100%. Как будете оплачивать?
Вас понял. Диагноз — «архитектурный астронавт, на работу не брать ни под каким соусом» принят и зафиксирован.
Потому что отговорки подобного плана — это как раз типичный признак: говорить умные слова мы умеем, писать код — нет. И неважно даже: вы считаете, что вы «переросли» этот этап и теперь можете работать «учёной совой» и «разрабатывать стратегию» или никогда не умели… важно что сейчас вы код не пишите — иначе пример того, во что вы так свято верите не требовал бы 20 часов работы, а занял бы 5 минут копи-паста.F376
05.02.2018 04:09Чукча не читатель, чукча — писатель?
Вы статью-то вообще читали, которую мы тут обсуждаем? И именно в этом контексте идёт всё обсуждение.
Всё что здесь обсуждается — это проблемы только и исключительно библиотек.
Обсуждаются здесь не «проблемы библиотек», как это может понять только странный человек, а проблема обязательности проверки malloc-подобных функция на результат вызова, и обоснование почему не проверять нельзя. Во-первых, автором указано что проверялся сам Chromium, но в нем не оказалось проблем, поскольку он, как это и предполагалось, имеет чисто C++ код. Всё, точка, ваша карта бита. А далее, анализируя Chromium, чтобы поиметь хоть какой-то улов, автор переходит к массе библиотек (а также вступал в диалог с автором библиотеки, да и советы дает автором библиотек), однако же делать отсюда алогичный вывод что это проблема якобы «ТОЛЬКО библиотек» — нельзя, это вывод по типу «посчитал количество слов, сделал вывод», смотрю в книгу — вижу фигу.
Ну я лично, будучи в здравом уме и твердой памяти такого вывода о ТОЛЬКО библиотеках не сделал. Речь идет о любом коде вообще, поскольку функции аллокации памяти встречаются везде где угодно.
А вот с этого момента обсуждение, собственно, и стоит начать.
То есть до этого это был с вашей стороны трёп?
Покажите пример кода, который предлагает использовать — и давайте его сравним с банальной проверкой ошибок. Не какие-то «сферические принципы в вакууме», а реальный код, который реально может захотеться засунуть в биболиотеку. Не обязательно приводить его здесь — ссылка на реализацию где-нибудь на GitHub'е тоже пойдёт.
Я-то покажу, а вот мне хотелось бы знать, с каких это щщей вы в разговоре общаетесь со мной личными повелениями — А ПОКАЖИТЕ-КА МНЕ? Вам не кажется это нахальством или троллингом?
Вас понял. Диагноз — «архитектурный астронавт, на работу не брать ни под каким соусом» принят и зафиксирован.
Нет это я вас понял, вы хам. Вы обзываетесь и наклеиваете ярлыки. Это хамство запредельных масштабов, вы с чего взяли что можете общаться в таком ключе?
Потому что отговорки подобного плана — это как раз типичный признак: говорить умные слова мы умеем, писать код — нет. И неважно даже: вы считаете, что вы «переросли» этот этап и теперь можете работать «учёной совой» и «разрабатывать стратегию» или никогда не умели… важно что сейчас вы код не пишите — иначе пример того, во что вы так свято верите не требовал бы 20 часов работы, а занял бы 5 минут копи-паста.
Её-мое. Да что же это такое тут творится?
Да какая вам вообще разница, кто я? Обсуждайте идеи, обсуждайте события, но не людей! См. высказывание Элеоноры Рузвельт: «Великие умы обсуждают идеи. Средние умы обсуждают события. Мелкие умы обсуждают людей.»
Постыдились бы!
Далее я уже второй раз привожу свой код, а вы только трепете языком и наезжаете на меня с хамством.
// main.c #include <stdio.h> #include <stdlib.h> #include <setjmp.h> #include "testmemlib.h" static int test_function() { const int TEST_MEM_SZ = 16; unsigned char *mem1 = NULL; MALLOC_TRY mem1 = (unsigned char *) test_malloc(TEST_MEM_SZ); MALLOC_EXCEPT { printf("memory allocation error!\n"); return 0; }; // some memory usage, dump contents { int i; printf("memory dump: "); for (i = 0; i < TEST_MEM_SZ; i++) printf("%02X,", mem1[i]); printf("\n"); } test_free(mem1); return 1; } int main() { printf("begin test\n"); if (test_function()) printf("test completed. well done!\n"); else printf("test failed.\n"); return 0; }
// testmemlib.c #include <stdlib.h> #include <setjmp.h> #include "testmemlib.h" jmp_buf malloc_buf123; void *test_malloc(size_t sz) { void *ptr = malloc(sz); if (!ptr) { longjmp(malloc_buf123,1); } return ptr; } void test_free(void *ptr) { free(ptr); }
// testmemlib.h #ifndef TESTMEMLIB #define TESTMEMLIB extern jmp_buf malloc_buf123; extern void *test_malloc(size_t sz); extern void test_free(void *ptr); #define MALLOC_TRY if (!setjmp(malloc_buf123)) #define MALLOC_EXCEPT else #endif
— Это либка из двух файлов. Родил за полчаса.
Не надо придираться, ныть о частностях и о возможных ошибках, это рождено реально, только что, за полчаса, в основном под воздействием вашего зашкаливающего троллинга. Это не является панацеей, commercial quality grade code и тестировалось методом двух запусков. На большее я пойтить не могу.
Тем не менее, это вполне рабочий код, он является либой чтобы снять все притязания, демонстрирует все обсуждаемые генеральные идеи и показывает что все решаемо.
Интересно, будет ли что-нибудь практическое по существу от вас, нетривиальный вы наш?
mayorovp
02.02.2018 20:20+1Знакома ли вам формальная логика?
Рассмотрим вашу программу. Верно ли в ней утверждение «результат каждого вызова malloc проверяется на 0»? Верно, потому что вызов malloc всего один, и его результат проверяется.
Так с чем же вы спорите-то, ёпрст?F376
02.02.2018 20:31Мне-то это очевидно, почему я свободно и высказался.
Но, видимо, это ясно далеко не всем моим оппонентам.
Я тоже искренне недоумеваю, что тут можно не понимать, и о чем тут можно спорить. Но, вот поди ж ты… Можно :-)mayorovp
02.02.2018 20:38Так спорите-то с этим именно вы…
F376
02.02.2018 21:05Приведите фрагмент моего текста, в котором я «спорю» со своим же предложением вынести все malloc'и, заменив их оберткой, и заявляя что это — неравноценное решение.
mayorovp
03.02.2018 09:39+2Самое первое же ваше сообщение:
Рекомендация загромождать код проверкой воспринимается мной как анти-паттерн, вредная рекомендация.
В нем вы, по форме, спорите с автором статьи, а автор статьи писал именно это:
Всегда сразу проверяйте указатель, который вернула функция malloc или аналогичная ей.
Вместо того чтобы оспаривать тот совет которому вы, в итоге, сами же следуете, вам следовало бы написать уточнение. Что-то вроде вот такого:
Если следовать рекомендации бездумно, то код окажется загроможден дополнительной проверкой. Иногда лучше вынести вызов malloc и эту проверку в отдельную функцию, в которой...
В таком случае было бы хотя бы сразу понятно что именно вы предлагаете сделать.
F376
03.02.2018 13:49Нет, я не спорю с автором статьи, а вместо повсеместных проверок предлагаю чуть более красивое решение.
Вместо того чтобы оспаривать тот совет которому вы, в итоге, сами же следуете, вам следовало бы написать уточнение.
...В таком случае было бы хотя бы сразу понятно что именно вы предлагаете сделать.
Прочтите мой исходный комментарий до конца.
В его конце содержится ровно то, чем вы мне пеняете. Я предлагаю сделать (цитирую конец своего комментария):
Как выглядит более корректное решение?
На мой взгляд лучше не использовать эти системные вызовы напрямую. Использовать или другие библиотечные функции, или написать свои обертки и использовать их. Основная задача — оставить в коде только семантику работы с памятью, не загромождая его ничем посторонним.
Ключевая мысль: оставить в коде только семантику работы с памятью, не загромождая его ничем посторонним.
Как и почему это «работает», какие у этого есть небольшие дополнительные преимущества, а также краткую иллюстрацию си-кодом вы найдете в этой ветке комментариев, там же я кратко выделил всю суть дела (повторно цитирую): Andrey2008 объяснил почему результат вызова malloc() проверять надо, и аргументировал это в общем случае UB в случае отказа функции вернуть память и возврата null pointer. Я на это дополнил тем, что не совсем красиво будет ставить везде проверки, и что это функцию лучше обернуть проверкой, создав условие что null pointer никогда не вернется, также как это делает xmalloc.
Разумеется, я понимаю что проверки в одном месте (в обертке) в целом изоморфны проверкам в каждом месте после вызова.
Можно добиться как полностью сходного поведения программ, так и различного, никакой автоматической «панацеей» данное решение не является.
Помимо этого иметь под «рукой» свою замену malloc (а-ля «перехваченный») очень удобно исходя из массы других соображений. У меня это must have.khim
03.02.2018 22:30Ключевая мысль: оставить в коде только семантику работы с памятью, не загромождая его ничем посторонним.
Прекрасно, великолепно, супер! Мыши, станьте ёжиками!
А теперь вспомним, наконец, о том, что мы говорим о библиотеках (в коде Хромиума, где мы можем контролировать всё и вся, разумеется в основном обёртки и используются), то есть у нас есть два дополнительных ограничения:
1. Библиотеки не должна обрушивать всю программу целиком — во многих программах подобное поведение просто недопустимо.
2. Использовать исключения также нельзя, потому что на многих платформах и во многих организациях их использование запрещено.
Что и переводит нашу проблему в практическую плоскость: как, собственно, вы собираетесь писать вашу обёртку, чтобы удовлетворить этим самоочевидным требованиям.
Пока ничего, кроме словоблудия и «махания руками» вы не предложили — а ведь это ключевой момент! Все виденные мною техники очень серьёзно влияют на программу и во многих случаях их использование было менее удобно, чем навешивание атрибута warn_unused_result на malloc и компиляция с -Werror.
Заметьте: в отличие от вас я, как раз, знаю несколько способов это сделать — но также вижу, что это не панацея. Вы, в отличие от меня, категорично заявляете, что это — единственный возможный подход, что немедленно вызывает реакцию Architecture astronautus detected, fire protocol enabledF376
04.02.2018 19:45-2Что и переводит нашу проблему в практическую плоскость: как, собственно, вы собираетесь писать вашу обёртку, чтобы удовлетворить этим самоочевидным требованиям.
Нет, не переводит. Экий вы шустрик. Это можно обсудить, вот только не в комментариях, и не в той форме, в которой вы это мне всё высказываете — не в форме похабного наезда.
Пока ничего, кроме словоблудия и «махания руками» вы не предложили — а ведь это ключевой момент!
Кто вам сказал, что в комментариях под чужой тематической статьёй я вообще, должен предлагать какие-то замечательные универсальные решения? Это вы с чего взяли? От большого ума?
Врать что я ничего не предлагал не надо.
Я предложил то, что посильно сделать в комментарии — самое первейшее решение a-must-have (задел на будущее) — это не обращаться НАПРЯМУЮ к malloc-подобным функциям, предусматривая в качестве задела нечто своё. Это первое что я написал в первом же сообщении! Первый и весьма разумный шаг.
В чем проблема? С чем спорим? Это что, как-то хуже? Нет, в простейшем случае это полностью равноценное решение прямому вызову malloc'а. Но зато чуть позже (вместе с аналогичным использованием своей free) — это даст ряд «вкусностей».
Это простейшее первичное решение «обёртка», вообще говоря, ничто иное, как собственный менеджер памяти. Со всеми вытекающими. В простейшем случае, как в комментарии — он заглушка, чекающая результат malloc. Далее у него можно спросить хэндл закладки для серии аллокаций с тем чтобы выйти из ряда вызовов, освобождая цепочки аллоцированных блоков. Можно попросить free(ptr1, ptr2); — т.е. «освободи-ка разом всю память что была между аллокациями такими-то и такими-то».
Заметьте: в отличие от вас я, как раз, знаю несколько способов это сделать Вы, в отличие от меня,
Да, с ЧСВ у вас всё в порядке. Прям интересно узнать — вы это такие выводы на базе чего делаете?
Вообще, годам к 30-ти должно уже отпускать кто «лучшее», а кто нет. Кто чего там «знает». На западе я тут вообще не замечал пальцев веером.
Но раз вы обозначились, охотно верю, поведайте несколько способов это сделать. Буду ждать.
категорично заявляете, что это — единственный возможный подход, что немедленно вызывает реакцию Architecture astronautus detected, fire protocol enabled
Зачем вы врёте?
Приведите ссылку на мое сообщение, где бы я © «категорично заявлял» что это единственный подход.
Исходная мысль была очень проста — вообще-то, размножать повсеместно один и тот же код не следует.
https://en.wikipedia.org/wiki/Don't repeat yourself
И следует подумать как от этого избавиться наиболее элегантным способом.
Чего вы хотите от комментариев?
Я привел самое простейшее и самое прямолинейное, очевидное решение. Не лучшее, просто пример. ЭТО НЕ ЗНАЧИТ ЧТО ЭТО ПАНАЦЕЯ НА ВЕКИ ВЕКОВ, ДЛЯ ВСЕХ СЛУЧАЕВ И ВОЛШЕБНОЕ РЕШЕНИЕ — кто так думает и кидается с пеной это обсуждать… ну… у меня плохие новости…
Не надо никого хватать за горло и тут же требовать волшебной панацеи, применимой всегда и везде, и причем написать это тут же, в комментах. И объяснять что это невозможно тоже не надо, надо уметь общаться на определенном уровне.khim
05.02.2018 01:36Я предложил то, что посильно сделать в комментарии — самое первейшее решение a-must-have (задел на будущее) — это не обращаться НАПРЯМУЮ к malloc-подобным функциям, предусматривая в качестве задела нечто своё. Это первое что я написал в первом же сообщении! Первый и весьма разумный шаг.
Вообще-то подобный шаг должен быть последним — после того, как вы посмотрели на альтернативы и убедились, что вариант с проверками в вашем конкретном случае действительно неприемлем.
В зависимости от специфики вашей библиотеки это может быть проблемой или не быть, никакого абсолютного и «очевидно правильного» решения тут нет.
Но раз вы обозначились, охотно верю, поведайте несколько способов это сделать. Буду ждать.
Первый вариант — описан выше.
Преимущества: не нужно бороться с проблемами выделения памяти вообще, код не содержит ни небезопасных операций «отката», ни связанной с этими «откатами» логики.
Недостатки: резервный буфер занимает место в памяти, которое почти никогда не используется, ошибка в оценке его размера приведёт к тому, что программа может упасть.
Второй вариант: setjmp/longjmp. В том месте, где ваша библиотека получает управление они вызывает setjmp, в обёрдке для malloc'а — соотвественно происходит выход из библиотеки.
Преимущества: относительно невысокие накладные расходы если ваша библиотека преимущественно «счётная» и имеет не слишком «широкий» API.
Недостатки: при бездумном использовании setjmp/longjmp исключения могут быть потеряны, потому в случае, когда вы всё-таки собираетесь на платформе с их поддержкой вам нужно будет их ловить, например, при вызове callback'ов и передавать дальше. Или, как альтернатива, потребовать, чтобы пользователи библиотеки исключениями не пользовался.
Вариант номер три (для Windows неприменим): вынести «тяжёлую» работу в отдельный процесс. При этом вы можете в случае нехватки памяти спокойно завершить его с помощьюexit
(как в вашем предложении).
Преимущества: так как это, фактически, развитие вашего варианта, то вы их и сами знаете.
Недостатки: порождение нового процесса достаточно дорого на MacOS и невозможно под Windows. Передача данных в код, работающий в отдельном процессе либо медленная (пайпы, сокеты), либо требует достаточно много дополнительного кода и аккуратного написания (разделяемая памяти).
Практически этот вариант приходится использовать только от безвыходности: если у вас уже есть библиотека, написанная в подобном стиле и вы не можете взять вместо неё что-то другое.
Приведите ссылку на мое сообщение, где бы я © «категорично заявлял» что это единственный подход.
Вот, например: Построить обертку и не использовать malloc() повсеместно (т.е. на application-level уровне) — это именно то, что следует сделать. В чем тут проблема и какие тут «овраги»?
С этого ведь всё началось: с вашего категоричного комментария, что обёртки — это то, что только и нужно использовать. Комментария к статье, где, ещё раз напоминаю, обсуждаются только и исключительно библиотеки.
Приложение может, в конце концов, заменить malloc на свою собственную реализацию (как, кстати, Chromium и делает) — никакие обёртки после этого не нужны. Но библиотеки такой роскоши себе позволить увы, не могут.F376
05.02.2018 04:23с вашего категоричного комментария, что обёртки — это то, что только и нужно использовать. Комментария к статье, где, ещё раз напоминаю, обсуждаются только и исключительно библиотеки.
Не надо видеть слова «только», там где вам угодно и выгодно их видеть. Когда человек читает только то, что ему хочется (и еще в этом начинает уверять меня!!!) — это батенька, шизофреническое расстройство восприятия.
То что вы пишете — я такого не имел в виду. Не надо произвольно расширять тезисы, ага? А то это враньё.
То что я имел в виду (и только это, без вашего произвольного расширения) — я написал выше другим людям НЕОДНОКРАТНО. Повторять это и цитировать вам уже по третьему разу — считаю будет бредово, давайте читайте сам, сам, сам. Но не расширяя тезисы как вам это угодно!
Таким образом, все что вы здесь написали (вы конечно молодец) никоим образом не противоречит сказанному мной, все что вы там себе надумали — это ваши личные проблемы.
F376
05.02.2018 05:34Приложение может, в конце концов, заменить malloc на свою собственную реализацию (как, кстати, Chromium и делает) — никакие обёртки после этого не нужны. Но библиотеки такой роскоши себе позволить увы, не могут.
Я уверен что и «обертку» вы как-то очень специфично по-своему понимаете. А что по-вашему «обертка», как не замена malloc'а на свою собственную реализацию? Я об этом в явном виде писал.
Вариант номер три (для Windows неприменим): вынести «тяжёлую» работу в отдельный процесс. При этом вы можете в случае нехватки памяти спокойно завершить его с помощью exit (как в вашем предложении).
Преимущества: так как это, фактически, развитие вашего варианта, то вы их и сами знаете.
Общение через shared memory достаточно быстрое. Процесс будет срублен только в случае фатальных происшествий, поэтому вполне себе рабочее решение — кстати приснопамятный Chrome представляет собой именно такой набор процессов.
Однако же должен упомянуть, (сам я, а не то, как вы додумали) ни в коем случае я не предлагал исходно рубить ВООБЩЕ ЛЮБОЙ ПРОИЗВОЛЬНЫЙ процесс, с помощью exit(), это вы уже произвольно додумали наблюдая просто образец кода, в котором exit() только для иллюстрации.
Который, впрочем, на практике-то как раз, вопреки всем исходно «навешанным на меня собакам» и всем крикам — имеет право на существование и имеет место быть! В особенности с развитием парадигмы Microservices. Причем на *NIX это вполне себе устоявшаяся парадигма программирования — порождение процессов вместо потоков и рубка их в случае чего.
А вот остальное, все что вы написали КРОМЕ отсылки к замене malloc и упомянутого мной выше коллбэка — это всё очень генеральные вещи, не имеющие НЕПОСРЕДСТВЕННОГО отношения к теме автора:
А именно, к функциям аллокации памяти + проверке возвращаемого значения. Это у вас произвольный текст «на тему». Он вообще никак не противоречит и пересекается с поднятой автором и мной темой постольку-поскольку, соприкасаясь с ней некоторыми фрагментами. То есть опять произвольное «расширение». Смысл? Ну смысл есть на поболтать. Но мои тезисы это никак не опровергло.
Насчет буфера.
Я также могу сейчас как это выше устроили, устроить балаган, закуситься с криком «где это вы возьмете доп. буфера в микроконтроллере» — и из этого устроить распальцовку с криками «аааа, не понимаете! А покажите мне доп. буферов на микроконтроллере с 2кб памяти». Слава богу, у меня все в порядке с рассудком. Но вы-то сделали по сути именно так!
Если обсуждать около-темы выделения памяти, вы не упомянули самые интересные и очень действенные методы. Что ж, «распальсую пальсы»:
Это:
а) Компрессия оперативной памяти.
б) Уничтожение содержащихся в памяти данных в случае нехватки памяти и воссоздание данных, там где можно (загрузка с диска).
в) Memory-mapped files.
г) Дефрагментация памяти.
д) Вызов garbage collector (шутка) :) если используется GC-архитектура.
Но это повторяю, к тому что мы обсуждаем постольку-поскольку. Не более чем «текст на тему».
P.S.
У меня тут отпуск и весь этот «обмен распальсовками» местных уникумов у меня просто отнимает время. Особо нового я ничего не узнал. Никто мне ничего уникального не поведал. Поэтому не обессудьте если я исчезну, смысл всех этих «распальсовок» исходно стремился к нулю.
datacompboy
02.02.2018 14:30Я добавлю еще вот это: github.com/gperftools/gperftools/wiki/OOM-handling
staspavlov92
02.02.2018 15:37+1Не очень понимаю споров в комментариях выше. Как можно защищать UB, конкретные примеры которого привели в статье? Это же абсолютное зло, Undefined может завтра привести к чему угодно. С каких пор в проге появился такой критерий «сейчас в подавляющем большинстве случаев сработает»? Код программы должен стремиться к 100% гарантии правильного поведения. Да, случайные ошибки в коде на то и случайные ошибки, что появляться будут все равно. Но сознательно ставить в код UB, потому что одна «лишняя» проверка режет глаз — это что-то крайне мне непонятное.
kogemrka
02.02.2018 16:04Как можно защищать UB, конкретные примеры которого привели в статье?
Психологическая защита же.
linuxover
02.02.2018 16:13Это же абсолютное зло, Undefined может завтра привести к чему угодно
а может и не привести.
мы же с Вами технические специалисты? давайте посмотрим/посчитаем? зачем сразу бросаться на амбразуру и дыры затыкать, которые может быть никому не интересны?
linuxover
02.02.2018 15:39-1> Не очень понимаю споров в комментариях выше. Как можно защищать UB, конкретные примеры которого привели в статье?
1. программа с UB в граничных условиях использования стоит X
2. программа без UB в граничных условиях использования стоит Y
все упирается в дельту `Y — X` и в то насколько часто программа попадает в эти граничные условия.onyxmaster
04.02.2018 10:55+2Вы совершенно правы в способе оценки, но на мой взгляд сильно ошибаетесь в самой оценке этой разницы для ПО, которое разрабатывается не в качестве одноразовой «поделки».
linuxover
04.02.2018 14:25но на мой взгляд сильно ошибаетесь в самой оценке этой разницы для ПО
если разницу сводить только к malloc, то наверно и разговаривать не о чем, правда? в том числе и статья становится ненужной?
а если поднятый вопрос обобщить, то тут сведется к вопросу "перфекционизм и его стоимость: в каких случаях платить?"
и если речь идет об UB в граничных условиях, то вопрос "стоит ли платить?" вполне себе в полный рост может стоять.
Andrey2008 Автор
04.02.2018 15:51+4Отсутствие проверки после malloc — это не отсутствие перфекционизма. Это отсутствие профессионализма. И печально, что многие это считают нормой. Мельчает C++ программист :).
P.S. А в прочем, это даже к лучшему. Будет больше возможностей продавать PVS-Studio, когда вдруг припрёт, и поделие таких «программистов» вдруг станет большим, востребованным, и окажется, что весь этот код надо как-то поддерживать и развивать. Благословляю на говнокодинг :).linuxover
04.02.2018 21:02-1Отсутствие проверки после malloc — это не отсутствие перфекционизма
в Вашем случае это именно перфекционизм. Имеется проект — браузер. Он работает у множества пользователей.
Этот проект ОЧЕНЬ большой и те 75 malloc что в нем без проверки — нефатальны.
а вот то что например этот проект нельзя использовать на современном HiDPI мониторе/ноутбуке — для меня, как для пользователя, гораздо более фатально чем то, что он "может упасть" (а может и не упасть) в условиях нехватки памяти.
чтобы пофиксить проблему HiDPI разработчикам хрома нужно пару дней, но в течение уже более трех лет они не могут их выделить.
чтобы пофиксить те 75 маллоков, нужно по видимому столько же.
фикс маллоков — чистый перфекционизм.
фикс HiDPI — решение РЕАЛЬНЫХ проблем пользователейkhim
05.02.2018 01:41а вот то что например этот проект нельзя использовать на современном HiDPI мониторе/ноутбуке
А мужики-то и не знают. Я уже четвёртый год использую Chrome на X1 Carbon в разрешении 2560x1440 (15", если что). Это — недостаточно Hi?linuxover
05.02.2018 07:28+1Это — недостаточно Hi?
ага, это — недостаточно Hi, ибо оно на уровне Вашей OS не совсем Hi.
Включите честные Hi на уровне ВСЕХ приложений (ЕМНИП это скачиванием программки делается) и посмотрите как станет выглядеть Хром.
PS: Файрфокс очень долго (с версии ЕМНИП 26) нормально работал с HiDPI, но в последних версиях тоже сломали. Сейчас у них есть тикет, и помимо тикета есть FF45, которым МОЖНО пользоваться.
У хрома, увы нет даже возможности пользоваться им на HiDPI мониторе (Ваш кейз — когда из Hi сделать недо-Hi на уровне ВСЕЙ операционки, то есть де-факто свести разрешение к половинному — я не рассматриваю как Hi).
linuxover
04.02.2018 21:09-1Это отсутствие профессионализма.
увы, поручаешь многим рассуждающим о профессионализме задачу, описание которой написано даже в википедии и они не справляются с ее решением.
потом начинаешь разбираться — почему? что же им помешало?
и видишь: что человек начал с нагромождения сумасшедшего "фреймворка для решения всех проблем вообще" и на деле делал не то что ему поручили, а как настоящий профессионал решал задачу "как будет вести себя моё ПО в граничных условиях, в которые никогда не попадет".
в итоге нарастил сложность до такого уровня что сам с ней не совладал.
особо это видно в проектах Яндекса: наваяли на python проект, потом переписали, потом заменили mysql на pg, затем переписывают на C++. Когда их пытаешься мягко осадить "ребята, может лучше об архитектуре сперва немного подумать?" тебе в ответ — "мы профессионалы, лучше знаем!"
Andrey2008 Автор
04.02.2018 23:06+2Как много болтологии, чтобы оправдать нежелание и неумение обрабатывать ошибки. Мы сейчас занимаемся полноценной поддержкой компилятора Keil. В целях проверки (ложных срабатываний и т.п.) я сейчас разбираю отчёт об анализе проекта RT-Thread. И вот что интересно, почему-то там разработчики умеют обращаться с malloc. Проверяют и обрабатывают нехватку памяти (первый попавшийся пример):
Им это не кажется чем-то нереально сложным или ненужным. Вот они молодцы. Просто взяли и сделали обработку ошибок, а не занялись отмазками./* allocate memory */ mp->start_address = rt_malloc((block_size + sizeof(rt_uint8_t *)) * block_count); if (mp->start_address == RT_NULL) { /* no memory, delete memory pool object */ rt_object_delete(&(mp->parent)); return RT_NULL; }
P.S. Есть ли там ошибки? Конечно есть. Например, вот эта понравилась.
PVS-Studio: V614 CWE-457 Uninitialized variable 'k' used. lpc_lcd.c 510void LCD_PutPixel (LCD_PANEL panel, uint32_t X_Left, uint32_t Y_Up, LcdPixel_t color) { uint32_t k; uint32_t * pWordData = NULL; uint8_t* pByteData = NULL; uint32_t bitOffset; uint8_t* pByteSrc = (uint8_t*)&color; uint8_t bpp = bits_per_pixel[lcd_config.lcd_bpp]; uint8_t bytes_per_pixel = bpp/8; uint32_t start_bit; if((X_Left >= lcd_hsize)||(Y_Up >= lcd_vsize)) return; if(panel == LCD_PANEL_UPPER) pWordData = (uint32_t*) LPC_LCD->UPBASE + LCD_GetWordOffset(X_Left,Y_Up); else pWordData = (uint32_t*) LPC_LCD->LPBASE + LCD_GetWordOffset(X_Left,Y_Up); bitOffset = LCD_GetBitOffset(X_Left,Y_Up); pByteData = (uint8_t*) pWordData; pByteData += bitOffset/8; start_bit = bitOffset%8; if(bpp < 8) { uint8_t bit_pos = start_bit; uint8_t bit_ofs = 0; for(bit_ofs = 0;bit_ofs <bpp; bit_ofs++,bit_pos++) { *pByteData &= ~ (0x01 << bit_pos); *pByteData |= ((*pByteSrc >> (k+bit_ofs)) & 0x01) << bit_pos; // переменная k неинициализированная } } else { for(k = 0; k < bytes_per_pixel; k++) { *(pByteData+ k) = *pByteSrc++; } } }
F376
05.02.2018 06:25-11. Я вообще за то чтобы по-возможности не одобрять в pure C++ прямые выделения/деаллокации памяти в сишном стиле и работу с памятью.
2. Можно двигаться в сторону GC — архитектур + «активных» указателей/ссылок, т.е. смартпоинтеров. У народа есть опыт написания всей 3D engine полностью на смартпоинтерах, падение производительности незначительное.
3. Можно двигаться через создание domain-specific language's. Втч возможно динамически комплируемых, (с автоматическим управлением памятью) возможно, но не обязательно с байткодом, и возможно, но не обязательно с отложенной/статической компиляцией. Сейчас пока с этим заминка, основные VM пока громоздкие и малоупотребимые, а Framework'и для создания собственных DSL слишком сложны. Но это пока. В ближайшем будущем очень вероятен взрыв Intra-CPP решений, когда внутри/параллельно C++ решения находится несколько самопальных язычков под свои задачи.
4. Парадигма вида «попросить память» — что-то сделать — «отдать память» — древняя, архаичная как г. мамонта. Это идет с майнфреймов с виртуальной памятью. Вообще с мохнатых годов существуют иные способы работы с данными, не требующие парадигмы «запрос-работа-освобождение» и вообще даже не требующие понятия «память».eao197
05.02.2018 09:38+3Вы бы статью написали с развернутыми изложением этих тезисов. Чтобы люди могли вдоволь по вашим заблуждениям оттоптаться.
Написать в комментариях к чужой статье «Я вообще за то чтобы по-возможности не одобрять в pure C++ прямые выделения/деаллокации памяти в сишном стиле и работу с памятью.» Не нужно ни большого ума, ни большого труда. Тем более, что в C++ работа с памятью в С-шном стиле не одобряется уже лет тридцать как.
А вот сделать статью и затем получить сотню комментариев с оценками степени вашей оригинальности и оторванности от жизни… Вот это совсем другое. Ну не обсуждать же "Парадигма вида «попросить память» — что-то сделать — «отдать память» — древняя, архаичная как г. мамонта." в комментариях к статье, в которой говорится, как правильно работать именно в этой парадигме.F376
05.02.2018 14:14Так © «в C++ работа с памятью в С-шном стиле не одобряется уже лет тридцать как» или «вдоволь по вашим заблуждениям оттоптаться». Должно быть что-то одно.
Это я ответил Andrey2008, но что-то пошло не так. Смысл риторического комментария был в том, что от malloc-free или new-delete по-хорошему надо бы вообще по возможности отдаляться. Ясно что он это понимает, и вы это понимаете, и мой последний оппонент понимает, да выше пример был — в Chromium функции не используются, но как риторическое замечание — почему бы и нет? Почему это надо воспринимать сразу в штыки? :)
Так что спасибо за предложение, но существует ли надобность в такой статье? Без данного материала необходимости не было. И вряд ли существует в реальности. Разве есть смысл писать статью для комментов.
Я подумаю написать статью, спасибо. Но мне кажется надо начинать с чего-то попроще.
С комментариев начал, а оно что-то в штыки вышло :) ...?
AEP
Неконструктивная статья. Да, проверять возвращаемое malloc значение необходимо, иначе неопределенное поведение. Но и обрабатывать по-умному ошибку, когда malloc вернул NULL, тоже нельзя, поскольку этот случай толком невозможно протестировать. Только еще ошибок при обработке ошибок наделаете.
Распространенное конструктивное решение (за отсутствие упоминания которого я и ставлю минус) состоит в написании функций-врапперов для выделения памяти. Распространенное название одной из них — xmalloc(). Поведение: вызывает malloc() с тем же параметром, если вернулся NULL — вызывает abort() для явного аварийного завершения программы, если не NULL — возвращает указатель на выделенную память. За xmalloc() проверять не надо.
Понимаю, что решение не универсальное, и что за него в некоторых случаях тоже ругают.
Vest
А что мешает везде проверять и валиться с «Out of memory», вместо непонятных segmentation faults? Одно дело я вижу проблему, а другое — внезапный crash, да и ещё, как я понимаю, может мне затереть память в неизвестном месте.
Поправьте меня, пожалуйста, если я не прав.
Andrey2008 Автор
kutelev
Очень даже можно. На Linux'е есть LD_PRELOAD, на macOS DYLD_INSERT_LIBRARIES. Пишем библиотеку которая оборачивает системный malloc. В своей обёртке над malloc'ом вы вольны возвращать NULL когда вам захочется.
Для Windows решение тоже имеется, но реализуется намного сложнее чем на NIX'ах.
Правда вынужден сказать, что заваливание malloc'а достойно переживает только Linux'вый рантайм. На macOS даже printf падает при невозможности выделить память. Эти неприятные мелочи приходится учитывать при тестировании.
AEP
Легко сказать «пишем библиотеку». Вопрос в том, как обвалить конкретно этот вызов malloc(), а не какой-то другой, чтобы протестировать код, обрабатывающий именно этот NULL, а не NULL где-то еще.
cdump
В gcc можно смотреть на __builtin_return_address (https://gcc.gnu.org/onlinedocs/gcc/Return-Address.html), а из него уже понять имя функции/строку (если собраны с дебагом)
splav_asv
Иногда прерывать нельзя, но все же: поставить под отладчиком остановку по условию и поменять результат на NULL чем не нравится?