Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Роббинс Д. - Отладка приложений для Microsoft .NET и Microsoft Windows - 2004

.pdf
Скачиваний:
353
Добавлен:
13.08.2013
Размер:
3.3 Mб
Скачать

 

 

 

ГЛАВА 19 Утилита Smooth Working Set

673

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

@ILT+30(??2@YAPAXI@Z):

 

 

 

 

00401023

jmp

operator new (401B90h)

 

 

 

@ILT+35(??_GCResString@@UAEPAXI@Z):

 

 

 

00401028

jmp

CResString::'scalar deleting destructor' (401B40h)

 

 

 

@ILT+40(??0CResString@@QAE@PAUHINSTANCE__@@@Z):

 

 

 

0040102D

jmp

CResString::CResString (401990h)

 

 

 

@ILT+45(??BCResString@@QBEPBGXZ):

 

 

 

00401032

jmp

CResString::operator unsigned short const * (401B20h)

 

 

 

Следовательно, команда CALL @ILT+15(?ShowHelp@@YAXXZ) на самом деле

 

 

вызывает переход к метке @ILT+15(?ShowHelp@@YAXXZ), которая представляет

 

 

собой переход к реальной команде.

 

 

 

 

 

 

 

 

Листинг 19-1. PENTERHOOK.CPP

/*——————————————————————————————————————————————————————————————————————

Отладка приложений для Microsoft .NET и Microsoft Windows Copyright © 199762003 John Robbins — All rights reserved.

——————————————————————————————————————————————————————————————————————*/ /*////////////////////////////////////////////////////////////////////// Включение заголовочных файлов

//////////////////////////////////////////////////////////////////////*/

#include "stdafx.h" #include "SWSDLL.h" #include "SymbolEngine.h" #include "VerboseMacros.h"

#include "ModuleItemArray.h"

/*////////////////////////////////////////////////////////////////////// Определения, константы, объявления typedef

//////////////////////////////////////////////////////////////////////*/

// Описатель события, который функция _penter проверяет,

 

// чтобы узнать, не запрещена ли обработка.

 

static HANDLE g_hStartStopEvent = NULL ;

 

// Простой автоматический класс 6 я использую

 

// его для разного рода инициализации.

 

class CAutoMatic

 

{

 

public :

 

CAutoMatic ( void )

 

{

 

g_hStartStopEvent = ::CreateEvent ( NULL

,

TRUE

,

FALSE

,

SWS_STOPSTART_EVENT

) ;

ASSERT ( NULL != g_hStartStopEvent ) ;

 

}

 

~CAutoMatic ( void )

 

{

 

VERIFY ( ::CloseHandle ( g_hStartStopEvent ) ) ;

 

см. след. стр.

674 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода

}

} ;

/*////////////////////////////////////////////////////////////////////// Глобальные объекты с областью видимости файл

//////////////////////////////////////////////////////////////////////*/

//Автоматический класс. static CAutoMatic g_cAuto ;

//Массив модулей.

static CModuleItemArray g_cModArray ;

/*////////////////////////////////////////////////////////////////////// Прототипы функций

//////////////////////////////////////////////////////////////////////*/

/*//////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////*/

extern "C" void SWSDLL_DLLINTERFACE __declspec(naked) _penter ( void )

{

DWORD_PTR dwCallerFunc ;

// Пролог функции.

 

__asm

 

 

{

 

 

PUSH

EBP

// Создание стандартного кадра стека.

MOV

EBP , ESP

 

PUSH

EAX

// Сохранение EAX, так как он нужен

 

 

// мне до сохранения всех регистров.

MOV

EAX , ESP

// Загрузка текущего указателя стека

 

 

// в регистр EAX.

SUB

ESP , __LOCAL_SIZE

// Резервирование пространства

 

 

// для локальных переменных.

PUSHAD

// Сохранение значений всех

 

 

// регистров общего назначения.

// Теперь я могу вычислить адрес возврата.

ADD

EAX , 04h + 04h

// Нужно учесть команды PUSH EBP

 

 

// и PUSH EAX.

MOV

EAX , [EAX]

// Получение адреса возврата.

SUB

EAX , 5

// Чтобы определить начало функции,

 

 

// надо вычесть 56байтовый переход,

 

 

// использованный для вызова _penter.

MOV

[dwCallerFunc] , EAX

// Сохранение нового адреса возврата.

ГЛАВА 19 Утилита Smooth Working Set

675

 

 

}

//Если событие начала/завершения находится

//в сигнальном состоянии, ничего не делаем.

if ( WAIT_TIMEOUT == WaitForSingleObject ( g_hStartStopEvent , 0 ))

{

// Выполняем всю работу. g_cModArray.IncrementFunctionEntry ( dwCallerFunc ) ;

}

 

// Эпилог функции.

 

__asm

 

{

 

POPAD

// Восстановление всех регистров

 

// общего назначения.

ADD ESP , __LOCAL_SIZE

// Удаление пространства, выделенного

 

// для локальных переменных.

POP EAX

// Восстановление регистра EAX.

MOV ESP , EBP

// Восстановление кадра стека.

POP EBP

 

RET

// Возврат в вызвавшую функцию.

}

 

}

Формат файла .SWS и перечисление символов

Как показывает листинг 19 1, в _penter ничего удивительного. Все становится интереснее, когда дело касается организации адресов функций. Так как мне нуж но связать адрес с именем функции, то в некоторых местах программы я прибе гаю к услугам своего старого друга — сервера символов DBGHELP.DLL. Однако про смотр символов при помощи сервера символов — не самая быстрая операция, а доступ к данным нужен при каждом вызове функции, поэтому я должен был най ти компактный и быстрый способ его выполнения.

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

Я решил, что SWS подобно WST должна хранить счетчики вызовов для каждо го запуска каждого модуля в отдельном файле данных. Я предпочел этот подход, потому что он позволяет удалить информацию о конкретном запуске приложе ния из объединенного набора данных, если она вам не нужна. WST использует для наименования файлов формат <имя модуля>.<номер вызова>, но я хотел, чтобы SWS поддерживала схему <имя модуля>.<номер вызова>.SWS, чтобы я мог в конеч

676 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода

ном счете написать графическую программу, облегчающую объединение данных обо всех запусках.

Выбрав способ обработки данных в период выполнения, я приступил к рас смотрению создания файла порядка. Как я уже говорил, мне нужен был способ объединения информации об отдельных запусках. Однако при размышлении над фактическим созданием файла порядка я понял, что у меня нет некоторых дан ных. Файл порядка должен содержать имена функций, а также их размеры, в то время как я планировал хранить только адреса функций. Хотя я опять мог бы использовать символьную машину при генерировании файла порядка, единствен ный способ получения размера символа — перечисление всех символов модуля. Так как я уже выполнял полное перечисление символов на первоначальных эта пах генерирования данных, я решил, что мне следует просто добавить в файл размеры функций. Я не нуждаюсь в хранении имен функций, потому что их все гда можно узнать, загрузив PDB файл для двоичного файла.

Если вы все же нашли книгу Расса Блейка «Optimizing Windows NT» и прочи тали главу «Tuning the Working Set of Your Application» (настройка рабочего набо ра программы), вас, вероятно, интересует, почему я ничего не говорю о наборах битов и интервалах времени. Группа, работавшая над производительностью Win dows NT, использовала при создании WST схему, в которой каждой функции со ответствует один бит из набора. Каждые столько то секунд WST регистрирует при помощи этого набора битов функции, выполненные за прошедший интервал вре мени. Меня часто удивляет, почему они реализовали WST именно так. На первый взгляд, набор битов позволяет сэкономить память, но при этом нужно помнить, что должен быть реализован некоторый способ отображения битов и адресов функций. Не думаю, что такая схема экономит намного больше памяти, чем мой метод. Мне кажется, что программисты, работавшие над производительностью Windows NT, использовали набор битов потому, что оптимизировали при помо щи WST целую ОС. Я же, напротив, работаю с отдельными двоичными файлами, так что это вопрос масштаба.

Разрабатывая структуры данных, я был озабочен одним моментом: при вызо ве функции я просто хотел увеличивать счетчик. В многопоточных программах я должен защищать это значение, чтобы в каждый конкретный момент времени им мог манипулировать только один поток. Я хотел сделать SWS как можно более быстрой, поэтому увеличение счетчика вызовов функций лучше всего выполнять при помощи API функции InterlockedIncrement. Так как она использует аппарат ный механизм блокировки (префикс команды LOCK), то гарантирует согласован ность данных в многопоточных приложениях. Однако в Microsoft Win32 наиболь шим числом, которое можно передать в InterlockedIncrement, является 32 разряд ное значение, в связи с чем возникает проблема с превышением 4 294 967 295 вы зовов функций. Четыре миллиарда — много, но и этого может не хватить для не которых циклов сообщений при долгом выполнении приложения.

Для решения этой проблемы программа WST в период выполнения только записывала вызовы функций, а общее их число подсчитывалось постфактум, ког да проще обрабатывать возможное переполнение. При настройке ОС вероятность выполнения некоторых функций более 4 миллиардов раз довольно высока. Од

ГЛАВА 19 Утилита Smooth Working Set

677

 

 

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

В тех крайне редких случаях, когда функция выполняется более 4 миллиардов раз, у вас есть два варианта. Во первых, можно реализовать переменную счетчи ка как 64 разрядное целое, защитив ее при помощи объекта синхронизации, от дельного для каждого модуля. Другой, более радикальный вариант — разработка схемы, подобной алгоритму WST. Конечно, есть и еще один вариант: так как про блема переполнения возможна только в Win32, можно реализовать SWS только для Microsoft Win64. Даже за всю свою жизнь вы не сможете выполнить 18 446 744 073 709 551 615 вызовов функции.

Изучив листинг 19 2, вы увидите, что формат SWS файла очень прост. Я быст ро понял, что для обработки всех манипуляций с файлами мне нужен общий ба зовый класс — CSWSFile, определенный в файлах SWSFILE.H и SWSFILE.CPP. По сути этот класс — не более чем оболочка для обычных действий с файлами Win32.

Листинг 19-2. FILEFORMAT.H

/*——————————————————————————————————————————————————————————————————————

Отладка приложений для Microsoft .NET и Microsoft Windows Copyright © 199762003 John Robbins — All rights reserved.

——————————————————————————————————————————————————————————————————————*/ #ifndef _FILEFORMAT_H

#define _FILEFORMAT_H

/*////////////////////////////////////////////////////////////////////// Директивы define и объявления структур

//////////////////////////////////////////////////////////////////////*/

// Сигнатура SWS6файла (SWS2). #define SIG_SWSFILE '2SWS' #define EXT_SWSFILE _T ( ".SWS" )

/*////////////////////////////////////////////////////////////////////// Заголовок SWS6файла.

//////////////////////////////////////////////////////////////////////*/

typedef struct tag_SWSFILEHEADER

{

//

Сигнатура файла.

См. определения SIG_*, указанные выше.

DWORD

dwSignature

;

//

Время компоновки

двоичного файла, ассоциированного с этим файлом.

DWORD

dwLinkTime ;

 

//Адрес загрузки двоичного файла. DWORD64 dwLoadAddr ;

//Размер образа.

DWORD dwImageSize ;

см. след. стр.

678

ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода

 

 

 

 

 

 

 

// Число записей в этом файле.

 

 

DWORD

dwEntryCount ;

 

 

// Поле

флагов.

 

 

DWORD

dwFlags ;

 

 

// Имя модуля для этого файла.

 

 

TCHAR

szModuleName[ MAX_PATH ] ;

 

 

DWORD

dwPadding ;

 

 

} SWSFILEHEADER , * LPSWSFILEHEADER ;

 

 

/*//////////////////////////////////////////////////////////////////////

 

 

Тип записи SWS6файла.

 

 

//////////////////////////////////////////////////////////////////////*/

 

 

typedef struct tag_SWSENTRY

 

 

{

 

 

 

 

// Адрес функции.

 

 

DWORD64

dwFnAddr ;

 

 

// Размер функции.

 

 

DWORD

dwSize ;

 

 

// Счетчик вызовов.

 

 

DWORD

dwExecCount ;

 

 

} SWSENTRY , * LPSWSENTRY ;

 

 

#endif

// _FILEFORMAT_H

 

 

 

 

 

 

В связи с форматом SWS файла я хочу обратить ваше внимание на то, что я храню в нем адрес загрузки двоичного файла. Сначала я хранил в нем только адреса функций, но потом вспомнил о возможности перемещения двоичного файла в памяти. В этой ситуации SWSDLL.DLL могла бы быть вызвана с каким либо адре сом, и у меня не было бы никакой записи для этого адреса в SWS файлах, загру женных для модуля. Хотя всем нам следует модифицировать базовые адреса на ших DLL, иногда мы про это забываем, и я хотел, чтобы SWS правильно обраба тывала такие случаи.

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

Функция обратного вызова для перечисления символов и некоторые мои по пытки ограничения их числа приведены в листинге 19 3. Прежде всего я реали зовал проверку того, имеет ли символ соответствующую информацию о номере строки. Так как я полагал, что функции, содержащие в себе вызовы _penter, будут скомпилированы правильно, с соблюдением всех вышеописанных этапов, я смог безопасно избавиться от многих посторонних символов. Следующий шаг на пути к устранению символов заключался в проверке, являются ли частью символов специфические строки. Например, все символы, начинающиеся с _imp__, представ ляют собой функции, импортируемые из других DLL. Еще две проверки я оставил

ГЛАВА 19 Утилита Smooth Working Set

679

 

 

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

Листинг 19-3. Перечисление символов SWS

/*——————————————————————————————————————————————————————————————————————

ФУНКЦИЯ: SymEnumSyms

ОПИСАНИЕ:

 

 

 

Функция обратного

вызова

для перечисления символов. Эта функция только

добавляет данные в SWS6файлы

и все.

ПАРАМЕТРЫ:

 

 

 

szSymbolName

6

имя символа.

ulSymbolAddress –

адрес символа.

ulSymbolSize

6

размер

символа в байтах.

pUserContext

6

файл SWS.

ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ:

TRUE 6 все отлично.

FALSE – при добавлении данных в файл возникла проблема.

——————————————————————————————————————————————————————————————————————*/

BOOL CALLBACK SymEnumSyms ( PSTR

szSymbolName

,

 

DWORD64

ulSymbolAddress

,

 

ULONG

ulSymbolSize

,

 

PVOID

pUserContext

)

 

{

 

 

 

LPENUMSYMCTX pCTX = (LPENUMSYMCTX)pUserContext ;

 

 

CImageHlp_Line cLine ;

 

 

 

DWORD dwDisp ;

 

 

 

if ( FALSE == g_cSym.SymGetLineFromAddr ( ulSymbolAddress

,

 

&dwDisp

,

 

&cLine

 

) )

{

 

 

 

//Если для символа не было обнаружено исходного файла

//и номера строки, игнорируем его.

return ( TRUE ) ;

}

//Будущие улучшения для игнорирования конкретных символов:

//1. Реализуйте проверку того, не находится ли файл

//в списке игнорируемых файлов.

//2. Проверяйте, относится ли адрес к разделу кода модуля.

//Это позволит избежать добавления в итоговые файлы символов IAT.

//Есть ли этот символ в списке игнорируемых символов?

см. след. стр.

680

ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода

 

 

 

 

 

 

 

 

 

 

for ( int i = 0 ; i < IGNORE_CONTAINING_COUNT ; i++ )

 

 

{

 

 

 

 

if ( NULL != strstr ( szSymbolName

,

 

 

g_szIgnoreContaining[ i ]

) )

 

 

{

 

 

 

 

// Выход.

 

 

 

 

return ( TRUE ) ;

 

 

 

 

}

 

 

 

 

}

 

 

 

 

if ( NULL != pCTX6>pfnVerboseOutput )

 

 

 

{

 

 

 

#ifdef _WIN64

 

 

 

 

pCTX6>pfnVerboseOutput(_T("

Adding Symbol : 0x%016I64X %S\n"),

#else

 

 

 

 

pCTX6>pfnVerboseOutput(_T("

Adding Symbol : 0x%08X %S\n" ) ,

#endif

 

 

 

 

(DWORD_PTR)ulSymbolAddress

,

 

szSymbolName

 

);

 

}

 

 

 

 

if ( FALSE == pCTX6>pSWSFile6>AddData ( ulSymbolAddress

,

 

 

ulSymbolSize

 

,

 

 

0

 

) )

 

{

 

 

 

ASSERT ( !"Adding to SWS file failed!" ) ; return ( FALSE ) ;

}

pCTX6>iAddedCount++ ; return ( TRUE ) ;

}

Период выполнения и оптимизация

Одна проблема с символами в период выполнения была связана с тем, что сим вольная машина не возвращает статические функции. Становясь подозрительным, если я не находил в модуле адрес, я, как обычно, включал в программу вызовы 6–7 диагностических информационных окон. Сначала я несколько смутился тем, что видел диагностические сообщения, так как в одной из моих тестовых программ никакая функция не была объявлена статической. Взглянув на стек в отладчике, я увидел символ с именем наподобие $E127. В функции имелся вызов _penter, и все казалось правильным. Наконец я понял, что это функция, сгенерированная ком пилятором, такая как конструктор копий. Хотя мне по настоящему нравится вы полнять проверку ошибок в коде, я заметил, что в некоторых программах хвата ло этих сгенерированных компилятором функций, поэтому я мог только сооб щить о проблеме в отладочных компоновках при помощи TRACE.

Последний интересный аспект SWS — оптимизация модуля. Функция TuneModule довольно объемна, поэтому в листинге 19 4 я привел только ее алгоритм. Как вы можете увидеть, на каждой странице кода я размещаю как можно больше функ

ГЛАВА 19 Утилита Smooth Working Set

681

 

 

ций, чтобы исключить пустое пространство. Наибольший интерес представляет поиск функции, лучше всего соответствующей странице. Я решил размещать на странице в первую очередь как можно больше функций с ненулевым числом вы зовов. Если я не мог найти такую функцию, я располагал на странице функцию с нулевым числом вызовов. Мой первоначальный алгоритм работал великолепно. Однако при оптимизации определенных модулей он стал приводить к ошибкам.

Небольшое исследование показало, что в таких ситуациях страница была по чти заполнена и при этом требовалось обработать функцию, размер которой превышал размер страницы. Да, я не ошибся: размер функции, сообщенный сим вольной машиной, был больше, чем размер страницы памяти. Изучив проблему тщательней, я заметил, что эти огромные функции появлялись, только когда они были последними символами в разделе кода. Очевидно, символьная машина счи тает всю информацию, расположенную после определенных символов, их частью, возвращая в результате этого ошибочное значение. В алгоритме оптимизации вы можете увидеть, что, когда размер символа превышает размер страницы, мне ничего не остается, кроме как записать символ в файл порядка. Это не лучшее решение, однако и подобная ситуация встречается не часто.

Листинг 19-4. Алгоритм настройки SWS

// Алгоритм функции TuneModule.

BOOL TuneModule ( LPCTSTR szModule )

{

Сгенерировать имя SWS6файла вывода.

Скопировать базовый SWS6файл во временный файл.

Открыть временный SWS6файл.

Для каждого файла szModule.#.SWS в этом каталоге

{

Проверить, что время компоновки этого файла #.SWS соответствует времени компоновки временного SWS6файла.

Для каждого адреса в этом файле #.SWS

{

Прибавить значение счетчика вызовов для этого адреса к аналогичному счетчику во временном файле.

}

}

Получить размер страницы этого компьютера.

Пока не готово.

{

Найти первую запись во временном SWS6файле, для которой указан адрес.

Если я проверил все адреса, но такой записи не нашел.

см. след. стр.

682 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода

{

готово = TRUE Прервать цикл.

}

Если счетчик вызовов для этой записи равен 0.

{

готово = TRUE Прервать цикл.

}

Если размер этой записи меньше, чем оставшееся пространство страницы.

{

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

пространства страницы.

}

Иначе, если размер этой записи больше, чем размер страницы

{

Просто записать адрес в PRF6файл, так как я ничего не могу сделать.

Присвоить значению оставшегося объема страницы размер всей страницы.

}

Иначе.

{

//Эта запись слишком велика для размещения

//на странице, поэтому выполняется поиск функции,

//лучше всего подходящей для этой страницы.

Для каждого элемента временного SWS6файла.

{

Если адрес имеет ненулевое значение.

{

Если наилучшее

соответствие не найдено.

{

 

Обозначить

эту запись как наилучшее соответствие

в целом.

 

Обозначить

эту запись как наилучшее соответствие

по числу

вызовов.

}

 

Иначе.

 

{

 

Если размер этой записи > размер наилучшего соотв6ия

{

Обозначить эту запись как наилучшее соответствие в целом.

}

Соседние файлы в предмете Программирование на C++