20150920

Перезагрузка. Ассемблер. Часть 3. Знакомимся с отладчиком.

Оглавление:
----------
Часть 0. [Установка и настройка]
Часть 1. [Пишем первую программу]
Часть 2. [Как работает процессор и что такое регистры]

Часть 3. [Знакомимся с отладчиком]

------------------------------------
Весь исходный код можно взять [тут].
------------------------------------


Привет, блога моего читатели!

Сегодня у нас будет чуть более практическая тема статьи. Я уже немного рассказал о том, как работает процессор и что такое регистры. Если коротко подвести итог, то процессор - тупая считалка. Ему нужно сказать, где брать код программы (по какому адресу), а затем он начнет выполнять этот код построчно, команда за командой, буква за буквой. Сам код (программа) при этом загружен в оперативную память компьютера. Чтобы каждый раз не загружать программу с диска в память и не указывать глупому процессору, откуда начинать исполнять программу, нужна операционная система. А вы думали, что в игры играть? Не-а. Так вот. Раньше программы писали путем переключения выключателей (0 и 1), затем - выбивая дырочки на перфокартах (те же 0 и 1) и скармливая стопки этих перфокарт компьютеру, а потом произошло чудо чудное и придумался язык ассемблера - *мнемоническая* запись команд процессора в форме, понятной для человека. После этого стали появляться более крутые (высокоуровневые) языки вроде фортрана или алгола, си, паскаля и так далее. Чем дальше в лес - тем сильнее ощущалась нужда в возможности контролировать ход выполнения программы, потому что эти самые программы становились все больше и сложнее. С этой мыслью программисты того времени придумали инструмент под названием отладчик - специальную программу, позволяющую контролировать ход исполнения другой программы. Эдакий воспитатель в детском саду - тут играть можно, еще можно кашу, а Машу за косички подергать - нельзя, и песком кидаться в Витю тоже нельзя.

В рамках этой серии статей мы будем рассматривать мой любимый отладчик (на самом деле, они не очень сильно друг от друга отличаются - принцип работы у всех одинаковый) под названием Olly Debugger. Скачать его можно или на официальном [сайте] или где-нибудь еще в интернете. На момент написания этой статьи используемая мной версия - 2.01. Скачиваем архив, распаковываем куда-нибудь и - это важно! - запускаем с правами администратора. Вот оно, окошко:



Как и у практически любой другой программы Windows, у этой тоже есть меню. Помните программу из предыдущих статей, которая выводит на экран сообщение? Ищем ее в скомпилированном виде и открываем в отладчике через его меню (File -> Open):




Вот это да, как много всего! Давайте по часовой стрелки, начиная с верхнего-левого угла. Там у нас вот такое:

CPU Disasm
Address   Hex dump               Command                                  Comments
00401000  /$  6A 00              PUSH 0                                   ; /Type = MB_OK|MB_DEFBUTTON1|MB_APPLMODAL
00401002  |.  6A 00              PUSH 0                                   ; |Caption = NULL
00401004  |.  E8 0E000000        CALL 00401017                            ; |Text = "j", jump over immediate data
00401009  |.  48 65 6C 6C 6F 2C  ASCII "Hello, World!",0                  ; |ASCII "Hello, World!"
00401017  |>  6A 00              PUSH 0                                   ; |hOwner = NULL
00401019  |.  FF15 80204000      CALL DWORD PTR DS:[<&USER32.MessageBoxA> ; \USER32.MessageBoxA
0040101F  |.  6A 00              PUSH 0                                   ; /ExitCode = 0
00401021  \.  FF15 60204000      CALL DWORD PTR DS:[<&KERNEL32.ExitProces ; \KERNEL32.ExitProcess

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

Я разберу одну строчку, а дальше станет немного понятней:

00401000 6A 00 PUSH 0 ; /Type = MB_OK|MB_DEFBUTTON1|MB_APPLMODAL

Итак, 00401000 - это адрес в 16-ричной системе счисления, где команда находится в оперативной памяти. Ну, грубо говоря. Адрес этот очень нужно знать глупому процессору, потому что сам он никогда не поймет, откуда ему нужно начинать выполнять код.

6A 00 - это то, как команда выглядит в исполняемом файле, в уже скомпилированном виде. Человеку такое запоминать трудновато, так что для него придумали вот это:

PUSH 0

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

Так, вроде немного разобрались. Переходим к следующему окну, которое верхнее-правое. В нем представлены регистры процессора и то, что в них сейчас находится. Еще там же представлены флаги - они показывают, что что-нибудь 0 или 1, например - если в результате последнего сравнения получилась истина. Или если при делении получился ноль. Специальные такие штуки.

Ниже, под окном команд, находится окно дампа. Это - данные программы. Если сверху - код, то снизу - то, с чем этот код работает. Цифры, буквы, строки текста, картинки и все такое прочее. Все это лежит в оперативной памяти.

Снизу-справа - самая страшная штука, про которую я еще не писал толком. Называется она стек (англ. stack). Это такая область в процессоре, где могут храниться данные. Обычно - промежуточные по ходу работы программы, тогда как более "долгоиграющие" лежат в оперативной памяти. При работе со стеком нужно запомнить порядок, как в него попадают данные и как они из него выходят обратно. Все помнят вот такую офигенскую штуку из детского сада?




Дык вот. У нас есть вершина стека (кстати, есть даже специальный регистр процессора, который постоянно указывает на вершину стека - ESP). Мы можем положить данные на вершину и снять с вершины. Только так. Смотрим на картинку с игрушкой. Чтобы снять нижнее, самое большое кольцо с палки, нужно сначала снять все, что выше. Но при этом нижнее колько на палку попало первым. Вот тут так же. Есть две основные команды для работы со стеком - PUSH и POP. Первая кладет, вторая вытаскивает. Примерно так:

PUSH 0 (|-> 0 )
PUSH 1 (|-> 1 0)
PUSH 2 (|-> 2 1 0)
POP EAX (|-> 1 0, в EAX при этом теперь лежит 2)
PUSH EAX (|-> 2 1 0, да, в стек можно засунуть содержимое регистра)
PUSH 25 (|-> 25 2 1 0)
POP EBX (|-> 2 1 0, EBX=25)
POP ECX (|-> 1 0, ECX=2)

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

invoke MessageBox,HWND_DESKTOP,"Hello, World!",0,MB_OK

invoke - это специальный макрос, позволяющий вот так просто, в одну строчку, вызывать функцию и сообщить нужные аргументы.
MessageBox - имя (адрес!) функции.
Все, что дальше идет - это аргументы.

В отладчике у нас нет никаких макросов и прочих удобств, там все сурово:

PUSH 0 ; Это MB_OK
PUSH 0 ; Это указатель на текстовую строку-заголовок, ее у нас нет.
PUSH 123321 ; Это указатель на текстовую строку-сообщение.
PUSH 0 ; Это HWND_DESKTOP, идентификатор окна-владельца сообщения
CALL MessageBoxA ; Это сам вызов функции

Как видите, в суровом варианте аргументы кладутся на стек по одному, причем в обратном порядке, а не так, как мы записываем в случае с invoke. Invoke при компиляции делает ровно одно - "разворачивается" в набор push и call. Итак, чтобы вызвать функцию, нужно положить на стек аргументы (в обратном порядке!), если они есть, а потом вызывать функцию через call.

Такие дела. Посмотрим, как это выглядит вживую? Конечно, посмотрим! Сейчас наша программа "поставлена на паузу" и отладчик ждет каких-то дальнейших указаний. Мы можем "проиграть" программу по шагам и посмотреть, что с ней происходит. Для этого у нас есть замечательная кнопка F8, позволяющая сделать один "шажок" (выполнить одну инструкцию), при этом "перепрыгивая" вызовы других процедур (call, например). Иначе отладчик "провалится" внутрь этой процедуры и начнет показывать код, который там внутри. Итак, видим команду:

PUSH 0

Что она делает? Она кладет 0 на вершину стека. Смотрим на стек - в самой верхней строчке лежит какая-то непонятная фигня. Давим F8! Получаем вот такую картину:




Процессор проехал на одну команду вниз, выполнив предыдущую, а на вершине стека оказался ноль. Круто? Еще как круто! Давим F8 еще пару раз, выскакивает наше сообщение, жмем в нем OK и на этом программа завершается, о чем нам и сообщает отладчик в строке состояния (Process terminated, exit code 0). Кстати, код возврата мы задаем при вызове функции ExitProcess, единственным ее аргументом. Так-то!

Пока что - на этом все. Можно выбрать в меню Debug->Restart, а можно найти какую-нибудь другую программу и попробовать посмотреть в отладчике на нее. В следующей статье я покажу, как можно при помощи отладчика исправить самый настоящий БАГ. :)

----------------------------------------
Вопросы? Пожелания? Предложения?
Вот как можно со мной связаться:
[email]
[vk]
[twitter]
[telegram]

----------------------------------------