Роббинс Д. - Отладка приложений для Microsoft .NET и Microsoft Windows - 2004
.pdf
Ч А С Т Ь I V
МОЩНЫЕ СРЕДСТВА И МЕТОДЫ ОТЛАДКИ
НЕУПРАВЛЯЕМОГО КОДА
Г Л А В А
12
Нахождение файла и строки ошибки по ее адресу
Ваша программа потерпела крах. ОС оказалась достаточно любезной и предос тавила вам адрес ошибки. Что дальше? Мой друг Крис Селлз (Chris Sells) называет такой сценарий проблемой «моя программа ушла в отпуск, оставив мне только этот никчемный адрес». Конечно, иметь адрес ошибки лучше, чем не иметь ниче го, но знать исходный файл и номер строки ошибки было бы еще полезнее. Вы можете предоставить исходные коды своим клиентам, чтобы они отладили про блему сами, но я не думаю, что это станет реальностью в ближайшем будущем.
Адрес ошибки — обычно все, что вы получаете, и то, если вы очень удачливы. Microsoft прилагает немалые усилия, чтобы облегчить своим сотрудникам поиск проблем с Microsoft Windows 2000/XP/Server 2003. Поиск ошибок усложняется, если ваши пользователи просто сообщают вам, что ваша программа не работает. Так, обработчик необработанных исключений, используемый по умолчанию, выводит «чудесное» диалоговое окно «Приносим извинения за неудобства» (рис. 12 1). К великому сожалению, в этом окне больше не выводится адрес ошибки! Класси ческий пример изменения, дружественного к пользователям, но неприятного для нас, программистов.
Для получения адреса ошибки щелкните ссылку To See What Data This Error Report Contains, Click Here (для просмотра данных, содержащихся в отчете, щелкните здесь), а в следующем диалоговом окне — ссылку To View Technical Information About The Error Report, Click Here (для просмотра технических сведений об ошибке щелкните здесь). В диалоговом окне Error Report Contents (содержание отчета об ошибке) (рис. 12 2) вы увидите адрес ошибки и все загруженные модули. Кроме того, вы получите по настоящему полезный дамп стека, достаточно большой, чтобы можно было изучить историю неправильного потока почти до начала времен. Уди вительно, но кто то принял очень странное решение и поместил всю эту замеча
ГЛАВА 12 Нахождение файла и строки ошибки по ее адресу |
445 |
|
|
тельную информацию в статический элемент управления, поэтому скопировать ее нельзя. Надеюсь, в следующем пакете обновлений для Windows XP эта досад ная оплошность будет исправлена. Есть и хорошие новости: Windows 2000/Server 2003 всегда выводят в диалоговом окне адрес ошибки.
Рис. 12 1. Стандартное диалоговое окно сообщения об ошибке Windows XP
Рис. 12 2. Диалоговое окно «Содержание отчета об ошибке» Windows XP
Вас порадует и то, что независимо от нажатой в диалоговом окне кнопки бу дет запускаться Dr. Watson, если, конечно, он назначен стандартным отладчиком, что имеет место по умолчанию. Если это не так, пусть ваши пользователи сдела ют Dr. Watson отладчиком по умолчанию, выполнив команду DRWTSN32.EXE –i. Что бы получать информацию Dr. Watson, вы должны объяснить пользователям, как запускать Dr. Watson и копировать аварийный дамп памяти при помощи пользо вательского интерфейса Dr. Watson (подробнее о журнале регистрации Dr. Watson и его интерпретации см. приложение А). Вы будете очень довольны, если пользо ватели смогут прислать вам минидампы, поэтому попросите их установить в окне Dr. Watson флажок Create Crash Dump File (создание файла аварийной копии па
446 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
мяти) и запомнить место записи файлов. Конечно, большинство из нас не боится получить адрес ошибки. Если вы обо всем позаботились заранее, то при ошибке ваши приложения автоматически будут высылать вам минидампы, о которых я расскажу в главе 13. Очевидно, что адрес ошибки нужно как то преобразовать в имя исходного файла и номер некорректной строки. Этой теме и посвящена данная глава. Я расскажу про два основных способа преобразования адреса в нужную информацию: при помощи MAP файлов и при помощи утилиты CrashFinder, при лагаемой к книге.
Чтобы получить максимум от рассматриваемых в этой главе методов, настройте компилятор, как указано в главе 2. Все заключительные компоновки нужно создавать с полным набором отладочных символов и MAP файлами. Кроме того, вам нуж но разрешать все конфликты с адресами загрузки DLL. Если эти условия соблю дены не будут, то описанные ниже методы окажутся бесполезными, и определить исходный файл и строку ошибки вы сможете только путем гадания.
Создание и чтение MAP-файла
Меня часто спрашивают, почему я так настойчиво рекомендую создавать MAP файлы для заключительных компоновок. Если коротко, то MAP файлы — единствен ный способ представления данных о глобальных символах, исходных файлах и номерах строк вашей программы в текстовом виде. Использовать CrashFinder проще, зато MAP файлы можно читать всегда и везде, и они не требуют никакой поддерживающей программы и двоичных файлов вашего приложения, предостав ляя ту же информацию. Поверьте, рано или поздно вам понадобится найти рас положение ошибки в более старой версии вашей программы, и у вас будет толь ко один способ сделать это — изучить MAP файл.
MAP файлы полезны только в случае заключительных компоновок, потому что при создании MAP файла компоновщик вынужден отключать компоновку с при ращением. Включить генерирование MAP файлов в Microsoft Visual C++ .NET го раздо проще, чем в предыдущих версиях Visual Studio. Откройте диалоговое окно Property Pages, папку Linker, страницу Debugging и просто выберите значение Yes
вполях Generate Map File (генерировать MAP файл), Map Exports (включать в MAP файл информацию об экспортируемых функциях) и Map Lines (включать в MAP файл информацию о номерах строк). Это установит ключи компоновщика /MAP, /MAPINFO:EXPORTS и /MAPINFO:LINES. Как вы, наверное, догадались, программа Settings Master из главы 9 с файлами проекта по умолчанию сделает это автоматически.
Если вы работаете над реальным проектом, то, вероятно, сохраняете двоичные файлы в отдельном каталоге. По умолчанию компоновщик записывает MAP файл
втот же каталог, что и промежуточные файлы, поэтому вам нужно явно указать, чтобы он хранился вместе с двоичными файлами. Для этого можно ввести в поле Map File Name (имя MAP файла) выражение $(OutDir)/$(ProjectName).map. $(OutDir) —
это встроенный макрос, который система сборки программы заместит именем реального каталога вывода, а вместо $(ProjectName) будет использовано название проекта. На рис. 12 3 показаны все значения параметров MAP файлов для заклю чительной компоновки проекта MapDLL, прилагаемого к книге.
ГЛАВА 12 Нахождение файла и строки ошибки по ее адресу |
447 |
|
|
Рис. 12 3. Значения параметров MAP файлов в диалоговом окне Property Pages
Вполне возможно, что в повседневной работе MAP файлы вам не понадобят ся, но почти наверняка они потребуются вам в будущем. Работа CrashFinder и ва шего отладчика основана на таблице символов, которую они читают при помо щи сервера символов. Если формат таблицы символов изменится или вы забуде те сохранить файлы базы данных программы (Program Database, PDB), у вас воз никнут крупные неприятности. Ответственность за сохранение PDB файлов ле жит на вас, но формат таблиц символов вам неподвластен. Он часто изменяется. Например, многие люди, перешедшие с Microsoft Visual Studio 6 на Microsoft Visual Studio .NET, заметили, что такие средства, как CrashFinder, отказываются работать с программами, откомпилированными при помощи Visual Studio .NET. Microsoft изменила формат таблицы символов и делает это регулярно. В таких случаях един ственное ваше спасение — MAP файлы.
Даже если лет через пять вы будете писать программы с помощью Visual Studio
.NET 2007 Service Pack 6 для Windows Server 2008, я абсолютно уверен, что неко торые ваши клиенты еще будут работать с вашими приложениями, созданными в 2003 году. Когда они обратятся к вам за помощью и предоставят адрес ошибки, несколько дней вам понадобится только на то, чтобы найти компакт диски со старой версией Visual Studio .NET, нужной для чтения сохраненных PDB файлов. Если у вас будут MAP файлы, вы найдете место проблемы за пять минут.
Содержание MAP-файла
Пример MAP файла показан в листинге 12 1. В начале MAP файла указывается имя модуля, время компоновки и предпочтительный адрес загрузки. После заголовка располагается информация о разделах, показывающая, какие разделы созданы компоновщиком из файлов OBJ и LIB.
После информации о разделах находятся действительно полезные данные: информация об открытых (public) функциях. Обратите внимание на слово «от крытых». Если у вас есть статические функции, сведения о них сохраняются в аналогичной таблице после таблицы об открытых функциях. К счастью, их но мера строк не разделяются и выводятся вместе.
448 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
Важные сведения заключены в именах открытых функций и данных столбца Rva+Base, содержащего стартовые адреса функций. Буква f после некоторых ад ресов Rva+Base означает, что адрес соответствует действительной функции, а не глобальной переменной или какой либо импортируемой функции. За разделом открытых функций следует информация о строках в формате:
24 0001:00000006
Первое число — номер строки, а второе — смещение от начала раздела кода, к которому эта строка относится. Согласен, это звучит несколько запутанно, поэтому позднее я объясню, что нужно сделать для преобразования адреса в исходный файл и номер строки.
Если модуль содержит экспортируемые функции, их список будет приведен в заключительном разделе MAP файла. Такую же информацию вы получите, запус тив программу DUMPBIN с параметрами /EXPORTS <имя модуля>.
Листинг 12-1. Пример MAP-файла
MapDLL
Timestamp is 3e2b44a3 (Sun Jan 19 19:36:51 2003)
Preferred load address is 03900000
Start |
Length |
Name |
Class |
|
0001:00000000 |
00000304H |
.text |
CODE |
|
0002:00000000 |
00000028H |
.idata$5 |
DATA |
|
0002:00000030 |
000000f8H |
.rdata |
DATA |
|
0002:00000128 |
00000063H |
.rdata$debug |
DATA |
|
0002:00000190 |
00000004H |
.rdata$sxdata |
DATA |
|
0002:00000194 |
00000004H |
.rtc$IAA |
DATA |
|
0002:00000198 |
00000004H |
.rtc$IZZ |
DATA |
|
0002:0000019c |
00000004H |
.rtc$TAA |
DATA |
|
0002:000001a0 |
00000004H |
.rtc$TZZ |
DATA |
|
0002:000001a4 |
00000014H |
.idata$2 |
DATA |
|
0002:000001b8 |
00000014H |
.idata$3 |
DATA |
|
0002:000001cc |
00000028H |
.idata$4 |
DATA |
|
0002:000001f4 |
00000082H |
.idata$6 |
DATA |
|
0002:00000280 |
0000007bH |
.edata |
DATA |
|
0003:00000000 |
00000004H |
.CRT$XCA |
DATA |
|
0003:00000004 |
00000004H |
.CRT$XCZ |
DATA |
|
0003:00000008 |
00000004H |
.CRT$XIA |
DATA |
|
0003:0000000c |
00000004H |
.CRT$XIZ |
DATA |
|
0003:00000010 |
00000004H |
.data |
DATA |
|
0003:00000014 |
00000014H |
.bss |
DATA |
|
Address |
Publics |
by Value |
Rva+Base |
Lib:Object |
0000:00000001 |
___safe_se_handler_count |
00000001 |
<absolute> |
|
0001:00000000 |
_DllMain@12 |
03901000 f |
MapDLL.obj |
|
0001:00000006 |
?MapDLLFunction@@YAHXZ |
03901006 f |
MapDLL.obj |
|
|
|
|
|
|
450 |
ЧАСТЬ IV |
|
Мощные средства и методы отладки неуправляемого кода |
||||||||||
|
|
|
|
|
|||||||||
|
|
|
|
|
|||||||||
|
|
|
|
|
|||||||||
0001:00000016 |
|
|
?InternalStaticFunction@@YAXXZ 03901016 f |
MapDLL .obj |
|||||||||
Line numbers for |
.\Release\MapDLL.obj(d:\dev\booktwo\disk\chapter |
|
|||||||||||
examples\chapter |
12\mapfile\mapdll\mapdll.cpp) segment .text |
|
|
||||||||||
11 |
0001:00000000 |
|
20 |
0001:00000000 |
21 |
0001:00000003 |
26 |
0001:00000006 |
|
||||
25 |
0001:00000006 |
|
27 |
0001:00000012 |
28 |
0001:00000015 |
31 |
0001:00000016 |
|
||||
32 |
0001:00000016 |
|
33 |
0001:00000022 |
37 |
0001:00000023 |
36 |
0001:00000023 |
|
||||
38 |
0001:00000028 |
|
39 |
0001:00000033 |
41 |
0001:0000003b |
|
|
|
|
|||
Line numbers for |
R:\VSNET2003\Vc7\lib\MSVCRT.lib(f:\vs70builds\2292\vc |
|
|||||||||||
\crtbld\crt\src\atonexit.c) segment |
.text |
|
|
|
|
|
|||||||
81 |
0001:000001e4 |
|
76 |
0001:000001e4 |
90 |
0001:00000209 |
96 |
0001:0000020a |
|
||||
95 |
0001:0000020a |
|
97 |
0001:0000021b |
|
|
|
|
|
|
|
||
Line numbers for |
R:\VSNET2003\Vc7\lib\MSVCRT.lib(f:\vs70builds\2292\vc |
|
|||||||||||
\crtbld\crt\src\crtdll.c) segment .text |
|
|
|
|
|
||||||||
134 |
0001:0000003c |
129 |
0001:0000003c |
|
135 |
0001:00000044 |
136 |
0001:0000 |
004c |
||||
158 |
0001:00000052 |
163 |
0001:00000065 |
|
168 |
0001:0000007a |
170 |
0001:0000 |
007e |
||||
172 |
0001:00000081 |
178 |
0001:0000008b |
|
179 |
0001:00000090 |
184 |
0001:0000 |
009a |
||||
189 |
0001:000000ab |
192 |
0001:000000b2 |
|
219 |
0001:000000b8 |
220 |
0001:0000 |
00c1 |
||||
225 |
0001:000000c3 |
226 |
0001:000000cf |
|
234 |
0001:000000e5 |
236 |
0001:0000 |
00ec |
||||
234 |
0001:000000f3 |
240 |
0001:000000f4 |
|
241 |
0001:000000f7 |
249 |
0001:0000 |
00fa |
||||
250 |
0001:00000106 |
252 |
0001:0000010c |
|
257 |
0001:00000111 |
258 |
0001:0000 |
011e |
||||
260 |
0001:00000124 |
262 |
0001:0000012d |
|
263 |
0001:00000136 |
265 |
0001:0000 |
0142 |
||||
266 |
0001:0000014b |
268 |
0001:0000015a |
|
269 |
0001:0000015c |
272 |
0001:0000 |
015e |
||||
275 |
0001:0000016e |
283 |
0001:00000177 |
|
286 |
0001:00000181 |
288 |
0001:0000 |
018a |
||||
289 |
0001:00000198 |
291 |
0001:0000019b |
|
292 |
0001:000001a9 |
298 |
0001:0000 |
01b7 |
||||
294 |
0001:000001bc |
295 |
0001:000001d4 |
|
299 |
0001:000001d6 |
|
|
|
||||
Exports |
|
|
|
|
|
|
|
|
|
|
|
|
|
ordinal |
name |
|
|
|
|
|
|
|
|
|
|
|
|
1?MapDLLFunction@@YAHXZ (int __cdecl MapDLLFunction(void))
2?MapDLLHappyFunc@@YAPADPAD@Z (char * __cdecl MapDLLHappyFunc(cha r *))
Получение информации об исходном файле, имени функции и номере строки
Алгоритм извлечения данных об исходном файле, имени функции и номере строки из MAP файла прост, но для этого нужно выполнить несколько действий в шест надцатеричной системе счисления. Допустим, ошибка произошла по адресу 0x03901038 в модуле MAPDLL.DLL из листинга 12 1.
Прежде всего из MAP файлов проекта нужно выбрать файл, содержащий ад рес ошибки. Для этого достаточно взглянуть на предпочтительный адрес загруз
ГЛАВА 12 Нахождение файла и строки ошибки по ее адресу |
451 |
|
|
ки и последний адрес в разделе открытых функций. Если адрес ошибки попадает в этот диапазон, значит, это и есть нужный вам MAP файл.
Чтобы определить нужную функцию, найдите в столбце Rva+Base первую функ цию с адресом, превышающим адрес ошибки. Предыдущий элемент в MAP файле и будет функцией, в которой случилась ошибка. В нашем случае это функция ?MapDLLHappyFunc@@YAPADPAD@Z с адресом 0x03901023. Любое имя функции, начина ющееся с вопросительного знака, — это расширенное имя C++.
Вы, возможно, удивляетесь, почему я не упоминал про расширение имен C++ в главе 6, когда рассказывал про расширения имен для различных соглашений вызова. Хотя оба типа расширений играют сходную роль, их источники различ ны. Расширения имен соглашений вызова просто указывают генератору кода, по каким правилам создавать код занесения параметров в стек и очистки стека, и основаны на определениях ОС. Расширение имен C++ обусловлено языком. C++ допускает перегрузку методов, поэтому компилятор должен как то их различать. Для этого он «расширяет» имя метода при помощи информации о типе возвра щаемого значения, соглашении вызова и параметрах. Так компилятор сможет точно узнать, какую функцию вы хотите вызвать. Для преобразования имени передайте его в командной строке программе UNDNAME.EXE из Visual Studio .NET. В нашем примере ?MapDLLHappyFunc@@YAPADPAD@Z преобразуется в char * __cdecl MapDLLHappyFunc(char *). Некоторые из вас, вероятно, смогли определить, что первоначальным именем функции было MapDLLHappyFunc, только по ее расширенному имени. Другие расши ренные имена C++ расшифровать труднее, особенно в случае перегруженных функций.
Для определения смещения строки нужно выполнить простое шестнадцатерич ное вычитание по формуле:
(адрес ошибки) – (предпочтительный адрес загрузки) – 0x1000
Помните: адреса представляют собой смещения от начала первого раздела кода, поэтому нужное преобразование выполняется именно по такой формуле. Вы скорее всего догадались, зачем вычитается предпочтительный адрес загрузки, но знаете ли вы, почему нужно вычесть еще и 0x1000? Адрес ошибки — это смещение от начала раздела кода, но раздел кода не является первой частью двоичного файла. Первая часть двоичного файла состоит из заголовка PE и связанной с ним заг лушки DOS, которые занимают 0x1000 байт. Да, все двоичные файлы Win32 по прежнему вынуждены бороться с тяжелым наследием MS DOS.
Мне неизвестно, почему компоновщик генерирует MAP файлы, требующие этого вычисления. Разработчики компоновщика включили столбец Rva+Base в MAP файл недавно, поэтому я не понимаю, что им помешало подкорректировать номера строк.
Как только вы подсчитали смещение, ищите в разделе строк наибольшее чис ло, не превышающее найденное значение. Помните, что при генерировании кода компилятор может перемешивать его, так что номера строк не всегда располага ются в восходящем порядке. Вот что получается в моем примере:
0x03901038 – 0x03900000 – 0x1000 = 0x38
Просмотрев листинг 12 1, вы увидите, что самое большое смещение, не пре вышающее 0x38, входит в выражение 39 0001:00000033 (строка 39), которое от носится к файлу MAPDLL.CPP.
452 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
PDB2MAP: создание MAP-файлов постфактум
При обсуждении поиска адресов ошибок меня постоянно спрашивают, что делать, если значительная часть цикла разработки была пройдена без создания MAP фай лов. Другие внимательные разработчики также обращают внимание, что для по лучения отличных MAP файлов нужно задавать базовые адреса всех DLL при сборке программы. Работая над проектом, приближающимся к завершению, вы, вероят но, не захотите дестабилизировать сборку приложения, изменяя некоторые па раметры. Кроме того, без программы SettingsMaster из главы 9 вносить эти гло бальные изменения проекта в Visual Studio неудобно, поэтому разработчики обычно предпочитают задавать базовые адреса своих DLL через REBASE.EXE.
Не оставляя без внимания ни одной интересной задачи, я рассмотрел эту про блему внимательней. Мне нужен был способ нумерации функций, файлов и строк исходного кода. Учитывая, что сервер символов DBGHELP.DLL уже включает эти возможности, следующий шаг на пути к генерированию MAP файла из PDB фай ла оказался простым.
Первая проблема, с которой я столкнулся, заключалась в том, что функции SymGetSymNext и SymGetSymPrev возвращают не то, что вы ожидаете. Я думал, что могу получить адрес в исходном файле, после чего вызывать SymGetSymPrev до тех пор, пока не окажусь в начале исходного файла и SymGetSymNext, пока не доберусь до его конца. Я забыл принять во внимание один небольшой аспект — встраиваемые функции. Эти функции и соответствующие им строки исходного кода располага ются в теле других функций, поэтому информация о номерах строк на самом деле хранится в виде диапазонов. Это означало, что для концентрации данных об ис ходном коде и номерах строк мне нужно было разработать схему остлеживания всех диапазонов. Как только я справился с этим препятствием, написать программу оказалось довольно просто.
Еще одна проблема с серверами символов была связана с библиотекой стан дартных шаблонов. Сначала я стал разрабатывать структуры данных при помощи STL, но вскоре обнаружил, что даже частичная реализация PDB2MAP.EXE работа ла мучительно медленно. Эта ошибка была на моей совести, потому что я исполь зовал класс вектора для линейного поиска, а это просто глупо. Попробовав не сколько похожих вариантов, я понял, что STL всегда будет работать гораздо мед ленней, так как она выполняет много неявных операций выделения памяти и ко пирования. Поскрипев зубами и попытавшись понять некоторые подробности ре ализации STL, я осознал, что напрасно усложнял проблему. В конце концов я сам написал простую систему из нескольких массивов, очень быструю и крайне лег кую для понимания. Она также оказалась гораздо проще для сопровождения, чем то, что я мог сделать при помощи STL.
Файлы, генерируемые PDB2MAP, очень похожи на MAP файлы. Так как сервер символов DBGHELP.DLL не предоставляет сведений о статических функциях, мне пришлось от этого отказаться. Увидев файл .P2M, вы поймете, что проблем с его пониманием у вас не будет. Я счел ненормальную систему нумерации строк MAP файлов пережитком прошлого, поэтому реализовал в PDB2MAP более простой и современный подход. Моя информация о строках генерируется на основании действительных адресов памяти.
