Как сравнить C-указатели?

Недавно я написал код для сравнения указателей, подобных этому:

if(p1+len < p2)

однако, некоторые сотрудники сказали, что я должен написать вот так:

if(p2-p1 > len)

чтобы быть в безопасности. Здесь p1 и p2 являются указателями char *, len - целым числом. Я понятия не имею об этом. Правильно?

EDIT1: конечно, p1 и p2 указатель на тот же объект памяти при попрошайничестве.

EDIT2: всего минуту назад я нашел bogo этого вопроса в своем коде (около 3K строк), потому что len настолько велик, что p1+len не может хранить в 4 байтах указателя, поэтому p1 + len < p2 истинно. Но на самом деле это не так, поэтому я думаю, что мы должны сравнить указатели, подобные этому, в некоторой ситуации:

if(p2 < p1 || (uint32_t)p2-p1 > (uint32_t)len)

Ответ 1

В общем, вы можете безопасно сравнивать указатели, только если они оба указывают на части одного и того же объекта памяти (или на одну позицию после конца объекта). Когда p1, p1 + len и p2 все соответствуют этому правилу, вы оба, if -tests эквивалентны, так что вам не о чем беспокоиться. С другой стороны, если известно, что только p1 и p2 соответствуют этому правилу, и p1 + len может быть слишком далеко за концом, только if(p2-p1 > len) безопасен. (Но я не могу представить, что это для вас. Я предполагаю, что p1 указывает на начало некоторого блока памяти, а p1 + len указывает на позицию после его конца, верно?)

То, о чем они могли думать, - целочисленная арифметика: если возможно, что i1 + i2 переполнится, но вы знаете, что i3 - i1 не будет, то i1 + i2 < i3 может либо обернуться (если они являются целыми числами без знака), либо вызвать неопределенное поведение (если они являются целыми числами со знаком) или и то, и другое (если ваша система выполняет обход по переполнению со знаком-целым числом), тогда как у i3 - i1 > i2 такой проблемы не будет.


Отредактировано, чтобы добавить: В комментарии вы пишете " len это значение из положительного эффекта, так что это может быть что угодно". В этом случае они совершенно правы, и p2 - p1 > len безопаснее, поскольку p1 + len может быть недействительным.

Ответ 2

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

void func(int len)
{
    char array[10];
    char *p = &array[0], *q = &array[10];
    if (p + len <= q)
        puts("OK");
}

Вы можете подумать о такой функции:

// if (p + len <= q)
// if (array + 0 + len <= array + 10)
// if (0 + len <= 10)
// if (len <= 10)
void func(int len)
{
    if (len <= 10)
        puts("OK");
}

Однако компилятор знает, что ptr <= q истинно для всех допустимых значений ptr, поэтому он может оптимизировать эту функцию:

void func(int len)
{
    puts("OK");
}

Гораздо быстрее! Но не то, что вы намеревались.

Да, есть компиляторы, которые существуют в дикой природе, которые делают это.

Заключение

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

if (p - q <= 10)

Ответ 3

Технически, p1 и p2 должны быть указателями в один и тот же массив. Если они не находятся в одном массиве, поведение undefined.

Для версии добавления тип len может быть любым целым типом.

Для версии с разницей результат вычитания ptrdiff_t, но любой целочисленный тип будет соответствующим образом преобразован.

В рамках этих ограничений вы можете написать код в любом случае; ни вернее. Отчасти это зависит от того, какую проблему вы решаете. Если вопрос заключается в том, являются ли эти два элемента массива более чем len элементами отдельно ", то вычитание является подходящим. Если вопрос:" есть p2 тот же самый элемент, что и p1[len] (aka p1 + len) ', тогда добавление является подходящим.

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

Ответ 4

Существующие ответы показывают, почему if (p2-p1 > len) лучше, чем if (p1+len < p2), но там все еще есть с ним - если p2 имеет значение BEFORE p1 в буфере, а len - неподписанный тип ( например size_t), тогда p2-p1 будет отрицательным, но будет преобразован в большое значение без знака для сравнения с беззнаковым len, поэтому результат, вероятно, будет истинным, что может быть не тем, что вы хотите.

Таким образом, для полной безопасности вам может понадобиться нечто вроде if (p1 <= p2 && p2 - p1 > len).

Ответ 5

Как уже сказал Дитрих, сравнение несвязанных указателей опасно и может рассматриваться как поведение undefined.

Учитывая, что два указателя находятся в диапазоне от 0 до 2 ГБ (в 32-битной системе Windows), вычитание двух указателей даст вам значение от -2 ^ 31 до +2 ^ 31. Это точно домен подписанного 32-битного целого. Поэтому в этом случае кажется, что имеет смысл вычесть два указателя, потому что результат всегда будет в пределах домена, который вы ожидаете.

Однако, если в вашем исполняемом файле включен флаг LargeAddressAware (это зависит от Windows, не знаю об Unix), то ваше приложение будет иметь адресное пространство 3 ГБ (при запуске в 32-битной Windows с /3G) или даже 4 ГБ (при запуске в 64-битной системе Windows). Если затем вычесть два указателя, результат может быть вне домена 32-битного целого числа, и ваше сравнение не будет выполнено.

Я думаю, что это одна из причин, по которой адресное пространство было первоначально разделено на 2 равные части 2 ГБ, а флаг LargeAddressAware по-прежнему является необязательным. Однако у меня сложилось впечатление, что текущее программное обеспечение (ваше собственное программное обеспечение и DLL, которое вы используете) выглядят вполне безопасными (никто больше не вычитает указателей, не так ли?), И мое собственное приложение по умолчанию имеет флаг LargeAddressAware.

Ответ 6

Ни один из вариантов не является безопасным, если злоумышленник контролирует ваши входы

Выражение p1 + len < p2 компилируется в нечто вроде p1 + sizeof(*p1)*len < p2, и масштабирование с размером указательного типа может переполнить ваш указатель:

int *p1 = (int*)0xc0ffeec0ffee0000;
int *p2 = (int*)0xc0ffeec0ffee0400;
int len =       0x4000000000000000;
if(p1 + len < p2) {
    printf("pwnd!\n");
}

Когда len умножается на размер int, оно переполняется до 0 поэтому условие оценивается как if(p1 + 0 < p2). Это очевидно верно, и следующий код выполняется со слишком большим значением длины.


Хорошо, так что насчет p2-p1 < len. То же самое, переполнение убивает вас:

char *p1 = (char*)0xa123456789012345;
char *p2 = (char*)0x0123456789012345;
int len = 1;
if(p2-p1 < len) {
    printf("pwnd!\n");
}

В этом случае разница между указателем оценивается как p2-p1 = 0xa000000000000000, что интерпретируется как отрицательное значение со p2-p1 = 0xa000000000000000. Таким образом, он сравнивает меньше чем len, и следующий код выполняется со слишком низким значением len (или слишком большой разницей в указателе).


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

if(p1 < p2 &&
   ((uintptr_t)p2 - (uintptr_t)p1)/sizeof(*p1) < (uintptr_t)len
) {
    printf("safe\n");
}

p1 < p2 гарантирует, что p2 - p1 не может дать действительно отрицательное значение. Второе предложение выполняет действия p2 - p1 < len, заставляя использовать арифметику без знака не-UB способом. Т.е. (uintptr_t)p2 - (uintptr_t)p1 дает точное количество байтов между большим p2 и меньшим p1, независимо от используемых значений.

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