Почему MISRA C утверждает, что копия указателей может вызвать исключение памяти?

директива 4.12 MISRA c 2012 "динамическое выделение памяти не должно использоваться".

в качестве примера в документе приведен пример кода:

char *p = (char *) malloc(10);
char *q;

free(p);
q = p; /* Undefined behaviour - value of p is indeterminate */

и в документе говорится, что:

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

Я в порядке почти со всем предложением, кроме конца. Поскольку p и q оба выделены в стеке, как копия указателей может вызвать исключение памяти ?

8 ответов


согласно стандарту, копирование указателя q = p;, это неопределенное поведение.

чтение J. 2 неопределенное поведение гласит:

используется значение указателя на объект, срок службы которого истек (6.2.4).

идешь к этой главе мы видим, что:

6.2.4 длительность хранения объектов

время жизни объекта-это часть выполнения программы во время которого хранение гарантированно зарезервировано для него. Объект существует, имеет постоянный адрес,33)и сохраняет свое последнее-сохраненное значение в течении своей продолжительности жизни.34)Если объект упоминается вне его время жизни, поведение не определено. значение указателя становится неопределенным, когда объект на который он указывает (или просто мимо) достигает конца своей жизни.

что неопределенно:

3.19.2 неопределено значение: либо неопределенное значение, либо представление trap


Как только вы освобождаете объект через указатель, все указатели на эту память становятся неопределенными. (Даже)чтение неопределенная память-это неопределенное поведение (UB). Ниже приводится UB:

char *p = malloc(5);
free(p);
if(p == NULL) // UB: even just reading value of p as here, is UB
{

}

во-первых, немного истории...

когда ISO/IEC JTC1/SC22/WG14 впервые начал формализовать язык C (для создания того, что теперь ISO / IEC 9899:2011), у них возникла проблема.

многие поставщики компиляторов интерпретировали вещи по-разному.

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

MISRA c пытается ловушка ямы-падает, что эти поведения вызовет. Вот и вся теория...

--

Теперь к конкретному вопросу:

учитывая, что из (), Чтобы освободить динамическую память обратно в кучу, было три возможных варианта, все из которых были "в диком виде":

  1. сбросить указатель на NULL
  2. оставьте указатель как был
  3. уничтожить указатель

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

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

--

Итак, TL; DR; ответ, К сожалению: потому что это так!


, а как p и q являются обеими переменными указателя в стеке, адрес памяти, возвращаемый malloc() не находится в стеке.

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

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

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

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


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

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

Я помню одну такую цель, еще в начале 1990-х годов он вел себя именно так. Не En embedded target тогда и скорее в широко распространенном использовании тогда: Windows 2.X. Он использовал архитектуру Intel в 16-битном защищенном режиме, где указатели были 32-битными, с 16-битным селектор и 16-битное смещение. Для доступа к памяти указатели загружались в пару регистров (регистр сегмента и регистр адреса) с конкретной инструкцией:

    LES  BX,[BP+4]   ; load pointer into ES:BX

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

компиляция невинного заявления q = p; может быть собрана по-разному:

    MOV  AX,[BP+4]    ; loading via DX:AX registers: no side effects
    MOV  DX,[BP+6]
    MOV  [BP-6],AX
    MOV  [BP-4],DX

или

    LES  BX,[BP+4]    ; loading via ES:BX registers: side effects
    MOV  [BP-6],BX
    MOV  [BP-4],ES

второй вариант имеет 2 Преимущества:

  • код более компактен, менее 1 инструкция

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

освобождение памяти может отменить сегмент и сделать селектор недействительным. Значение становится значением trap и загружает его в ES:BX срабатывает исключение, также называемое ловушка на некоторых архитектурах.

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

стандарт C допускает это и описывает форму неопределенного поведения кода, где:

используется значение указателя на объект, срок службы которого истек (6.2.4).

потому что это значение стало неопределенный, как определено таким образом:

3.19.2 неопределенное значение: неопределенное значение или представление ловушки

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

/* dumping the value of the free'd pointer */
unsigned char *pc = (unsigned char*)&p;
size_t i;
for (i = 0; i < sizeof(p); i++)
    printf("%02X", pc[i]);   /* no problem here */

/* copying the value of the free'd pointer */
memcpy(&q, &p, sizeof(p));   /* no problem either */

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

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

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

IMHO, нет причин, по которым равенство, реляционное или разность указателей операторы на освобожденных указателях должны оказывать отрицательное влияние на любые современная система, а потому модно для компиляторов применять сумасшедшие "оптимизации", полезные конструкции, которые должны использоваться на commonplace площадки стали опасными.


плохая формулировка в примере кода выбрасывает вас.

Он говорит: "значение p неопределенно", но это не значение p, которое неопределенно, потому что p все еще имеет то же значение (адрес блока памяти, который был освобожден).

вызов free (p) не изменяет p -- p изменяется только после того, как вы покидаете область, в которой определен p.

, Это значение того, что P указывает на это, неопределенно, поскольку блок памяти был освобожден, и он также может быть отключен операционной системой. Доступ к нему через p или через псевдонимный указатель (q) может привести к нарушению доступа.

важным понятием для интернализации является значение" неопределенного "или" неопределенного " поведения. Именно это: неизвестное и непознаваемое. Мы часто говорили студентам: "совершенно законно, чтобы ваш компьютер расплавился в бесформенный шарик, или чтобы диск улетел на Марс". Когда я читал оригинальную документацию, я не видел места, где он сказал Не использовать malloc. Это просто указывает на то, что ошибочная программа потерпит неудачу. На самом деле, наличие программы занимает память исключение-это хорошо, потому что оно сразу говорит вам, что ваша программа неисправна. Почему документ предполагает, что это может быть плохо, ускользает от меня. Что плохо, так это то, что на большинстве архитектур он не будет принимать исключение памяти. Продолжение использования этого указателя приведет к ошибочным значениям, потенциально сделает кучу непригодной для использования и, если этот же блок хранения выделяется для другого использования, повредит допустимые данные этого использования или интерпретирует его значения как свои собственные. Итог: Не используйте "устаревшие" указатели! Или, другими словами, написание дефектного кода означает, что он не будет работать.

кроме того, акт присвоения p q наиболее определенно не является "неопределенным". Биты, хранящиеся в переменной p, которые являются бессмысленной бессмыслицей, довольно легко и правильно копируются в q. Все это означает, что любое значение, к которому обращается p, теперь может быть доступно q, а поскольку p-неопределенная бессмыслица, q теперь неопределенная бессмыслица. Таким образом, используя либо один из них для чтения или записи приведет к "неопределенным" результатам. Если вам посчастливилось работать на архитектуре, которая может привести к сбою памяти, вы легко обнаружите неправильное использование. В противном случае использование любого указателя означает, что ваша программа неисправна. Планирую потратить много часов на его поиски.