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

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

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

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

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

Ответ 1

Стандарт 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
  • Как отметили в кафе, обновите семантику для инициализации статических объектов (§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.

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

В качестве побочного примечания может быть возможно реализовать эти изменения с помощью этапа трансформации исходного кода перед собственно компилятором. То есть вместо обычного потока препроцессора → компилятор → ассемблер → компоновщик, вы должны добавить препроцессор → NULL transform → compiler → assembler → linker. Затем вы можете делать преобразования, такие как:

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

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

Ответ 2

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

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

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

Ответ 3

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

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

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

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

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

Ответ 4

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

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

Ответ 5

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

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

Ответ 6

Учитывая чрезвычайную сложность переопределения 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)
  {
     ...
  }

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

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

Ответ 7

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

   if (myPointer)
   {
      // myPointer is not null
      ...
   }