Рассылка закрыта
При закрытии подписчики были переданы в рассылку "О карьере и профессиональном развитии IT-специалистов" на которую и рекомендуем вам подписаться.
Вы можете найти рассылки сходной тематики в Каталоге рассылок.
← Октябрь 2003 → | ||||||
1
|
2
|
3
|
4
|
5
|
||
---|---|---|---|---|---|---|
6
|
7
|
8
|
9
|
10
|
11
|
12
|
13
|
14
|
15
|
16
|
18
|
19
|
|
20
|
21
|
22
|
23
|
24
|
26
|
|
27
|
28
|
30
|
31
|
Статистика
+11 за неделю
Низкоуровневое программирование для дZeнствующих #0045 - 1
Информационный Канал Subscribe.Ru |
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 1.
Требования повышаются!
|
Символы отладки и эталонный
отладчик
|
Если вам в силу тех или иных причин необходимо
исследовать Windows, то сразу же возникает необходимость в настройке
среды для работы. Очевидно, одного джентельменского набора из IDA/Soft-Ice/HIEW/IceExt
и дампера процессов 3-кольца типа PE Tools тут уже недостаточно.
Это шаг выше. Поэтому вам потребуется обзавестись символами отладки,
которые можно сгрузить с сайта MS по этому адресу: 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; }
Ну, поскольку, рисунок всегда нагляднее, вот:
![]() |
рис 1 |
А если и рисунка мало, тогда смотрите на дамп из 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;
![]() |
рис 2 |
Для чего мы все это вам рассказываем? Положим, вас жутко заинтересовал механизм работы 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. Однако, предельно кратко, в виде рисунка, показать некоторые вещи стоит. Так нагляднее:
![]() |
рис 3 |
Таким образом вы легко сможете преобразовать примеры выше в удобоваримый код, с которым можно сравнительно легко работать дальше:
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-файла
В первой части мы представили и должным образом дополнили работу 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. Весь процесс этот достаточно сложный, но, быть может, этот рисунок поможет немного разобраться (маленько улучшенная копия оного из книги Соломона-Руссиновича):
![]() |
рис 4 |
Все семейство 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. Опытные программисты, вероятно, уже догадались в чем тут дело.
Действительно, — строка 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
|
|
------------- |
###########################################################################
Рассылка составлена HI-TECH GROUP 28 октября 2003 года. |
(c) HI-TECH 2000 - 2003
http://subscribe.ru/
E-mail: ask@subscribe.ru |
Отписаться
Убрать рекламу |
В избранное | ||