Переопределение NULL - PullRequest
       0

Переопределение NULL

118 голосов
/ 28 февраля 2011

Я пишу код C для системы, в которой адрес 0x0000 действителен и содержит порт ввода-вывода. Поэтому любые возможные ошибки, которые обращаются к нулевому указателю, останутся незамеченными и в то же время вызовут опасное поведение.

По этой причине я хочу переопределить NULL, чтобы он стал другим адресом, например, к недействительному адресу. Если я случайно получу доступ к такому адресу, я получу аппаратное прерывание, где я могу обработать ошибку. У меня есть доступ к stddef.h для этого компилятора, поэтому я могу изменить стандартный заголовок и переопределить NULL.

Мой вопрос: будет ли это противоречить стандарту C? Насколько я могу сказать из 7.17 в стандарте, макрос определяется реализацией. Есть ли что-нибудь еще в стандарте, утверждающее, что NULL должно быть 0?

Другая проблема заключается в том, что множество компиляторов выполняют статическую инициализацию, устанавливая все на ноль, независимо от типа данных. Хотя в стандарте сказано, что компилятор должен устанавливать целые числа в ноль и указатели в NULL. Если бы я переопределил NULL для моего компилятора, то я знаю, что такая статическая инициализация не удастся. Могу ли я считать это неправильным поведением компилятора, хотя я смело изменял заголовки компилятора вручную? Потому что я точно знаю, что этот конкретный компилятор не обращается к макросу NULL при статической инициализации.

Ответы [ 7 ]

82 голосов
/ 28 февраля 2011

Стандарт C не требует, чтобы нулевые указатели были по нулевому адресу машины. ОДНАКО приведение константы 0 к значению указателя должно привести к указателю NULL (§6.3.2.3 / 3), а оценка нулевого указателя как логического значения должна быть ложной. Это может быть немного неловко, если вы действительно делаете хотите нулевой адрес, а NULL не является нулевым адресом.

Тем не менее, с (тяжелыми) модификациями компилятора и стандартной библиотеки, не исключено, что NULL будет представлен альтернативным битовым шаблоном, оставаясь при этом строго совместимым со стандартной библиотекой. Однако не достаточно, чтобы просто изменить определение самого NULL, так как тогда NULL будет иметь значение true.

В частности, вам необходимо:

  • Организовать буквальные нули в назначениях указателей (или приведений к указателям) для преобразования в какую-то другую магическую ценность, такую ​​как -1.
  • Организовать тесты на равенство между указателями и постоянным целым числом 0, чтобы вместо этого проверить магическое значение (§6.5.9 / 6)
  • Организовать для всех контекстов, в которых тип указателя оценивается как логическое значение, для проверки на равенство магическому значению вместо проверки на ноль. Это следует из семантики проверки на равенство, но компилятор может реализовать ее внутренне иначе. См. §6.5.13 / 3, §6.5.14 / 3, §6.5.15 / 4, §6.5.3.3 / 5, §6.8.4.1 / 2, §6.8.5 / 4
  • Как указано в caf, обновите семантику для инициализации статических объектов (§6.7.8 / 10) и частичных составных инициализаторов (§6.7.8 / 21), чтобы отразить новое представление нулевого указателя.
  • Создать альтернативный способ доступа к истинному нулевому адресу.

Есть некоторые вещи, которые вы должны не обрабатывать. Например:

int x = 0;
void *p = (void*)x;

После этого p НЕ гарантированно будет нулевым указателем. Необходимо обрабатывать только постоянные назначения (это хороший подход для доступа к истинному нулевому адресу). Точно так же:

int x = 0;
assert(x == (void*)0); // CAN BE FALSE

Также:

void *p = NULL;
int x = (int)p;

x не гарантируется равным 0.

Короче говоря, это самое условие, по-видимому, рассматривалось комитетом по языку Си, и учитывались те, кто выбрал бы альтернативное представление для NULL. Все, что вам нужно сделать сейчас, это внести серьезные изменения в ваш компилятор, и, эй, готово:)

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

p = 0;
if (p) { ... }
/* becomes */
p = (void*)-1;
if ((void*)(p) != (void*)(-1)) { ... }

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

19 голосов
/ 28 февраля 2011

Стандарт гласит, что целочисленное константное выражение со значением 0 или такое выражение, преобразованное в тип void *, является константой нулевого указателя.Это означает, что (void *)0 всегда является нулевым указателем, но с учетом int i = 0;, (void *)i не обязательно.

Реализация C состоит из компилятора вместе с его заголовками.Если вы измените заголовки так, чтобы переопределить NULL, но не изменили компилятор для исправления статических инициализаций, то вы создали несоответствующую реализацию.Вся реализация, взятая вместе, имеет некорректное поведение, и если вы ее сломали, вам больше некого винить;)

Вы должны исправить больше, чем просто статические инициализации, конечно - с указателемp, if (p) эквивалентно if (p != NULL) из-за вышеуказанного правила.

7 голосов
/ 28 февраля 2011

Если вы используете библиотеку C std, у вас возникнут проблемы с функциями, которые могут возвращать NULL. Например, в документации malloc указано:

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

Поскольку malloc и связанные функции уже скомпилированы в двоичные файлы с определенным значением NULL, если вы переопределите NULL, вы не сможете напрямую использовать библиотеку C std, если не сможете перестроить всю цепочку инструментов, включая C std ЛИЭС.

Также из-за того, что библиотека std использует NULL, если вы переопределите NULL перед включением заголовков std, вы можете перезаписать определение NULL, указанное в заголовках. Все встроенное будет несовместимо с скомпилированными объектами.

Вместо этого я бы определил ваш собственный NULL, "MYPRODUCT_NULL", для ваших собственных нужд, а также избегайте или переводите из / в библиотеку C std.

6 голосов
/ 28 февраля 2011

Оставьте NULL в покое и обрабатывайте IO для порта 0x0000 как особый случай, возможно, используя подпрограмму, написанную на ассемблере, и, следовательно, не подчиняющуюся стандартной семантике C. IOW, не переопределяйте NULL, переопределяйте порт 0x00000.

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

3 голосов
/ 28 февраля 2011

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

  #define CREATE_HW_ADDR(x)(x+1)
  #define DEREFERENCE_HW_ADDR(x)(*(x-1))

  int* wellKnownIoPort = CREATE_HW_ADDR(0x00000000);

  printf("IoPortIs" DEREFERENCE_HW_ADDR(wellKnownIoPort));

Если адреса, по которым вы работаете, сгруппированы вместе, и вы можете чувствовать себя в безопасности, если добавление 1 к адресу не будет конфликтовать ни с чем (что не должно происходить в большинстве случаев), вы можете сделать это безопасно , И тогда вам не нужно беспокоиться о перестройке цепочки инструментов / std lib и выражений в виде:

  if (pointer)
  {
     ...
  }

все еще работает

Сумасшедший, я знаю, но просто подумал, что я добавлю эту идею:).

2 голосов
/ 28 февраля 2011

Битовый шаблон для нулевого указателя может не совпадать с битовым шаблоном для целого числа 0. Но расширение макроса NULL должно быть константой нулевого указателя, то есть постоянным целым числом значения 0, которое может быть приведеноto (void *).

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

1 голос
/ 28 февраля 2011

Вы просите неприятностей.Переопределение NULL к ненулевому значению нарушит этот код:

   if (myPointer)
   {
      // myPointer is not null
      ...
   }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...