Стандарт C относительно арифметики указателей вне массивов - PullRequest
4 голосов
/ 29 мая 2019

Я много чего прочитал об арифметике указателей и неопределенном поведении ( ссылка , ссылка , ссылка , ссылка , ссылка ). Это всегда заканчивается одним и тем же выводом: арифметика указателя хорошо определена только для типа массива и между массивом [0] и массивом [array_size + 1] (один элемент после конца действителен в отношении стандарта C).

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

В этом примере:

char test[10];
char * ptr = &test[0];
printf("test[-1] : %d", *(ptr-1))

Под «неопределенным поведением» понимается, что это значение вообще не является гарантией (может быть мусор, отображение памяти и т. Д.), Но мы все еще можем с уверенностью сказать, что мы обращаемся к адресу памяти, смежному с массивом 8 байтов. до старта? Или это «неопределенное поведение» таким образом, что компилятор может вообще не генерировать этот код?

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

#include <stdint.h>
#include <stdio.h>

void func1()
{}

void func2()
{}

int main()
{
  uint8_t * ptr1 = (uint8_t*) &func1;
  uint8_t * ptr2 = (uint8_t*) &func2;

  printf("Func 1 size : %ld", ptr2-ptr1);

  return 0;
}

Поскольку ptr1 и ptr2 не являются частью массива, это рассматривается как неопределенное поведение. Опять же, означает ли это, что компилятор не может выдать этот код? Или «неопределенное поведение» означает, что вычитание не имеет смысла в зависимости от системы (функции, не смежные в памяти, с заполнением и т. Д.), Но все же происходит, как и ожидалось? Есть ли какой-либо четко определенный способ вычисления вычитания между двумя не связанными указателями?

Ответы [ 4 ]

5 голосов
/ 29 мая 2019

Стандарт C не определяет степени неопределенности для неопределенного поведения.Если он не определен, то все ставки выключены.

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

Если выЕсли вам нужна математическая арифметика указателей без возможности UB, вы можете попытаться навести указатель на uintptr_t до выполнения математических операций.


Например:

#include <stdio.h>
int main()
{
    char a,b;
    printf("&a=%p\n", &a);
    printf("&b=%p\n", &b);
    printf("&a+1=%p\n", &a+1);
    printf("&b+1=%p\n", &b+1);
    printf("%d\n", &a+1==&b || &b+1==&a);
}

на моей машине, скомпилированный с gcc -O2, приводит к:

&a=0x7ffee4e36cae
&b=0x7ffee4e36caf
&a+1=0x7ffee4e36caf
&b+1=0x7ffee4e36cb0
0

Т.е. &a+1 имеет тот же числовой адрес, что и &b, но обрабатывается как неравный &b, поскольку адреса получены из разных объектов.

(Эта оптимизация gcc несколько противоречива. Она не переносит границы единиц вызова / трансляции функций, clang этого не делает и не обязательна, как 6.5.9p6 допускают случайное равенство указателей. Для получения более подробной информации см. dbush на этот ответ Кита .

1 голос
/ 29 мая 2019

Стандарт C должен указывать неопределенное поведение просто потому, что такие вещи, как отображение памяти, выходят за рамки стандарта.

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

Если вы посмотрите на среднюю встроенную систему, вам часто нужно обращаться к адресам, где нет массивов, и насколькокомпилятор знает, никаких объектов вообще (регистры с отображением в памяти и т. д.).Поэтому все такие встроенные компиляторы C имеют гарантии того, что такой код ведет себя предсказуемо, даже если такие гарантии являются «нестандартными расширениями».Что на практике означает, что указатели сводятся к целым числам, представляющим физические адреса.

Лучшая практика заключается в написании кода, который является безопасным независимо от того.Например, если мы хотим написать программу, которая выводит содержимое страницы флэш-памяти, мы хотим перебирать ее побайтно (чтобы отбросить результат на некоторой последовательной шине).При использовании обычного компилятора встроенных систем можно просто установить volatile const uint8_t* в первый байт страницы флэш-памяти, а затем выполнить итерацию вне зависимости от того, какие переменные и типы там хранятся.Но с точки зрения C, это неопределенное поведение.

Мы можем удовлетворить и требования C, и реальный мир, поместив все переменные, которые будут размещены на этой странице, в один огромный struct foo { ... } bar;.Который нам разрешено перебирать побайтно, используя указатель на символьный тип, такой как uint8_t.(С17 6.3.2.3/7).

Так что усилия по уклонению от неопределенного поведения не обязательно должны быть такими громоздкими.Часто есть обходные пути со структурами, объединениями, преобразованием указателей в целые числа и т. Д. И т. Д.

0 голосов
/ 30 мая 2019

На самом деле, доказать, что любая произвольная арифметика указателей «не связана с каким-либо массивом» очень сложно (может быть, похоже на проблему остановки? Не уверен), потому что указатель может быть «скрытно» назначен через глобальную переменную указатель на указательглядя на файл карты, чтобы найти фактический адрес указателя и изменить его и т. д.

Стандарт говорит о том, что компилятор, вероятно, будет выполнять "ожидаемые действия" в терминах сгенерированного кода (то есть обычной арифметики указателей).), но это не гарантирует, что результирующий указатель будет указывать на что-либо действительное.Таким образом, поведение "неопределено".В частности, если вы объявляете переменную до и после массива, и если ваш указатель идет даже на один элемент до или после массива, вы не гарантируете, что будете касаться этих переменных или фактически любой действительной памяти.В системе с защитой памяти это может даже привести к сбою.Реальное поведение зависит от системы, выполняющей код.

0 голосов
/ 29 мая 2019

Комитет по стандартам C не видел необходимости запрещать компиляторам вести себя глупо, что делало бы их непригодными для многих целей. Действительно, в соответствии с опубликованным Обоснованием, Комитет признал, что реализация могла бы вести себя так, чтобы она соответствовала, но была бесполезна, но посчитала, что люди, стремящиеся создать качественные реализации языка, описанного в Стандарте, будут воздерживаться от такая глупость. Рассмотрим программу:

void byte_copy(unsigned char *dest, unsigned char *src, int len)
{
  while(len--) *dest++ = *src++;
}
unsigned char src[10][10], dest[100];
void test(int mode)
{
  if (mode == 0)
    byte_copy(dest, src[0], 11);
  else
    byte_copy(dest, (unsigned char*)src, 100);
}

Для реализации может быть полезно перехватить test, если mode равно нулю, на основании того, что программист, вероятно, намеревался скопировать элементы из первой строки src, и авторов стандарта вероятно, не хотел этого запрещать. С другой стороны, язык был бы серьезно нарушен, если бы подобный код в случае mode != 0 не мог использоваться для создания побайтной копии объектов всех типов, включая многомерные массивы, и Комитет, вероятно, признал это. Тем не менее, стандарт не признает различий между указателями, переданными в двух случаях.

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

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

...