Обнаружение трюка со сглаживанием массива с помощью gcc - PullRequest
0 голосов
/ 03 августа 2020

Некоторый код сглаживает многомерные массивы вроде этого:

int array[10][10];
int* flattened_array = (int*)array;
for (int i = 0; i < 10*10; ++i)
   flattened_array[i] = 42;

Это, насколько мне известно, поведение undefined.

Я пытаюсь обнаружить подобные случаи с помощью g cc дезинфицирующие средства, однако, ни -fsanitize=address, ни -fsanitize=undefined не работают.

Есть ли параметр дезинфицирующего средства, который мне не хватает, или, возможно, другой способ обнаружить это во время выполнения? Или, может быть, я ошибаюсь и код правильный?

Изменить: дезинфицирующие средства обнаруживают этот доступ как ошибку:

array[0][11] = 42;

, но не обнаруживают этого:

int* first_element = array[0];
first_element[11] = 42;

Кроме того, clang обнаруживает первый доступ статически и выдает предупреждение

предупреждение: индекс массива 11 находится за концом массива (который содержит 10 элементов) [ -Warray-bounds]

Изменить: приведенное выше не изменяется, если int в объявлении заменяется на char.

Изменить: Там являются двумя потенциальными источниками UB.

  1. Доступ к объекту (типа int[10]) через lvalue несовместимого типа (int).
  2. За пределами доступ с указателем типа int* и индексом >=10, где размер базового массива равен 10 (а не 100).

Дезинфицирующие средства, похоже, не обнаруживают первый вид нарушение. Ведутся споры о том, является ли это нарушением вообще. В конце концов, существует также объект типа int по тому же адресу.

Что касается второго потенциального UB, дезинфицирующее средство UB обнаруживает такой доступ, но только если это делается напрямую через сам 2D-массив а не через другую переменную, которая указывает на его первый элемент, как показано выше. Я не думаю, что эти два доступа должны различаться по законности. Они должны быть либо законными (и тогда ubsan имеет ложное срабатывание), либо незаконными (и тогда ubsan имеет ложноотрицательный).

Edit: Приложение J2 говорит, что array[0][11] должно быть UB, даже если это только информативный.

Ответы [ 4 ]

2 голосов
/ 03 августа 2020

С точки зрения языкового юриста, это обычно рассматривается как недопустимый код, потому что массивы целых чисел имеют только размер 10, а код имеет доступ к сверх объявленного размера массива . Но раньше это была обычная идиома, и я не знаю компилятора, который бы ее не принял. Тем не менее, со всеми компиляторами реального мира, которые я знаю, результирующая программа будет иметь ожидаемое поведение.

После второго (на самом деле гораздо большего) чтения проекта стандарта C11 (n1570) цель стандарта все еще не Чисто. 6.2.5 Типы В § 20 говорится:

Тип массива описывает непрерывно выделенный непустой набор объектов с определенным типом объекта-члена, называемым типом элемента.

Показывает, что массив содержит непрерывно размещенные объекты. Но ИМХО неясно, является ли непрерывно выделенный набор объектов массивом.

Если вы отвечаете «нет», то показанный код вызывает UB, обращаясь к массиву за последним элементом

Но если вы отвечаете «да», тогда набор из 10 смежных наборов из 10 смежных целых чисел дает 100 смежных целых чисел и может рассматриваться как массив из 100 целых чисел. Тогда показанный код будет законным.

Это последнее допущение кажется обычным в реальном слове, потому что оно согласуется с динамическим c распределением массива: вы выделяете достаточно памяти для ряда объектов, и вы можете доступ к нему, как если бы он был объявлен как массив - и функция распределения гарантирует отсутствие проблем с выравниванием.

Мой вывод на данный момент:

  • это красивый и чистый код: конечно нет, и я бы избегал его в производственном коде
  • вызывает ли он UB: я действительно не знаю и мое личное мнение, вероятно, нет

Давайте посмотрим на код, добавленный при редактировании:

array[0][11] = 42;

Компилятор знает, что массив объявлен как int[10][10]. Таким образом, он знает, что оба индекса должны быть меньше 10, и может вызвать предупреждение.

int* first_element = array[0];
first_element[11] = 42;

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

Вдобавок, когда компилятор внутренне кодирует арифметику указателей на обычных платформах, он просто вычисляет адрес памяти, который является исходным адресом и байтовым смещением . Таким образом, он может выдавать тот же код, что и:

char *addr = (char *) first_element;  // (1)
addr += 11 * sizeof(int);             // (2)
*((int *) addr) = 42;                 // (3)

(1) является допустимым, потому что указатель на любой объект (здесь int) может быть преобразован в указатель на char, который требуется для указания на первый байт представления объекта

(2) трюк здесь в том, что (char *) first_element совпадает с (char *) array, потому что первый байт массива 10 * 10 равен первый байт первого int первой строки, и у одного байта может быть только один единственный адрес. Поскольку размер array равен 10 * 10 * sizeof(int), 11 * sizeof(int) является допустимым смещением в нем.

(3) по той же причине, (char *) &array[1][1] равно addr, потому что элементы в массиве являются смежными, поэтому их байтовые представления также являются смежными. А поскольку прямое и обратное преобразование между двумя типами является допустимым и требуется для возврата исходного указателя, (int *) addr равно (int*) ((char*) &array[1][1]). Это означает, что разыменование (int *) addr допустимо и будет иметь тот же эффект, что и array[1][1] = 42.

Это не означает, что first_element[11] не включает UB. array[0] имеет заявленный размер, равный 10. Это просто объясняет, почему все известные компиляторы принимают его (в дополнение к нежеланию нарушать унаследованный код).

1 голос
/ 04 августа 2020

Дезинфицирующие средства не особенно хороши для перехвата доступа за пределами границ, если рассматриваемый массив не является полным объектом.

Например, они не перехватывают доступ за пределами границ в этом случае:

struct {
   int inner[10];
   char tail[sizeof(int)];
} outer;

int* p = outer.inner;
p[10] = 42;

, что явно незаконно. Но они перехватывают доступ к p[11].

Уплощение массива по сути не отличается от такого доступа. Код, сгенерированный компилятором, и способ его обработки с помощью дезинфицирующих средств должны быть очень похожими. Так что мало надежды на то, что эти инструменты могут обнаружить сглаживание массива.

0 голосов
/ 04 августа 2020

Проблема здесь в том, что Стандарт описывает как эквивалентные две операции, одна из которых явно должна быть определена, а одна из которых в Стандарте прямо говорится, что не определена.

Самый чистый способ решить эту проблему, который чтобы совпадать с тем, что уже делают clang и g cc, то есть применение оператора [] к массиву lvalue или отличному от l значению не приводит к его распаду , а вместо этого ищет элемент напрямую, давая lvalue, если операнд массива был lvalue, и значение, отличное от l, в противном случае.

Признание использования [] с массивом как отдельного оператора очистило бы ряд углов случаев в семантике, включая доступ к массиву внутри структуры, возвращаемой функцией, массивы с регистром, массивы битовых полей и т. д. c. Это также проясняет, что должны означать ограничения внутреннего массива. Учитывая foo[x][y], компилятор будет иметь право предположить, что y будет в пределах внутреннего массива, но с учетом *(foo[x]+y) он не будет иметь права делать такое предположение.

0 голосов
/ 03 августа 2020

Требуется, чтобы многомерные массивы располагались непрерывно (C использует старшие строки). И не может быть никакого заполнения между элементами массива - хотя это не указано явно в стандарте, это может быть выведено с помощью определения массива, в котором говорится: « непрерывно выделенный непустой набор объектов » и определение sizeof operator.

Таким образом, «выравнивание» должно быть допустимым.

Re. доступ к array[0][11]: хотя в Приложении J2 прямо приводится пример, что именно является нарушением в нормативе, не очевидно. Тем не менее, все еще можно сделать легальным промежуточное приведение к char*:

*((int*)((char*)array + 11 * sizeof(int))) = 42;

(писать такой код явно не рекомендуется;)

...