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

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

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

ГЛАВА 17 Стандартная отладочная библиотека C и управление памятью

653

 

 

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

Стандартный вопрос отладки

Как убедиться в том, что при обработке строк я не допустил ошибок?

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

Вы можете до потери сознания просматривать программу в поисках этих ошибок и все же упустить их. К счастью, некоторые умные люди из Microsoft поняли, что пора изменить положение дел. Новая библиотека STRSAFE при звана обезопасить обработку строк. STRSAFE входит в Platform SDK за июль 2002 года и включена в Visual Studio .NET 2003.

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

Мне очень жаль, но STRSAFE появилась после того, как я написал почти весь код для этой книги. Работая над своими проектами, вы поймете, что настройка STRSAFE очень похожа на действия, нужные для преобразования кода ANSI в формат Unicode. Это требует некоторого времени, но затраты того стоят. Ко времени поступления этой книги в продажу или чуть позже я переработаю все программы из нее и выложу их на веб сайте компании Wintellect.

Ключ проверки безопасности буфера

Проверки в период выполнения очень полезны, но есть и еще один ключ, кото рый также следует устанавливать всегда. Это ключ /GS, выполняющий проверку безопасности буфера (Buffer Security Check). В отличие от ключей /RTCx его сле дует устанавливать и для отладочных, и для заключительных компоновок. Ключ /GS отслеживает адреса возврата из функции и предотвращает его перезапись, что часто делают вирусы и троянские кони для передачи управления на свой код. Для

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

этого он резервирует в стеке дополнительное пространство перед адресом воз врата. При входе в функцию код пролога сохраняет в этом месте результат вы полнения операции ИСКЛЮЧАЮЩЕЕ ИЛИ над адресом возврата и специальным значением (security cookie). Это значение подсчитывается во время загрузки мо дулей, чем достигается его уникальность для отдельных модулей. При возврате из функции специальная функция _security_check_cookie проверяет, не изменилось ли сохраненное значение. При обнаружении различия выводится информационное окно, и программа завершается. Если вы хотите посмотреть этот механизм в дей ствии, изучите в исходном коде стандартной библиотеки C файлы SECCINIT.C, SECCOOK.C и SECFAIL.C

Обеспечения безопасности ключу /GS мало, поэтому он еще оказывает нам огромную помощь при отладке. Ключи /RTCx отслеживают множество ошибок, но случайную перезапись адреса возврата они все же иногда пропускают. Благодаря /GS вы получаете проверку таких ситуаций и в отладочных компоновках. Конеч но, сотрудники Microsoft при разработке этого ключа не забывали про нас, по этому вы можете заменить функцию вывода информационного окна по умолча нию собственным обработчиком, вызвав функцию _set_security_error_handler. Если вы повредите стек, ваш обработчик должен выполнять после записи ошибки вы зов ExitProcess.

Резюме

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

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

Мы также обсудили методы и инструменты обработки самых отвратительных проблем с памятью: записи посредством неинициализированных указателей и записи вне выделенных блоков памяти. Пусть применение грубой силы кажется очень неэлегантным способом решения этих проблем, но только так вы получа ете реальный шанс на успех. Когда дело доходит до записи данных после окон чания блока памяти, их отслеживание поможет облегчить инструмент PageHeap из состава AppVerifier, одного из приложений пакета Application Compatibility Toolkit. Хотя AppVerifier не лишен недостатков, я уверен, что в будущем Microsoft их ис правит. Наконец, огромную помощь в избавлении от ошибок оказывают новые ключи компилятора: ключи проверки ошибок в период выполнения и ключ про верки безопасности буферов.

Г Л А В А

18

FastTrace:

высокопроизводительная утилита трассировки серверных приложений

Все мы знаем, что быстродействие — основное требование, предъявляемое к сер верным приложениям. От скорости их выполнения напрямую зависит масштаби руемость программы. Мы создаем приложения, которые должны обрабатывать тысячи и даже миллионы отдельных запросов, при этом любое дополнительное действие может иметь огромные последствия. Что делает серверные приложения еще «интереснее», так это то, что они в высокой степени многопоточны, из за чего причины низкой производительности иногда найти очень трудно.

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

Для отладки серверных приложений у нас есть старое спасительное сред ство — трассировка. Это единственный способ, позволяющий увидеть общую кар тину, особенно при запуске готовой программы в реальных условиях. Мне дово

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

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

иповысить свои шансы на обнаружение и исправление ошибок.

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

На собственном опыте убедившись в плохой производительности многих си стем трассировки, я захотел решить эту проблему раз и навсегда. Для этой главы я написал программу FastTrace, призванную обеспечить трассировку в каком угодно объеме, не вызывая значительного снижения быстродействия. Прежде чем перейти к обсуждению использования и реализации FastTrace, я хочу объяснить, в чем же заключается недостаток большинства методов трассировки.

Фундаментальная проблема и ее решение

Самая крупная проблема с серверными приложениями объясняется тем, что нам, людям, трудно представить себе множество вещей одновременно. Наш мозг орга низован линейным образом. Чтобы облегчить отладку серверных приложений, мы неосознанно организуем вывод трассировочной информации последовательно. А вот большинство современных серверов являются многопроцессорными, и многие приложения имеют до 20 и более потоков, так что многие из этих линей ных процессов на самом деле выполняются параллельно. Пытаясь внести поря док в кажущийся хаос, мы выводим трассировочные данные всего приложения в один единственный файл.

Если результаты трассировки многопоточной программы сохраняются в од ном файле, вы попадаете в ситуацию, показанную на рис. 18 1. Именно линейная регистрация трассировочных вызовов для всех потоков приводит к возникнове нию узких мест (bottle neck1 ). Как я уже говорил в главе 15, вся суть разработки многопоточных программ сводится к тому, чтобы потоки были максимально за няты и как можно меньше простаивали.

Очевидно, что для создания самого быстрого средства трассировки в запад ном мире требуется такой линейный способ вывода информации, который не влиял бы на нормальный ход выполнения программы. Если б каждый поток выполнял собственную трассировку, все было бы прекрасно. Это и делает утилита FastTrace: она предоставляет каждому потоку отдельный файл вывода, что избавляет пото ки от перехода в состояние ожидания или блокировки. После сохранения трас сировочной информации нескольких потоков в файлах журнала вы можете объе динить файлы и увидеть истинную последовательность трассировки. В разделе «Реализация FastTrace» вы увидите, что ничего сложного в этом нет.

1 Буквально «бутылочное горлышко». — Прим. пер.

ГЛАВА 18 FastTrace: высокопроизводительная утилита трассировки приложений

657

 

 

Поток 1

Поток 2

Поток 3

Поток 4

Поток 5

Поток 6

вызывает

вызывает

вызывает

вызывает

вызывает

вызывает . . .

трассировку

трассировку

трассировку

трассировку

трассировку

трассировку

Баба-а-ах!

Чертово

«бутылочное

горлышко»!

Единственная запись трассировки

Результаты трассировки

Поток n вызывает трассировку

Рис. 18 1. Типичная система трассировки серверных приложений

Использование FastTrace

Детали реализации FastTrace я скрыл, поэтому для ее использования вам нужно только скомпоновать свое приложение с библиотекой FASTTRACE.DLL и вызывать в нужных случаях одну из ее экспортируемых функций, названную, конечно же, FastTrace. Вот ее прототип:

FASTTRACE_DLLINTERFACE void FastTrace ( LPCTSTR szFmt

,

...

) ;

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

<Имя EXE6файла>_<ID процесса>_<ID потока>.FTL

Открыв какой нибудь файл, вы заметите, что информация хранится в двоич ном виде. Каждая выводимая FastTrace строка сохраняется с соответствующим ей номером; это нужно для соблюдения правильного порядка строк при объедине нии файлов. Кроме того, каждая строка может содержать метку времени.

Чтобы сделать FastTrace простой и быстрой, я решил ограничить длину строк 80 символами. При выполнении отладочной компоновки на попытку записи бо лее длинной строки вам укажет диагностическое сообщение SUPERASSERT. Если вы хотите использовать более длинные строки, нужно просто переопределить зна чение MAX_FASTTRACE_LEN в файле FastTrace.H и перекомпоновать FastTrace.

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

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

отключено с целью небольшого повышения производительности. Вы можете вклю чать/отключать его когда угодно. Наконец, последняя команда позволяет прика зать FastTrace вызвать функцию отладочного вывода, прототип которой соответ ствует функции OutputDebugString. По умолчанию FastTrace не вызывает OutputDebug6 String; как я объяснил в главе 4, эта функция генерирует исключение и будет за медлять ваше приложение. Однако вам, вероятно, хотелось бы видеть эти исклю чения при выполнении отладочных компоновок, поэтому я предоставляю и та кую возможность.

Наконец, я реализовал две функции, предназначенные для управления трасси ровкой. Первая из них — FlushFastTraceFiles — как можно догадаться по ее име ни, записывает все буферы FastTrace на диск. Возможно, вам захочется создать фо новый поток, следящий за работой серверного приложения и периодически «сбра сывающий» трассировочную информацию на диск в те моменты, когда оно не занято. Так вы сможете гарантировать, что нужная информация будет в файлах даже при аварийном завершении приложения.

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

SNAP_<Номер снимка>_<Имя EXE6файла>_<ID процесса>_<ID потока>.FTL

Номер снимка представляет собой уникальное для каждого процесса десятич ное число, благодаря чему вы сможете увидеть всю цепь своих снимков. После закрытия и переименования всех активных файлов трассировки SnapFastTraceFiles снова открывает файлы для каждого потока, чтобы не пропустить никакой инфор мации. Как и в случае с FlushFastTraceFiles, вам, вероятно, захочется реализовать возможность вызова SnapFastTraceFiles в нужный вам момент. Помните: для защиты внутренних структур данных и SnapFastTraceFiles, и FlushFastTraceFiles использу ют критическую секцию, что приводит к временной блокировке всех трассиро вочных вызовов.

Объединение журналов трассировки

Как я уже говорил, файлы журнала имеют двоичный формат, поэтому для изуче ния отдельных файлов или их слияния с целью исследования линейного потока трассировочных вызовов потребуется программа FastTraceLog.EXE. Для вывода дампа журнала на экран нужно только передать ей в командной строке выраже ние –d <отдельный файл журнала>. В результате вы увидите порядковый номер, дату/время (если было включено создание меток времени) и сохраненную в ука занный момент строку трассировочной информации.

Слияние, или объединение файлов журнала чуть сложнее. Для этого FastTrace Log.EXE нужно передать в командной строке выражение –c <SNAP_номер снимка (необязательный параметр)>_<имя EXE файла>_<ID процесса>. Например, выпол нив тестовую программу FTSimpTest.EXE, вы увидите, что она генерирует файлы трассировки с такими именами:

ГЛАВА 18 FastTrace: высокопроизводительная утилита трассировки приложений

659

 

 

FTSimpTest_2720_0400.FTL

FTSimpTest_2720_1644.FTL

FTSimpTest_2720_2332.FTL

FTSimpTest_2720_2368.FTL

FTSimpTest_2720_2424.FTL

FTSimpTest_2720_2560.FTL

FTSimpTest_2720_2584.FTL

FTSimpTest_2720_2640.FTL

FTSimpTest_2720_2688.FTL

Для объединения этих файлов отдельного потока нужно выполнить команду:

FastTraceLog.exe 6c FTSimpTest_2720

В итоге вы получите текстовый файл — в нашем случае с именем FTSimp Test_2720.TXT. Информация в текстовом файле очень похожа на дамп, и в приве денном ниже фрагменте вы можете увидеть, что до последней строки создание меток времени было включено, а перед ней отключено:

[0x0B3C 57

1/1/2003 17:52:47:205]

Hello from

CThread

6> 3!

[0x0B50 58

1/1/2003 17:52:47:205]

Hello from

CThread

6> 3!

[0x0B50 59

1/1/2003 17:52:47:486]

Hello from

CThread

6> 4!

[0x0B20 60

1/1/2003 17:52:47:486]

Hello from

CThread

6> 4!

[0x0B20 61

1/1/2003 17:52:47:767]

Hello from

CThread

6> 5!

[0x0830

62]

 

 

THIS SHOULD BE THE

LAST LINE!

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

Реализация FastTrace

Работать с FastTrace совсем просто, да и реализовать ее было не намного слож нее. Главное чудо FastTrace — поддержка памяти для каждого потока. Каждый по ток имеет свой объект класса записи в файл и записывает информацию только в один файл. Реализация основных механизмов FastTrace почти не требует поясне ний. Исходный код см. на CD в проекте FastTrace.

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

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

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

Второе интересное решение было связано с форматом хранения меток вре мени. Если вы когда нибудь заглядывали в справочный раздел, описывающий их хранение в ОС, то знаете, что количество форматов просто огромно. Изучив их подробнее, я решил использовать формат FILETIME, потому что он занимает толь ко 8 байт, в то время как формат SYSTEMTIME требует 16 байт. Кроме того, я рас смотрел их обработку и обнаружил, что самый быстрый способ получения вре мени обеспечивает функция GetSystemTimeAsFileTime.

Резюме

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

Г Л А В А

19

Утилита Smooth Working Set

Как утверждалось в фильме «Годзилла» и как вы сами можете убедиться по объе му спама в вашем почтовом ящике, «размер имеет значение». У всех разработчи ков просто слюнки текут при мысли о более быстрых компьютерах с большим объемом памяти, но даже в этом случае нам нужно беспокоиться о размере на ших программ. Старая пословица «компактный код — хороший код» ничуть не утратила своей актуальности в современном мире, в котором оперативная память компьютеров разработчиков часто превышает 512 Мб, а серверов — 2 Гб. Если объем памяти, имеющейся в вашем распоряжении, кажется вам бесконечным, это не значит, что ее нужно использовать полностью!

После устранения явных проблем с производительностью программы, таких как циклическое вычисление числа π с точностью до миллиардного разряда, на ступает самое время позаботиться о минимизации рабочего набора. Рабочий набор — это объем памяти, выделенной для выполняемых в текущий момент блоков вашей программы. Чем меньше вы сделаете рабочий набор, тем быстрее будет работать ваше приложение благодаря уменьшению числа ошибок страниц. Ошибка страницы происходит при доступе к блоку вашей программы, который или не находится в кэш памяти (мягкая ошибка страницы), или не находится в памяти вообще и должен быть загружен с жесткого диска (жесткая ошибка страницы). Как мне однажды сказал один мудрый человек, «ошибки страниц способны испортить вам весь день!».

Объем текущего рабочего набора своей программы вы можете увидеть в столбце Mem Usage (память), выбрав в окне Task Manager (диспетчер задач) вкладку Processes (процессы). Сведения о рабочем наборе показывают также многие другие диаг ностические и информационные средства, такие как PerfMon. Убедившись, что ваши алгоритмы экономно расходуют память, вам следует попытаться уменьшить объем памяти, занимаемой самим кодом. В начале этой главы я подробнее опишу ошибки страниц во время выполнения программы и объясню, почему они так нежелатель

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

ны. После этого, как вы уже, наверное, догадались, я представлю программу Smooth Working Set (SWS), которая сделает оптимизацию рабочего набора тривиальной.

Оптимизация рабочего набора

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

На рис. 19 1 — пример того, что может произойти: ОС поддерживает только шесть функций фиксированного размера на страницу памяти и в любой момент времени может хранить в памяти только четыре страницы; кроме того, 10 вызы ваемых функций — самые часто вызываемые функции программы.

При загрузке этой программы в память ОС, не долго думая, загружает четыре первых страницы двоичного файла. В ходе выполнения первая функция вызыва ет функцию 2, которая по стечению обстоятельств располагается на той же стра нице. Однако функция 2 вызывает функцию 3, которая находится на странице 5. Так как страницы 5 в памяти нет, происходит ошибка страницы. Теперь ОС долж на выгрузить одну из загруженных страниц. Так как страница 4 не изменялась, ОС решает выгрузить ее и загружает на ее место страницу 5. Теперь можно выпол нить функцию 3. Увы, функция 3 вызывает функцию 4, находящуюся на только что выгруженной странице 4, поэтому у нас происходит вторая ошибка страни цы. ОС выгружает страницу 3, к которой дольше всего не было обращений, воз вращает в память страницу 4 и выполняет функцию 4. Как вы можете видеть, пос ле этого произойдет еще одна ошибка страницы, поскольку функция 4 вызывает функцию 5 — только что выгруженную. Я мог бы продолжить этот процесс даль ше, но уже и так ясно, что при ошибках страниц происходит очень много работы.

Главное, что обработка ошибок страниц требует массу времени. Программа на рис. 19 1 вместо выполнения «отсиживается» в коде ОС. Если бы мы могли ука зать компоновщику порядок размещения функций на страницах, мы избежали бы многих дополнительных затрат, связанных с обработкой этих ошибок.

На рис. 19 2 показана та же программа после перемещения самых часто ис пользуемых функций в начало двоичного файла. Вся программа занимает тот же объем памяти (4 страницы), но благодаря объединению часто вызываемых фун кций они не приводят ни к каким ошибкам страниц, в результате чего приложе ние будет работать быстрее.

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

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