Comments 16
И заодно вопрос: Сделать какую-то диагностику в PVS-Studio на тему статьи это безумие, да?
Сделать какую-то диагностику в PVS-Studio на тему статьи это безумие, да?
Да)
И сбрасывать все не прошедшие диагностику файлы в Корзину.
Может ли начальный вариант компилятор преобразовать не только к:
if (p < regionStart + regionSize)
но, также, и к (нет, ну а вдруг на целевой архитектуре < работает намного медленнее, чем !=):
if (p != regionStart + regionSize)
int in_region(int* p)
{
return p >= regionStart && p < regionStart + regionSize;
}
можно преобразовать в (как-то так, на C я не пишу):
int in_region(int* p)
{
if (p == regionStart + regionSize)
return 0;
int* x = regionStart;
while (x < regionStart + regionSize)
{
if (x == p)
return 1;
else
++x;
}
UndeffinedBehaviorDestroyTheWorldNotImplementedYet();
}
т.е.:
- либо p указывает на 1 елемент за пределами массива (возвращаем 0)
- либо указывает в пределах массива (возвращаем 1)
- либо указывает в любое другое место и тогда компилятор может делать что угодно, ибо испольовать <, <=, >, >= в этом случае нельзя
ну… поскольку ub в программах не существует, то можно оптимизировать до:
int in_region(int* p)
{
if (p == regionStart + regionSize)
return 0;
return 1;
}
или же:
int in_region(int* p)
{
return p != regionStart + regionSize;
}
Или я что-то не учёл?
Хорошо, теперь мы можем построить контрпример. Пусть regionStart = 0101:0000, а regionSize = 0x00020000.
Пример (и весь его последующий анализ, соответственно) некорректен, в 16-бит системе не может быть regionSize больше чем 0x10000 (на самом деле и это число не влезет в 16-битный size_t, но это уже проблемы языка, а сегмент такого размера сделать всё же можно, а вот больше — нет). Селектор это индекс сегмента в весьма произвольной таблице, а не линейный номер участка памяти.
if ((uintptr_t)p >= (uintptr_t)regionStart && (uintptr_t)p < (uintptr_t)regionStart + (uintptr_t)regionSize)
Этот вариант, в целом, хороший, но иногда может и создать проблему, если кто-то захочет использовать вместо реального regionSize какое-то абстрактное большое число (допустимость этого — отдельная тема). А ещё он может доставить проблемы уже в полностью законном случае — если конец проверяемой области памяти совпадает с концом адресного пространства (p+regionSize=0x100000000 для 32бит превратится в 0). Вариант, который ничем не уступает процитированному, но лишен указанного недостатка:
if ((uintptr_t)p >= (uintptr_t)regionStart && (uintptr_t)p - (uintptr_t)regionStart < (uintptr_t)regionSize)
А если сделать -fno-strict-overflow то можно ещё и опустить первую половину условия (но это уже некоторые могут посчитать плохим).
Компилятор с си (родной для полуоси) генерировал код как для реального, так и для защищенного режима, но он также имел руководство программиста толщиной в пять K&R, где черным по белому было написано что-то вроде следующего (как я по памяти помню) — «чтобы сравнивать разнотиповые указатели, их должно (will) приводить к обобщенным (generic) указателям типа void *, однако компилятор в режиме сборки непривилегированных программ по умолчанию соответсвующий кастинг делает автоматом, поскольку сложно представить себе случай, когда понадобилось бы иное. На более низком уровне был системный вызов к ядру AllocMem(), который тем не менее, как это было описано в руководстве, выделял память физически непрерывным куском. Разместить объект в двух таких даже смежных экстентах было невозможно, поэтому указатели всегда пробегали непрерывный ряд значений.
В случае стека было одно отличие — сегмент рос при необходимости, тем не менее обеспечивалась непрерывная логическая адресация в его пределах.
В случае 32-разрядных систем (за исключением бортовой экзотики, для которой-то и компиляторов с языка си нет) указатель содержит чистый логический/виртуальный адрес. Это просто порядковый номер байта, на который он указывает в непрерывной области адресов. Этот логический адрес формируется в процессе сегментации, происходит все это внутри процессора и недоступно прикладному программисту.
Адресное пространство процесса непрерывно и это принципиально. Как там и что отображается на физические адреса при выделении физической или другой какой памяти, на непривилегированном уровне узнать невозможно. Это вообще знает только та часть ядра, которая непосредственно занимается виртуальной памятью, и больше никто.
Если Вы не доверяете своему компилятору или супераккуратист, просто используйте в сравнениях кастинг на void *. Тип void * всегда имеет гранулярность минимально адресуемого объекта — байта.
Как проверить, находится ли значение указателя в заданной области памяти