№45. Мир в коробочке - упаковщики

То, что я понял – прекрасно, из этого я заключаю, что
остальное, чего я не смог понять – прекрасно так же.

Сократ

# СловоБредие

Птицы летают, потому что мы ходим.

Помните монолог в «Грозе»: «А почему люди не летают?» А глупый это вопрос. Я так вам скажу: «Если бы люди летали, они бы думали, что ходят». Человек – такое существо, не умеющее ценить то, что у него есть. Вот и возникают вопросы вроде: «А почему люди не птицы?». Я бы так сказал: «Научитесь сперва ходить, а потом за крылья беритесь». Да нет же. Спрашивается, чего человеку не хватает? Крыльев что ли?

Вот и началось это всё воздухоплавание ?

К чему это я? Гм…. Мистика. Завтра буря магнитная будет. Говорят связь сдохнет, болезни прорежутся, порча напортится, сглаз сгладится, и вообще. Мракобесие. Интернет упадёт, и не только он. Ой, горе то какое!!! Свят, свят.. Тфу, тфу, тфу через плечё, как бы не случилось чё.

А у меня ещё недавно примус сломался. Теперь гренки жарить не на чем. А как это я утром без гренок!!! Вот телефончик добрые люди дали с WASM, какой-то там xzazet примусы чинит. Где-то я уже это имя слышал. А ещё программисты рассказывают, что в эхе TNX (кажется) есть этот, как его… - забыл. Имя такое на языке верится, а не скажешь. Как то на В… В… Владимир Путин? Нет… Но тоже страшный такой…. Он ещё что-то про подсолнечное масло рассказывал…. Вот я и говорю Магнитные бури, тёмные пятна…. ЧЕРТОВЩИНА КАКАЯ-ТО!!!!!

* * *

(продолжение)

Родился, значит, у Александра Санька. Сан Саныч, внук камердинера. Отличный парень рос. Добрый, послушный, в общем радость родителям, гордость деду. Только малость странный парнишка - задумчивый больно. Сидит, бывало, по часу, и все о чем-то думает, как вспоминает что. Ну, дети, они разные ведь - может и пройдет с возрастом, а может, качество какое развивается.

Ну, значит, стукнуло ему шесть лет. День рождения. Отец и матерью задумали подарок сделать. Ну, отец так исподволь, ненавязчиво к сыну - о чем, мол, мечтаешь? Может, там водителем хочешь быть, или пожарником? Ну, под игру подводит, вдруг сын заветное выскажет. А Шурка-малый и скажи: "Хочу, папа, шар биллиардный. Круглый, белый". Ну, отец вспомнил, что в воскресенье с дружком по работе свиделся, а тот и похвастайся, какой в районном клубе отдыха биллиард поставили, с шарами красивыми. Малый под рукой был, разговор слыхал. Вот, чудит теперь. Ну да пусть.

День рождения, торт, мороженое, дети с родителями, шарики воздушные. И подарки всякие. Ну, отец, конечно, подарок хороший купил, и про шарик биллиардный не забыл. Погуляли, разошлись гости, а Санька в комнату свою пошел. Мама в щелочку подглядывает, смотрит, значит, как сын подарки разбирает. А тот все подарки в сторону сгреб, а сам на полу сидит и шарик рассматривает. Подивилась мать еще тогда. А на утро шара того и не видать. И вообще потом его не видать было, чтобы Шурка с ним игрался. Да и не говорил ничего. Ну, ребенок же, потерял может, просто забросил или подарил кому - известное дело.

Через год - Шурку в школу. Тут его усидчивость и проявилась. Отличник стал. Что ни день - пятерку тащит. Учителя в радости, способный ученик, хороший такой. Математику любил очень. Попозже стал в библиотеку ходить, учебники с книгами читать. Знания - как вода в сухой песок, все в голову льется. Стали его, значит, как школьную гордость показывать, карточку фотографическую на доску почета повесили, почетным звеньевым в классе сделался. На олимпиадах места первые, призовые получал. Родители не нарадуются, дед особенно счастлив - вот, не зря бар да господ на корню вывели, смену вот настоящую, рабоче-крестьянскую растим.

Победил Шурка на олимпиаде республиканской. Школу-то он раньше срока закончил. Слово модное появилось - вундеркинд. Еще только Саньке 14, а его уж в университет зовут, профессура за ним вьется, мол, науку вперет не то что семимимильными шагами, а бегом просто, никакая заграница не угонится. Да и Шурка повзрослел быстро.

Ну, отец - передовик производства, фронтовик, дед-революционер, льготники, решили значит, один ему машину взять, без очереди (себе держал, под старость), а второй-квартиру переписать (дети призрят, а внуку - радость). Ну, а мать, значит, выяснить решила, что сын хочет - может, по миру поездить, на иностранные государства посмотреть, или еще что. Гений, прямо сказать, натура загадочная, чтоб не вышло чего. Ну вот к сыну мама и подходит с вопросом. Что ж, сын, хорошо-то все, а ты все думаешь о чем-то. Или печалишься может? Или нехватает чего? Сан Саныч сразу маму успокаивает, отлично все, мама, вот, безделицы сущей нет, да только голову себе не забивайте. Как безделицы? Говори - тебе звезду с неба завсегда пожалста.

Ну тут Шурик-то и выдал.

(продолжение следует...)

Volodya/HI-TECH, NEOx/UINC.RU

Об упаковщиках в последний раз

(часть 2.1)

<>“That people seeking education should have the opportunity to find it.”
Nick Parlante
“Binary Trees”

Рецензент:
Dr.Golova/UINC.RU

Сорецензенты:
Four-F/HI-TECH, Quantum, Sten, Fixer

Главный корректор:
Edmond/HI-TECH

Требования повышаются!

Итак, мы продолжаем. Подразумевается, что вы ознакомились с первой частью статьи и теперь владеете минимумом теории. Однако мы еще более повышаем требования. Теперь, для успешного и полного понимания SEH (Structured Exception Handling) и некоторых приемов, тут продемонстрированных, вам придется иметь базовое представление о С++. Мы не будем особо далеко лезть в дебри родовых классов, виртуальных функций и т.п., но самые основы OOП, такие как наследование, вам необходимо (по крайней мере, желательно!) знать.

Предполагается, также, что вы имеете джентльменский набор знаний по защищенному режиму - т.е. знаете, что такое IDT/GDT/PDE/PTE и прочие страшные аббревиатуры.

Кроме того, вам придется хлебнуть информации о недрах Windows для чего ОЧЕНЬ предлагается прочесть какую-нибудь хорошую книжку по системному программированию под Windows – например, Марка Руссиновича или Свена Шрайбера (список рекомендуемой литературы в конце статьи). Также по ходу дела мы постараемся давать линки на статьи (к сожалению, подавляющая масса литературы подобного характера на английском языке, на русском тоже что-то есть, но не слишком много).

Как вы уже, должно быть, догадались, все статьи этого цикла будут напирать на теорию. Мы предпочтем рассмотреть вопрос "почему", а не вопрос "как". Обязательно учтите, что здесь мы не рассматриваем 9x!

Практические примеры требуют воспроизводимости. Это означает, что вы должны иметь возможность воспроизвести примеры, показанные здесь, т.к. возможны недопонимания и все следует проверять практически. Во всех примерах упаковщиков используется один-единственный файл – calc.exe из поставки Windows 2000.

Вместо введения

написано совместно с Four-F

Что представляет собой процесс в Windows? Как запускается программа? Какие объекты создает и поддерживает операционная система? Что такое и как происходит диспетчеризация системных вызовов? Все это - достаточно важные вопросы, которые надо четко себе представлять. Всю оставшуюся часть статьи мы будем глубоко погружаться в мир недокументированных функций Windows, ввиду чего вам настоятельно рекомендуется ознакомится с предложенной литературой. Это статья слишком мала, чтобы детально рассматривать все внутренние системные структуры данных, это не было целью, но вот некоторые нюансы, которые помогут вам сориентироваться в этом, ох каком нелегком, деле, мы здесь продемонстрируем.

Известно, что процессоры Intel предлагают операционной системе 4 кольца защиты, из которых Windows использует только два - кольцо-0 и кольцо-3. Часть структур, сопровождающих программу, создается операционной системой в режиме ядра (ring-0) и программе ring-3 они недоступны. Другая часть создается в кольце-3 и, теоретически, доступна программе, однако документация отсутствует, примеров кода нет, словом MS постаралась, чтобы мы знали как можно меньше о внутренностях операционной системы.

Все трюки, продемонстрированные ниже, нельзя исполнить без файлов символов.

Символы отладки и эталонный отладчик

Если вам в силу тех или иных причин необходимо исследовать Windows, то сразу же возникает необходимость в настройке среды для работы. Очевидно, одного джентельменского набора из IDA/Soft-Ice/HIEW/IceExt и дампера процессов 3-кольца типа PE Tools тут уже недостаточно. Это шаг выше. Поэтому вам потребуется обзавестись символами отладки, которые можно сгрузить с сайта MS по этому адресу:
http://www.microsoft.com/whdc/ddk/debugging/symbolpkg.mspx установить и корректно настроить их. Обязательно учтите – символы должны ТОЧНО соответствовать вашей ОС и установленному SP. Для этого лучше использовать технологии MS. NuMega Symbol Retriever показал себя нестабильно работающей утилитой. Поэтому не поленитесь, если есть такая возможность, сгрузить файлы ручками, прогнать их через NMS-транслятор и проверить командной table, все ли у нас в порядке. Кстати, если по каким-то, одним разработчикам понятным причинам, Soft-Ice ну никак не желает откликаться на bpx, возможно, установка nms-символов на ntoskrnl сможет помочь.

IDA, включая версию 4.5, невероятно глючно накладывает информацию из pdb файла на свою базу, поэтому соизвольте сгрузить либо с сайта datarescue, либо с wasm.ru изумительный и очень шустрый плагин - PDB Plus. Показал себя безукоризненно.

Было бы неплохо также иметь какую-то утилиту, способную извлекать информацию из PDB-файла. Есть и такие – pdbdump – http://sourceforge.net/projects/pdbdump. Просто удивительно, сколько полезной информации можно извлечь из PDB. Нам с вами еще не раз предстоит в этом убедится на протяжении статьи. Наиболее полезный файл из всех – ntosrknlsym.pdb.

Следующая потенциальная проблема – это выбор отладчика, который был бы способен показывать внутренности ОС с достаточно большой степенью достоверности. Разумеется, нативный отладчик должен лучше понимать "свою" ОС, чем это делают все остальные. Разработчики Soft-Ice не раз подчеркивали, что все структуры реверсированы, а это не всегда самый надежный способ. Поэтому эталонным отладчиком можно смело считать отладчик самой MS – MS kd. MS kd тоже не идеален и грешит сокрытием информации (например, об объектах - проблема была описана Шрайбером, который предложил и решение для некоторых частных случаев), но это лучше, чем подавляющее большинство утилит.

Известно, что kd требует установки двух машин, соединенных между собой. Марк Руссинович разработал утилиту LiveKd (доступна на sysinternals.com и wasm.ru), которая позволяет запускать kd на одной и той же машине. Для отладки, разумеется, можно и нужно применять Soft-Ice, однако, когда возникает необходимость подглядеть какую-то структуру или адрес функции - лучше kd нет ничего (кроме, ессно, ручек и дизассемблера). kd доступен по адресу: http://www.microsoft.com/whdc/ddk/debugging/default.mspx

Установка не должна вызвать проблем. Поместите LiveKd в ту же директорию, программа все сделает сама.

Очень важными структурами пользовательского режима являются TIB, TEB и PEB. Последний мы в этой статье затрагивать не будем, а касательно двух первых - Thread Environment Block и Thread Information Block - необходимо развеять кое-какую путаницу. Здесь мы хотели бы вам сказать, что читать НЕ стоит! Не стоит читать статью Питрека - http://www.microsoft.com/msj/archive/S2CE.aspx и главы «Обработка исключений в реальном и защищенном режимах», «Как противостоять трассировке» и «Как противостоять контрольным точкам останова» и «Как обнаружить отладку средствами Windows» из книги Криса Касперски «Фундаментальные основы хакерства». Почему так? По поводу статьи Питрека – уж слишком она устарела. По поводу Касперски – автор не озаботился проверить, а соответствует ли то, что он написал, действительности, дочитайте данную статью до конца и станет ясно почему. Теперь выдержка из статьи Питрека: "The Windows 95 code calls it a Thread Information Block (TIB). In Windows NT, it's called the Thread Environment Block (TEB)." Утверждение неверно. Структура TIB существует и в Windows NT+ и называется _NT_TIB (полностью документирована в winnt.h), а структура TEB (недокументирована) включает в себя структуру TIB, т.е. является ее надмножеством. Где-то так (заметьте, полные описания структур мы не приводим, для этого есть исходники к книге Шрайбера на wasm.ru и кода ReactOS – ссылки в конце статьи):

      typedef struct _NT_TIB {
    /*0*/  struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList; 
    //fs:[0] - рассматривается в главе о SEH
    /*остальные поля, за исключением поля *Self,
    // здесь не рассматриваются - есть статьи Питрека*/
     ...
    /*0x18*/ struct _NT_TIB *Self; //fs:[18]
    /*вот из-за этого поля вся и путаница - это указатель на начало структуры TIB,
    // и, одновременно, на начало структуры TEB*/
    } NT_TIB;
    typedef NT_TIB *PNT_TIB;

    typedef struct _TEB {
    /*0*/  NT_TIB TIB; //теперь становится очевидным, что TEB вмещает TIB!
    /*0x1С*/ PVOID EnvironmentPointer; 
    /*обратите внимание, смещение этого поля - 1Сh,
    // т.к. перед ним идет вся структура TIB*/
     ...
    /*0x2C*/ PPVOID ThreadLocalStorage; //будет рассмотрен подробнее чуть попозже
    /*0x30*/ PPEB Peb; /*см. ниже*/
    /*0x34*/ DWORD LastErrorValue; 
    }

Ну, поскольку, рисунок всегда нагляднее, вот:

{рисунок поскипан}

А если и рисунка мало, тогда смотрите на дамп из kd:

    kd> !teb
    TEB at 7FFDE000
        ExceptionList:    6d474  ;ExceptionList – первое поле TIB
    ...
        PEB Address:      7ffdf000 
    ;а это указатель на родимый PEB – по адресу видно, что это структура кольца-3
    ...

А теперь глянем в Шрайбера (w2k_def.h) касательно PEB. Оговоримся сразу – нас не интересует большинство полей, глянем только на два:

    typedef struct _PEB
            {
    ...
    /*002*/ BOOLEAN              BeingDebugged; 
    /*используется функций IsDebuggerPresent – см. ниже*/
    ...
    /*00C*/ PPROCESS_MODULE_INFO ProcessModuleInfo; /*а вот это – ошибка!*/
    ...
    /*1E8*/} PEB, * PPEB;

Теперь ReactOS (teb.h):

    typedef struct _PEB
    {
    ...
       UCHAR BeingDebugged;                             /* 02h */
    ...
       PPEB_LDR_DATA Ldr;                               /* 0Ch */
    ...
    } PEB;

Видите, структуры различаются. Кто же прав? Ответ нам даст kd:

    kd> !peb
    PEB at 7FFDF000
        InheritedAddressSpace:    No
        ReadImageFileExecOptions: No
        BeingDebugged:            No
        ImageBaseAddress:         01000000
        Ldr.Initialized: Yes
        Ldr.InInitializationOrderModuleList: 71f38 . 76660
        Ldr.InLoadOrderModuleList: 71ec0 . 76650
        Ldr.InMemoryOrderModuleList: 71ec8 . 76658

Но даже более того! Не стоит полностью доверять и kd. То, что он показывает – верно на 100%, но есть одно маленькое но – он показывает НЕ ВСЕ. Часть структур просто скромно умалчивается. Однако есть одна вещь, которая никогда не солжет – дамп pdb-файла. Мы уже упоминали о pdbdump – давайте им и воспользуемся (не забудьте слить MS DIA SDK с wasm.ru или обзавестись Visual Studio .NET 2002+):

    struct _PEB_LDR_DATA {
    /*некорректно названа у Шрайбера*/
      // non-static data --------------------------------
      /*<thisrel this+0x0>*/ /*|0x4|*/ unsigned long Length;
      /*<thisrel this+0x4>*/ /*|0x1|*/ unsigned char Initialized;
      /*<thisrel this+0x8>*/ /*|0x4|*/ void* SsHandle;
    /*смысл полей ниже одинаков – они все показывают на одну и ту же структуру,
     просто упорядочены по-разному*/
      /*<thisrel this+0xc>*/ /*|0x8|*/ 
    struct _LIST_ENTRY InLoadOrderModuleList;
      /*<thisrel this+0x14>*/ /*|0x8|*/ 
    struct _LIST_ENTRY InMemoryOrderModuleList;
      /*<thisrel this+0x1c>*/ /*|0x8|*/ 
    struct _LIST_ENTRY InInitializationOrderModuleList;
    };

Уходим еще глубже - в двусвязные списки LIST_ENTRY. Структура определена в winnt.h как

    typedef struct _LIST_ENTRY {
    /*в случае одного-единственного элемента в списке
     Flink/Blink показывают сами на себя*/
       struct _LIST_ENTRY *Flink;  //forward
       struct _LIST_ENTRY *Blink;  //backward
    } LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

Указатели, в нашем случае, показывают на структуру LDR_DATA_TABLE_ENTRY:

    struct _LDR_DATA_TABLE_ENTRY {
    /*на http://undocumented.ntinternals.net/UserMode/Structures/LDR_MODULE.html
     структура ошибочно названа
    _LDR_MODULE*/ 
      /*<thisrel this+0x0>*/ /*|0x8|*/ struct _LIST_ENTRY InLoadOrderLinks;
      /*<thisrel this+0x8>*/ /*|0x8|*/ struct _LIST_ENTRY InMemoryOrderLinks;
      /*<thisrel this+0x10>*/ /*|0x8|*/ struct _LIST_ENTRY InInitializationOrderLinks;
      /*<thisrel this+0x18>*/ /*|0x4|*/ void* DllBase;
      /*<thisrel this+0x1c>*/ /*|0x4|*/ void* EntryPoint;
      //давайте поиграемся с этим полем
      /*<thisrel this+0x20>*/ /*|0x4|*/ unsigned long SizeOfImage;
      /*<thisrel this+0x24>*/ /*|0x8|*/ struct _UNICODE_STRING FullDllName;
      /*<thisrel this+0x2c>*/ /*|0x8|*/ struct _UNICODE_STRING BaseDllName;
      /*<thisrel this+0x34>*/ /*|0x4|*/ unsigned long Flags;
      /*<thisrel this+0x38>*/ /*|0x2|*/ unsigned short LoadCount;
      /*<thisrel this+0x3a>*/ /*|0x2|*/ unsigned short TlsIndex;
      /*<thisrel this+0x3c>*/ /*|0x8|*/ struct _LIST_ENTRY HashLinks;
      /*<thisrel this+0x3c>*/ /*|0x4|*/ void* SectionPointer;
      /*<thisrel this+0x40>*/ /*|0x4|*/ unsigned long CheckSum;
      /*<thisrel this+0x44>*/ /*|0x4|*/ unsigned long TimeDateStamp;
      /*<thisrel this+0x44>*/ /*|0x4|*/ void* LoadedImports;
    };

Для чего мы заставляем вас проходить через это? Смотрите на код, он теперь должен иметь немного больше смысла:

    void main(void)
    {
     __asm
     {
      mov eax, fs:[30h]  ;Teb.Peb
      mov eax, [eax+0Ch]     ;Peb.Ldr - PEB_LDR_DATA
      ;не совсем корректно доступаться по указателю списка
           ;не проверив его сначала,
      ;но для ясности мы проверку опустим
    mov eax, [eax+0Ch] ;Ldr.InLoadOrderModuleList.Flink – сам на себя
      lea ebx, [eax+20h] ;LDR_DATA_TABLE_ENTRY.SizeOfImage
      add [ebx], 88h  
    ;LDR_DATA_TABLE_ENTRY.SizeOfImage += 0x88
    ;число выбрано просто так
     }
    }

Потрассируйте такой код и посмотрите в LordPe, что он вам покажет в поле SizeOfImage... Посмотрели? Угадайте, что будет, если дампер будет пробовать читать память, которой НЕТ? А теперь прогоните через PE Tools… Ну как результат?

Обратите внимание, что структура TEB (и входящая в нее структура TIB) создается Windows для каждого потока в момент его порождения и "сопровождает" поток до прекращения выполнения. Структура эта в кольце-3 доступна через регистр fs. Обратите внимание - мы говорим в кольце-3, т.к. содержимое, доступное через fs, РАЗЛИЧАЕТСЯ в ring-3 и ring-0. Почему так – ответ вы найдете в GDT. К Soft-Ice прилагается шикарная книженция – “Using Soft-Ice”. Есть там и глава – «Exploring Windows NT” где рассматривается, что куда и как отображается, и приведен примерчик.

В остальной части этой статьи мы будем употреблять термин "TEB" только тогда, когда смещение в fs-регистре превысит размер структуры TIB. Пример из книги Джона Роббинса "Отладка приложений": "реализация GetCurrentThreadId (из Windows 2000) получает сначала линейный адрес TIB-блока и затем, в позиции со смещением 0х24 (в TIB-блоке) - фактический идентификатор (ID) потока". Нет такого смещения в TIB-структуре! Нет и не было никогда. А вот в TEB - есть. Удивительно, как много пользы от таких простых знаний. Положим, вас заинтересовала работа функций GetCurrentThreadId, GetLastError и IsDebuggerPresent:

    GetCurrentThreadId:
    mov     eax, large fs:18h ;NT_TIB.Self – линейный адрес структуры TEB,
                               ; расположенной в ring-3
    mov     eax, [eax+24h]  ;TEB.Cid.UniqueThread

    GetLastError:
    mov     eax, large fs:18h  ;NT_TIB.Self
    mov     eax, [eax+34h]   ;TEB.LastErrorValue


    IsDebuggerPresent
    mov     eax, large fs:18h  ;NT_TIB.Self
    mov     eax, [eax+30h]  ;TEB.Peb – извлекается УКАЗАТЕЛЬ на структуру
    movzx   eax, byte ptr [eax+2] ;Peb.BeingDebugged

Продолжаем. Следующее, что мы рассмотрим здесь - это интерфейс 2Eh и таблицы системных вызовов. Мы попытаемся отследить путь вызова процедуры вплоть до ядра Windows. За теорией - к Руссиновичу. Не имеете возможность купить эту книгу - вот линк на статью по теме http://www.sysinternals.com/ntw2k/info/ntdll.shtml. Обязательно учтите, что int 2E в XP+ отсутствует! Вместо этого используется команда sysenter.

Итак, мы предполагаем - вам известно, что такое Native API. Когда, скажем, вызывается функция kernel32.dll CreateFile, что происходит потом? Управление передается в ntdll.dll, где имеем код вида:

    .text:77F83DA8 _NtCreateFile@44 proc near              ; CODE XREF: .text:77FA0B3Cp
    ;вот это и есть пример самой настоящей Native API функции
    .text:77F83DA8 
    .text:77F83DA8 arg_0           = byte ptr  4
    .text:77F83DA8 
    .text:77F83DA8                 mov     eax, 20h        ; NtCreateFile
    .text:77F83DAD                 lea     edx, [esp+arg_0]
    .text:77F83DB1                 int     2Eh
    .text:77F83DB3                 retn    2Ch
    .text:77F83DB3 _NtCreateFile@44 endp

Что происходит потом, когда выполняется int 2Eh? Поскольку это прерывание, то оно имеет свой обработчик в IDT. Обратите внимание - все функции ntdll.dll, обращающиеся к ядру, используют int 2E (в Win 2k, в XP+ используется специальная команда PII+ sysenter). Как же обработчик определяет, что делать дальше? Для этого полезем в Soft-Ice:

    :idt 2е
    Int   Type     Sel:Offset     Attributes Symbol/Owner
    IDTbase=80036400  Limit=07FF
    002E  IntG32   0008:804655CD  DPL=3  P   _KiSystemService

Т.о. обработчик называется _KiSystemService и сидит в ntoskrnl.exe. Дальше имело бы смысл привести дизассемблированный листинг этой функции, но все это уже сделано за нас - Peter Kosyh в своем замечательном сборничке очень подробно расписал что к чему - сборничек можно слить с wasm.ru. Глава - "Интерфейс системных вызовов". Там предельно подробно рассказывается, как обработчик находит нужную функцию. Единственное что, имеет смысл привести описания структур SDT/SST, в которых обработчик ее ищет, и рисуночек:

    typedef struct _SERVICE_DESCRIPTOR_TABLE {
    /*SDT доступна через идентификатор ntoskrnl - KeServiceDescriptorTable,
     заметьте, мы не рассматриваем здесь KeServiceDescriptorTableShadow
     - это далеко выходит за рамки статьи
     - подробнее см. великолепную книгу Шрайбера*/
    /*0*/  SYSTEM_SERVICE_TABLE ntoskrnl;  //SST для ntoskrnl.exe
    /*0x10*/ SYSTEM_SERVICE_TABLE win32k;    //SST для win32k.sys
    /*0x20*/ SYSTEM_SERVICE_TABLE iis;  
    //SST для MS IIS Server (заполнено, ТОЛЬКО если установлен IIS)
    /*0x30*/ SYSTEM_SERVICE_TABLE unused;    //не используется
    } SERVICE_DESCRIPTOR_TABLE;

    typedef struct _SYSTEM_SERVICE_TABLE{
    /*в ядре также есть идентификтор KiServiceTable, который является, по сути, 
    SERVICE_DESCRIPTOR_TABLE.ntoskrnl, остальные здесь не рассматриваются*/
    /*0*/ PVOID ServiceTableBase;             //указатель на начало таблицы,
                                                //содержащей адреса функций
    /*4*/ PVOID ServiceCounterTable(0); 
    /*поле содержит количество вызовов той или иной системной функции
     и используется только в т.н. checked build версиях ОС,
     где KiSystemService занимается его заполнением*/
    /*8*/ unsigned int NumberOfServices; 
    /*количество записей в таблице - учтите,
     что индекс функции (eax) НЕ должен превышать это значение*/
    /*0xC*/ PVOID ParamTableBase;
    /*если вам интересно, как KiSystemService узнает,
     сколько параметров принимает функция на стороне ядра,
     то количество таковых берется как раз отсюда*/
    } SYSTEM_SERVICE_TABLE;

Для чего мы все это вам рассказываем? Положим, вас жутко заинтересовал механизм работы NtCreateSection (ZwCreateSection). Никаких проблем. Вы идете в ntdll.dll и находите ее вызов. Хм... Он скатывается к int 2Eh... Что дальше? Ладно, в этом случае все просто - функция экспортируется ядром - ntoskrnl.exe, следовательно, ничто не мешает прийти с дизассемблером и туда. А что вы скажете по поводу NtContinue (в eax - 1Ch)? Точно также - int 2Eh. А вот в таблице экспорта ядра такой функции нет. Тупик? Нет. Используя знания о структуре SDT можно легко отследить место расположения NtContinue в ядре, а затем найти эту функцию в ntoskrnl.exe на диске. Примерно так:

    :exp KeService     
    ;проверяем наличие такого символа, достаточно частичного имени
    ntoskrnl
    0008:8046DFA0 KeServiceDescriptorTable ;ага, Soft-Ice знает этот символ
    :dd KeServiceDescriptorTable
    0023:8046DFA0 804742B8  00000000  000000F8  8047469C      .BG..........FG. 
    /*теперь вам уже известно строение SDT:
    (отображена первая SST, принадлежащая ntoskrnl.exe)
    804742B8 - соответствует ServiceTableBase
    00000000 - соответствует ServiceCounterTable
    000000F8 - соответствует NumberOfServices
    8047469C - соответствует ParamTableBase
    */
    ...
    :dd KiServiceTable
    /*
    ServiceTableBase - это, по сути, массив из указателей на функции - void*
    */
    0023:804742B8 8049DD52  804AF6C1  804B043A  8050D5B8      R.I...J.:.K...P.
    0023:804742C8 804B0470  8045CEA2  8050F7BE  8050F7FE      p.K...E...P...P.
    0023:804742D8
    80494A38  8050A9F2  804ADED8  804FD82D      8JI...P...J.-.O.
    ...
    :u *(KiServiceTable+1c*4)          ;ну и где у нас в ServiceTableBase
                                       ;лежит элемент с индексом 1Ch?
    _NtContinue
    0023:804692A0  55                  PUSH      EBP 
    ;полагаем, пересчитать этот адрес в памяти в реальное смещение
    ;в файле не составит труда - общая методика
    ;такова: просто используйте команду mod, вычтите Base address из вашего VA,
    ;если это необходимо, и можете брать ;дизассемблер и идти в гости
    0023:804692A1  8B1D24F1DFFF        MOV       EBX,[P0BootThread]
    0023:804692A7  8B553C              MOV       EDX,[EBP+3C]
    0023:804692AA  899328010000        MOV       [EBX+00000128],EDX
    0023:804692B0  8BEC                MOV       EBP,ESP
    0023:804692B2  8B4500              MOV       EAX,[EBP+00]

Все, описанное выше, можно сделать еще проще. Разумеется, такой мощный инструмент как Soft-Ice имеет средство для просмотра SDT - это команда ntcall (обязательно учтите, что ntcall покажет только функции, принадлежащие ntoskrnl). Единственное что, использование таких команд освобождает от необходимости знать некоторые тонкости работы, что не всегда хорошо, т.к., к примеру, SST может быть использована для антиотладочных процедур почти на самом низком из всех возможных уровней (ниже - только драйвер). Пример - статья Тима Роббинса (Tim Robbins) - http://www.wiretapped.net/~fyre/sst.html.

Также очень неплохо было бы ознакомится с http://www.windowsitlibrary.com/Documents/Book.cfm?DocumentID=356 - Undocumented Windows NT - очень сильная книжка, хотя немного и устарела. Возможно, имеет смысл скомпоновать ее главы в виде .chm-формата и поместить на wasm.

Также необходимо предельно кратко ознакомиться со структурами режима ядра. Мы и здесь вполне осознаем, сколь много было сделано и описано Руссиновичем и Шрайбером, поэтому километровые листинги структур здесь даны не будут, мы рассмотрим лишь наиболее общие, а также наиболее ценные, в практическом плане, вопросы.

Мы уже упоминали, что содержимое, доступное через fs, различно для ring-0 и ring-3. Как вы теперь понимаете, в ring-3 fs:[0] показывает на структуру TEB. А что же мы видим, к примеру, в этом случае в кольце-0:

    mov     eax, large fs:124h
    mov     al, [ebx+134h]

На что показывает fs:124h при DPL = 0? На что показывает fs:0 при DPL = 0? Все это подробнейшим образом описывает Шрайбер - его книга действительно великолепна. Мы здесь описания структур приводить не будем, достаточно слить некоторые файлы из раздела инструментов на wasm.ru и обзавестить DDK. Однако, предельно кратко, в виде рисунка, показать некоторые вещи стоит. Так нагляднее:

{рисунок поскипан}

Таким образом вы легко сможете преобразовать примеры выше в удобоваримый код, с которым можно сравнительно легко работать дальше:

    mov     eax, large fs:124h  ;KTHREAD
    mov     al, [ebx+KTHREAD.PreviousMode

Помните, что Windows отображает адрес 0xFFDFF000 на fs:[0] (гляньте в GDT!). К примеру, по fs:[50] будет лежать KPCR.DebugActive, по fs:[120] - KPRCB (0xFFDFF020), по fs:[13C] - структура CONTEXT. Единственное, что может смутить - по 0xFFDFF020 лежит указатель на KPRCB, значение которого равно 0xFFDFF120:

    mov     eax, ds:0FFDFF020h ;KPRCB по адресу FFDFF120
    inc     dword ptr [eax+4A8h] 
    ;KPRCB.KeExceptionDispatchCount – да,
    ;увы и ах, документированная в ntddk.h структура KPRCB, конечно, что-то
    ;описывает, но опять-таки НЕ ПОЛНОСТЬЮ! 
    ;лезьте в pdbdump и отчет о файле ntoskrnlsym.pdb – там много чего есть!

Еще, вероятно, вас может смутить поле NT_TIB в составе KPCR. Как же так – ведь структура NT_TIB принадлежит кольцу-3! А вот и не совсем так. И кольцо-0 и кольцо-3 владеют каждый по NT_TIB – по одной на брата. Т.о. и в кольце-0 и в кольце-3 fs:[0] показывает на TIB, только в первом случае TIB входит в KPCR и ни о каком TEB речи нет, а вот во втором случае TIB входит в TEB. Вот вам и дамп из kd в доказательство:

    kd> !pcr
    PCR Processor 0 @ffdff000     ;адресочки-то нулевого колечка, однако
            NtTib.ExceptionList: f4347c68
            NtTib.StackBase: f4347df0
            NtTib.StackLimit: f4344000
    ...

Учитывайте эти нюансы и все будет ОК.

Очень многое осталось за бортом этого, предельно короткого, обзора. Однако для успешного понимания оставшейся части статьи этого более чем достаточно, при условии, что все понято. Если нет - Руссинович и Шрайбер. Особенно Шрайбер! Только читая его, обязательно учитывайте ГОД написания книги. Например, цитата: "Внутреннее строение структур WIN32_PROCESS и WIN32_THREAD - это еще одна пока что непознанная область Windows 2000, исследовать которую только предстоит". Уже не совсем так. У Шрайбера код выглядит так:

    typedef struct _EPROCESS
            {
    /*000*/ KPROCESS Pcb;
    ...
    /*214*/ struct _WIN32_PROCESS *Win32Process;
    /*как видите, многие поля названы просто по порядку, не более*/
    /*218*/ DWORD                  d218;
    /*21C*/ DWORD                  d21C;
    /*220*/ DWORD                  d220;
    /*224*/ DWORD                  d224;
    /*228*/ DWORD                  d228;
    /*22C*/ DWORD                  d22C;
    /*230*/ PVOID                  Wow64;
    /*234*/ DWORD                  d234;
    /*238*/ IO_COUNTERS           
    IoCounters;
    /*268*/ DWORD                  d268;
    /*26C*/ DWORD                  d26C;
    /*270*/ DWORD                  d270;
    /*274*/ DWORD                  d274;
    /*278*/ DWORD                  d278;
    /*27C*/ DWORD                  d27C;
    /*280*/ DWORD                  d280;
    /*284*/ DWORD                  d284;
    /*288*/ } 
            EPROCESS,
         * PEPROCESS,
        **PPEPROCESS;

А теперь посмотрите, СКОЛЬКО информации нам дает pdbdump, написанный позже:

      /*<thisrel this+0x214>*/ /*|0x4|*/ void* Win32Process;
      /*<thisrel this+0x218>*/ /*|0x4|*/ struct _EJOB* Job;
      /*<thisrel this+0x21c>*/ /*|0x4|*/ unsigned long JobStatus;
      /*<thisrel this+0x220>*/ /*|0x8|*/ struct _LIST_ENTRY JobLinks;
      /*<thisrel this+0x228>*/ /*|0x4|*/ void* LockedPagesList;
      /*<thisrel this+0x22c>*/ /*|0x4|*/ void* SecurityPort;
      /*<thisrel this+0x22c>*/ /*|0x4|*/ struct _UNICODE_STRING* AuditImageName;
      /*<thisrel this+0x230>*/ /*|0x4|*/ struct _WOW64_PROCESS* Wow64Process;
      /*<thisrel this+0x238>*/ /*|0x8|*/ union _LARGE_INTEGER ReadOperationCount;
      /*<thisrel this+0x240>*/ /*|0x8|*/ union _LARGE_INTEGER WriteOperationCount;
      /*<thisrel this+0x248>*/ /*|0x8|*/ union _LARGE_INTEGER OtherOperationCount;
      /*<thisrel this+0x250>*/ /*|0x8|*/ union
    _LARGE_INTEGER ReadTransferCount;
      /*<thisrel this+0x258>*/ /*|0x8|*/ union _LARGE_INTEGER WriteTransferCount;
      /*<thisrel this+0x260>*/ /*|0x8|*/ union _LARGE_INTEGER OtherTransferCount;
      /*<thisrel this+0x268>*/ /*|0x4|*/ unsigned long CommitChargeLimit;
      /*<thisrel this+0x26c>*/ /*|0x4|*/ unsigned long CommitChargePeak;
      /*<thisrel this+0x270>*/ /*|0x8|*/ struct _LIST_ENTRY ThreadListHead;
      /*<thisrel this+0x278>*/ /*|0x4|*/ struct _RTL_BITMAP* VadPhysicalPagesBitMap;
      /*<thisrel this+0x27c>*/ /*|0x4|*/ unsigned long VadPhysicalPages;
      /*<thisrel this+0x280>*/ /*|0x4|*/ unsigned long AweLock;

Мы надеемся, что это достаточно эффективный пример. Soft-Ice может использовать свои имена (чего только стоят названия KTEB и UTEB – кого угодно запутать можно), kd может скрыть часть информации (введите команду !processfields и посмотрите как мало она дает), кода ReactOS, временами, выдают ТАКОЕ... Мы можем доверять лишь pdb-файлу и дизассемблеру. Не верьте именам структур, если они недокументрованы – любой их назовет как угодно, придерживайтесь имен самой MS – pdb-файлы не соврут.

Ну, а если вы истинный, то есть, ленивый (это синонимы) программист, то уже должны думать про себя: «Неужели мне, каждый раз, когда я вижу ebx+134h, придется каждый раз делать такие комментарии в IDA (да, кстати, недокументированные функции полностью отсутствуют в этом дизассемблере!)?». Ну, разумеется, нет! Все уже сделано за вас. Озаботьтесь загрузить себе idc-скрипты, описывающие некоторые структуры нулевого кольца с http://www.alkor.ru/~00077500/kb/winnt.htm или, как всегда, с wasm.ru (в последнем случае картина более полная, т.к. Four-F создал замечательный здоровенный idc-скрипт).

Ничто также не мешает вам самим перевести .h файлы в idc-скрипты. Частично проблема решена Леонидом Лисовским (Leonid Lisovsky) в его скрипте h2enum (слить с сайта datarescue), однако более разумным кажется приспособить готовый лексический анализатор для этих целей, к примеру, lex, совместить его с перловским скриптом, который будет подставлять нужные функции IDA и все. Почитать о lex и yacc можно, например, тут: http://www.codeproject.com/cpp/introlexyacc.asp. Хм. Добровольцы?

Также обязательно ознакомьтесь с набором команд IceExt (скачать с http://stenri.pisem.net либо с wasm.ru) – уникальный плагин для Soft-Ice, способный не только выполнять анти-антиотладку, но, к примеру, могущий также показать список PTE, содержание теневой SDT и т.п.

Мы довольно прилично осознаем, что от такого введения недолго и в обморок упасть. Поэтому не торопитесь. Вы, должно быть, уже сообразили, что эту статью нельзя прочесть с наскока – это не бульварное чтиво. В помощь при разборе ассемблерщики пусть возьмут себе уникальный KmdKit by Four-F и внимательно разберутся с файлом w2kundoc.inc. Программисты на С пусть возьмут основательно подправленный Volodya файл Шрайбера w2k_def.h. Оба доступны с wasm.

Как Windows работает с секциями PE-файла

написано совместно с Four-F

В первой части мы представили и должным образом дополнили работу Rustell Osterlund о работе ntdll.dll и тех проверках на валидность PE-файла, которые она выполняет. Теперь пришло время двинуться дальше и рассмотреть, какие ограничения на формат накладывает само ядро Windows. Это означает, что придется идти глубоко – в ntoskrnl.exe. В принципе, новички могут пропустить эту главу, так как она способна запросто привести в ужас кого угодно, кроме самих создателей Windows. Единственное что – в самом ее конце мы описываем практическое использование полученных знаний и реализацию оных в PE Tools.

Продвинутые читатели наверняка знают некоторые подробности об объектах Windows, поэтому смягчать выражения мы особо не будем. Единственный объект, который нас интересует – это объект «секция». Объект полностью недокументирован. Лишь в главе 7 книги Руссиновича есть легкое упоминание о такой вещи и симпатичный рисуночек.

Итак, объект "секция" не стоит путать с термином "секция" из PE-файла, это вовсе не одно и тоже. Section object создается в ЕДИНСТВЕННОМ экземпляре на файл. Доказательство: функция NtCreateSection, вызывается ОДИН раз - в коде лоадера (ntdll.dll) из LdrpCreateDllSection (эта, в свою очередь, из LdrpMapDll), и в коде CreateProcess также единожды, в последовательности:

    ...

      NtOpenFile(...);

      ...

      NtCreateSection(...);

      ...

      NtQuerySection(...);

     ...

Подробнее – Петр Косых aka gloomy. Материалы можно слить с wasm.ru. Кода ReactOS, довольно часто упоминаемые нами в этой статье, по данному поводу можно просто выбросить – там чушь. NtCreateSection является лишь тонкой прослойкой вокруг MmCreateSection, которая и выполняет всю работу по заполнению объекта «секция», работе с PTE, а, точнее, с гиперпространством, проверке валидности, переводу флагов PE-секций в атрибуты структур SUBSECTION (см. ниже) через хитрые массивы ядра и т.п. Внутри MmCreateSection могут вызываться три функции:

      MiCreateDataFileMap

      MiCreatePagingFileMap 

      MiCreateImageFileMap
 

Сначала вызывается MiCreatePagingFileMap. Далее, Windows на основании флагов из структуры CONTROL_AREA (см. рисунок ниже) решает как ей быть дальше – либо работать с файлом как с данными через MiCreateDataFileMap, либо как с исполняемым файлом через MiCreateImageFileMap, принимающей указатель на FILE_OBJECT. Весь процесс этот достаточно сложный, но, быть может, этот рисунок поможет немного разобраться (маленько улучшенная копия оного из книги Соломона-Руссиновича):

{рисунок поскипан}

Все семейство Mi*-функций, активно используемых в Mm-функциях, невероятно интересно. Однако целиком мы его рассматривать не будем. Внутри MiCreateImageFileMap заголовок PE-файла (ТОЛЬКО заголовок) безусловно отображается на гиперпространство по адресу 0x0С050000 (mov eax, 0C0500000h) функцией MiMapImageHeaderInHyperSpace (в функцию жестко зашито значение для отображения). После этого отображенный заголовок принимаются активно проверять – функция MiVerifyImageHeader. А вот эта функция уже безусловно интерестна для нас с вами:

    NTSTATUS MiVerifyImageHeader(PIMAGE_NT_HEADERS pPE, ...)
    {
     DWORD FileAlign;

     if(pPE->Signature != IMAGE_NT_SIGNATURE)
     {
      if(pPE->Signature != IMAGE_OS2_SIGNATURE)
       return STATUS_INVALID_IMAGE_PROTECT; //0C0000130h
      else
      {
          /*...код для проверки формата NE...
          не рассматривается, здесь используются два других параметра функции
          и функция MiCheckDosCalls*/
       if (NE is invalid)
        return STATUS_INVALID_IMAGE_WIN_16; //0C0000131
      }
     }
     if (!pPE->FileHeader.Machine)
     {
      if(!pPE->FileHeader.SizeOfOptionalHeader)
       return STATUS_INVALID_IMAGE_PROTECT;
     }
       //IMAGE_FILE_EXECUTABLE_IMAGE
     if (!(pPE->FileHeader.Characteristics & 2))
      return STATUS_INVALID_IMAGE_FORMAT; //0C000007Bh
     if(!(pPE & 3))
       //проверка на выравнивание на границу DWORD
      return STATUS_INVALID_IMAGE_FORMAT;
     if (pPE->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR32_MAGIC)
      return STATUS_INVALID_IMAGE_FORMAT;
     FileAlign = pPE->OptionalHeader.FileAlignment;
       //проверка на кратность 512 байтам
     if (!(FileAlign & 0x1FF))
     {
      if(FileAlign != pPE->OptionalHeader.SectionAlignment)
       return STATUS_INVALID_IMAGE_FORMAT;
     }
     if (!FileAlign)
      return STATUS_INVALID_IMAGE_FORMAT;
       //проверка на степень двойки
     if (!(FileAlign & (FileAlign-1)))
      return STATUS_INVALID_IMAGE_FORMAT;
     if (pPE->OptionalHeader.SectionAlignment < FileAlign)
      return STATUS_INVALID_IMAGE_FORMAT;
     if (pPE->OptionalHeader.SizeOfImage > 0x77000000)
      return STATUS_INVALID_IMAGE_FORMAT;
     return 
       (pPE->FileHeader.NumberOfSections > 0x60) ? 
       (STATUS_INVALID_IMAGE_FORMAT):(0);
    }

В принципе, этот псевдокод особых проблем вызвать не должен. Особое внимание обратите на проверку выравнивания файла и секций.

Для вступления этого вполне достаточно. С оставшейся частью, при большом желании, наличии времени и обладании DDK и интернетом, расправиться не так уж и сложно. Код, правда, насыщен функциями для работы с IRQL и спин-блокировками, однако Соломон и Руссинович достаточно подробно осветили этот вопрос. Также рекомендуем почитать статью Matt - http://www.tuningsoft.com/documents/irql.htm - «Understanding IRQL». Заметьте, MiVerifyImageHeader не единственное место, где MiCreateImageFileMap решает валиден ли образ или нет, однако оставшийся код активно оперирует с гиперпространством, что выводит обсуждение этого вопроса далеко за рамки данной статьи. И соваться туда стоит не раньше, чем прочтете (как следует!) главы Руссиновича о памяти, и всю доступную литературу о PTE/PDE/PFN.

Что до практического применения полученных знаний - NtCreateSection будет использоваться в качестве проверки валидности PE-файла в PE Tools (вероятно, с версии 1.6). Опция - "Validate PE". Псевдокод может выглядеть где-то так:

    /*как вы помните из первой части, ntdll.dll БЕЗУСЛОВНО отображается
    на адресное пространство каждого Win32-приложения Windows*/
    GetModuleHandle(“ntdll.dll”);
    ...
    /*вызывать только динамически – через GetProcAddress
    т.к. нам нужна именно платформенная специфичность,
    поэтому никаких статических линковок с ntdll.lib*/
    ZwCrSec = GetProcAddress(..., “ZwCreateSection”);
    if (STATUS_CODE = ZwCrSec)
     /*значит, ошибка, будем думать, что случилось*/
    else
     /*все параметры секций PE-файла валидны,
         содержимое директорий - ?*/
    ...

Если NtCreateSection вернула что-либо отличное от нуля – файл валидным не является – это невероятно надежный источник проверки валидности файла! Если неуверенно себя чувствуете с нативными приложениями (т.е. программами, использующими ntdll.dll напрямую с помощью статической линковки, или динамически, через GetProcAddress), то вот замечательный линк: http://www.osr.com/ntinsider/1996/native.htm

(продолжение следует...)

Крис Касперски

ТЕХНИКА ОПТИМИЗАЦИИ ПРОГРАММ

(избранное)

(продолжение)

Часть 1 (продолжение)

Удельное время выполнения

Если время выполнения некоторой точки программы не постоянно, а варьируется в тех или иных пределах (например, в зависимости от рода обрабатываемых данных), то трактовка результатов профилировки становится неоднозначной, а сам результат — ненадежным. Для более достоверного анализа требуется:

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

б) определить время их исполнения в лучшем, худшем и среднем случаях.

Очень немногие профилировщики могут похвастаться способностью определять удельное время выполнения машинных команд (иначе называемое растактовкой). К счастью, профилировщик VTune это умеет! Обратимся к сгенерированному им протоколу динамического анализа. Быть может, он поможет нам разрешить загадку "неповоротливости" загрузки указателя pswd?

Листинг 1.4. Удельное время выполнения машинных команд внутри профилируемого фрагмента программы
  
 Line       Instructions                   Dyn-Retirement Cycles  
 107 pswd[p] = '!';   
 107       mov    edx,    DWORD PTR [ebp+0ch]       13 ************
 107 ;     ^ загрузить в регистр EDX указатель pswd

 107       add    edx,   DWORD PTR [ebp-4]          2  **

 107 ;     ^ сложить регистр EDX с переменной p

 107       mov    BYTE PTR [edx],    021h           3  ***
 107 ;     ^ записать в *(pswd+p) значение '!'

 109 y = y | y << 8;
 109       mov    eax,    DWORD PTR [ebp-28]        2  **
 109 ;     ^ загрузить в регистр EAX переменную y

 109       shl    eax,    08h                       1  *
 109 ;     ^ сдвинуть EAX на 8 позиций влево

 109       mov      ecx, DWORD PTR [ebp-28]         (0,7.3,80)
 109 ;     ^ загрузить в регистр ECX переменную y      *******

 109       or       ecx, eax                        1  *
 109 ;     ^ ECX = ECX | EAX (tmp = y | y)

 109       mov      DWORD PTR [ebp-28], ecx         1  *
 109 ;     ^ записать полученный результат в y

 110 x -= k;
 110       mov      edx, DWORD PTR [ebp-24]         0  
 110 ;     ^ загрузить в регистр EDX переменную x

 110       sub      edx, DWORD PTR [ebp-36]         1  *
 110 ;     ^ вычесть из регистра EDX переменную k

 110       mov      DWORD PTR [ebp-24], edx         1  *

 110 ;     ^ записать полученный результат в x

Ну, вот опять, — все команды, как команды, а загрузка указателя pswd "разлеглась" прямо как объевшаяся свинья, "сожравшая" целых тринадцать тактов, в то время как остальные свободно укладываются в один–два такта, а некоторые и вовсе занимают ноль, ухитряясь завершиться одновременно с предыдущей инструкций.

За исключением команды, загружающей содержимое переменной y в регистр ECX, время выполнения всех остальных команд строго постоянно и не меняется от случая к случаю. Наша же "подопечная" в зависимости от еще не выясненных обстоятельств, может "отъедать" даже восемьдесят тактов, что на время делает ее самой "горячей" точкой данного фрагмента программы. Восемьдесят тактов — это вообще полный беспредел! И пускай среднеарифметическое время ее выполнения составляет всего лишь семь тактов, а минимальное — и вовсе ноль, мы не успокоимся пока не выясним: на что и при каких именно обстоятельствах уходит такое количество тактов?

Информация о пенальти

Уже сам факт существования "горячей" точки говорит о том, что в программе что-то неправильно. Хорошо, если это чисто алгоритмическая ошибка, которую видно невооруженным глазом (например, наиболее узким местом приложения оказалась пузырьковая сортировка). Хуже, если процессорное время исчезает буквально на пустом месте без всяких видимых на то причин. Еще хуже, если "хищения" тактов происходят не систематически, а совершаются при строго определенном стечении каких-то неизвестных нам обстоятельств.

Возвратимся к предыдущему вопросу: почему указатель pswd загружается так долго? И при каких именно обстоятельствах загрузка переменной y "подскакивает" со своих обычных семи до восьмидесяти тактов? Судя по всему, процессору что-то не понравилось и он обложил эти две машинные команды "штрафом" (penalty), на время притормозив их исполнение. Можем ли мы узнать, когда и за какой "проступок" это произошло? Не прибегая к полной эмуляции процессора — вряд ли (хотя современные процессоры x86 с некоторыми ограничениями позволяют получить эту информацию и так).

Гранды компьютерной индустрии — Intel и AMD уже давно выпустили свои профилировщики, содержащие полноценные эмуляторы выпускаемых ими процессоров, позволяющие визуализировать нюансы выполнения каждой машинной инструкции.

Просто щелкните по строке mov ecx, DWORD PTR [ebp-28] и профилировщик VTune выдаст всю, имеющуюся у него информацию (листинг 1.5).

Листинг 1.5. Вывод профилировщиком VTune дополнительной информации о выполнении инструкции
Decoder Minimum Clocks = 1 ; Минимальное время декодирования – 1 такт
Decoder Average Clocks = 8.7 ; Эффективное время декодирования – 8,7 тактов
Decoder Maximum Clocks = 86 ; Максимальное время декодирования – 86 тактов

Retirement Minimum Clocks = 0, ; Минимальное время завершения – 0 тактов
Retirement Average Clocks = 7.3 ; Эффективное время завершения – 7,3 такта
Retirement Maximum Clocks = 80 ; Максимальное время завершения – 80 тактов

Total Cycles = 80 (00,65%) ; Всего времени исполнения – 80 тактов

Micro-Ops for this instruction = 1 ; Кол-во микроопераций в инструкции – 1

The instruction had to wait 0 cycles for it's sources to be ready
("Инструкция ждала 0 тактов пока подготавливались ее операнды, т.е. попросту она их не ждала совсем")

Dynamic Penalty: IC_miss
The instruction was not in the instruction cache, so the processor loads the instruction from the L2 cache or main memory.
("Инструкция отсутствовала в кодовом кэше, и процессор был вынужден загружать ее из кэша второго уровня или основной оперативной памяти")
Occurances = 1 ; Такое случалось 1 раз

Dynamic Penalty: L2instr_miss
The instruction was not in the L2 cache, so the processor loads the instruction from main memory.
("Инструкция отсутствовала в кэше второго уровня и процессор был вынужден загружать ее из основной оперативной памяти")
Occurances = 1 ; Такое случалось 1 раз

Dynamic Penalty: Store_addr_unknown
The load instruction stalls due to the address calculation of the previous store instruction.
("Загружающая инструкция простаивала по той причине, что адрес источника рассчитывался предыдущей инструкцией записи")
Occurances = 10 ; Такое случалось 10 раз

Так, кажется, наше расследование превращается в самый настоящий детектив, еще более запутанный, чем у Агаты Кристи. Если приложить к полученному результату даже самый скромный арифметический аппарат, получится полная чепуха и полное расхождение "дебита с кредитом". Судите сами. Полное время выполнения инструкции — 80 тактов, причем, известно, что она выполнялась 11 раз (см. в листинге 1.3 колонку count отчета профилировщика). А наихудшее время выполнения инструкции составило… 80 тактов! А наихудшее время декодирования и вовсе — 86! То есть, худшее время декодирования инструкции превышает общее время ее исполнения и при этом в десяти итерациях она еще ухитряется простаивать как минимум один такт за каждую итерацию по причине занятости блока расчета адресов. Да… тут есть от чего "поехать крышей"!

Причина такого несоответствия заключается в относительности самого понятия времени. Вы думаете время относительно только у Эйнштейна? Отнюдь! В конвейерных процессорах (в частности процессорах Pentium и AMD K6/Athlon) понятие "времени выполнения инструкции" вообще не существует в принципе (см. подразд. "Конвейеризация или пропускная способность VS-латентность" гл. 1). В силу того, что несколько инструкций могут выполняться параллельно, простое алгебраическое суммирование времени их исполнения даст значительно больший результат, нежели исполнение занимает в действительности.

Ладно, оставим разборки с относительностью до более поздних времен, а пока обратим внимание на тот факт, что в силу отсутствия нашей инструкции в кэше (она как раз находится на границе двух кэш-линеек) и вытекающей отсюда необходимости загружать ее из основной памяти, в первой итерации цикла она выполняется значительно медленнее, чем во всех последующих. Отсюда, собственно, и берутся эти пресловутые восемьдесят тактов.

При большом количестве итераций (а при "живом" исполнении программы оно и впрямь велико) временем начальной загрузки можно и пренебречь, но… Стоп! Ведь профилировщик исполнил тело данного цикла всего 11 раз, в результате чего среднее время выполнения этой инструкции составило 7,3 тактов, что совершенно не соответствует реальной действительности!

Ой! Оказывается, это вовсе не "горячая" точка! И тут совершенного нечего оптимизировать. Если мы увеличим количество прогонов профилировщика хотя бы в четыре раза, среднее время выполнения нашей инструкции понизится до 1,8 тактов и она окажется одним из самых "холодных" мест программы! Точнее — это вообще абсолютный ноль, поскольку эффективное время исполнения данной инструкции — ноль тактов (т. е. она завершается одновременно с предыдущей машинной командой). Словом, мы "промахнулись" по полной программе.

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

Короткое лирическое отступление на тему: почему же все так произошло. По умолчанию VTune прогоняет профилируемый фрагмент 1.000 раз. Много? Не спешите с ответом. Наш хитрый цикл устроен так, что его тело получает управление лишь каждые 'z' '!' = 0x59 итераций (см. листинг 1.2). Таким образом, за все время анализа тело цикла будет исполнено всего лишь 1.000/89 = 11 раз! Причем, ни в коем случае нельзя сказать, что это надуманный пример. Напротив! В программном коде такое встречается сплошь и рядом.

Листинг 1.6. Демонстрация кода, некоторые участки которого прогоняются профилировщиком относительно небольшое количество раз, что искажает результат профилировки
			while((++pswd[p])>'z') // <- данный цикл прогоняется профилировщиком 1.000 раз
{
pswd[p] = '!'; // <- данная инструкция прогоняется всего 11 раз

}

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

Впрочем нет, постойте. Нам еще предстоит разобраться со второй "горячей" точкой и на удивление медленной скоростью загрузки указателя pswd. Опытные программисты, вероятно, уже догадались в чем тут дело.

Действительно, — строка pswd[p] = '!' — это первая строка тела цикла, получающая управление каждые 0x59 итераций, что намного превосходит "проницательность" динамического алгоритма предсказания ветвлений, используемого процессором для предотвращения остановки вычислительного конвейера.

Следовательно, данное ветвление всегда предсказывается ошибочно и выполнение этой инструкции процессору приходится начинать с нуля. А процессорный конвейер — длинный. Пока он заполниться… Собственно, тут виновата вовсе не команда mov edx, DWORD PTR [ebp+0ch] — любая другая команда на ее месте исполнялась бы столь же непроизводительно! "Паяльная грелка, до красна нагревающая" эту точку программы, находится совсем в другом месте!

Поднимем курсор чуть выше, на инструкцию условного перехода предшествующую этой команде, и дважды щелкнем мышкой. Профилировщик VTune выдаст следующую информацию:

    Decoder Minimum Clocks = 0  ; Минимальное  время декодирования – 0 тактов
    Decoder Average Clocks = 0  ; Эффективное  время декодирования – 0 тактов
    Decoder Maximum Clocks = 4  ; Максимальное время декодирования – 4 такта

    Retirement  Average Clocks = 1 ; Эффективное время завершения – 1 такт

    Total Cycles = 1011 (08,20%)  ; Всего времени исполнения – 1010 тактов (8,2%)

    Micro-Ops for this instruction = 1 ; Кол-во микроопераций в инструкции – 1

    The instruction had to wait (8,11.1,113) cycles for it's sources to be ready
    ("Эта инструкция ждала минимально 8, максимально 113,
     а в основном 11,1 тактов пока ее операнды не были готовы")

    Dynamic Penalty: BTB_Miss_Penalty ; Штраф типа BTB_Miss_Penalty
    This instruction stalls because the branch was mispredicted. 
    ("Эта инструкция простаивала потому что ветвление не было предсказано")
    Occurances =  13   ; Такое случалось 13 раз

Наша гипотеза полностью подтверждается. Это ветвление тринадцать раз предсказывалось неправильно, о чем VTune и свидетельствует! Постой, как тринадцать?! Ведь тело цикла исполняется только одиннадцать! Да, правильно, одиннадцать. Но ведь процессор наперед этого не знал, и дважды пытался передать на него управление, и лишь затем, "увидев", что ни одно из двух предсказаний не сбылось, "плюнул и поклялся", что никогда–никогда не поставит свои фишки на эту ветку.

ОК. Когда загадки разрешаются — это приятно. Но главный вопрос несколько в другом: как именно их разрешать? Хорошо, что в нашем случае непредсказуемый условный переход находился так близко к "горячей" точке, но ведь в некоторых (и не таких уж редких) случаях "виновник" бывает расположен даже в других модулях программы! Ну что на это сказать… Подходите к профилировке комплексно и всегда думайте головой. Пожалуй, ничего более действенного я не смогу посоветовать…

(продолжение следует)

# Эпилог

    ; #########################################################################
     include cstr.inc

    ;;
    ;; А на закуску, небольшая функция из ULIB,
    ;; над которой можно помедитировать :)
    ;; Edmond / HI-TECH
    ;; 
    ;; Функция возвращает длинну C-строки
    ;; eax - 
    ;; размер строки
    ;;
    ;; Параметры::
    ;; esi - указатель на строку 

    cstr@@@len macro string:REQ
    IFDIFI <string>,<esi>
      mov esi,string
    ENDIF
      call cstr@len
      ENDM

    ; #########################################################################
     .code

    cstr@len  proc

            mov     ecx,esi
            test    ecx,11b
            je      short main_loop

    ;; Цикл учитывающий невыровненность данных

    aligned:
            mov     al,byte ptr [ecx]
            inc     ecx
            test    al,al
            je      short byte_3
            test    ecx,11b
            jne     short aligned

    align   16

    cstr@len_align::                        ; Метка, для обращения к процедуре 
                                            ; если известно, что строка выровнена,
                                            ; но в этом случае ecx = esi

    comment /************************************************************
    Проверка байта на содержание нуля происходит при помощи учёта переноса бита
    из соседнего байта, когда складываются числа X с числом 7efefeffh
    7efefeffh = 01111110111111101111111011111111b
    Если какой-то байт НЕ РАВЕН нулю, происходит перенос в младший бит
    соседнего байта.
    Обратите внимание на ноль в старшем бите. Если бы его не было, то 
    переполнение могло бы произойти из старшего байта.
    *********************************************************************/

    main_loop:
            mov     eax,dword ptr [ecx]
            mov     edx,7efefeffh
            add     edx,eax
            xor     eax,-1
            xor     eax,edx
            add     ecx,4
            test    eax,81010100h
            je      short main_loop

    ;; Теперь нужно определить в каких из 4 байт был ноль?
    ;; Если мы здесь
    ;; нам нужно обратить знак адреса ecx
            neg     ecx
            test    al,al                   ;0
            je      short byte_0
            test    ah,ah                   ;1
            je      short byte_1
            test    eax,00ff0000h           ;2
            je      short byte_2

    ;; Обратите внимание ВЫЧИТАНИЕ ЗАМЕНЕНО СЛОЖЕНИЕМ!!!
    ;; Должно было бы быть lea eax,[ecx-esi], но такой команды нет

    byte_3:
            lea     eax,[esi + ecx]
            neg     eax
            ret
    byte_2:
            lea     eax,[esi + ecx + 1]
            neg     eax
            ret
    byte_1:
            lea     eax,[esi + ecx + 2]
            neg     eax
            ret
    byte_0:
            lea     eax,[esi + ecx + 3]
            neg     eax
            ret

    cstr@len  endp

            end

© 2014-2017 Сергей Воробьев

0.02