
Роббинс Д. - Отладка приложений для Microsoft .NET и Microsoft Windows - 2004
.pdf
ГЛАВА 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, которые указывают только на отдельные аспекты общего состояния программы. Вообще то все еще хуже: будьте готовы к тому, что никакие нетривиальные ошибки никогда не обнаружат себя в удобной управляемой среде ваших систем контроля качества, а покажутся только в джунглях готовой программы.
Для отладки серверных приложений у нас есть старое спасительное сред ство — трассировка. Это единственный способ, позволяющий увидеть общую кар тину, особенно при запуске готовой программы в реальных условиях. Мне дово



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 страницы), но благодаря объединению часто вызываемых фун кций они не приводят ни к каким ошибкам страниц, в результате чего приложе ние будет работать быстрее.
Процесс обнаружения и упорядочения наиболее часто вызываемых функций называется оптимизацией рабочего набора и состоит из двух этапов. На первом нужно определить, какие функции вызываются чаще всего, а на втором компо новщику нужно задать порядок размещения функций в файле, чтобы все они были расположены соответствующим образом.