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