c000007b - ошибка при запуске приложения

Метки:  , , , , , , , ,

Не так давно я столкнулся с одной, достаточно редко встречающейся в моей практике, ошибкой. Она носит название "Ошибка при запуске приложения (0xc000007b)" и влечет за собой отказ в запуске приложения в операционной системе Windows. Проще говоря, некоторые приложения перестают запускаться, либо всё же запускаются, но с вероятностью ниже 100%. Щелкаешь в проводнике по имени запускного exe-модуля, либо пытаешься запустить исполняемый файл из командной строки, и наблюдаешь следующую картину:

ошибка при запуске приложения

Забавно, но что то в этой ошибке c000007b показалось мне знакомым, моя дырявая долговременная память сохранила смутные образы подобных проблем еще с незапамятных времен, при этом не оставив никаких ясных обстоятельств. Изучая информацию по данной ошибке в Сети я начал припоминать, что наблюдал подобные ошибки еще во времена Windows 2000, но на самом то деле характерны они были для исполняемых бинарных модулей еще со времен Windows 98, просто формулировались иначе. Начиная с Windows 2000 и по сей день ошибка c000007b выглядит в точности так, как представлено выше, и, скорее всего, на протяжении нескольких поколений ОС имеет родственные причины возникновения. Специфика найденного мной в Сети материала заключалась в том, что давались многочисленные рекомендации по исправлению, однако не было никакой конкретики, в связи с чем появилось желание попытаться изучить проблему самостоятельно.

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

Данная статья представляет собой теоретические выкладки по исследованию загрузчика образов операционной системы Windows. Изучение проводилось с целью создать базис для предполагаемого дальнейшего изучения особенностей кода загрузчика. На данный момент рекомендаций по исправлению ошибки c000007b в данной статье не предоставлено, поскольку практика показала неоднозначность возвращаемых утилитами Dependency Walker и Process Monitor результатов.

Формат исполняемых файлов

Приступим. Для начала давайте посмотрим, что же нам сообщают разработчики об ошибке со статусом c000007b на официальной странице значений NTSTATUS? Ошибка носит символическое имя STATUS_INVALID_IMAGE_FORMAT (НЕКОРРЕКТНЫЙ ФОРМАТ ОБРАЗА), а в описании к ней присутствует следующая формулировка: "Образ либо не предназначен для выполнения в Windows или содержит ошибку..", ну и далее даются общие рекомендации. Как всегда, довольно пространное определение, не содержащее в себе значимых подробностей, однако уже позволяющее нам сделать как минимум два предположения: ошибка может содержаться в служебных структурах заголовка основного запускаемого модуля (exe-файл) | ошибка может содержаться в подключаемых образах (dll-библиотеках). Ну хорошо, а что же в себе таит утверждение о некорректном формате? Возможно, подразумевается некое повреждение структуры PE-образа? Но ведь близких по природе ошибок в PE-образе, чисто теоретически, может быть великое множество, и скорее всего так оно и есть. Одним словом, это утверждение о повреждении структуры исполняемого образа требует проверки и аргументации!! В свою очередь, для проверки следовало бы отследить, какой именно код ядра и на каком этапе выводит данное сообщение. Вероятно, если мы наблюдаем подобную ошибку до запуска приложения, то от момента щелчка правой кнопкой мыши по бинарному исполняемому файлу (например: exe-файл) в проводнике Windows (explorer.exe) и до непосредственного начала выполнения кода приложения, проходит некая скрытая от глаз пользователя работа на уровне ядра операционной системы и вот как раз эта самая работа иногда завершается с ошибкой. Одни вопросы порождают другие. Думаю, для лучшего понимания того, какая именно логика скрывается за безмолвием загрузки, надо сперва задаться вопросом "что же вообще такое представляет из себя исполняемый файл в среде Windows и как он загружается"? Всякий бинарный исполняемый файл в Windows построен на основе формата под названием PE (Portable Executable, переносимый исполняемый), который содержит в себе великое множество разнообразных секций, предназначенные для подготовки участков файла (они же блоки кода и данных), критичных для непосредственного выполнения приложения.

Portable Executable (PE) — формат исполняемых файлов и динамических библиотек, используемый в операционных системах Microsoft Windows. Фактически это довольно сложная структура данных, в которой содержится информация, необходимая системному загрузчику исполняемых образов, то есть коду ядра, который производит всю необходимую работу по подготовке исполняемого файла к непосредственному запуску (создание адресного пространства процесса, отображение файла в память, создание всех необходимых для функционирования системных конструкций и прч). В операционных системах Windows описываемый формат PE применяется при создания таких исполняемых файлов как: exe, dll, sys и некоторых других.

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

  • Операционная система: Windows 7 Профессиональная (Русская), 32-разрядная (6.1.7601) (32-битную среду я выбрал для простоты, дабы WOW64 не вносил свои коррективы);
  • Простой исполняемый модуль lasterr.exe из комплекта примеров FASM (EXAMPLES\DLL) с минимумом подключаемых библиотек. Опять же, для простоты;
  • Отладчик WinDbg 10.0.14321.1024 x86 из комплекта Debugging Tools for Windows (x86);
  • Утилита Process Monitor;

Процесс загрузки образа

Весь этот огромный пласт собранной нами информации опытного специалиста уже давно бы привел к однозначным выводам относительно источника ошибки, однако в моем случае, при отсутствии необходимых знаний, придется двигаться с самого начала, при этом интуитивно выискивая направления по косвенным признакам. Все те данные, которыми мы обладаем на данный момент, подводят нас к мысли, что надо каким-то образом зацепиться за этот пресловутый загрузчик образов, но как? Единственное, что приходит в голову, так это использовать всеми нами любимую утилиту Process Monitor. Зная специфику программы, можно заранее предположить, что это будут лишь поверхностные сведения, поскольку утилита не покажет нам подробности многих этапов загрузки образа на выполнение. Но ведь что-то она нам все же даст, ведь пока мы идем в темноте и пытаемся нащупать хотя бы какие-то ориентиры для дальнейшего продвижения. Просто будем надеяться, что информация, предоставляемая нам утилитой Procmon поможет нам двинуться дальше, найти зацепку в виде каких-нибудь малозаметных деталей. Тут стратегия у нас будет достаточно простая, ничего необычного.

  1. Из-под учетной записи с правами локального администратора запускаем утилиту Procmon;
  2. В разделе Filter создаем фильтр на наше приложение lasterr.exe;
  3. Останавливаем захват событий (Ctrl+E или иконка лупы на панели);
  4. Очищаем лог (Ctrl+X или значок ластика на панели);
  5. Снова включаем захват (Ctrl+E) событий;
  6. Стартуем наблюдаемое приложение lasterr.exe, как обычно двойным щелчком из проводника;

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

  1. Образ приложения lasterr.exe загружается по адресу 0x400000. То есть мы видим уже работающий загрузчик образов;
  2. Образ системной библиотеки ntdll.dll загружается в адресное пространство процесса по адресу 0x77c30000;
  3. Активизируется ускоритель prefetcher. В системной директории C:\Windows\Prefetch проверяется .pf-файл(ы) для запускаемого приложения;
  4. Чуть ниже мы можем увидеть группу событий по операциям (CreateFile, SetBasicInformationFile, QueryAttributeTagFile, CreateFileMapping, QueryStandardInformationFile) с библиотеками ntdll.dll, kernel32.dll, apisetschema.dll, kernelbase.dll, locale.nls, errormsg.dll, user32.dll и прочими. Судя по всему это не проецирование образов, а события, генерируемые prefetcher'ом;
  5. Ниже по списку мы наблюдаем первые события с операцией Load Image, которые знаменуют собой загрузку необходимых приложению DLL библиотек в адресное пространство процесса;
  6. Еще ниже по списку событий мы наблюдаем чтение ветки HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options. Имеется в виду механизм, которых позволяет запускать образы под отладчиком;
  7. Далее мы видим считывание ключа реестра HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVrsion\Windows\LoadAppInit_DLLs. Ключ ответственен за механизм AppInit_DLLs, который позволяет определить списки DLL, загружаемые в адресное пространство "каждого" процесса;
  8. Далее идет работа с различными языковыми опциями и MUI;

Это все, что предоставляет нам Procmon. Но на самом деле в приведенном выше списке мы видим лишь очень малую часть логики, так сказать её вершину. Обусловлено это тем, что Process Monitor показывает лишь видимые ему этапы, и вовсе не отражает всего многообразия операций по подготовке приложения к запуску. (прим.: хотя, возможно я забыл отключить какие-то встроенные фильтры). Но даже малое порой представляет собой большое, все дело в точке зрения :) Давайте посмотрим, о чем на могут рассказать захваченные Process Monitor'ом события? Например, событие Load Image применительно к образу lasterr.exe по загрузке себя же самого по базовому адресу 0x400000 в адресное пространство процесса, определенно будет нам интересно. Щелкнем по нему пару раз и перейдем во вкладку Stack. Стек вызовов начинается с функции RtlUserThreadStart, которая является частью системной библиотеки ntdll.dll. Библиотека ntdll.dll предоставляет собой "родной" интерфейс (Native API) пользовательского режима для функций ядра, это своеобразный "мост" между функциями библиотек пользовательского режима и кодом, который реализует соответствующий функционал в ядре. Функция RtlUserThreadStart именуется подобным образом не спроста, название говорит само за себя, намекая нам на назначение данной функции. Предположительно она предназначена для старта потока внутри процесса, при содержит функционал части пользовательского режима.

RtlUserThreadStart - общая стартовая функция для всех потоков в Windows.

Вот уже есть у нас какая-никакая а зацепка в виде данной функции, с которой стартуют все потоки в Windows. Проходим далее по списку событий и в общем потоке обращаем своё внимание на событие Load Image, которое относится к загрузке библиотеки kernel32.dll. Поступаем аналогичным образом, выполняем двойной щелчок левой кнопкой мыши, открываем дополнительную информацию о событии, переходим на вкладку Stack и наблюдаем следующее:

Load Image Stack

Тут уже у нас в начале стека вызовов присутствует фрейм функции LdrInitializeThunk из библиотеки ntdll.dll. Странно, в этот раз стек начинается с другой функции, хотя, поскольку нам не известна взаимосвязь функций в библиотеке ntdll.dll, это мало нам о чем-то говорит, поскольку LdrInitializeThunk может вызываться позже, то есть во многих случаях мы можем иметь дело с одной и той же логикой.

LdrInitializeThunk - функция обратного вызова (режим ядра -> режим пользователя) в составе библиотеки ntdll.dll, которая не столько обратный вызов, сколько точка входа, с которой все потоки пользовательского режима начинают свое выполнение. Другими словами, создание любого потока пользовательского режима всегда происходит посредством данной функции. В некоторых источниках фигурирует как процедура инициализация загрузчика, начальный его этап, отвечает за инициализацию непосредственно загрузчика, менеджера кучи, таблиц FLS, NLS, TLS, структур критической секции. Судя по всему они с RtlUserThreadStart работают в связке, но как именно, пока не ясно.

Мы получили имена двух функций, которые, судя по всему, имеют отношение к созданию потока и подготовке его к выполнению. Теперь можно переходить непосредственно к практической части исследования. Надеюсь, перед началом экспериментов у Вас уже готова тестовая среда и установлены Debugging Tools for Windows из комплекта Windows SDK и в системную переменную Path добавлен путь к необходимой версии каталога с отладчиками. Теперь задача перед нами стоит не совсем тривиальная, поскольку нам необходимо поймать момент запуска приложения, до его непосредственного выполнения и даже до момента подготовки адресного пространства процесса. К превеликой нашей радости, основными отладчиками от Microsoft поддерживается специфичный отладочный сценарий начальной стадии загрузки приложения. Насколько я понял, он может быть активирован двумя путями:

  • Настройка отладчика через GUI: через меню Debug - Event Filters. На фильтре Create process указываем опцию Execution: Enabled. После перезапуска процесса отладчик остановится до функции LdrInitializeThunk.
  • Запуском отладчика со специальными опциями командной строки.

Например, следующая команда позволяет изучить процесс и установить точку останова до того, как код пользовательского режима запустится:

windbg.exe -xe ld:ntdll.dll lasterr.exe

опция xe подразумевает останов после того, как библиотека ntdll.dll будет загружена в адресное пространство процесса. И не спроста, потому как в самом отладчике, в командной строке, мы должны поставить точку останова:

bp ntdll!LdrInitializeThunk

а затем продолжить выполнение командой "g". Для чего это все? А для того, чтобы пропустить некоторое количество кода, которое выполняется до функции LdrInitializeThunk. Мы бы в любом случае вышли на неё, однако потратили бы дополнительное время.
Отладчик останавливается на точке остановка в самом начале функции:

Ну, судя по всему, функция LdrInitializeThunk содержит на верхнем своем уровне всего три функции: LdrpInitialize, ZwContinue, RtlRaiseStatus, причем, я так понимаю, две последних функции являются для нас не значимыми. Выполнив отладчиком по клавише F10 команду call ntdll!LdrpInitialize я убедился, что приложение запустилось. Получается, вся работа происходит в глубинах функции Ldrpinitialize, надо идти внутрь. Первым параметром в функцию LdrpInitialize передается адрес дампа PE-файла в адресном пространстве процесса, а вторым адрес какой-то неизвестной структуры (?). Судя по всему, когда я впервые остановился на точке останове в начале функции, в первом параметре я увидел адрес подмапленной в память библиотеки ntdll.dll, поскольку она загружается самой первой. С этого момента я опускаю ряд подробностей нахождения мест кода, которые могли быть ответственны за генерацию ошибки c000007b, поскольку описание всего процесса отладки превратило бы повествование в бесконечное. Скажу лишь, что всю работу по анализу кода я проводил при помощи отладчика WinDbg и дизассемблера IDA.

Первая причина

Первая удача сопутствовала мне при обработке функции LdrpSnapIAT. Очередной раз анализируя код я поставил точку останова на данную функцию, и при выполнении отладчик остановился на следующем коде:

Самое последнее условие je ntdll!LdrpSnapIAT должно уводить нас на следующую ветку кода:

Понятное дело, что в штатном режиме управление туда не передается, поэтому я просто взвел флаг zero, чтобы принудительно инициировать переход. Ну и в самом конце фрагмента кода мы видим долгожданную команду mov eax, 0C000007Bh, после чего я запускаю выполнение командой g и.. вот оно:

c000007b

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

Судя по описанию с MSDN, данная функция ищет запись внутри PE-заголовка и возвращает адрес данных для этой записи (при наличии возвращает заголовок секции (IMAGE_SECTION_HEADER)) и длину, а случае ошибки возвращает ноль. Используется для поиска различных таблиц в PE-файлах.

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

Похоже что за инициализацию процесса отвечает функция LdrpInitializeProcess, которая, на определенном этапе своей работы начинает загружать библиотеки, необходимые для работы основного приложения, при помощи функции LdrLoadDll, а та, в свою очередь, начинает просматривать таблицу импорта и заполнять Import Address Table (IAT) через функцию LdrpSnapIAT. Однако стек не всегда остается подобным, при прогоне программы по команде g и остановке на точке останова, стек вызовов иногда меняется, показывая, что код загружает модули, присутствующие в таблице импорта.

Функция LdrpSnapIAT "связывает" таблицу адресов импорта с импортируемыми DLL библиотеками, перезаписывает каждую запись в таблице адресов импорта актуальным адресом импортируемой функции.

В коде функции LdrpSnapIAT имеется вызов функции RtlImageDirectoryEntryToData, которая, в свою очередь, использует RtlImageDirectoryEntryToDataEx, а та вызывает функцию RtlpImageDirectoryEntryToData32, которая уже проверяет следующие условия:

  1. Если значение входного параметра DirectoryEntry (Порядковый номер требуемой записи каталога) больше или равно (>=) значения PeHeader.OptionalHeader.NumberOfRvaAndSizes (число записей в массиве DataDirectory. обычно имеет значение 16); Поскольку DirectoryEntry может иметь только определенные значения: IMAGE_DIRECTORY_ENTRY_ARCHITECTURE (7), IMAGE_DIRECTORY_ENTRY_BASERELOC (5), IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (11), IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR (14), IMAGE_DIRECTORY_ENTRY_DEBUG (6), IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT (13), IMAGE_DIRECTORY_ENTRY_EXCEPTION (3), IMAGE_DIRECTORY_ENTRY_EXPORT (0), IMAGE_DIRECTORY_ENTRY_GLOBALPTR (8), IMAGE_DIRECTORY_ENTRY_IAT (12), IMAGE_DIRECTORY_ENTRY_IMPORT (1), IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG (10), IMAGE_DIRECTORY_ENTRY_RESOURCE (2), IMAGE_DIRECTORY_ENTRY_SECURITY (4), IMAGE_DIRECTORY_ENTRY_TLS (9), он действительно не должен быть >=16!
  2. Если значение поля PeHeader.DataDirectory.Export библиотеки = 0. В коде функции для поиска местоположения структуры Object Table используется yнивеpсальная формула: pe_header_offset + rva_entries_number*8 + 78h; здесь: pe_header_offset - смещение от начала файла до PE-заголовка (смещение 3Ch), rva_entries_number - количество записей в таблице объектов. После вычисления мы попадаем на первую запись структуры IMAGE_DATA_DIRECTORY, которая содержит относительный адрес (RVA) таблицы экспорта. Предположение: в уже загруженной в адресное пространство процесса DLL, которая должна предоставлять экспортируемые функции нашей основной программе (иначе бы она не была загружена), отсутствует таблица экспорта? Подобная ситуация не допустима и явно ошибочна.

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

Памятка: Когда после запуска исследуемого приложения под отладчиком при первом останове на функции LdrpSnapIAT пытаешься принудительно выйти на данную ветку кода, то ошибка не появляется! Могу предположить, что при первом вызове функции анализируется какая-то важная системная библиотека, поскольку из данных Procmon было видно, что первой загружается ntdll.dll, то можно предположить что так оно и есть. Соответственно, отказ в загрузке её самой (вероятно) не может быть завершен ошибкой, и загрузка тестового приложения lasterr.exe в этом случае просто тихо завершается не выводя никаких сообщений. Поэтому, я несколько раз выполнил команду g и начал анализировать функцию со второго и последующих её вызовов, что принесло видимые результаты.

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

Наименование функции Предполагаемая причина
LdrpResGetMappingSize Проверка кода NTSTATUS при возврате из функции RtlImageNtHeaderEx. Если он не нулевой (ошибочный), то проверяются первые два байта опционального заголовка на значение 10Bh (32-битный заголовок) и 20Bh (64-битный заголовок), если не соответствует ни одной из них - ошибка.
LdrpResGetResourceDirectory Код функции уходит на ошибку после нулевого результата выполнения вложенной функции LdrpSectionTableFromVirtualAddress, которая возвращает 0 в случае нулевого смещения PE-заголовка (в нормальной ситуации должно быть 00400080) и нулевого значения слова по смещению в заголовке = начало PE заголовка (00400080) + SizeOfOptionalHeaders + 18h, то есть первое слово Таблицы Секций (Section Table). Опять же, если мы получили Section Table, далее выполняется еще несколько проверок уже непосредственно над полями самой секции, и c000007b генерируется если некоторые поля (SizeOfRawData и прочие) в Section Table некорректны.
LdrpResCompareResourceNames Ошибка генерируется внутри функции по нескольким условиям, в основном это некорректные значения смещений и имен (длин имен) секций ресурсов.
LdrpResSearchResourceInsideDirectory Ошибка генерируется внутри функции по нескольким условиям, в основном это некорректные значения смещений и имен (длин имен) секций ресурсов.

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

Выводы

Ошибка c000007b может генерировать по нескольким условиям внутри кода загрузчика исполняемых образов операционной системы Windows, и не все из этих условий напрямую связаны с импортируемыми и экспортируемыми функциями подключаемых библиотек. Вот по этой то причине, программа Dependency Walker, которая часто предлагается как универсальное средство для устранения проблемы, не всегда дает положительный результат. Так же, утилита Process Monitor зачастую не видит всех деталей работы кода загрузчика, к тому же не может найти проблемы в ресурсных секциях и заголовках. Конечно, описанный инструментарий позволяет обнаруживать большинство причин возникновения ошибки c000007b, но не все. Рекомендованные утилиты могут быть использованы в качестве стартового инструментария, но при невозможности решить проблему с их помощью, единственным средством остается отладчик. С другой стороны, разработчики могли бы значительно облегчить определение причины ошибки c000007b путем расширения статусных кодов и введения подробного описания, однако это потребовало бы доработки загрузчика образов. Но поскольку подобная глобализация множества разнородных ошибок под одним статусным кодом в программах под Windows характерна еще со времен Windows 2000, думаю, что ситуация в ближайшее столетие не поменяется :)

  • Поделиться:

2 комментария:

  1. ProstoArtem

    Ачешуительный разбор! Так держать..

    • einaare

      не полный :) скорее всего упустил, как минимум, пару условий внутри RtlImageNtHeaderEx. позже доделаю.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *