Могу ли я когда-нибудь получить доступ к нулевому адресу?

Константа 0 используется как нулевой указатель в C и C++. Но, как и в вопросе "Указатель на конкретный фиксированный адрес", кажется, есть некоторое возможное использование назначения фиксированных адресов. Есть ли когда-нибудь мыслимая потребность в любой системе для любой задачи низкого уровня для доступа к адресу 0?

Если есть, как это решается с 0, являющимся нулевым указателем и все?

Если нет, то почему он уверен, что такой необходимости нет?

17 ответов

Решение

Ни в C, ни в C++ значение нулевого указателя никак не связано с физическим адресом 0, Тот факт, что вы используете постоянный 0 в исходном коде установка указателя на значение нулевого указателя является не чем иным, как кусочком синтаксического сахара. Компилятор должен преобразовать его в фактический физический адрес, используемый в качестве значения нулевого указателя на конкретной платформе.

Другими словами, 0 в исходном коде не имеет никакого физического значения вообще. Это могло бы быть 42 или же 13, например. Т.е. авторы языка, если бы они были так довольны, могли бы сделать так, чтобы вам пришлось делать p = 42 для того, чтобы установить указатель p к значению нулевого указателя. Опять же, это не значит, что физический адрес 42 должен быть зарезервирован для нулевых указателей. Компилятор будет обязан переводить исходный код p = 42 в машинный код, который будет заполнять фактическое значение физического нулевого указателя (0x0000 или же 0xBAAD) в указатель p, Это именно так, как сейчас с постоянной 0,

Также обратите внимание, что ни C, ни C++ не предоставляют строго определенной функции, которая позволила бы вам назначить конкретный физический адрес указателю. Таким образом, ваш вопрос о том, "как можно присвоить 0 адрес указателю", формально не имеет ответа. Вы просто не можете назначить конкретный адрес указателю в C/C++. Однако в области функций, определенных реализацией, явное преобразование целочисленных значений в указатели должно иметь такой эффект. Итак, вы сделали бы это следующим образом

uintptr_t address = 0;
void *p = (void *) address;

Обратите внимание, что это не то же самое, что делать

void *p = 0;

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

На заметку: вас может заинтересовать тот факт, что с помощью компилятора Microsoft C++ NULL-указатель на член будет представлен в виде битового шаблона 0xFFFFFFFF на 32-битной машине. То есть:

struct foo
{
      int field;
};

int foo::*pmember = 0;     // 'null' member pointer

У pmember будет битовый паттерн "все единицы". Это потому, что вам нужно это значение, чтобы отличить его от

int foo::*pmember = &foo::field;

где битовая комбинация действительно будет иметь "все нули", поскольку мы хотим сместить 0 в структуре foo.

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

Вы начинаете с ошибочной предпосылки. Когда вы назначаете целочисленную константу со значением 0 указателю, это становится нулевой константой указателя. Это, однако, не означает, что нулевой указатель обязательно ссылается на адрес 0. Наоборот, стандарты C и C++ очень ясны, что нулевой указатель может ссылаться на некоторый адрес, отличный от нуля.

Это сводится к следующему: вам нужно выделить адрес, на который будет ссылаться нулевой указатель, - но это может быть практически любой адрес, который вы выберете. Когда вы конвертируете ноль в указатель, он должен ссылаться на этот выбранный адрес - но это все, что действительно требуется. Например, если вы решили, что преобразование целого числа в точку будет означать добавление 0x8000 к целому числу, то нулевой указатель на будет фактически ссылаться на адрес 0x8000 вместо адреса 0.

Стоит также отметить, что разыменование нулевого указателя приводит к неопределенному поведению. Это означает, что вы не можете сделать это в переносимом коде, но это не значит, что вы не можете сделать это вообще. Когда вы пишете код для небольших микроконтроллеров и т. Д., Довольно часто включаются некоторые фрагменты кода, которые вообще не переносимы. Считывание с одного адреса может дать вам значение с какого-либо датчика, в то время как запись на тот же адрес может активировать шаговый двигатель (например, только). Следующее устройство (даже использующее точно такой же процессор) может быть подключено, поэтому оба этих адреса относятся к обычному ОЗУ.

Даже если нулевой указатель ссылается на адрес 0, это не мешает вам использовать его для чтения и / или записи того, что происходит по этому адресу - это просто мешает вам делать это переносимо - но это не так действительно имеет большое значение. Единственная причина, по которой адрес ноль, как правило, важен, заключается в том, что он был декодирован для подключения к чему-то другому, чем обычное хранилище, так что вы, вероятно, не сможете использовать его полностью переносимым.

Компилятор позаботится об этом за вас ( comp.lang.c FAQ):

Если машина использует ненулевой битовый шаблон для нулевых указателей, компилятор обязан сгенерировать его, когда программист запросит, записав "0" или "NULL", нулевой указатель. Следовательно, #defining NULL как 0 на машине, для которой внутренние нулевые указатели отличны от нуля, является таким же допустимым, как и на любом другом, потому что компилятор должен (и может) генерировать правильные нулевые указатели машины в ответ на неукрашенные 0, наблюдаемые в контекстах указателей.

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

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

Адрес 0 может быть полезен во встроенных системах (общий термин для процессора, который не находится в компьютере; они запускают все от вашей стереосистемы до вашей цифровой камеры). Обычно системы разрабатываются так, что вам не нужно писать по адресу 0. В каждом известном мне случае это какой-то особый адрес. Даже если программисту необходимо написать в него (например, настроить таблицу прерываний), им нужно будет записать в него только во время начальной последовательности загрузки (обычно это небольшой кусочек ассемблера для настройки среды для C).

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

На x86 адрес 0 (или, вернее, 0000: 0000) и его окрестности в реальном режиме являются местоположением вектора прерывания. В старые добрые времена вы обычно записывали значения в вектор прерываний для установки обработчиков прерываний (или, если вы были более дисциплинированными, использовали службу MS-DOS 0x25). Компиляторы C для MS-DOS определили тип дальнего указателя, который при назначении NULL или 0 получит битовую комбинацию 0000 в своей части сегмента и 0000 в своей части смещения.

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

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

У вас даже нет ОС с точки зрения настольного / серверного ПК, и у вас нет виртуальной памяти и тому подобного. Так что все в порядке и даже необходимо для доступа к памяти по определенному адресу. На современном настольном / серверном ПК это бесполезно и даже опасно.

Я скомпилировал некоторый код, используя gcc для Motorola HC11, у которого нет MMU и 0 - это совершенно хороший адрес, и был разочарован, узнав, что для записи по адресу 0 вы просто пишете в него. Там нет никакой разницы между NULL и адресом 0.

И я понимаю почему. Я имею в виду, что на самом деле невозможно определить уникальный NULL в архитектуре, где каждая ячейка памяти потенциально допустима, поэтому я предполагаю, что авторы gcc только что сказали, что 0 достаточно для NULL, независимо от того, является ли он действительным адресом или нет.

      char *null = 0;
; Clears 8-bit AR and BR and stores it as a 16-bit pointer on the stack.
; The stack pointer, ironically, is stored at address 0.
1b:   4f              clra
1c:   5f              clrb
1d:   de 00           ldx     *0 <main>
1f:   ed 05           std     5,x

Когда я сравниваю его с другим указателем, компилятор генерирует регулярное сравнение. Это означает, что это никак не учитывает char *null = 0 быть специальным указателем NULL, и фактически указатель на адрес 0 и указатель "NULL" будут равны.

; addr is a pointer stored at 7,x (offset of 7 from the address in XR) and 
; the "NULL" pointer is at 5,y (offset of 5 from the address in YR).  It doesn't
; treat the so-called NULL pointer as a special pointer, which is not standards
; compliant as far as I know.
37:   de 00           ldx     *0 <main>
39:   ec 07           ldd     7,x
3b:   18 de 00        ldy     *0 <main>
3e:   cd a3 05        cpd     5,y
41:   26 10           bne     53 <.LM7>

Итак, чтобы ответить на исходный вопрос, я думаю, что мой ответ - проверить реализацию вашего компилятора и выяснить, не удосужились ли они реализовать NULL с уникальным значением. Если нет, вам не нужно беспокоиться об этом.;)

(Конечно, этот ответ не соответствует стандарту.)

Помните, что во всех обычных случаях вы не видите конкретных адресов. Когда вы выделяете память, ОС предоставляет вам адрес этой части памяти.

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

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

int* i = new int(); // suppose this returns a pointer to address zero
*i = 42; // now we're accessing address zero, writing the value 42 to it

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

Значение 0 == null действительно становится проблемой, только если по какой-то причине вы обращаетесь к физической памяти напрямую. Возможно, вы пишете ядро ​​ОС или что-то в этом роде самостоятельно. В этом случае вы будете выполнять запись по определенным адресам памяти (особенно по тем, которые сопоставлены с аппаратными регистрами), и, таким образом, вам, вероятно, может потребоваться запись по нулевому адресу. Но тогда вы действительно обходите C++ и полагаетесь на специфику вашего компилятора и аппаратной платформы.

Конечно, если вам нужно написать по адресу ноль, это возможно. Только постоянная 0 представляет нулевой указатель. Непостоянное целочисленное значение, равное нулю, не будет, если назначено указателю, дать нулевой указатель.

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

int i = 0;
int* zeroaddr = (int*)i;

теперь zeroaddr будет указывать на адрес ноль (*), но, строго говоря, он не будет нулевым указателем, поскольку нулевое значение не было постоянным.

(*): это не совсем так. Стандарт C++ гарантирует только "определяемое реализацией отображение" между целыми числами и адресами. Он может конвертировать 0 в адрес 0x1633de20` или любой другой адрес, который ему нравится. Но отображение обычно является интуитивно понятным и очевидным, где целое число 0 отображается на нулевой адрес)

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

Да, вы можете захотеть получить доступ к адресу памяти 0x0h. Почему вы хотите это сделать, зависит от платформы. Процессор может использовать это для вектора сброса, так что запись в него приводит к сбросу процессора. Он также может быть использован для вектора прерывания, как отображенный в памяти интерфейс к какому-либо аппаратному ресурсу (счетчик программ, системные часы и т. Д.), Или он может быть допустимым как простой старый адрес памяти. Нет ничего волшебного в нулевом адресе памяти, это просто тот, который исторически использовался для специальных целей (векторы сброса и тому подобное). С-подобные языки следуют этой традиции, используя ноль в качестве адреса для указателя NULL, но в действительности базовое оборудование может видеть или не видеть нулевой адрес как особый.

Необходимость доступа к нулевому адресу обычно возникает только в деталях низкого уровня, таких как загрузчики или драйверы. В этих случаях компилятор может предоставить опции / прагмы для компиляции части кода без оптимизации (для предотвращения извлечения нулевого указателя в виде указателя NULL) или встроенная сборка может использоваться для доступа к истинному нулевому адресу.

Если я правильно помню, в микроконтроллере AVR файл регистра отображается в адресном пространстве ОЗУ, а регистр R0 находится по адресу 0x00. Это было сделано специально, и, по-видимому, Atmel считает, что существуют ситуации, когда удобнее обращаться к адресу 0x00 вместо явной записи R0.

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

Запись по нулевому адресу может быть выполнена, но это зависит от нескольких факторов, таких как ваша ОС, целевая архитектура и конфигурация MMU. На самом деле, это может быть полезным инструментом отладки (но не всегда).

Например, несколько лет назад, работая над встроенной системой (с небольшим количеством доступных инструментов отладки), у нас была проблема, которая приводила к перезагрузке. Чтобы помочь найти проблему, мы отлаживали с помощью sprintf(NULL, ...); и 9600 бод последовательный кабель. Как я уже сказал, доступно несколько инструментов для отладки. С нашей настройкой мы знали, что горячая перезагрузка не повредит первые 256 байтов памяти. Таким образом, после горячей перезагрузки мы можем приостановить загрузчик и сбросить содержимое памяти, чтобы узнать, что произошло до перезагрузки.

Время от времени я использовал нагрузки с нулевого адреса (на известной платформе, где это гарантированно могло бы вызвать segfault) для преднамеренного сбоя с информативно именованным символом в коде библиотеки, если пользователь нарушает какое-то необходимое условие, и нет никакого хорошего способа бросить исключение доступно для меня. "Segfault at someFunction$xWasnt16ByteAligned"довольно эффективное сообщение об ошибке, чтобы предупредить кого-то о том, что он сделал неправильно и как это исправить. Тем не менее, я бы не советовал заводить подобные вещи.

C/C++ не позволяет писать по любому адресу. Именно ОС может выдавать сигнал, когда пользователь получает доступ к какому-либо запрещенному адресу. C и C++ гарантируют, что любая память, полученная из кучи, будет отличаться от 0.

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

Код ниже даже не компилируется, так как NULL не определен:

int main(int argc, char *argv[])
{
    void *p = NULL;
    return 0;
}

OTOH, код ниже компилируется, и вы можете читать и записывать адрес 0, если оборудование / ОС позволяет:

int main(int argc, char *argv[])
{
    int *p = 0;
    *p = 42;
    int x = *p; /* let's assume C99 */
}

Обратите внимание, я не включил ничего в приведенные выше примеры. Если мы начнем включать вещи из стандартной библиотеки C, NULL станет магически определенным. Насколько я помню, это исходит от string.h,

NULL по-прежнему не является основной функцией C, это СОДЕРЖАНИЕ многих функций библиотеки C, указывающих на недействительность указателей. Библиотека C на данной платформе определит NULL для области памяти, которая в любом случае недоступна. Давайте попробуем это на ПК с Linux:

#include <stdio.h>
int main(int argc, char *argv[])
{
        int *p = NULL;
        printf("NULL is address %p\n", p);
        printf("Contents of address NULL is %d\n", *p);
        return 0;
}

Результат:

NULL is address 0x0
Segmentation fault (core dumped)

Таким образом, наша библиотека C определяет NULL для адреса ноль, который оказывается недоступным. Но это был не компилятор C, даже не функция C-библиотеки printf() это обрабатывало нулевой адрес специально. Все они счастливо пытались работать с ним нормально. Именно ОС обнаружила ошибку сегментации, когда printf пытался читать с адреса ноль.

Другие вопросы по тегам