Дата публикации статьи: 29.07.2006 14:24

Разъяснение промежуточного языка Microsoft (Microsoft Intermediate Language). Часть 3 – Отладка

Автор: Kamran Qamar
[Оригинал статьи] [Обсудить в форуме]
Перевод с английского: Виталий Готовцов
WWW: http://www.vitgot.narod.ru

 

    Кто может утверждать, что написал программу без ошибок с первого раза? Человеческие существа склонны к ошибкам и не имеет значения, насколько мы умны и насколько великими программистами мы являемся, мы всегда пишем код с ошибками, неумышленно, конечно J. Эта ошибка может быть такой же простой, как синтаксическая ошибка, или сложной, как логическая ошибка. В любом случае нам нужен способ отладки нашей программы. Эта необходимость возрастает, если вы пишете программу на языке низкого уровня, таком как промежуточный язык, который трудно отлаживать.
    Вы можете отлаживать вашу программу, вставляя множество операторов WriteLine, но этот метод очень утомителен для MSIL-программы. Вам пришлось бы писать не одну строку, а три, вроде этих:

ldstr      "Hello World"
call       void [mscorlib]System.Console::WriteLine(string)
ret

    Представьте себе практическую ситуацию, в которой вам нужно вставить множество строк кода для отладки. Конечно, это скучно, но необходимо. К счастью, .NET SDK приходит с двумя инструментами для отладки IL, а вернее, любой .NET-сборки. В этой статье мы исследуем мощь этих инструментов. Это понимание отладки IL-кода поможет нам в будущем, когда мы будем писать более сложные приложения с помощью MSIL.

Отладочные инструменты

    У .NET SDK есть две великих отладочных утилиты в ящике с инструментами, это:
1. Отладчик Microsoft CLR (Microsoft CLR Debugger (DbgCLR.exe))
Предлагает отладочные службы с графическим интерфейсом, чтобы помочь разработчикам приложений найти и исправить ошибки в программах, которые запускаются в реальном времени.
2. Отладчик исполнения (Runtime Debugger (Cordbg.exe))
    Обеспечивает отладочные службы командной строки с помощью стандартной среды выполнения Debug API. Ее используют для нахождения и исправления ошибок в запущенных программах.
    На первый взгляд кажется, что обе утилиты решают одну и ту же проблему, а именно, находят и исправляют ошибки в программах, запущенных в среде исполнения .NET. Однако они немного отличаются своими функциями. DbgCLR.exe является оконным приложением, которое обеспечивает вас визуальным интерфейсом и легко определяемыми точками останова и наблюдения, в то время, как Cordbg.exe является инструментом командной строки, которая позволяет вам создавать отладочные сценарии. В этой статье я сконцентрирую внимание на оконных приложениях, т.е. DbgCLR.exe.

Отладчик Microsoft CLR (DbgCLR.exe)

    Отладчик Microsoft CLR (DbgCLR.exe) обеспечивает отладочные службы с графическим интерфейсом, чтобы помочь разработчикам приложений найти и исправить ошибки в программах в стандартной среде исполнения.
    Для того, чтобы понять возможности Microsoft CLR Debugger, нам нужна программа с ошибками. К сожалению, мы не настолько опытны, чтобы написать сами такую программу в MSIL. Чтобы сделать все это ясным и более понятным, я напишу программу с ошибкой на C#. Затем мы получим ее MSIL-эквивалент с помощью ildasm.exe.

Программа с ошибкой на C#

    Откройте ваш любимый редактор кода и вставьте следующую C#-программу. Назовите файл ErrorneousApp.exe

using System;
namespace ErrorneousApp
{
    class ErrorneousClass
    {
        [STAThread]
        static void Main(string[] args)
        {
            int operand1;
            int operand2;
            int sum;

            operand1 = int.Parse(args[0]);
            operand2 = int.Parse(args[1]);

            sum = Add(operand1 , operand2);

            Console.WriteLine(sum);
        }

        private static int Add(int op1, int op2)
        {
            // obvious error, I am multiplying by 5 along with adding 
            // two numbers :)
            return 5 * (op1 + op2);
        }
    }
}

    Вы можете заметить, что в этой маленькой исполнимой программе я потенциально могу совершить два вида ошибок:
1. Логическая ошибка – функция Add возвращает неправильные результаты.
2. Функциональная ошибка – если я не передам два аргумента, как ожидает функция, то она обрушится.
    Кстати, не относитесь к этому, как к единственному методу отладки. Отладчик Microsoft CLR способен отлаживать ошибки любого рода. Сейчас нам нужен код, который мы можем компилировать, а потом вернуть его MSIL-код с помощью ildasm.exe. Так что давайте двинемся вперед и скомпилируем код с помощью csc.exe. Следующее, что нам нужно – это MSIL-код, я запущу ildasm.exe, чтобы извлечь этот код, и вы уже знаете, как это сделать. Поэтому я не буду углубляться в детали. Вот MSIL-код этого приложения.

.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89)  .ver 1:0:3300:0
}
.assembly ErrorneousApp
{
  .ver 1:0:1026:17140
}
.module ConsoleApplication1.exe
.namespace ErrorneousApp
{
  .class private auto ansi beforefieldinit ErrorneousClass
         extends [mscorlib]System.Object
  {
    .method private hidebysig static void 
            Main(string[] args) cil managed
    {
      .entrypoint
      .locals init ([0] int32 operand1, [1] int32 operand2, [2] int32 sum)
        ldarg.0
        ldc.i4.0
        ldelem.ref
        call       int32 [mscorlib]System.Int32::Parse(string)
        stloc.0
        ldarg.0
        ldc.i4.1
        ldelem.ref
        call       int32 [mscorlib]System.Int32::Parse(string)
        stloc.1
        ldc.i4.5
        ldloc.0
        ldloc.1
        add
        mul
        stloc.2
        ldloc.2
        call       void [mscorlib]System.Console::WriteLine(int32)
        ret
    } // end of method ErrorneousClass::Main

    .method private hidebysig static int32 
            Add(int32 op1, int32 op2) cil managed
    {
      .locals init ([0] int32 CS$00000003$00000000)
        ldc.i4.5
        ldarg.0
        ldarg.1
        add
        mul
        stloc.0
        ldloc.0
        ret
    } // end of method ErrorneousClass::Add

    .method public hidebysig specialname rtspecialname 
            instance void  .ctor() cil managed
    {
        ldarg.0
        call       instance void [mscorlib]System.Object::.ctor()
        ret
    } // end of method ErrorneousClass::.ctor
  } // end of class ErrorneousClass
} // end of namespace ErrorneousApp

    Я закомментировал весь ненужный код и комментарии, генерированные ildasm.exe.

Объяснение MSIL-кода

    Прежде, чем мы начнем использовать этот код, позвольте объяснить некоторые его части. Мы пользуемся функциями Console.WriteLine и Int.Parse, определенными во внешней сборке mscorlib, поэтому нам нужно создать ссылку на нее. Это мы делаем с помощью директивы assembly с атрибутом external. Далее, мы назвали нашу сборку ErrorneousApp, снова используя директиву assembly.
    Далее, мы определяем наш класс, как ErrorneousClass, с помощью директивы class. Затем мы создали статическую функцию, названную Main, с помощью директивы method, мы также установили этот метод, как метод entrypoint в приложении. И мы инициализировали три из наших локальных полей с помощью директивы local:

.locals init ([0] int32 operand1, [1] int32 operand2, [2] int32 sum)

    Теперь нам нужно присвоить значения этим трем локальным полям. Мы должны извлечь значения из массива аргументов, это делается с помощью ldelm, который обозначает загрузить элемент массива (load an element of array). Однако чтобы указать CLR, какой аргумент соответствует какому локальному полю, нам нужно обеспечить, чтобы элемент массива ссылался на число, которое получается при помещении аргументов в массив с помощью кода ldarg. (Инструкция ldarg num помещает входящий аргумент с номером num туда, где аргументы нумеруются, начиная с 0, в стек значений.) Команда ldc помещает фактический номер в стек. ldc.i4.0 означает помещение нулевого значения с типом int32 в стек. Затем мы вызываем метод System.Int32.Parse, чтобы преобразовать строковые значения в integer. После того, как оба локальных поля инициализированы, мы вызываем локальный метод Add, чтобы сложить operand1 и operand2. И, наконец, мы отображаем результат с помощью метода System.Console.WriteLine.
    Выполнение статического метода Add является совершенно простым. Вам нужно помнить, что значение поля, которое мы собираемся использовать в конце, должно быть помещено первым, потому что MSIL работает с моделью памяти, основанной на стеке. Так как мы намереваемся сложить два числа, а затем умножить их на 5, мы должны сначала загрузить 5 в стек с помощью команды ldc.i4.5. Затем мы загружаем два операнда, складываем их с помощью команды add, которая автоматически вытаскивает последние два значения из стека, складывает их и помещает результат обратно. И, наконец, мы используем команду mul, чтобы перемножить два операнда.
Заметьте, в функции Main мы не имеем прямого вызова статической функции Add, это происходит, потому что компилятор C# заменяет вызов статической функции встроенным кодом этой функции – интересное открытие!
    Я не надеюсь, что вы понимаете весь код до этого места. Помните, что наша цель – понять, как отлаживать IL-код и особенно, как работает отладчик Microsoft CLR. В дальнейших статьях я проведу вас, шаг за шагом, в написание IL-программ.
    Позвольте еще раз подчеркнуть, что наша цель состоит в том, чтобы понять, как проводить отладку MSIL-кода, но так как я не еще показал вам, как писать MSIL-приложения, мне пришлось вернуться и использовать ildasm.exe, чтобы создать одно рабочее, но содержащее ошибку, приложение. Я использовал этот IL-код для исследования функциональных возможностей Microsoft CLR Debugger и, в конце концов, исправить это приложение.

Отладка MSIL-кода

    Для отладки MSIL-кода нам понадобится файл с отладочной информацией – ErrorneousApp.pdb. Файл PDB (program database (база данных программы)) содержит отладочную информацию и информацию о состоянии проекта, что позволяет проводить компоновку «с приращением» конфигурации Отладчика вашей программы. Мы можем получить этот файл, компилируя исходный код файла Errorneous.il, используя ildasm.exe с переключателем debug.

 ilasm errorneousApp.il /debug

    При этом для нас будет создан файл ErrorneousApp.pdb. Теперь запустим DbrClr.exe и откроем ErrorneousApp.il с помощью меню File | Open. Вы получите такое решение:

    Следующим шагом будет установка программы для запуска. Это вы можете сделать с помощью опции Debug | Program To Debug главного меню. Это вызовет следующее окно:

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

Выполнение прерываний

    Главная задача Отладчика – отобразить информацию о состоянии отлаживаемой программы. Для контроля и изменения состояния программы существуют разные инструменты. Большая часть инструментов функционируют только в режиме останова. Отладчик останавливает выполнение программы, когда выполнение достигает точки останова или когда происходит исключение.
    Мы можем установить точки останова двумя способами:
1. Щелчок на правом поле любой исполняемой строки кода обозначает эту строку, как точку останова.
2. С помощью опции меню Debug | New Breakpoint.

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

  • Установка названия функции, на которой выполнение должно остановиться
  • Установка строки и номера символа в определенном файле, или
  • Установка определенного адреса

    Помните, контрольные точки данных не поддерживаются для отладки IL-кода. Такой подход, доступный нам, сужает условия остановки определенными условиями и количеством точек останова. При достижении точки останова выражение получает отметку о том, что условие выполнено, если значение выражения или равно true или «изменилось», выполнение останавливается. Каждый раз, когда выполнение останавливается, это называется щелчком и отладчик для себя ведет учет таких щелчков. Вы можете настроить условия остановок, основываясь на значении этого счетчика, например, вы можете настроить, чтобы остановка происходила только тогда, когда количество щелчков кратно пяти.

    В этом примере мы можем установить точки останова только для Адресов и Файлов. Отладчик Visual Studio обеспечивает разнообразие инструментов для наблюдения и изменения состояния нашей программы. Большая часть инструментов работают только в режиме останова. Когда мы достигаем остановленного состояния с помощью одного из методов, определенных выше, мы можем пользоваться этими инструментами. Позвольте мне рассказать о них, один за другим.

Окно Locals (Locals Window)

    Окно Locals отображает переменные, локальные в данном контексте. Вы можете всегда изменить контекст, переключаясь в новый контекст из окна Call Stack, Thread или Debug Location.

    Большим преимуществом этого инструмента является то, что вы можете менять значение локальной переменной или выражения «на лету». Просто совершите двойной щелчок на значении или поле, которое хотите изменить и замените его новым значением.

Быстрый просмотр (QuickWatch)

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

    Я также могу использовать то же окно, чтобы изменить значение Operand1. Я даже могу изменить значение реестра. QuickWatch является модальным диалоговым окном. Вы не можете оставлять его открытым, если хотите посмотреть переменную или выражение во время прохождения внутри вашей программы. Чтобы сделать это, вы можете добавить переменную или выражение в окно Watch.

Окно Наблюдения (Watch Window)

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

    Диалоговое окно QuickWatch обеспечивает более быстрый, более простой способ получения и редактирования простых переменных или выражений. В то время как Окно Watch обеспечивает совместный просмотр всех наблюдаемых значений.

Окно Журналов (Registers Window)

    Окно Registers отображает значения реестра. Если вы оставляете окно Registers открытым во время прохождения в вашей программе, вы можете видеть изменения значений реестра при выполнении вашего кода. Значения, которые изменились недавно, становятся красного цвета.

Окно стека вызовов (Call Stack Windows)

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

Окно Дизассемблера (Disassembly Window)

    Окно Disassembly показывает код сборки, соответствующий инструкции, созданной компилятором. Если вы отлаживаете управляемый код, эти инструкции сборки соответствуют нативному коду, созданному JIT-компилятором, а не промежуточному языку, созданному компилятору Visual Studio.

Окно Памяти (Memory Window)

    Вы можете использовать окно Memory для просмотра больших буферов, строк и других данных, которые не могут хорошо отображаться в окнах Watch или Variables. Если вы хотите быстро переместиться в выбранный участок памяти, вы можете сделать это перетаскивая мышью или изменяя значение в окне Address. Окно Address допускает выражения и числовые значения. По умолчанию, окно Memory рассматривает выражение Address, как «живое» выражение, которое определяется заново при каждом выполнении вашей программы.

Запуск с отладкой…

    Теперь, когда мы вооружены отладчиком Microsoft CLR, мы легко можем исправить наше приложение. Давайте запустим его в режиме отладки и будем наблюдать за локальными переменными. Во время движения шаг за шагом мы замечаем, как значение суммы меняется от нуля до 15. Мы также заметим, что у нас есть оператор умножения в дополнение к оператору сложения. Мы можем прервать исполнение этого оператора, чтобы решить проблему. Вот измененный код:

.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89)  .ver 1:0:3300:0
}
.assembly ErrorneousApp
{
  .ver 1:0:1026:17140
}
.module ConsoleApplication1.exe
.namespace ErrorneousApp
{
  .class private auto ansi beforefieldinit ErrorneousClass
         extends [mscorlib]System.Object
  {
    .method private hidebysig static void 
            Main(string[] args) cil managed
    {
      .entrypoint
      .locals init ([0] int32 operand1, [1] int32 operand2, [2] int32 sum)
        ldarg.0
        ldc.i4.0
        ldelem.ref
        call       int32 [mscorlib]System.Int32::Parse(string)
        stloc.0
        ldarg.0
        ldc.i4.1
        ldelem.ref
        call       int32 [mscorlib]System.Int32::Parse(string)
        stloc.1
        //ldc.i4.5
        ldloc.0
        ldloc.1
        add
        //mul
        stloc.2
        ldloc.2
        call       void [mscorlib]System.Console::WriteLine(int32)
        ret
    } // end of method ErrorneousClass::Main

    .method public hidebysig specialname rtspecialname 
            instance void  .ctor() cil managed
    {
        ldarg.0
        call       instance void [mscorlib]System.Object::.ctor()
        ret
    } // end of method ErrorneousClass::.ctor
  } // end of class ErrorneousClass
} // end of namespace ErrorneousApp
Отладка файла библиотеки

    Мы не можем открыть и выполнить шаг за шагом файл библиотеки в отладчике CLR. Чтобы отладить файл библиотеки вам понадобится контрольное исполняемое приложение, которое ссылается на эту библиотеку и использует ее классы и методы. Вам также будут нужны файлы базы данных программ (.pdb) и для контрольного приложения, и для файла библиотеки. Тогда и только тогда вы сможете открыть файл контрольного приложения в отладчике CLR и он автоматически проведет вас в программные строки внешней библиотеки, которую вам в действительности нужно отладить.

Применение техник отладки

    Надеюсь, у вас уже есть сборка, которая замечательно работает и выводит результат в консоль. Для простоты давайте предположим, что эта сборка (FancyGreetings.dll) имеет свойство, называемое Name, которое принимает имя пользователя и выводит в консоли приветствие, такое как это:

Welcome Mr. Name

    Вы легко заметите проблему на выходе. Результат предполагает, что имя принадлежит мужчине. Дело не в вашем приложении. У вас нет доступа к исходному коду, и вы не можете ждать выхода нового релиза. Вы можете использовать мощь ildasm.exe вместе с отладчиком, чтобы исправить эту ошибку.
    Вы можете создать код MSIL с помощью ildasm.exe, изменить метод Name и снова компилировать его с помощью ildasm.exe. Если бы эта сборка FancyGreetings.dll была большой и сложной, вы не были бы в состоянии понять, где в действительности нужно исправлять ошибку. В этом сценарии полезным становится DbgClr.exe. Эта утилита позволит вам пройти сквозь код и найти проблемную область. Помните, отладчику нужна база данных программы (.pdb), чтобы запуститься, и вы можете генерировать ее с помощью переключателя debug.
    Этот пример подводит нас к одной очень важной теме, а именно к инженерному анализу и защите авторских прав. MSIL не только обеспечивает общую основу кода, он также открывает дверь обратному проектированию. К счастью, на рынке есть доступные обфускаторы, которые, в некоторой степени, скрывают код, или, по крайней мере, делают инженерный анализ весьма трудным (Как? Это выходит за пределы темы данной статьи). Большинство продуктов, которые вы покупаете, находится под действием некоторого лицензионного соглашения, которое обычно запрещает вам любой вид обратного проектирования.