На что был похож код в Imaging

Чтобы год с лишним в Imaging не прошли даром, я попробую записать если не “what did we learn from it?” (the obvious answer is not to do it again), то хотя бы уж “what the fuck it is we did”. (это если что цитаты).
Забавно, что почти год спустя про WIC все еще спрашивают и даже за дизайн приходится чуть спорить. Кто бы со мной столько про DX разговаривал.


Итак, WIC (Windows Imaging Component), компонент для работы с картинками в разных форматах, декодинг, энкодинг, фильтры, конвертеры, регистрируемые кодеки, вся фигня. Не очень продвинутый COM, без ATL.
О чем больше всего заботятся при декодинге картинок – о security, то есть чтобы с помощью картинки нельзя было выполнить произвольный код (через buffer overrun, integer overflow и прочее). Заботиться надо не только при декодинге как таковом, но и в любом фильтре/конвертере, работающем с внешними данными.

В общем-то WIC хороший пример того, как пишутся библиотеки в Windows внизу user mode, с упором на security. Кернел, конечно, пишут не так, user mode-компоненты уровнем выше (скажем, shell), тоже совсем не такие, а вот базовые графические библиотеки (WIC, MIL, DX в XP итд) – это где-то там.

Так вот, как такое кодают в Windows.

Пишут фактически на C с классами. Разумеется, наследуются от COM-интерфейсов, кроме них есть несколько базовых классов BaseEncoder, BaseDecoder, BaseBitmap да и все. Иерархии больше двух предков фактически нет.

Из темплейтов есть только один суровый джедайский контейнер DynArray, аналог std::vector, и все. Он настолько суров, что наверное следует сказать о нем пару слов. Он принципиально не зовет никаких конструкторов и никаких деструкторов, хочешь иницалироваться – зови inplace new или делай Init. Копирует, разумеется, тупо память, без всякой пидарасни с copy constructor. Большая часть реализации DynArray – не темплейтная, сам DynArray – ее темплейтный потомок, который только подставляет sizeof(T) базовому классу, практически все инлайнится. Есть вариант темплейта с начальным размером массива, он тогда выделится на стеке. Отличный контейнер, ничего лишнего.
Никаких смартпойнтеров (по мне, кстати, стоило бы).

Нет exceptions, вся обработка ошибок через return и HRESULT. Разумеется, библиотека готова к тому, что может не выполниться любая операция – любая аллокация памяти, любая операция с файлом или потоком итд. К счастью, чаще всего достаточно передавать ошибку наверх и перевести объект в невалидный стейт. И для этого мы используем goto (буахаха).
Есть магическое макро IFC, условно, вот такое (spare me the bad macro bullshit):


#define IFC(x) if (FAILED(hr=x)) { goto cleanup; }
_Winnie C++ Colorizer

Код большинства функций выглядит примерно так:


HRESULT Foo(...)
{
    HRESULT hr;
    IFC(Foo1(...));
    ...
    IFC(Foo2(...));
    ...
cleanup:
    // Do the cleanup
    return hr;
}
_Winnie C++ Colorizer

Когда привыкаешь, удобно.

Так вот, о security.
Почти нигде в коде не найти простого сложения или умножения двух целочисленных переменных, самое сложное – i++ в цикле. Потому что будет integer overflow и сразу жопа-кеды, запись с неправильным смещением, привет новому эксплойту.
Везде вместо


a = b*c
_Winnie C++ Colorizer

написан


IFC(UIntMult(b,c,&a));
_Winnie C++ Colorizer

И также для сложения и прочего. То есть, функция умножения возвращает HRESULT и может всегда зафейлиться. Представьте себе, как весело с такой арифметикой писать сложные преобразования.
(Петя, расскажи им еще раз про RSX?)

Дальше, про самое веселое – buffer overrun.
Весь Imaging про вычисление цвета пикселя и его запись в память, и поэтому проверять каждую запись в массив нельзя по соображениям производительности.
Для этого (кроме здравого смысла и security review) есть аццкие тулы PREfix/PREfast, статические анализаторы кода. Чтобы они работали, все параметры функций надо аннотировать – вход это или выход, и если массив – то какой размерности.

Например:


HRESULT Foo(
    __in UINT inparam, 
    __in_ecount(inparam) UINT* inarray, 
    __out UINT* outparam, 
    __deref_out_ecount(outparam) UINT** outarray)
_Winnie C++ Colorizer

То есть, функция принимает inparam в качестве входного параметра, массив inarray из inparam элементов, и возвращает размер массива и указатель на массив именно этого размера. Причем возвращаемый указатель не может быть NULL.
И значит по таким декларациям тул анализирует код функции и смотрит, возможна ли запись по индексу больше заданного. Отслеживает все арифметические действия внутри кода, проверяет соответствие декларациям вызываемых функций, выдает диагностику – “а вот если передали такое-то, то в ту переменную запишется такое, в ту такое, потом это вот так сложится в третью, потом вычтется вот здесь, потом будет использовано в качестве индекса, и ЖОПА”. Ты сидишь и медитируешь, что же он такое пытается тебе сказать.
Иногда не может догадаться, ты пишешь ему хинты вроде “а считай пожалуйста, что такое-то условие верно”.
Но обычно – натурально находит баги.

Разумеется, необходимая часть всех тестов – запуск под Application Verifier, он в числе прочего ловит buffer overrun. И не надо никаких умных контейнеров.

Вот на таких state of art-технологиях делается некая часть Windows. С с классами, goto, HRESULT, функции для умножений и сложений. Дедовскими методами, без темплейтов, прогресса и прочего rocket science. Люди аккуратно пишут код на языке C, как писали 10 и 20 лет назад.

С++ и ATL-зеалотам – пламенные приветы.

  • IronPeter

    Отличный пост.

    >Петя, расскажи им еще раз про RSX?

    Вероятно, это был риторический вопрос, но все же. В RSX все регионы памяти защищены, в аннотации DMA объекта пишется begin & end. Причем end – это последняя доступная ячейка памяти, size – 1. Если передать в гипервизор в качестве size 0, то end становился равным 0xffffffff. Пользователю на DMA отдавалась вся доступная DDR память. Ковырять дальше и мапить 3D было делом техники.

  • dDIMA

    Отличный пост. С++ и ATL-зеалотам – также пламенный привет )))).
    На выходных напишу про страшную багу в обработке изображений, которая ловилась долго только потому, что мы были молодые и глупые. Интересно, встречается ли схожая бага в WIC…. :)

  • volodya

    Ну, не то, чтобы драйвера уж сильно иначе писали. Там тоже активно юзаются всякие новомодные приблуды:
    1. Тот же префаст – обязательно: http://www.microsoft.com/whdc/devtools/tools/PREfast.mspx
    2. ntstrsafe – http://www.microsoft.com/whdc/devtools/tools/PREfast.mspx
    3. Для дров, которые соблюдают драйверные модели майкрософта юзается sdv (к сожалению, для других случаев он практически бесполезен) – http://www.microsoft.com/whdc/DevTools/tools/SDV.mspx
    4. В обязательном порядке юзается верификатор драйверов – http://www.microsoft.com/whdc/DevTools/tools/vistaverifier.mspx – считай, аналог appverifier, только для r0.
    5. Вместо ExAllocatePool используется ExAllocatePoolWithTag + тулза pooltags для отлова ликов памяти.

  • CEMEH

    Не, ну кернел – это святое, над кернелом все кудахчут и бегают кружочками.
    Мой пойнт, что в обычном таком user mode (по большому счету, обычное приложение) – пишут со схожей строгостью.

  • volodya

    Да, согласен. Строго написан код. Уже дал своим почитать статью почитать :)

    Вероятно, можно припомнить еще Ховарда с его блогом и всякие NX-биты и ASLR.

    Я бы, дополнительно, еще через mingw прогнал. Правда, mingw не понимает аннотаций префаста и (nt)strafe. Это надо дописывать. Работаем над этим.

  • volodya

    Да, кстати. Все эти prefast и иже с ним – бледнеют перед clang (http://clang.llvm.org/). prefast разбирает код on per-function basis и это накладывает достаточно сильные ограничения.

    Был, помнится, еще у нас и случай, когда игрались с sdv (а этот парень разбирает весь код) и он не находил простейших багов.

    Поэтому, я бы ставил на то, что когда-нибудь clang превратиться во что-нибудь достойное. А до той поры – увы, префаст.

  • CEMEH

    PREfix (серверная версия PREfast) уже смотрит не только на функцию, а на весь код. Разумеется, во время чекина не запустишь, но в процессе разработки он полным анализом находит всякое.

  • IronPeter

    clang вроде совсем неживой пока. С++ не умеет. Static Analyzer в зародыше.

    Ходили слухи про новую платформу для компиляторов от MS, аналоге llvm. С промежуточным представлением кода, мега-оптимизациями и прочими радостями. Что-то затихло. Или не затихло?

  • CEMEH

    Называлось Phoenix. Как назвали, так и поплыло. Утверждается, что возрождается где-то там в процессе. Хер знает, что получится.

  • Dmitry Tyurev

    > PREfix (серверная версия PREfast) уже смотрит не только на функцию, а на весь код.

    А Coverity лучше или хуже? :)

  • volodya

    >clang вроде совсем неживой пока. С++ не умеет. Static Analyzer в зародыше.

    Что значит, не умеет? http://llvm.org/devmtg/2007-05/09-Naroff-CFE.pdf

    >PREfix (серверная версия PREfast) уже смотрит не только на функцию, а на весь код

    Погуглил я, погуглил и сдался. Упоминания есть, самой тулзы – нет. Хде достать этого зверя?

  • CEMEH

    Видимо, internal only пока.

  • IronPeter

    Вроде не умеет

    http://clang.llvm.org/cxx_status.html цитата Currently most of the C++ features are missing; here you can find features that are at least partially supported in Clang

    Презентация рассказывает про “C” фронтэнд. C++ и C – разные языки вообще-то.

  • http://kss.livejournal.com/ Joes

    Одно непонятно – скажем чего с JPEGLib делали? Переписывали с нуля?

  • Pingback: highly professional scums » Blog Archive » Рецепты отладки. Падение на ровном месте.()

  • Pingback: free(wasker);()

  • CEMEH

    Jpeglib много дописывали. Расставляли аннотации, добавляли SSE код в критические места, добавляли transcoding без изменения данных.
    Местами и safe int функции писали, да.

  • http://www.sdl.ru TSS

    2CEMEH:
    Отличная статейка.

  • Pingback: highly professional scums » Blog Archive » Best tools noone uses: PREfast()

  • gauri

    спасибо, прочитал. с выколупыванием prefast в отдельный пакетик думаю его использовать в работе.
    одно не радует — это синтаксис аннотирования параметров…