Мне кажется, что следующая программа вычисляет неверный указатель, так как NULL
ничего хорошего, кроме присваивания и сравнения на равенство:
#include <stdlib.h>
#include <stdio.h>
int main() {
char *c = NULL;
c--;
printf("c: %p\n", c);
return 0;
}
Однако, похоже, что ни одно из предупреждений или инструментария в GCC или Clang, нацеленных на неопределенное поведение, не говорит о том, что это на самом деле 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
флаги позволяют динамический проверки четкости в сгенерированном коде. Это то, что они должны были поймать?
Арифметика указателя на указатель, не указывающий на массив, является неопределенным поведением.
Кроме того, разыменование нулевого указателя является неопределенным поведением.
char *c = NULL;
c--;
Неопределенное определенное поведение, потому что c
не указывает на массив.
C ++ 11 Standard 5.7.5:
Когда выражение, имеющее целочисленный тип, добавляется или вычитается из указателя, результат имеет тип операнда указателя. Если операнд-указатель указывает на элемент объекта массива, и массив достаточно велик, результат указывает на смещение элемента от исходного элемента, так что разность индексов результирующего и исходного элементов массива равна интегральному выражению. Другими словами, если выражение P указывает на i-й элемент объекта массива, выражения (P) + N (эквивалентно, N + (P)) и (P) -N (где N имеет значение n) указывают соответственно i + n-м и i-n-м элементам массива, если они существуют. Кроме того, если выражение P указывает на последний элемент объекта массива, выражение (P) +1 указывает один за последним элементом объекта массива, и если выражение Q указывает на один последний элемент последнего элемента массива, выражение (Q) -1 указывает на последний элемент объекта массива. Если и операнд указателя, и результат указывают на элементы одного и того же объекта массива или одного последнего
последний элемент объекта массива, оценка не должна производить переполнение; в противном случае поведение не определено.
Да, это неопределенное поведение, и это то, что -fsanitize=undefined
должен был поймать; это уже в моем списке TODO, чтобы добавить проверку для этого.
FWIW, правила C и C ++ здесь немного отличаются: добавление 0
для нулевого указателя и вычитания одного нулевого указателя из другого имеют неопределенное поведение в C, но не в C ++. Вся другая арифметика с нулевыми указателями имеет неопределенное поведение в обоих языках.
Не только арифметика с нулевым указателем запрещена, но и неудача реализаций, которые перехватывают попытки разыменования, чтобы перехватить арифметику с нулевыми указателями, значительно снижает преимущество ловушек с нулевым указателем.
Стандартом никогда не определяется ситуация, в которой добавление чего-либо к нулевому указателю может привести к допустимому значению указателя; кроме того, ситуации, в которых реализации могут определять любое полезное поведение для таких действий, встречаются редко и, как правило, их лучше обрабатывать с помощью встроенных функций компилятора (*). Однако во многих реализациях, если арифметика нулевого указателя не перехвачена, добавление смещения к нулевому указателю может привести к указателю, который, хотя и недопустим, больше не является узнаваемый как нулевой указатель. Попытка разыменования такого указателя не будет захвачена, но может вызвать произвольные эффекты.
Перечисление вычислений указателя в форме (null + offset) и (null-offset) устранит эту опасность. Обратите внимание, что защита не обязательно требует прерывания (pointer-null), (null-pointer) или (null-null), в то время как значения, возвращаемые первыми двумя выражениями, вряд ли будут иметь какую-либо полезность [если реализация должна была бы указывать что null-null даст ноль, код, который нацелен на эту конкретную реализацию, иногда может быть более эффективным, чем код, который был на особый случай null
] они не будут генерировать недействительные указатели. Кроме того, наличие (null + 0) и (null-0) либо дает нулевые указатели, чем перехватывает, не ставит под угрозу безопасность и может избежать необходимости иметь пользовательские коды нулевых указателей специального случая, но преимущества будут менее убедительными, так как компилятор пришлось бы добавить дополнительный код, чтобы это произошло.
(*) Такая встроенная функция в компиляторах 8086, например, может принимать 16-разрядные целые числа без знака «seg» и «ofs» и читать слово по адресу seg: ofs без нулевой ловушки, даже если адрес оказался нулевым , Адрес (0x0000: 0x0000) на 8086 является вектором прерывания, к которому некоторым программам может понадобиться доступ, и в то время как адрес (0xFFFF: 0x0010) обращается к тому же физическому местоположению, что и (0x0000: 0x0000) на более старых процессорах с только 20 адресными строками, это доступ к физическому местоположению 0x100000 на процессорах с 24 или более адресными строками). В некоторых случаях альтернативой может быть специальное обозначение указателей, которые ожидаемый указывать на вещи, не распознаваемые стандартом C (такие вещи, как векторы прерываний, могут быть определены), и воздерживаться от их обнуления, или же указать, что volatile
указатели будут обрабатываться таким образом. Я видел первое поведение по крайней мере в одном компиляторе, но не думаю, что видел второе.