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

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

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

Пример случая (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.


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


person Margaret Bloom    schedule 21.02.2016    source источник
comment
нулевой указатель и адрес 0x0 не совпадают.   -  person 2501    schedule 21.02.2016
comment
Я думаю, вам следует попробовать это с компилятором для предполагаемой среды.   -  person Martin Zabel    schedule 21.02.2016
comment
Не существует стандартного способа сделать это, поскольку стандарт не предоставляет возможности доступа к произвольной памяти. Вам нужно будет сделать что-то, зависящее от реализации. Проверьте документацию к вашему компилятору, чтобы узнать, что позволяет ваша реализация.   -  person Raymond Chen    schedule 21.02.2016
comment
@ 2501 Думаю, я знаю. Я просто не хотел, чтобы этот вопрос был слишком абстрактным. Могу я попросить вас уточнить терминологию, чтобы убедиться, что я не ошибся?   -  person Margaret Bloom    schedule 21.02.2016
comment
Ваш компоновщик для такой среды должен позволять вам определять раздел, начинающийся с 0.   -  person Martin James    schedule 21.02.2016
comment
Не меняйте вопрос, укажите, что вы редактировали, это делает ответы странными.   -  person Trevor    schedule 21.02.2016
comment
@ 2501: Чтобы быть более точным, цитируя стандарт: целочисленное постоянное выражение со значением 0 или .... Итак, 0 - это всего лишь константа нулевого указателя; это отличается от нулевого указателя.   -  person too honest for this site    schedule 21.02.2016
comment
Я спрашиваю о том, что об этом говорится в стандарте, а НЕ о том, как достичь этого способом, определенным реализацией. - Это приводит к отсутствию ответа. Поскольку преобразование 0, присвоенного целочисленному типу zero, в указатель уже является неопределенным поведением. Стандарт разрешает преобразование только указателя в этот тип и обратно. Даже использование другого типа указателя - это уже UB.   -  person too honest for this site    schedule 21.02.2016
comment
@Olaf Я бы подумал, что константа нулевого указателя должна быть нулевым указателем.   -  person Andrew Henle    schedule 21.02.2016
comment
@ 2501: Нет! Как это проверить, остается на усмотрение реализации. Стандарт просто требует, чтобы нулевой указатель давал 0, если он используется в условии. Странно, что теперь вы пишете полную противоположность тому, что писали несколько комментариев назад: значение нулевого указателя всегда равно 0, но его битовое представление нет.   -  person too honest for this site    schedule 21.02.2016
comment
@ 2501 Значение нулевого указателя также равно 0, иначе его нельзя было бы использовать в операторе if, где он неявно сравнивается с 0. Нет. В стандарте четко сказано, что Any two null указатели должны сравниваться как равные. и Целочисленное постоянное выражение со значением 0 ... является константой нулевого указателя. Он оставляет фактические значения любого нулевого указателя, определенные в реализации, с несколькими возможными значениями.   -  person Andrew Henle    schedule 21.02.2016
comment
@AndrewHenle Как тогда работает оператор if, if( pointer ), где указатель является нулевым указателем?   -  person 2501    schedule 21.02.2016
comment
@AndrewHenle: Нет. 0 - это константа нулевого указателя, только контекст указателя. Но нулевой указатель - это переменная-указатель, которая равна константе нулевого указателя. (Я действительно ненавижу это C11 здесь не следовал C ++ 11 и предоставил конкретное ключевое слово, например _Nullptr - с заголовком + макрос nullptr). Другие языки, такие как Паскаль, с самого начала были более интеллектуальными.   -  person too honest for this site    schedule 21.02.2016
comment
@ 2501: реализация, например, может использовать битовый тест (при условии, что нулевой указатель просто имеет установленный бит, который в противном случае очищается). Как преобразовать что-то вроде _Bool b = 5;?   -  person too honest for this site    schedule 21.02.2016
comment
@Olaf Хорошо, я понимаю, что вы пытаетесь сказать. int i = null_pointer; i== 0 сравнение может дать что угодно, но сравнение null_pointer == 0 всегда вернет истину.   -  person 2501    schedule 21.02.2016
comment
@ 2501: int i = null_pointer определяется реализацией, если null_pointer является типом указателя. Если вы имеете в виду 0, это только константа нулевого указателя в контексте указателя, в противном случае это целочисленная константа (в других языках это называется целочисленным литералом ). Я избавляю нас от еще одной напыщенной речи об этой ненужной двойственности. Обратите внимание, что C ++ 11 ввел nullptr именно для того, чтобы избавиться от этого взлома (который хуже в C ++, поскольку вам нужно преобразовать void * в указатель, поэтому не может быть #define NULL ((void *)0), как в C. К вашему сведению: gcc использует встроенное имя для больше времени в этом макросе уже.   -  person too honest for this site    schedule 21.02.2016
comment
@ Олаф Нет. 0 - константа нулевого указателя, только контекст указателя. ОК. Я предполагал подразумеваемый контекст указателя.   -  person Andrew Henle    schedule 21.02.2016
comment
@AndrewHenle Ну, я тоже.   -  person 2501    schedule 21.02.2016
comment
@Olaf Я отказываюсь от своего первого комментария о значениях указателей. Нет смысла говорить, что у него есть значение, поскольку его можно сравнивать только с другими указателями и 0. Я думаю, что больше не буду использовать значение с указателями, поскольку это бессмысленно.   -  person 2501    schedule 21.02.2016
comment
@ 2501: Это зависит от обстоятельств. В обычном, совместимом со стандартами контексте значение указателя совершенно бессмысленно. По сути, указатель может быть нулевым указателем или указывать на массив (который включает отдельные объекты, которые для этого являются массивами длины 1). В любом случае фактическое битовое представление зависит от реализации. А сравнение двух указателей разрешено только для нулевых указателей или если они указывают на один и тот же массив - или точно за последний элемент. Но, например, во встроенных системах вы должны подчиняться правилам и полагаться на конкретное, то есть определяемое реализацией поведение.   -  person too honest for this site    schedule 21.02.2016
comment
@IlDivinCodino Есть два ответа, и они все еще имеют смысл (по крайней мере, один ..). Я отредактировал для большей ясности, стараясь не изменить смысл.   -  person Margaret Bloom    schedule 21.02.2016
comment
С редактированием возникает неоднозначный вопрос: хотите ли вы знать, как этого добиться стандартным способом или что об этом говорится в стандарте?   -  person edmz    schedule 22.02.2016
comment
@Black Какая разница? Ответ типа «Ты можешь использовать этот код» или «Нет, ты не можешь сделать» будет неудовлетворительным без ссылки на соответствующие строки из стандарта.   -  person Margaret Bloom    schedule 22.02.2016
comment
Единственная совместимая часть здесь - <type> * p= 0;, каждая реализация должна заполнять указатель null в p независимо от реализации или битовых шаблонов. Все остальное попадет под УБ.   -  person Henk Holterman    schedule 29.11.2016
comment
Обратите внимание, что стандарт создает достаточно места между спецификацией и реализацией, чтобы компилятор мог изменить направление памяти: p++ может уменьшить p. Пока все операторы, включая сравнение, участвуют в этом.   -  person Henk Holterman    schedule 29.11.2016
comment
Я считаю, что ответ всегда volatile. Некоторым модераторам не нравятся ответы, основанные на этом, и они удаляют их.   -  person curiousguy    schedule 25.05.2019


Ответы (5)


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

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

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


В разделе 6.3.2.3. это читается

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

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

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

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

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

Учитывая приведенный ниже код

char* p = (char*)241;

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

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

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

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

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

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


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

В стандарте сказано, что

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

и это

Следовательно, любое выражение, относящееся к такому объекту [volatile], должно оцениваться строго в соответствии с правилами абстрактной машины.

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

return *(char*)0;

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

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


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

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

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

person Margaret Bloom    schedule 21.02.2016
comment
Думаю, у вас есть основная идея. По сути, вы не должны думать об указателях как об адресах в памяти, и это позволяет избежать большинства заблуждений. - person Kerrek SB; 21.02.2016
comment
Кажется, это работает: volatile uintptr_t addr = 0; return *(volatile char *)(addr);. Но это вызывает дополнительную операцию с памятью. Возможно, лучше всего будет записать доступ к адресу 0 непосредственно в машинном коде. - person Kerrek SB; 24.02.2016
comment
Адреса - это не просто числа. См. Мои многочисленные вопросы (в основном плохо полученные) об указателях в C и C ++, например Являются ли переменные указателя просто целыми числами с некоторыми операторами или они «символические» ? - person curiousguy; 25.05.2019

Поскольку OP правильно заключила в своем ответе на свой вопрос:

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

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

Вариантом этой схемы было бы определение символа по нулевому адресу и простое использование его для получения любого другого необходимого адреса. Для этого добавьте что-то вроде следующего в SECTIONS часть скрипта компоновщика (при условии синтаксиса 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, и каждый компоновщик может иметь другой синтаксис для своего сценария компоновщика. Кроме того, сгенерированный код может быть менее эффективным, потому что компилятору неизвестен адрес, к которому осуществляется доступ. Но я думаю, что это все же добавляет интересную перспективу к вопросу, поэтому, пожалуйста, простите за ответ, который немного не по теме ..)

person CliffordVienna    schedule 15.05.2016
comment
Обратите внимание, что при использовании таких конструкций может потребоваться отключить определенные оптимизации, а некоторые компиляторы, которые не могут отключить такие оптимизации, могут вообще не поддерживать такие конструкции надежно. Например, учитывая char *p = _memory; ... if (p) ... или даже if ((uintptr_t)p), компилятор может решить, что адрес p не может совпадать с адресом нулевого указателя (так как ему был назначен адрес _memory) и пропустить сравнение, что приведет к неизвестному количеству хаоса. - person supercat; 17.10.2018
comment
Этот. Это не только правильный ответ на вопрос, но и единственный правильный способ работы с данными, которые следует размещать по определенным фиксированным адресам памяти, зависящим от платформы. Использование жестко запрограммированных указателей является обычным, но неправильным. - person Igor Zhirkov; 27.03.2019

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

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

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

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

§ 5.1.2

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

§ 5.1.2.1, запятая 1

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

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


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

Все цитаты взяты из черновика № 1570.

person edmz    schedule 21.02.2016
comment
Стандарт не требует, чтобы какая-либо реализация подходила для какой-либо конкретной цели. Действительно, авторы признают (в Обосновании), что реализация может быть одновременно согласованной и бесполезной. Хотя автономные реализации не обязаны определять какие-либо средства, с помощью которых программа могла бы вести себя способом, отличным от качественного int main(void) { volatile int dummy; while(!dummy) {} }, автономные реализации будут определять полезное поведение даже в тех случаях, когда этого не требует Стандарт. - person supercat; 13.08.2018

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

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

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

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

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

person supercat    schedule 25.02.2016

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

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

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

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


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

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

person M.M    schedule 21.02.2016
comment
Даже если бы я попросил получить доступ к адресу 100, это было бы невозможно сделать стандартным способом C. Хотя я обнаружил эту проблему из-за невозможности получить доступ к адресу нулевого указателя, это не проблема с указателями со значением 0, это проблема с указателями любого значения. Целочисленные константы просто не указывают машинные адреса (карта определяется реализацией), и это то, чего мне не хватало. Что касается специфического для реализации способа, с целочисленными константами GCC фактически указываются адреса, а -fno-isolate-erroneous-paths-dereference предотвращает генерацию ловушки ud2. - person Margaret Bloom; 22.02.2016
comment
Реализация может определять преобразование (char *)100. Я думаю это отдельная тема - person M.M; 22.02.2016
comment
Указатель NULL гарантированно сравнивается с указателем на любой объект или функцию, что подразумевает, что компилятор никогда не может сгенерировать объект, адрес которого является местоположением нулевого указателя, это не означает, что на самом деле не может быть объекта в этом местоположение (только то, что вы не можете взять (действительный) указатель на этот объект). Доступ к допустимому объекту по адресу NULL является определенным реализацией, но не неопределенным поведением. NULL может указывать на действительный объект, это просто не будет действительным указателем (обрабатывается так же, как если бы он был смещен, т.е. определен реализацией). - person yyny; 31.07.2020
comment
@yyny нулевые указатели не указывают на местоположение (в абстрактной машине, как определяется C) - person M.M; 01.08.2020