Выполняет ли арифметику поведение с нулевым указателем undefined?

Мне кажется, что следующая программа вычисляет недопустимый указатель, так как NULL не подходит ни для чего, кроме назначения и сравнения для равенства:

#include <stdlib.h>
#include <stdio.h>

int main() {

  char *c = NULL;
  c--;

  printf("c: %p\n", c);

  return 0;
}

Однако, похоже, что ни одно из предупреждений или инструментов в GCC или Clang, ориентированных на поведение undefined, не говорит о том, что это на самом деле UB. Действительно ли эта арифметика действительна, и я слишком педантичен, или это недостаток в механизмах проверки, о которых я должен сообщить?

Испытано:

$ clang-3.3 -Weverything -g -O0 -fsanitize=undefined -fsanitize=null -fsanitize=address offsetnull.c -o offsetnull
$ ./offsetnull
c: 0xffffffffffffffff

$ gcc-4.8 -g -O0 -fsanitize=address offsetnull.c -o offsetnull
$ ./offsetnull 
c: 0xffffffffffffffff

Похоже, что довольно хорошо документировано, что AddressSanitizer, используемый Clang и GCC, более сфокусирован на разыменовании плохих указателей, так что это достаточно справедливо. Но другие проверки не поймают его: -/

Изменить: часть причины, по которой я задал этот вопрос, заключается в том, что флаги -fsanitize позволяют динамически проверять четкость в сгенерированном коде. Это что-то, что они должны были поймать?

Ответы

Ответ 1

Арифметика указателя на указателе, не указывающем на массив, - это поведение Undefined.
Кроме того, разыменование указателя NULL является Undefined.

char *c = NULL;
c--;

является Undefined определенным поведением, потому что c не указывает на массив.

С++ 11 Стандарт 5.7.5:

Когда выражение, которое имеет интегральный тип, добавляется или вычитается из указателя, результат имеет тип операнда указателя. Если операнд указателя указывает на элемент объекта массива, и массив достаточно велик, результат указывает на смещение элемента от исходного элемента, так что разность индексов результирующих и исходных элементов массива равна интегральному выражению. Другими словами, если выражение P указывает на i-й элемент объекта массива, выражения (P) + N (эквивалентно, N + (P)) и (P) -N (где N имеет значение n) соответственно, я + n-й и i-й элементы массива, если они существуют. Более того, если выражение P указывает на последний элемент объекта массива, выражение (P) +1 указывает один за последним элементом объекта массива, и если выражение Q указывает один за последним элементом объекта массива, выражение (Q) -1 указывает на последний элемент объекта массива. Если оба операнда указателя и результат указывают на элементы одного и того же объекта массива или одно прошлое последний элемент объекта массива, оценка не должна приводить к переполнению; в противном случае поведение undefined.

Ответ 2

Да, это поведение undefined, и это то, что должен был -fsanitize=undefined поймать; это уже в моем списке TODO, чтобы добавить проверку для этого.

FWIW, правила C и С++ здесь несколько отличаются: добавление 0 к нулевому указателю и вычитание одного нулевого указателя из другого имеют поведение undefined в C, но не в С++. Все остальные арифметические операции с нулевыми указателями имеют поведение undefined на обоих языках.

Ответ 3

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

В Стандарте никогда не возникает ситуации, когда добавление чего-либо к нулевому указателю может привести к допустимому значению указателя; Кроме того, ситуации, в которых реализации могут определять любое полезное поведение для таких действий, являются редкими и, как правило, лучше обрабатываются с помощью встроенных функций компилятора (*). Однако во многих реализациях, если арифметика с нулевым указателем не попадает в ловушку, добавление смещения к нулевому указателю может дать указатель, который, хотя и недействителен, больше не распознается как нулевой указатель. Попытка разыменовать такой указатель не будет захвачена, но может вызвать произвольные эффекты.

Вычисления указателя захвата формы (нуль + смещение) и (нулевое смещение) устранили бы эту опасность. Обратите внимание, что защита не обязательно требует захвата (указатель-null), (нулевой указатель) или (null-null), в то время как значения, возвращаемые первыми двумя выражениями, вряд ли будут иметь какую-либо полезность [если реализация должна была указывать что null-null будет давать нуль, код, который предназначался для этой конкретной реализации, иногда может быть более эффективным, чем код, который должен был иметь специальный случай null], они не будут генерировать недопустимые указатели. Кроме того, имея (нуль + 0) и (нуль-0) либо выдавать нулевые указатели, а не улавливать, это не поставит под угрозу безопасность и может избежать необходимости вводить нулевые указатели с особым случаем кода пользователя, но преимущества будут менее привлекательными, поскольку компилятор придется добавить дополнительный код, чтобы это произошло.

(*) Такой встроенный компилятор 8086, например, может принимать беззнаковые 16-битные целые числа "seg" и "ofs" и читать слово по адресу seg: ofs без нулевой ловушки, даже когда был указан адрес равным нулю. Адрес (0x0000: 0x0000) на 8086 является вектором прерывания, к которому могут потребоваться некоторые программы, и в то время как адрес (0xFFFF: 0x0010) обращается к тому же физическому местоположению, что и (0x0000: 0x0000), на более старых процессорах с только 20 адресными строками, это доступ к физическому местоположению 0x100000 для процессоров с 24 или более адресными строками). В некоторых случаях альтернативой было бы иметь специальное обозначение для указателей, которые, как ожидается, будут указывать на вещи, не признанные стандартом C (такие вещи, как векторы прерываний, будут квалифицироваться) и воздерживаться от улавливания нулями, или указать, что volatile указатели будут обрабатываться таким образом. Я видел первое поведение, по крайней мере, в одном компиляторе, но не думаю, что видел второй.