Друк нульових покажчиків з% p є невизначеною поведінкою?


93

Це невизначена поведінка для друку нульових покажчиків за допомогою %pспецифікатора перетворення?

#include <stdio.h>

int main(void) {
    void *p = NULL;

    printf("%p", p);

    return 0;
}

Питання стосується стандарту С, а не реалізації С.


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

це так, як printf відображає лише значення, а не торкається (у значенні читання або запису загостреного об'єкта) - не може бути UB i покажчик має дійсне для свого типу значення (NULL є дійсним значенням)
P__J__

3
@PeterJ, скажімо, те, що ви говорите, є правдою (хоча, очевидно, стандарт зазначає інакше), сам факт, що ми дискутуємо з цього приводу, робить питання правильним і правильним, оскільки, схоже, наведена нижче частина стандарту робить звичайному розробнику дуже важко зрозуміти, що, чорт візьми, відбувається .. Значення: питання не заслуговує на голосування проти, тому що ця проблема вимагає роз’яснення!
Пітер Варо,

1
alk

2
@PeterJ це вже інша історія, дякую за роз'яснення :)
Пітер Варо

Відповіді:


93

Це один із тих дивних випадків, коли ми поширюємось на обмеження англійської мови та непослідовну структуру стандарту. Тож у кращому випадку я можу навести переконливий контраргумент, оскільки це неможливо довести :) 1


Код у питанні демонструє чітко визначену поведінку.

Оскільки [7.1.4] є основою питання, почнемо з цього:

Кожне з наступних тверджень застосовується, якщо прямо не зазначено інше в детальних описах, що подаються нижче: Якщо аргумент функції має недійсне значення ( наприклад, значення поза доменом функції або покажчик поза адресним простором програми, або нульовий покажчик , [... інші приклади ...] ) [...] поведінка невизначена. [... інші твердження ...]

Це незграбна мова. Одне з тлумачень полягає в тому, що елементи у списку є UB для всіх функцій бібліотеки, якщо це не замінено окремими описами. Але список починається з "типу", вказуючи на те, що він наочний, а не вичерпний. Наприклад, у ньому не згадується правильне закінчення нульових рядків (критично для поведінки, наприклад strcpy).

Таким чином, очевидно, що намір / область застосування 7.1.4 просто полягає в тому, що "недійсне значення" веде до UB ( якщо не вказано інше ). Ми повинні розглянути опис кожної функції, щоб визначити, що вважається "недійсним значенням".

Приклад 1 - strcpy

[7.21.2.3] говорить лише це:

В strcpyфункція копіює рядок , зазначену s2(включаючи завершальний нульовий символ) в масив , на який вказуєs1 . Якщо копіювання відбувається між об'єктами, що перекриваються, поведінка не визначена.

У ньому не згадується явних нульових покажчиків, але не згадується також і про нульові термінатори. Натомість один робить висновок із "рядка, на який вказуєs2 ", що єдиними допустимими значеннями є рядки (тобто вказівники на масиви символів, що закінчуються нулем).

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

  • [7.6.4.1 (fenv)] зберегти поточну середу з плаваючою точкою в зазначених вище об'єкта шляхомenvp

  • [7.12.6.4 (frexp)] зберігати ціле число в Int зазначених вище об'єкта з допомогоюexp

  • [7.19.5.1 (fclose)] потік вказав наstream

Приклад 2 - printf

[7.19.6.1] говорить про %p:

p- Аргумент повинен бути вказівником на void. Значення покажчика перетворюється на послідовність друку символів, визначеним реалізацією.

Null є дійсним значенням покажчика, і в цьому розділі явно не згадується, що null є особливим випадком, і що вказівник повинен вказувати на об'єкт. Таким чином визначається поведінка.


1. Якщо не виступить автор стандартів, або якщо ми не знайдемо щось подібне до обґрунтованого документа, який пояснює речі.


Коментарі не призначені для розширеного обговорення; цю розмову переміщено до чату .
Бхаргав Рао

1
"але в ньому немає згадок про нульові термінатори" є слабким у Прикладі 1 - strcpy, оскільки специфікація говорить "копіює рядок ". рядок явно визначено як такий, що має нульовий символ .
chux

1
@chux - Це дещо в моєму розумінні - потрібно зробити висновок про те, що дійсне / недійсне з контексту, а не припускати, що перелік у 7.1.4 є вичерпним. (Однак існування цієї частини моєї відповіді мало дещо більше сенсу в контексті коментарів, які з тих пір були видалені, аргументуючи це тим, що strcpy був контраприкладом.)
Олівер Чарлсворт,

1
Суть питання полягає в тому, як читач буде інтерпретувати такі, як . Чи означає це кілька прикладів можливих невірних значень ? Чи означає це деякі приклади, які завжди є неприпустимими значеннями ? Для протоколу я йду з першою інтерпретацією.
ninjalj

1
@ninjalj - Так, погодився. Це по суті те, що я намагаюся передати у своїй відповіді тут, тобто "це приклади типів речей, які можуть бути неприпустимими значеннями". :)
Олівер Чарлсворт

20

Коротка відповідь

Так . Друк нульових покажчиків за допомогою %pспецифікатора перетворення має невизначену поведінку. Сказавши це, я не знаю жодної відповідної реалізації, яка могла б поводитися неправильно.

Відповідь стосується будь-якого зі стандартів С (C89 / C99 / C11).


Довга відповідь

Специфікатор %pперетворення очікує, що аргумент вказівника типу буде void, перетворення вказівника на друкувані символи визначається реалізацією. Це не говорить, що очікується нульовий покажчик.

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

C99 / C11 §7.1.4 p1

[...] Якщо аргумент функції має недійсне значення (наприклад, [...] нульовий покажчик, [...] поведінка не визначена.

Приклади функцій (стандартної бібліотеки), які очікують нульові вказівники як дійсні аргументи:

  • fflush() використовує нульовий покажчик для змивання "всіх потоків" (що застосовуються).
  • freopen() використовує нульовий вказівник для позначення файлу, "який на даний момент пов'язаний" із потоком.
  • snprintf() дозволяє передавати нульовий покажчик, коли 'n' дорівнює нулю.
  • realloc() використовує нульовий покажчик для виділення нового об'єкта.
  • free() дозволяє передавати нульовий покажчик.
  • strtok() використовує нульовий покажчик для наступних викликів.

Якщо взяти випадок за snprintf(), має сенс дозволити передачу нульового вказівника, коли 'n' дорівнює нулю, але це не так для інших функцій (стандартної бібліотеки), які допускають подібний нуль 'n'. Наприклад: memcpy(), memmove(), strncpy(), memset(), memcmp().

Це не тільки зазначено у вступі до стандартної бібліотеки, але також ще раз у вступі до цих функцій:

C99 §7.21.1 p2 / C11 §7.24.1 p2

Якщо аргумент, оголошений як size_tn, визначає довжину масиву для функції, n може мати значення нуля при виклику цієї функції. Якщо прямо не зазначено інше в описі певної функції в цьому підпункті, аргументи покажчика для такого виклику все одно повинні мати дійсні значення, як описано в 7.1.4.


Це навмисно?

Я не знаю, чи UB %pз нульовим покажчиком насправді є навмисним, але оскільки стандарт явно вказує, що нульові вказівники вважаються недійсними значеннями як аргументи стандартних функцій бібліотеки, а потім він іде і явно вказує випадки, коли нульовий покажчик є дійсним аргументом (snprintf, безкоштовно, і т.д.), а потім він йде і ще раз повторює вимогу аргументи дійсними навіть в нулі випадків «п» ( memcpy, memmove, memset), то я думаю , що це розумно припустити , що Комітет з питань стандартів С не надто стурбований тим, що такі речі не визначені.


Коментарі не призначені для розширеного обговорення; цю розмову переміщено до чату .
Бхаргав Рао

1
@JeroenMostert: Який намір цього аргументу? Наведена цитата 7.1.4 досить чітка, чи не так? Що можна сперечатися щодо "якщо прямо не вказано інше", коли не зазначено інше? Що можна сперечатися з приводу того, що (не пов’язана) бібліотека функцій рядків має подібне формулювання, тож формулювання не видається випадковим? Я думаю, що ця відповідь (хоча насправді не дуже корисна на практиці ) є якомога правильнішою.
Деймон

3
@Damon: Ваше міфічне обладнання не є міфічним, існує безліч архітектур, де значення, які не відображають дійсних адрес, можуть не завантажуватися в адресні регістри. Однак передача нульових покажчиків як аргументів функції все одно необхідна для роботи на цих платформах як загальний механізм. Просто введення одного в стек не підірве справи.
Jeroen Mostert

1
@anatolyg: На процесорах x86 адреси мають дві частини - сегмент та зсув. На 8086 завантаження реєстру сегментів подібно до завантаження будь-якого іншого, але на всіх наступних машинах він отримує дескриптор сегмента. Завантаження недійсного дескриптора викликає пастку. Багато коду для 80386 і пізніших процесорів, однак, використовує тільки один сегмент, і , таким чином , ніколи не завантажує сегментні регістри на всіх .
supercat

1
Думаю, усі погодяться, що друк нульового вказівника з %pне повинно бути невизначеною поведінкою
М.М.

-1

Автори стандарту С не докладали зусиль, щоб вичерпно перерахувати всі поведінкові вимоги, яким має відповідати реалізація, щоб бути придатною для якоїсь конкретної мети. Натомість вони очікували, що люди, які пишуть компілятори, будуть проявляти певну кількість здорового глузду незалежно від того, вимагає це Стандарт чи ні.

Питання про те, чи щось викликає UB, рідко є самим собою і корисним. Справжні важливі питання:

  1. Чи повинен хтось, хто намагається написати якісний компілятор, зробити так, щоб він поводився передбачувано? Для описаного сценарію відповідь однозначно так.

  2. Чи повинні програмісти мати право очікувати, що якісні компілятори для будь-чого, що нагадує звичайні платформи, будуть поводитися передбачувано? В описаному сценарії я б сказав, що відповідь так.

  3. Чи не можуть деякі тупі автори компіляторів розтягнути інтерпретацію Стандарту, щоб виправдати щось дивне? Я сподівався б, що ні, але не виключав би цього.

  4. Чи повинні дезінфікуючі компілятори пирхати про поведінку? Це залежатиме від рівня параної у їхніх користувачів; дезінфікуючий компілятор, мабуть, не повинен за замовчуванням пирхати про таку поведінку, але, можливо, надає можливість конфігурації, якщо програми можуть бути перенесені на "розумні" / німі компілятори, які поводяться дивно.

Якщо розумна інтерпретація Стандарту передбачає поведінку, але деякі автори компіляторів розширюють інтерпретацію, щоб виправдати інше, чи справді важливо, що говорить Стандарт?


1. Нерідкі випадки, коли програмісти вважають, що припущення сучасних / агресивних оптимізаторів суперечать тому, що вони вважають "розумним" або "якісним". 2. Коли справа стосується неоднозначностей у специфікації, не рідко випадки, коли виконавці розходяться щодо свободи, яку вони можуть прийняти. 3. Що стосується членів комітету зі стандартів С, навіть вони не завжди погоджуються щодо того, що таке "правильне" тлумачення, не кажучи вже про те, яким воно має бути. Враховуючи вищезазначене, чиєму розумному тлумаченню слід дотримуватися?
Дрор К.

6
Відповісти на запитання "чи викликає цей фрагмент коду UB чи ні" дисертацією про те, що ви думаєте про корисність UB або про те, як слід поводитись компіляторам, є поганою спробою відповіді, тим більше, що ви можете скопіювати та вставити це як відповідь майже на будь-яке питання щодо конкретного УБ. У відповідь на ваш риторичний розквіт: так, насправді важливо, що говорить Стандарт, незалежно від того, що роблять деякі автори компіляторів або що ви думаєте про них за це, адже Стандарт - це те, з чого починають як програмісти, так і автори компіляторів.
Jeroen Mostert

1
@JeroenMostert: Відповідь на питання "Чи X посилається на невизначену поведінку" часто залежатиме від того, що розуміється під питанням. Якщо програма розглядається як невизначена поведінка, якщо Стандарт не встановлює жодних вимог щодо поведінки відповідної реалізації, то майже всі програми викликають UB. Автори Стандарту чітко дозволяють реалізаціям поводитися довільно, якщо програма вкладає виклики функції занадто глибоко, до тих пір, поки реалізація може правильно обробити принаймні один (можливо, надуманий) вихідний текст, який застосовує обмеження перекладу в Стадарді.
supercat

@supercat: дуже цікаво, але printf("%p", (void*) 0)невизначена поведінка чи ні, згідно зі стандартом? Глибоко вкладені виклики функцій мають таке ж значення, як і ціна чаю в Китаї. І так, UB дуже поширений у реальних програмах - що з цього?
Jeroen Mostert

1
@JeroenMostert: Оскільки Стандарт дозволить тупий реалізації розглядати майже будь-яку програму як таку, що має UB, важливою буде поведінка нетупих реалізацій. Якщо ви не помітили, я не просто написав копію / вставку про UB, а відповів на запитання щодо %pкожного можливого значення запитання.
supercat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.