Выглядит достаточно странно. Как в этом случае должны работать функции типа memset, strcpy и прочие, если нет гарантии, что проход по указателю корректно выдаст все элементы подряд?
проход по указателю корректно выдаст все элементы подряд, именно так. Здесь нет противоречия
Противоречия тут нет. Указанные функции допустимо вызывать при условии, что все подлежащие изменению/копированию данные находятся в пределах одного и того же массива, т.е. что все вычисляемые в процессе работы функции значения указателей допустимо вычислять.
Не уверен, что получился удачный перевод: мало голосов и тихо. Поэтому для оживления приглашаю посмотреть запись нашего нового доклада: C++ CoreHard Autumn 2017, Поиск уязвимостей с использованием статического анализа кода (доклад с этой статьей никак не связан).

И заодно вопрос: Сделать какую-то диагностику в 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;
}

Или я что-то не учёл?

потому могут быть оптимизированы, в результате чего компилятор упрощает нашу проверку до следующего кода:

if (p < regionStart + regionSize)

Не оптимизирует, если компилятор не совсем идиот. Контрпример в compiler explorer.
Хорошо, теперь мы можем построить контрпример. Пусть 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 то можно ещё и опустить первую половину условия (но это уже некоторые могут посчитать плохим).

Это ж беззнаковый тип, ему no-strict-overflow не нужен.
А в стандартных библиотеках этим замарачиваются?
да, в стандартных библиотеках приходится заморачиваться десятками вещей, с которыми обычный пользователь библиотеки возможно никогда не встретится: habrahabr.ru/company/yandex/blog/323972/#comment_10133110
Понял, спасибо.
286-й процессор в защищенном режиме и программирование для него на си. Вообще, это еще тот геморрой. Эксклюзив.
Компилятор с си (родной для полуоси) генерировал код как для реального, так и для защищенного режима, но он также имел руководство программиста толщиной в пять K&R, где черным по белому было написано что-то вроде следующего (как я по памяти помню) — «чтобы сравнивать разнотиповые указатели, их должно (will) приводить к обобщенным (generic) указателям типа void *, однако компилятор в режиме сборки непривилегированных программ по умолчанию соответсвующий кастинг делает автоматом, поскольку сложно представить себе случай, когда понадобилось бы иное. На более низком уровне был системный вызов к ядру AllocMem(), который тем не менее, как это было описано в руководстве, выделял память физически непрерывным куском. Разместить объект в двух таких даже смежных экстентах было невозможно, поэтому указатели всегда пробегали непрерывный ряд значений.
В случае стека было одно отличие — сегмент рос при необходимости, тем не менее обеспечивалась непрерывная логическая адресация в его пределах.
В случае 32-разрядных систем (за исключением бортовой экзотики, для которой-то и компиляторов с языка си нет) указатель содержит чистый логический/виртуальный адрес. Это просто порядковый номер байта, на который он указывает в непрерывной области адресов. Этот логический адрес формируется в процессе сегментации, происходит все это внутри процессора и недоступно прикладному программисту.
Адресное пространство процесса непрерывно и это принципиально. Как там и что отображается на физические адреса при выделении физической или другой какой памяти, на непривилегированном уровне узнать невозможно. Это вообще знает только та часть ядра, которая непосредственно занимается виртуальной памятью, и больше никто.
Если Вы не доверяете своему компилятору или супераккуратист, просто используйте в сравнениях кастинг на void *. Тип void * всегда имеет гранулярность минимально адресуемого объекта — байта.
Только полноправные пользователи могут оставлять комментарии.
Войдите, пожалуйста.