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

118

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

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

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

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

Лундин
источник
3
Это действительно хороший вопрос. У меня нет ответа для вас, но я должен спросить: вы уверены, что невозможно переместить ваш действительный материал в 0x00 и позволить NULL быть недопустимым адресом, как в "обычных" системах? Если вы не можете, тогда единственными безопасными недействительными адресами для использования будут те, которые вы можете быть уверены, что можете выделить, а затем mprotectзащитить. Или, если платформа не имеет ASLR и т.п., адреса за пределами физической памяти платформы. Удачи.
Borealid
8
Как это будет работать, если ваш код использует if(ptr) { /* do something on ptr*/ }? Будет ли это работать, если NULL определен отличным от 0x0?
Xavier T.
3
Указатель C не имеет принудительного отношения к адресам памяти. Пока соблюдаются правила арифметики указателей, значение указателей может быть любым. Большинство реализаций предпочитают использовать адреса памяти в качестве значений указателей, но они могут использовать что угодно, если это изоморфизм.
datenwolf
2
@bdonlan Это также нарушит (рекомендательные) правила в MISRA-C.
Lundin
2
@Andreas Ага, это тоже мои мысли. Людям, занимающимся аппаратным обеспечением, нельзя позволять разрабатывать оборудование, на котором должно работать программное обеспечение! :)
Lundin

Ответы:

84

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

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

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

  • Сделайте так, чтобы буквальные нули в присваиваниях указателям (или приведения к указателям) преобразовывались в какое-либо другое магическое значение, например -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.

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

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

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

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

bdonlan
источник
2
Хорошо , я не нашел текст в §6.3.2.3, но я подозревал , что бы такое заявление где - то :). Я думаю, это отвечает на мой вопрос, по стандарту мне не разрешено переопределять NULL, если я не хочу писать новый компилятор C для поддержки меня :)
Lundin
2
Хороший трюк - взломать компилятор так, чтобы целочисленные преобразования указатель <-> выполняли XOR определенного значения, которое является недопустимым указателем и все еще достаточно тривиально, чтобы целевая архитектура могла сделать это дешево (обычно это будет значение с одним набором битов. , например 0x20000000).
Саймон Рихтер
2
Еще одна вещь, которую вам нужно будет изменить в компиляторе, - это инициализация объектов с составным типом - если объект частично инициализирован, то любые указатели, для которых нет явного инициализатора, должны быть инициализированы NULL.
caf,
20

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

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

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

кафе
источник
8

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

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

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

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

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

Дуг Т.
источник
6

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

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

апалала
источник
Проблема возникнет только при случайном доступе к NULL, а не при намеренном доступе к порту. Зачем мне тогда переопределять ввод-вывод порта? Уже работает как надо.
Lundin
2
@Lundin Случайно или нет, NULL можно разыменовать только в программе C с использованием *p, p[]или p(), поэтому компилятору нужно заботиться только о них, чтобы защитить порт ввода-вывода 0x0000.
Apalala
@Lundin Вторая часть вашего вопроса: как только вы ограничите доступ к нулевому адресу изнутри C, вам понадобится другой способ доступа к порту 0x0000. Это может сделать функция, написанная на ассемблере. Изнутри C порт может быть сопоставлен с 0xFFFF или чем-то еще, но лучше использовать функцию и забыть о номере порта.
Apalala
3

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

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

Безумно знаю, но просто подумал, что выкину эту идею там :).

Дуг Т.
источник
Проблема возникнет только при случайном доступе к NULL, а не при намеренном доступе к порту. Зачем мне тогда переопределять ввод-вывод порта? Уже работает как надо.
Lundin
@LundIn Я думаю, вам нужно выбрать, что более болезненно, настроить перестройку всей цепочки инструментов или изменить эту часть вашего кода.
Doug T.
2

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

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

AProgrammer
источник
1

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

   если (myPointer)
   {
      // myPointer не равен нулю
      ...
   }
Тони Пони
источник