Диапазон адресов указателя C на разных платформах

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

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

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

#include <stdlib.h>
#include <stdio.h>

#define ERROR_NULL 0x0
#define ERROR_ZERO 0x1

int *example(int *a) {
    if (*a < 0)
        return ERROR_NULL;
    if (*a == 0)
        return (void *) ERROR_ZERO;
    return a;
}

int main(int argc, char **argv) {
    if (argc != 2) return -1;
    int *result;
    int a = atoi(argv[1]);
    switch ((int) (result = example(&a))) {
        case ERROR_NULL:
            printf("Below zero!\n");
            break;

        case ERROR_ZERO:
            printf("Is zero!\n");
            break;

        default:
            printf("Is %d!\n", *result);
            break;
    }
    return 0;
}

Знание некоторого специального диапазона адресов, который никогда не будет использоваться пользовательскими приложениями, может быть эффективно использовано для более эффективной и чистой обработки условий. Если вы знаете об этом, для каких платформ это применимо? Я предполагаю, что промежутки будут зависеть от операционной системы. Я в основном интересуюсь Linux, но было бы неплохо узнать об OS X, Windows, Android и других системах.

7 ответов

Решение

Ответ во многом зависит от вашего компилятора C, а также от вашего процессора и ОС, где будет работать ваша скомпилированная программа C.

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

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

Стандарт C никоим образом не определяет, что является допустимым диапазоном для указателей и не является. В Си действительные указатели либо NULL указатели или указатели на объекты, время жизни которых еще не закончилось, и это могут быть ваши глобальные и локальные переменные, а также созданные в malloc()'d память и функции. ОС может расширить этот диапазон, возвращая:

  • указатели на код или объекты данных, которые явно не определены в вашей C-программе на уровне исходного кода (ОС может предоставлять приложениям прямой доступ к некоторому коду или данным, но это редко, или ОС может предоставлять приложениям доступ к некоторым их частям, которые либо создаются ОС при загрузке приложения, либо создаются компилятором при компиляции приложения, одним из примеров может быть Windows, позволяющая приложениям проверять свой исполняемый образ PE, вы можете спросить Windows, где образ начинается в памяти)
  • указатели на буферы данных, выделенные ОС для / от имени приложений (здесь, как правило, ОС будет использовать свои собственные API, а не приложения) malloc()/free()и вам потребуется использовать соответствующую функцию для конкретной ОС, чтобы освободить эту память)
  • Специфичные для ОС указатели, которые не могут быть разыменованы и служат только в качестве индикаторов ошибок (например, у вас может быть больше, чем один недопустимый указатель типа NULL и ваш ERROR_ZERO это возможный кандидат)

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

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

char ErrorVars[5] = { 0 };
void* ErrorPointer1 = &ErrorVars[0];
void* ErrorPointer2 = &ErrorVars[1];
...
void* ErrorPointer5 = &ErrorVars[4];

Затем вы можете вернуться ErrorPointer1 через ErrorPointer1 на различных условиях ошибки, а затем сравните возвращенное значение с ними. Здесь есть оговорка. Вы не можете юридически сравнить возвращенный указатель с произвольным указателем, используя >, >=, <, <=, Это допустимо, только когда оба указателя указывают на один и тот же объект. Итак, если вы хотите быструю проверку, как это:

if ((char*)(p = myFunction()) >= (char*)ErrorPointer1 &&
    (char*)p <= (char*)ErrorPointer5)
{
  // handle the error
}
else
{
  // success, do something else
}

это будет только законно, если p равняется одному из этих 5 указателей ошибок. Если это не так, ваша программа может по закону вести себя любым мыслимым и невообразимым образом (это потому, что так говорит стандарт Си). Чтобы избежать этой ситуации, вам придется сравнивать указатель с каждым указателем ошибки в отдельности:

if ((p = myFunction()) == ErrorPointer1)
  HandleError1();
else if (p == ErrorPointer2)
  HandleError2();
else if (p == ErrorPointer3)
  HandleError3();
...
else if (p == ErrorPointer5)
  HandleError5();
else
  DoSomethingElse();

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

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

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

Это не совсем верно: есть компьютеры, где NULL Указатель не является внутренним нулем ( ссылка).

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

Четное NULL не универсален; нет других универсально неиспользуемых адресов памяти, что неудивительно, учитывая количество различных платформ, программируемых на C.

Однако никто не мешает вам определить свой собственный специальный адрес в памяти, установить его в глобальную переменную и рассматривать его как индикатор ошибки. Это будет работать на всех платформах и не потребует специального адреса.

В шапке:

extern void* ERROR_ADDRESS;

В файле C:

static int UNUSED;
void *ERROR_ADDRESS = &UNUSED;

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

Это полностью зависит как от компьютера, так и от операционной системы. Например, на компьютере с отображенным в память вводом-выводом, таким как Game Boy Advance, вы, вероятно, не хотите путать адрес для "какого цвета верхний левый пиксель" с данными пользовательского пространства:

http://www.coranac.com/tonc/text/hardware.htm

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

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

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

В Linux в 64-битной среде и при использовании архитектуры x86_64 (от Intel или AMD) используется только 48-битное 64-битное общее адресное пространство (аппаратное ограничение AFAIK). По сути, любой адрес после 2^47 до 2^62 может быть использован сейчас, так как он не будет выделен.

Для некоторого фона виртуальное адресное пространство процесса Linux состоит из пространства пользователя и ядра. В вышеупомянутой архитектуре первые 47 бит (128 ТБ) используются для пользовательского пространства. Пространство ядра используется в конце спектра, поэтому последние 128 ТБ в конце полного 64-битного адресного пространства. Между терра инкогнита. Хотя это может измениться в любое время в будущем, и это не переносимо.

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

Этот код:

#define ERROR_NULL 0x0
#define ERROR_ZERO 0x1

int *example(int *a) {
    if (*a < 0)
        return ERROR_NULL;
    if (*a == 0)
        return (void *) ERROR_ZERO;
    return a;
}

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

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

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

#define SUCCESS     0x0
#define ERROR_NULL  0x1
#define ERROR_ZERO  0x2

int example(int *a, int** out) {
    if (...)
        return ERROR_NULL;
    if (...)
        return ERROR_ZERO;
    *out = a;
    return SUCCESS;
}
...
int* out = NULL;
int retVal = example(..., &out);
if (retVal != SUCCESS)
    ...

На самом деле NULL(0) является действительным адресом. Но это не тот адрес, на который вы обычно пишете.

По памяти NULL может быть другим значением на старом оборудовании VAX с очень старым компилятором c. Может быть, кто-то может это подтвердить. Теперь это будет всегда 0, как определяет стандарт C - см. Этот вопрос Всегда ли NULL false?

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

Лично я предпочитаю не возвращать void*, а заставить функцию взять void** и вернуть результат там. Затем вы можете вернуть код ошибки напрямую, где 0 = успех.

например

int posix_memalign(void **memptr, size_t alignment, size_t size);

Обратите внимание, что выделенная память возвращается в memptr. Код результата возвращается вызовом функции. В отличие от malloc.

void *malloc(size_t size)

Как говорили другие, это сильно зависит. Однако если вы находитесь на платформе с динамическим распределением, то -1 (вероятно) является безопасным значением.

Это потому, что распределитель памяти выдает память в БОЛЬШИХ БЛОКАХ, а не только в одиночных байтах. Поэтому последний адрес, который можно вернуть, будет -block_size, Например, если block_size равен 4, последний блок будет охватывать адреса { -4, -3, -2, -1 }, а последний возможный адрес будет -4 = 0xFFFF...FFFC. В результате -1 никогда не будет возвращено malloc() семья

Различные системные функции в Linux также возвращают -1 для неверного указателя вместо NULL. Например mmap() а также shmat(), Они должны это делать, поскольку иногда NULL является действительным адресом памяти. На самом деле, если вы используете гарвардскую архитектуру, то нулевое местоположение в пространстве данных вполне пригодно для использования. И даже на архитектурах фон Неймана то, что вы сказали

"NULL - это просто специальный адрес памяти 0x0, который никогда не используется ни для чего, кроме как для обозначения возникновения специального условия"

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

Фактически, совершенно вопреки первоначальному предпочтительному использованию нулевой страницы, некоторые современные операционные системы, такие как FreeBSD, Linux и Microsoft Windows, фактически делают нулевую страницу недоступной для захвата указателей NULL. Это полезно, так как указатели NULL - это метод, используемый для представления значения ссылки, которая ничего не указывает

https://en.wikipedia.org/wiki/Zero_page

В MSVC указатель NULL на член также представляется как битовая комбинация 0xFFFFFFFF на 32-битной машине


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

В настоящее время выравнивание по умолчанию для malloc имеет размер 8 или 16 байт в зависимости от того, используете ли вы 32- или 64-разрядную ОС, что означает, что для отчетов об ошибках у вас будет как минимум 3 бита. И если вы используете указатель на тип шире, чем char, то он всегда выровнен. Так что обычно не о чем беспокоиться, если только вы не хотите вернуть указатель на символ, который не выводится из malloc, Просто проверьте младший значащий бит, чтобы увидеть, является ли он действительным указателем или нет

int* result = func();
if ((uintptr_t)result & 1)
    error_happened(); // now the high bits can be examined to check the error condition

В случае 16-байтового выравнивания последние 4 бита действительного адреса всегда равны 0, а общее количество действительных адресов составляет всего ¹⁄₁₆ от общего количества битовых комбинаций, что означает, что вы можете вернуть самое большее ¹⁵⁄₁₆ × 2 64 кода ошибки с 64-битным указателем. Тогда есть aligned_alloc если вы хотите более младшие разряды.

Этот прием использовался для хранения некоторой информации в самом указателе. На многих 64-битных платформах вы также можете использовать старшие биты для хранения большего количества данных. См. Использование дополнительных 16 битов в 64-битных указателях.

Смотрите также


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

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