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

44

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

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

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

  • 2
    Интересный вопрос! Я полагаю, потому что адрес 0 это всего 1 байт, без которого вы можете жить ...
  • 3
    Считаете ли вы это правильным использованием: чтобы вызвать сбой вашей программы, выполнить произвольный код и захватить вашу систему? :)
Показать ещё 9 комментариев
Теги:
pointers
memory

17 ответов

53
Лучший ответ

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

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

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

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

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

void *p = 0;

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

  • 0
    Прекрасный ответ. Хотел бы я дважды проголосовать за этот ответ.
  • 0
    Благодарю. У меня есть несколько вопросов, понимающих ваш ответ stackoverflow.com/questions/52207010/…
18

В тангенциальной ноте: вам может быть интересно узнать, что с компилятором Microsoft С++ указатель NULL для члена будет представлен как битовый шаблон 0xFFFFFFFF на 32-битной машине. То есть:

struct foo
{
      int field;
};

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

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

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

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

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

  • 2
    Интересно! +1 даже если это мало связано с вопросом :)
  • 0
    Чего ждать? Что произойдет, когда я начну присваивать этот указатель различным типам указателей - с помощью (void *) приведения при необходимости? Останется ли все 1 или что будет? Означает ли это, что все нули "-1" в MS C ++?
Показать ещё 3 комментария
12

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

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

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

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

  • 15
    Его предпосылка в порядке. Он хочет знать: как вы обращаетесь к адресу памяти 0, поскольку (void *) 0 особенный?
  • 2
    @Daniel: (void *) 0 особенный. int x = 0; (void *) x нет. Он, конечно, не переносимый, но это не то, о чем беспокоятся люди, которым нужен доступ к определенным ячейкам памяти.
Показать ещё 7 комментариев
8

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

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

Вы можете обращаться к нулю, ссылаясь на нуль из контекста не указателя.

  • 0
    @gf ты меня побил :)
7

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

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

  • 0
    Например, в архитектуре ARM адрес 0 является вектором сброса. Если вы перезаписали то, что есть (должен быть переход к реальному коду сброса), вы нарушили свою способность к мягкому сбросу. (Тем не менее, адрес 0 часто отображается во флэш-память, которую не так просто перезаписать.)
  • 1
    RE In practice, C compilers will happily let your program attempt to write to address 0. Я не верю, что это больше точно. Компилятор увидит наивные попытки записи по адресу 0 и оптимизирует его по UB, например, при перехвате, смотрите мой ответ здесь для более подробной информации.
6

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

  • 6
    Это верно для компьютеров x86, но не для встраиваемых систем.
  • 2
    Там нет никакой гарантии памяти в страницах вообще, ни о том, что есть ядро.
Показать ещё 2 комментария
6

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

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

  • 3
    На самом деле, немедленная плохая ситуация не будет вызвана записью в 0 на x86. 0 для вектора прерывания исключения Divide By Zero, и это редко срабатывает ..
  • 0
    IIRC, в серии Motorola 68K, 0 был начальным адресом - первая инструкция, которая должна быть выполнена при запуске, была загружена с нулевого адреса. Прошло несколько лет, поэтому я могу ошибаться, но я уверен в этом на 90%, хотя у меня нет рекомендаций для подтверждения.
Показать ещё 1 комментарий
5

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

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

  • 2
    А в микроконтроллерах адрес 0 часто является точкой входа в программу. Сброс программы может быть выполнен путем перехода к адресу 0.
3

Я скомпилировал некоторый код, используя 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 уникального значения. Если нет, вам не о чем беспокоиться.;)

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

  • 0
    0 является уникальным значением для NULL. Это просто значение, которое система позволит вам написать. На других системах это не так, поэтому есть ошибка. В других системах вы можете писать в нее, но, вероятно, не должны, поэтому возникает какой-то другой неясный результат [я думаю, что HC11 подпадает под эту категорию; адрес 0 особенный, но я не помню, что он представляет]. Все три поведения «соответствуют стандарту», поскольку стандарты оставляют поведение неопределенным. По крайней мере, это так в C ++; Я предполагаю, что C то же самое.
  • 0
    @Dennis Zickefoose: в соответствии со стандартами я имел в виду части стандарта, в которых упоминается, что указатель NULL гарантированно не указывает на объект, подразумевая, что сравнение любого другого указателя с NULL вернет false. На HC11 адрес 0 - это просто обычное ОЗУ, ничего особенного, и вы должны использовать адрес 0 для чего-то, потому что вы можете использовать прямую адресацию для ОЗУ от 0 до 255 байт, что приводит к меньшим инструкциям и более быстрому доступу.
Показать ещё 1 комментарий
1

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

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

1

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

0

Это может удивить многих людей, но в основном языке C нет такой вещи, как специальный нулевой указатель. Вы можете свободно читать и писать по адресу 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 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 попытался прочитать нулевой адрес.

  • 1
    NULL должен быть определен в 0 или (void *)0 , и это часть компилятора (не библиотеки), которая выполняет преобразование целого в указатель, который преобразует целое число 0 во все, что компилятор хочет, чтобы нулевой указатель выглядел как , Библиотека C не принимает решение о представлении нулевого указателя. int *p = 0; и int *p = NULL; определены как идентичные стандарту C (при условии, что включен заголовок, который определяет NULL ).
0

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

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

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

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

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

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

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

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

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

0

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

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

0

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

0

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

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

0

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню