Как проверить, находится ли значение указателя в заданной области памяти

в 17:25, , рубрики: C, c language, c/c++, c++, Блог компании PVS-Studio, низкоуровневое программирование, Си, си/си++, системное программирование, указатели

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

byte* regionStart;
size_t regionSize;

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

if (p >= regionStart && p < regionStart + regionSize)

Но гарантирует ли стандарт ожидаемое поведение этого кода?

Соответствующий пункт стандарта языка C (6.5.8 Операторы отношения)(1) гласит следующее:

Если два указателя на объект или на неполный тип ссылаются на один и тот же объект или на позицию сразу за последним элементом одного и того же массива, эти указатели равны. Если указываемые объекты являются членами одного и того же составного объекта, то указатели на члены структуры, объявленные позже, больше указателей на члены, объявленные раньше, а указатели на элементы массива с большими индексами больше указателей на элементы того же массива с меньшими индексами. Все указатели на члены одного и того же объединения равны. Если выражение P указывает на элемент массива, а выражение Q — на последний элемент того же массива, то значение указателя-выражения Q+1 больше, чем значение выражения P. Во всех остальных случаях поведение не определено.

Теперь вспомним, что язык C предназначался для работы с широким спектром архитектур, многие из которых уже стали музейными экспонатами. По этой причине он крайне консервативен в отношении выбора допустимых действий, так как необходимо оставить возможность писать программы на C для устаревших систем. (Хотя в свое время они были вполне передовыми.)

Тем не менее при выделении памяти возможно появление такого указателя, который будет удовлетворять нашему условию, хотя в действительности он не будет ссылаться на заданную область. Такое случится, например, при работе на процессоре 80286 в защищенном режиме, который использовался операционными системами Windows 3.x в стандартном режиме и OS/2 1.x.

Указатель в такой системе представляет собой 32-битное значение, состоящее из двух частей по 16 бит, — его принято записывать как XXXX:YYYY. Первая 16-битная половина (XXXX) — это «селектор», который служит для выбора сегмента памяти размером 64 Кбайт. Вторая 16-битная половина (YYYY) — «смещение», с помощью которого выбирается байт внутри сегмента, заданного первой половиной. (На самом деле этот механизм сложнее, но в рамках данного обсуждения обойдемся таким объяснением.)

Блоки памяти размером больше 64 Кбайт разбиваются на сегменты по 64 Кбайт. Для перемещения к следующему сегменту необходимо прибавить 8 к селектору текущего сегмента. Например, байт, следующий за 0101:FFFF, записывается как 0109:0000.

Но почему прибавлять надо именно 8? Почему нельзя просто увеличивать селектор на один? Дело в том, что младшие три бита селектора используются для других целей. В частности, самый младший бит селектора служит для выбора таблицы селекторов. Касаться битов 1 и 2 мы здесь не будем, так как они не имеют отношения к нашему вопросу. Для удобства просто представим, что они всегда установлены в ноль.(2)

Соответствие селекторов физическим адресам памяти описывается двумя таблицами: Глобальной таблицей дескрипторов (Global Descriptor Table; определяет сегменты памяти, общие для всех процессов) и Локальной таблицей дескрипторов (Local Descriptor Table; определяет сегменты памяти, выделенные в личное пользование конкретному процессу). Таким образом, селекторы для локальной памяти процесса — 0001, 0009, 0011, 0019 и т.д., а селекторы для глобальной памяти — 0008, 0010, 0018, 0020 и т.д. (Селектор 0000 является зарезервированным.)

Хорошо, теперь мы можем построить контрпример. Пусть regionStart = 0101:0000, а regionSize = 0x00020000. Это означает, что диапазон защищенных адресов составляет с 0101:0000 по 0101:FFFF и с 0109:0000 по 0109:FFFF. Кроме того, regionStart + regionSize = 0111:0000.

А теперь представим, что в диапазоне 0108:0000 выделяется сегмент глобальной памяти, — на то, что это глобальная память, указывает четное число в селекторе.

Заметьте, что область глобальной памяти не входит в диапазон защищенных адресов, однако значение указателя на этот участок удовлетворяет неравенству 0101:0000? 0108:0000 < 0111:0000.

Еще немного текста: Наша проверка может провалиться даже на архитектурах с плоской моделью памяти. Современные компиляторы слишком охотно оптимизируют неопределенное поведение. Обнаружив сравнение указателей, они вправе предположить, что эти указатели ссылаются на один и тот же составной объект или массив (либо на позицию за последним элементом массива), поскольку любой другой вид сравнения приводит к неопределенному поведению. В нашем случае, если regionStart указывает на начало массива или составного объекта, то корректно сравниваться с ним могут только указатели вида regionStart, regionStart + 1, regionStart + 2, ..., regionStart + regionSize. Все они удовлетворяют условию p >= regionStart и потому могут быть оптимизированы, в результате чего компилятор упрощает нашу проверку до следующего кода:

if (p < regionStart + regionSize)

Теперь условию будут удовлетворять все указатели, значение которых меньше regionStart.

(Вы можете столкнуться с этой ситуацией, если — как автор исходного вопроса, ответом на который является данная статья, — выделяете область памяти с помощью выражения regionStart = malloc(n) либо если выделенная область используется как пул preallocated объектов для быстрого доступа и нужно решить, освобождать ли указатель с помощью функции free.)

Мораль: Данный код небезопасен — даже на архитектурах с плоской моделью памяти.

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

    if ((uintptr_t)p >= (uintptr_t)regionStart &&
        (uintptr_t)p < (uintptr_t)regionStart + (uintptr_t)regionSize)

Примечания:

  1. Обратите внимание, что «равно» и «не равно» не являются операторами отношения.
  2. Я знаю, что на самом деле это не так, — равными нулю я принимаю их для удобства.

(Данная статья основана на моем комментарии на StackOverflow.)

Обновлено: Уточнение: оптимизация «начала области памяти» производится только тогда, когда указатель regionStart ссылается на начало массива или составного объекта.

This is a translation of «How to check if a pointer is in a range of memory» into Russian. Click the link to see the original English version.

Автор: Andrey2008

Источник

Поделиться

* - обязательные к заполнению поля