Перевизначення NULL


118

Я пишу код C для системи, де адреса 0x0000 є дійсною і містить порт вводу / виводу. Тому будь-які можливі помилки, які отримують доступ до вказівника NULL, залишаться невиявленими та водночас спричинятимуть небезпечну поведінку.

З цієї причини я хочу змінити NULL як іншу адресу, наприклад, адресу, що не відповідає дійсності. Якщо я випадково отримав доступ до такої адреси, я отримаю апаратне переривання, де я можу виправити помилку. У мене, можливо, є доступ до stddef.h для цього компілятора, тому я фактично можу змінити стандартний заголовок і переглянути нову NULL.

Моє запитання: чи буде це конфліктом зі стандартом С? Наскільки я можу сказати з 7,17 у стандарті, макрос визначений реалізацією. Чи є ще щось у стандарті, що говорить про те, що NULL має бути 0?

Інша проблема полягає в тому, що велика кількість компіляторів здійснює статичну ініціалізацію, встановлюючи все на нуль, незалежно від типу даних. Навіть незважаючи на те, що стандарт говорить, що компілятор повинен встановити цілі числа на нуль, а покажчики на NULL. Якщо я міг би визначити NULL для свого компілятора, то я знаю, що така статична ініціалізація вийде з ладу. Чи можу я вважати це неправильним поведінкою компілятора, хоча я сміливо змінював заголовки компілятора вручну? Тому що я точно знаю, що саме цей компілятор не отримує доступу до макросу NULL, коли робить статичну ініціалізацію.


3
Це справді гарне запитання. У мене немає відповіді на вас, але я мушу запитати: ви впевнені, що неможливо перемістити ваші дійсні речі на відстані 0x00 і дозволити NULL недійсною адресою, як у "звичайних" системах? Якщо ви не можете, то єдиними безпечно недопустимими адресами можуть бути ті, які ви можете бути впевнені, що можете виділити та mprotectзахистити їх. Або, якщо на платформі немає ASLR тощо, адреси, що виходять за межі фізичної пам'яті платформи. Удачі.
Borealid

8
Як це буде працювати, якщо використовується ваш код if(ptr) { /* do something on ptr*/ }? Чи буде це працювати, якщо значення NULL визначено відмінним від 0x0?
Ксав'є Т.

3
C покажчик не має вимушеного відношення до адрес пам'яті. Доки дотримуються правила арифметики вказівника, значення вказівника може бути будь-чим. Більшість реалізацій вирішили використовувати адреси пам'яті як значення вказівника, але вони можуть використовувати що завгодно, якщо це ізоморфізм.
datenwolf

2
@bdonlan Це також порушило б (дорадчі) правила і в MISRA-C.
Лундін

2
@Andreas Так, це теж мої думки. Людям з обладнання не слід дозволяти розробляти обладнання, в якому повинно працювати програмне забезпечення! :)
Лундін

Відповіді:


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
  • Як зазначає кафе, оновіть семантику для ініціалізації статичних об'єктів (§ 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)) { ... }

Для цього знадобиться повний синтаксичний аналізатор, а також аналізатор типу та аналіз typedefs та змінних оголошень, щоб визначити, які ідентифікатори відповідають покажчикам. Однак, зробивши це, можна уникнути необхідності внесення змін до частин генерації коду компілятора. Кланг може бути корисним для здійснення цього - я розумію, що він був розроблений на зразок таких перетворень. Вам, ймовірно, знадобиться внести зміни до стандартної бібліотеки.


2
Гаразд, я не знайшов текст в §6.3.2.3, але я підозрював, що десь буде така заява :). Я думаю, це відповідає на моє запитання, за стандартом мені не дозволяється переосмислювати NULL, якщо я не можу написати новий компілятор C, щоб підтримати мене :)
Lundin,

2
Хороший трюк - зламати компілятор, щоб покажчик <-> цілочисельних перетворень XOR визначав конкретне значення, яке є недійсним покажчиком і все ще достатньо тривіальним, щоб цільова архітектура могла це зробити дешево (зазвичай це було б значення за допомогою одного набору бітів , наприклад, 0x20000000).
Саймон Ріхтер

2
Ще одна річ, яку вам знадобиться змінити в компіляторі, - це ініціалізація об'єктів із складеним типом - якщо об’єкт частково ініціалізується, то будь-які покажчики, для яких явний ініціалізатор відсутній, повинні бути ініціалізовані NULL.
caf

20

Стандарт стверджує, що ціле постійне вираз зі значенням 0 або такий вираз, перетворений у void *тип, є нульовою постійною покажчиком. Це означає, що (void *)0завжди є нульовим покажчиком, але дано 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 lib.

Також через використання NULL бібліотекою std, якщо ви повторно визначите NULL перед включенням заголовків std, ви можете перезаписати визначення NULL, вказане в заголовках. Все, що вкладено, було б невідповідним для складених об'єктів.

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


6

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

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


Проблема виникне лише при випадковому доступі до NULL, а не тоді, коли до порту навмисно звертається. Чому б тоді я переглянув порт вводу / виводу? Це вже працює як слід.
Лундін

2
@Lundin Випадково чи ні, NULL може бути скасовано лише в програмі C за допомогою *p, p[]або p(), тому компілятору потрібно дбати лише про тих, щоб захистити порт IO 0x0000.
Апалала

@Lundin Друга частина вашого запитання: Після обмеження доступу до нульової адреси зсередини С вам потрібен інший спосіб дістатися до порту 0x0000. Функція, записана в асемблері, може це зробити. Зсередини C порт може бути відображений на 0xFFFF чи будь-що інше, але найкраще скористатися функцією та забути про номер порту.
Апалала

3

Враховуючи надзвичайну складність у переосмисленні NULL, про яку згадували інші, можливо, легше переосмислити перенаправлення для відомих апаратних адрес. Створюючи адресу, додайте 1 до кожної відомої адреси, щоб ваш відомий порт IO був:

  #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 Я маю на увазі, що вам доведеться вибрати те, що болісніше, налаштуйте перебудову цілого ланцюжка інструментів або змінивши цю частину коду.
Дуг Т.

2

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

Щоб досягти результату, який ви хочете, зберігаючи відповідність, вам доведеться змінити (або, можливо, налаштувати) ланцюжок інструментів, але це досяжно.


1

Ти просиш клопоту. Повторне NULLвизначення ненульового значення порушить цей код:

   якщо (myPointer)
   {
      // myPointer не є нульовим
      ...
   }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.