Подтвердить что ты не робот

C стандартный способ доступа к нулевому адресу указателя?

В C отсрочка нулевого указателя имеет значение Undefined Behavior, но значение нулевого указателя имеет двоичное представление, которое в некоторых архитектурах указывает на действительный адрес (например, адрес 0).
Позвольте называть этот адрес нулевым адресом указателя для ясности.

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

Пример case (IA32e):

#include <stdint.h>

int main()
{
   uintptr_t zero = 0;

   char* p = (char*)zero;

   return *p;
}

Этот код при компиляции с gcc с -O3 для IA32e преобразуется в

movzx eax, BYTE PTR [0]
ud2

из-за UB (0 - битное представление нулевого указателя).

Так как C близко к программированию на низком уровне, я считаю, что должен быть способ доступа к адресу нулевого указателя и избежать UB.


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

4b9b3361

Ответ 1

Я прочитал (часть) стандарта C99, чтобы очистить свой разум. Я нашел разделы, которые представляют интерес для моего собственного вопроса, и я пишу это как ссылку.

ОТКАЗ
Я абсолютный новичок, 90% или более того, что я написал, ошибается, не имеет смысла или может сломать тостер. Я также пытаюсь сделать обоснование из стандартного, часто с катастрофическими и наивными результатами (как указано в комментарии).
Не читайте.
Обратитесь к @Olaf за официальным и профессиональным ответом.

В дальнейшем термин "архитектурный адрес" предназначен для адреса памяти, который рассматривается процессором (логический, виртуальный, линейный, физический или адрес шины). Другими словами, адреса, которые вы будете использовать в сборке.


В разделе 6.3.2.3. он читает

Целочисленное константное выражение со значением 0 или выражением, выражаемым типом void *, называется константой с нулевым указателем. Если константа нулевого указателя преобразуется в тип указателя, результирующий указатель, называемый нулевым указателем, гарантированно сравнивает неравные к указателю на любой объект или функцию.

и преобразование целых чисел в указатель

Целое число может быть преобразовано в любой тип указателя. За исключением случаев, указанных ранее [т.е. для случая константы нулевого указателя], результат определяется реализацией, может быть не правильно выровнен, может не указывать на объект ссылочного типа и может быть представлением ловушки .

Это подразумевает, что компилятор, чтобы быть совместимым, должен только реализовать функцию int2ptr от целого к указателям, которые

  • int2ptr (0) по определению является нулевым указателем.
    Примечание, что int2ptr (0) не обязано быть 0. Это может быть любое представление бит.
  • * int2ptr (n!= 0) не имеет ограничений.
    Примечание, что это означает, что int2ptr не должна быть функцией идентификации, а также функцией, возвращающей действительные указатели!

С учетом кода ниже

char* p = (char*)241;

Стандарт абсолютно не гарантирует, что выражение *p = 56; будет записываться на архитектурный адрес 241.
И поэтому он не дает прямого доступа к любому другому архитектурному адресу (включая int2ptr (0), адрес, созданный нулевым указателем, если он действителен).

Проще говоря, стандарт не касается архитектурных адресов, но с указателями, их сравнением, преобразованиями и их операциями .

Когда мы пишем код типа char* p = (char*)K, мы не говорим компилятору, что p указывает на архитектурный адрес K, мы говорим ему, чтобы сделать указатель из целого числа K, или в другом терминах, чтобы сделать p указателем на (C абстрактный) адрес K.

Нулевой указатель и (архитектурный) адрес 0x0 не совпадают (cit.), и это верно для любого другого указателя, сделанного из целого числа K и (архитектурного) адреса K.

По каким-то причинам, детским наследствам, я думал, что целые литералы в C могут использоваться для выражения архитектурных адресов, вместо этого я ошибался, и это только бывает (вроде) правильно в компиляторах Я использовал.

Ответ на мой собственный вопрос просто: Нет стандартного способа, поскольку в стандартном документе C нет (архитектурного) адреса. Это верно для каждого (архитектурного) адреса, а не только int2ptr (0) один 1.


Примечание о return *(volatile char*)0;

В стандарте говорится, что

Если недопустимое значение [значение нулевого указателя - недопустимое значение] было присвоено указателю, поведение унарного * оператора - undefined.

и что

Поэтому любое выражение, ссылающееся к такому [изменчивому] объекту оценивается строго в соответствии с правилами абстрактной машины.

В абстрактной машине указано, что * имеет значение undefined для значений нулевого указателя, поэтому код не должен отличаться от этого

return *(char*)0;

который также является undefined.
Действительно, они не отличаются, по крайней мере, с GCC 4.9, оба компилируются в соответствии с инструкциями, изложенными в моем вопросе.

Реализованный способ доступа к архитектурному адресу 0 для GCC - использование флага -fno-isolate-erroneous-paths-dereference, который создает "ожидаемый" код сборки.


<суб > Функции отображения для преобразования указателя в целое число или целое число в указатель предназначены для быть совместимым с структурой адресации среды выполнения.

К сожалению, он говорит, что & дает адрес своего операнда, я считаю, что это немного неправильно, я бы сказал, что он дает указатель на его операнд. Рассмотрим переменную a, которая, как известно, находится по адресу 0xf1 в 16-разрядном адресном пространстве и рассматривает компилятор, реализующий int2ptr (n) = 0x8000 | п. &a даст указатель, битовое представление которого равно 0x80f1, которое не адресу a.

1 Это было особенное для меня, потому что это было единственное, в моих реализациях, к которому нельзя было получить доступ.

Ответ 2

Как OP правильно выполнил в своем ответе на свой вопрос:

Нет стандартного способа, поскольку в стандартном документе C нет (архитектурного) адреса. Это верно для каждого (архитектурного) адреса, а не только для int2ptr (0).

Тем не менее, ситуация, когда вы хотите получить доступ к памяти напрямую, скорее всего, будет использоваться, когда используется пользовательский компоновщик script. (Т. Е. Какой-то материал встраиваемых систем.) Итак, я бы сказал, стандартный способ выполнения того, что запросит OP, - это экспортировать символ для (архитектурного) адреса в компоновщике script и не беспокоить точный адрес в самом коде C.

Вариантом этой схемы будет определение символа по адресу нуль и просто использование этого для получения любого другого требуемого адреса. Для этого добавьте что-то вроде следующего в SECTIONS часть компоновщика script (предполагая синтаксис GNU ld):

_memory = 0;

И затем в вашем C-коде:

extern char _memory[];

Теперь можно, например, создайте указатель на нулевой адрес, используя, например, char *p = &_memory[0]; (или просто char *p = _memory;), не преобразовывая int в указатель. Аналогично, int addr = ...; char *p_addr = &_memory[addr]; создаст указатель на адрес addr без технически литья int в указатель.

(Это, конечно, позволяет избежать исходного вопроса, поскольку компоновщик не зависит от C-стандартного и компилятора C, и каждый компоновщик может иметь другой синтаксис для своего компоновщика script. Кроме того, сгенерированный код может быть менее эффективным, потому что компилятор не знает об адресе, к которому обращаются. Но я думаю, что это все еще добавляет интересную точку зрения на вопрос, поэтому, пожалуйста, простите немного не по теме ответ.)

Ответ 3

Какое бы решение не зависело от реализации. Needfully. ISO C не описывает среду, на которой работают программы C; скорее, то, что соответствует программе C в разных средах ( "системы обработки данных" ). Стандарт не может гарантировать того, что вы получите, обратившись к адресу, который не является массивом объектов, то есть чем-то выделенным видимо, а не средой.

Следовательно, я бы использовал что-то стандартное, как вариант, определяемый реализацией (и даже условно поддерживаемый), а не undefined поведение *: встроенная сборка. Для GCC/clang:

asm volatile("movzx 0, %%eax;") // *(int*)0;

Также стоит упомянуть автономную среду, та, в которой вы, кажется, находитесь. Стандарт говорит об этой модели исполнения (выделение мое):

§ 5.1.2

Определены две среды исполнения: freestanding и размещены. [...]

§ 5.1.2.1, запятая 1

В автономной среде (, в которой выполнение программы C может выполняться без какой-либо выгоды от операционной системы), имя и тип функции, вызываемой при запуске программы, определяются по реализации. Любые библиотечные средства, доступные для автономной программы, отличные от минимального набора, требуемого в соответствии с разделом 4, определяются реализацией. [...]

Обратите внимание, что он не говорит, что вы можете получить доступ к любому адресу по своему усмотрению.


Что бы это ни значило. Все немного отличается, когда вы выполняете стандартное управление делегатами.

Все цитаты из проекта N. 1570.

Ответ 4

Я предполагаю, что вы задаете вопрос:

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

В соответствии с буквальным чтением стандарта это невозможно. В 6.3.2.3/3 говорится, что любой указатель на объект должен сравнивать неравнозначный с нулевым указателем.

Поэтому этот указатель, о котором мы говорим, не должен указывать на объект. Но оператор deference *, применяемый к указателю объекта, указывает только поведение в том случае, если он указывает на объект.


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

Мы видим пример этого в других ответах, в которых gcc-оптимизатор обнаруживает указатель на все бит-нуль на поздней стадии обработки и помещает его как UB.

Ответ 5

Стандарт C не требует, чтобы реализации имели адреса, которые в какой-то мере напоминают целые числа или форму; все, что требуется, состоит в том, что если существуют типы uintptr_t и intptr_t, акт преобразования указателя на uintptr_t или intptr_t даст число и преобразует это число непосредственно обратно в тот же тип, что и исходный указатель, даст указатель, равный оригиналу.

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

Тем не менее, я бы предположил, что если качественная реализация указывает, что он выполняет преобразование целого-на-указатель путем простого побитового отображения, и если могут быть вероятные причины, по которым код хочет получить доступ к адресу нуль, он должен рассматривать утверждения как:

*((uint32_t volatile*)0) = 0x12345678;
*((uint32_t volatile*)x) = 0x12345678;

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