понедельник, 11 мая 2009 г.

Практика ассемблирования

От теоретического материала я уже немного устал. Решил попрактиковаться, а заодно и поэкспериментировать с сегментами. Для экспериментов выбрал программку из книги Феногенова, и чуток изменил ее (текст можно взять тут):

Она ассемблируется и TASM и MASM.

Ассемблеры, в процессе трансляции, позволяют создавать, так называемые, файлы листингов. Файл листинга содержит не только исходный текст, но и транслированный машинный код в шестнадцатеричном формате. Файлы листингов TASM и MASM немного различаются. Листинги TASM содержат еще и номера строк в самое левой колонке, после которой уже идут шестнадцатеричные адреса смещения команд и данных (в MASM колонка адресов первая) относительно начала сегмента. Затем показан ассемблированный машинный код в шестнадцатеричном формате и затем соответствующие ему команды ассемблера.
Итак, к практике! Для каждой программы я создаю отдельную папку и в ней две подпапки MASM и TASM. Чтобы ассемблировать программу я захожу в каждую из них FAR-ом и запускаю соответственно m.bat или t.bat (расширение .bat можно не указывать, просто m или t). И из открывшегося окна уже ассемблирую программу. Ассемблируем hello.asm TASM-ом с ключом получения листинга (/l). Вводим последовательно команды: tasm /l hello и затем tlink hello. Обратите внимание что для TASM расширение .asm указывать не обязательно. Смотрим скриншот:

В директории, помимо объектного файла и исполняемого, образовался файл листинга - hello.lst, и файл карты данных - hello.map. Смотрим hello.lst.

Команды имеют различную длину и располагаются в памяти вплотную друг к другу. Так, первая команда mov ax,data, начинается со смещения 0000 сегмента кода и занимает 3 байта. Соответственно вторая команда начинается со смещения 0003. Вторая команда имеет длину 2 байта, поэтому третья команда начинается с байта 0005 и т.д.
Предложения программы с директивами segment, assume, end не транслируются в какие-либо машинные коды (как это уже говорилось) и не находят отражения в памяти. Они нужны лишь для передачи транслятору служебной информации, управляющей ходом трансляции.
Транслятор не смог полностью определить код команды mov ax,data. В этой команде в AX засылается адрес сегмента data. Однако этот адрес станет известен лишь в процессе загрузки исполняемого файла программы в память. Поэтому в листинге на месте этого адреса стоят нули. Символ s указывает на то, что в дальнейшем вместо нулей сюда будет подставлен сегментный адрес.
Еще одна помеченная команда с кодом ВА 0000 располагается в строке 6 листинга. В этой команде в регистр DX заносится смещение поля с именем msg, расположенное в сегменте данных (ключевое слово offset, указанное перед именем поля, говорит о том, что речь идет не о содержимом ячейки msg, а об ее смещении). Поле msg расположено в самом начале сегмента данных, и его смещение от начала сегмента равно 0, что и указано в коде команды. Почему же эта команда помечена буквой r, являющейся сокращением слова relocatable - перемещаемый?
Чтобы ответить на этот вопрос, нам придется рассмотреть, как сегменты программы размещаются в памяти. Как уже говорилось, любой сегмент может располагаться в памяти только с адреса, кратного 16, т.е. на границе 16-байтового блока памяти (параграфа). Конкретный адрес программы в памяти зависит от конфигурации компьютера, - какой размер занимает DOS, сколько загружено резидентных программ и драйверов, а также в каком режиме запускается программа - в отладчике или без него. Предположим, что сегментный адрес сегмента команд оказался равным 1306h (а). В нашей программе сегмент команд имеет размер 11h байт (что указано в строке 10 и в разделе Groups & Segments листинга), т.е. занимает целый параграф плюс один байт. Сегмент данных имеет размер 14h байт (строка 15 листинга и раздел Groups & Segments) и тоже требует для своего размещения немного больше одного параграфа. Из-за того, что сегмент данных должен начаться на границе параграфа, ему будет назначен сегментный адрес 1308h и между сегментами образуется пустой промежуток размером 15 байт. По той же причине между сегментом данных и стека образуется пустой промежуток в 12 байт.
Потеря 27 байт из многомегабайтовой памяти, разумеется, не имеет никакого значения. Однако в некоторых случаях, например, при компоновке единой программы из большого количества модулей с небольшими по размеру подпрограммами, суммарная потеря памяти может оказаться значительной.

Для того, чтобы устранить потери памяти, можно сегмент данных объявить с выравниванием на байт:

data segment byte

Такое объявление даст возможность системе загрузить сегмент данных так, как показано на рис. (б). Сегмент данных частично перекрывает сегмент команд, начинаясь на границе его последнего параграфа (в нашем случае по адресу 1307h). Для того, чтобы данные не наложились на последние команды сегмента команд, они смещаются вниз так, что начинаются сразу же за сегментом команд. В нашем примере, где сегмент команд "выступает" за сегментный адрес 1307h всего на 1 байт, данные и надо сместить на этот 1 байт. В результате поле msg, с которого начинается сегмент данных, и которое в листинге имело смещение 0, получит смещение 1. Все остальные адреса в сегменте данных также сместятся на один байт вперед. В результате данные будут располагаться в физической памяти вплотную за командами, без всяких промежутков, однако все обращения в сегменте команд к данным должны быть скорректированы на величину перекрытия сегментов, в нашем случае - на 1 байт. Эта коррекция выполняется системой после загрузки программы в память, но еще до ее запуска. Адреса, которые могут потребовать описанной коррекции, и помечаются в листинге трансляции буквой "r".Из сказанного следует очень важный и несколько неожиданный вывод: коды команд программы в памяти могут не совпадать с кодами, показанными в листинге трансляции. Это обстоятельство необходимо учитывать при отладке программ с помощью интерактивного отладчика, который, естественно, показывает в точности то, что находится в памяти, и что не всегда соответствует листингу трансляции.
Вернемся к рассмотрению листинга трансляции. Данные, введенные нами в программу, также оттранслировались: вместо символов текста в загрузочный файл попадут коды ASCII этих символов.
При выводе этих кодов на экран видеосистема компьютера преобразует их назад в изображения символов, записанных в исходном тексте программы.
Из раздела листинга Groups & Segments можно увидеть размер сегментов программы в байтах в шестнадцатеричном представлении. Например размер сегмента data равен 0014h байтам, что соответствует десятичным 20 байтам (количество символов между апострофами). Под стек мы выделяли 256 байт – 0100h. Размер сегмента команд равен 0011h байт, что в десятичном равно 17 байт.
Размер же всей программы окажется больше суммы длин сегментов, во-первых, из-за пустых промежутков между сегментами (у нас на них уйдет 15 + 12 = 27 байт), и, во-вторых, за счет подсоединения к EXE программе обязательного заголовка , имеющего минимальный размер 512 байт (он может быть и больше, если программа содержит большое количество настраиваемых элементов). Итого считаем:
17 байт – сегмент кода
20 байт – сегмент данных
256 байт – сегмент стека
27 байт - "пустые" байты между сегментами
512 байт - заголовок программы
Итого 832 байта

что и видим в выводе команды DIR:

Теперь посмотрим на исполняемый файл hello.exe при помощи встроенного просмотровщика FAR.

На скриншоте красной рамкой выделен сегмент кода нашей программы, желтой рамкой сегмент данных, зелеными цифрами на синем фоне подсвечен стек (заполнение его, для лучшей визуализации, символом * определено в 16 строке программы), пустые промежутки подсвечены зелеными цифрами на черном фоне. Очень полезно, однако, смотреть и анализировать полученные EXE файлы в hex редакторах или просмотровщиках (viewers).
С форматом заголовка EXE файла решил разобраться чуть позже, так как сейчас надо получше разобраться с сегментами и вообще с правилами написания программ.
Решил посмотреть работу программы в отладчике, ибо внимательное созерцание работы программы под отладчиком просветляет. Запускаем debug hello.exe. Смотрим состояние регистров и памяти и пытаемся с этим разобраться.
После загрузки программы в отладчик были последовательно даны команды:
r (просмотр состояния регистров и команды которая должны быть выполнена), d cs:0 (просмотр сегмента кодов с его начала), d ds:0 (просмотр сегмента данных с его начала). Смотрим скриншот:

Команда отладчика d (dump), по умолчанию, выводит 8 параграфов памяти. Поэтому стек, который мы заполнили звездочками, виден не весь.
Итак, что же мы видим. В дампе сразу же бросаются в глаза пустые промежутки между сегментом кода и данных (15 байт), а так же, между сегментом данных и стека (12 байт). Это же мы видели и при просмотре программы hello.exe в hex просмотровщике. В общем практика подтверждает теорию.
Так же мы видим в выводе команды d cs:0 и код нашей программу, и наши данные, и часть стека. Но в выводе команды d ds:0 данных, почему-то, не видать. К чему бы это? Давайте разбираться...
Смотрим на содержимое регистров CS, DS, ES. Регистры DS и ES получили одинаковое значение – 143B. Регистр CS получил значение 144B. Попытаемся понять, почему так. Вспомним, что в конце каждого сегментного адреса подразумевается 0(ноль ;) ). Таким образом, получаем адреса 143B0 и 144B0. От сюда можно видеть что адрес, хранящийся в регистрах DS и ES, стоит выше, то есть находится перед адресом, хранящимся в CS. И разница между адресами составляет 256 байт (144B0h-1443B0h=100h). То есть регистры DS и ES указывают выше регистра кодов – CS, на 256 байт. А сегмент данных, как мы видим и как мы его определили в тексте программы, идет за сегментом кода. Естественно, что в выводе команды d ds:0 (на данном этапе) мы не видим наших данных. А что же мы видим? А видим мы PSPProgram Segment Prefix. Теперь попробуем разобраться от куда он взялся.
При загрузке программы сегменты размещаются в памяти, как показано на рисунке.

Образ программы в памяти начинается с сегмента префикса программы (Program Segment Prefics, PSP), образуемого и заполняемого системой. PSP всегда имеет размер 256 байт; он содержит таблицы и поля данных, используемые системой в процессе выполнения программы. Вслед за PSP располагаются сегменты программы в том порядке, как они объявлены в программе. Сегментные регистры автоматически инициализируются следующим образом: ES и DS указывают на начало PSP (что дает возможность, сохранив их содержимое, обращаться затем в программе к PSP), CS - на начало сегмента команд, a SS - на начало сегмента стека. В указатель команд IP загружается относительный адрес точки входа в программу (из операнда директивы end), а в указатель стека SP - величина, равная объявленному размеру стека, в результате чего указатель стека указывает на конец стека (точнее, на первое слово за его пределами).
Таким образом, после загрузки программы в память адресуемыми оказываются все сегменты, кроме сегмента данных. Инициализация регистра DS в первых двух строках программы позволяет сделать адресуемым и этот сегмент.

Итак, первой исполняемой командой по адресу 144B мы видим:

144B:0000 B84D14 MOV AX,144D

Посмотрим внимательней на адресок 144D, который заносится в регистр AX. По идее это должен быть адрес сегмента данных, так как в тексте нашей программы этой строке соответствует команда mov ax,data. Код нашей программы (с учетом выравнивания на границу параграфа, 17 байт кода + 15 пустых байт) занимает 32 байта (20h байт), то есть два параграфа. Сегмент данных мы определили в нашей программе сразу за сегментом кода, то есть сегмент данных начинается через 20h байт от начала программы. Адрес в CS у нас равен 144B0 (не забываем про нолик). Прибавим 20h к 144B0h и получаем 144D0h. 144B0h+20h=144D0h. В листинге трансляции эта же команда определена как B8 0000s. Вспоминаем, что говорилось про буковку s. Этот адрес подставляется ОС в процессе загрузки программы в память. Кстати стоит еще заметить как выглядит код этой команды уже в памяти при исполнении - B84D14. Как видим B8 осталась, а адрес записан сюда ОС с изменением очередности байт. Выполняем эту команду и смотрим состояние регистров. Затем выполняем вторую команду и смотрим состояние регистров и дамп сегмента кодов.

Обращаем так же внимание на изменения в регистрах AX,DS и IP.
Продолжаем выполнение программы и когда доходим до выполнения команды INT 21. Вместо команды t, даем команду g 10D для вывода сообщения программы. Иначе при выполнении команды t отладчик перейдет в BIOS.

Еще стоит обратить внимание на изменение в регистре AX, после команды mov ah,09. Изменился только старший байт, который принял занчение 09, а младший байт сохранил свое занчение 4D. А так же на то, что в регистр DX было помещено смещение (0000) от начала сегмента данных (в нашем случае выводимой на экран фразы).
Полный дам выполнения программы приведен ниже:

-r
AX=0000 BX=0000 CX=0140 DX=0000 SP=0100 BP=0000 SI=0000 DI=0000
DS=143B ES=143B SS=144F CS=144B IP=0000 NV UP EI PL NZ NA PO NC
144B:0000 B84D14 MOV AX,144D
-d cs:0
144B:0000 B8 4D 14 8E D8 B4 09 BA-00 00 CD 21 B8 00 4C CD .M.........!..L.
144B:0010 21 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 !...............
144B:0020 2D 3D 2A 20 48 65 6C 6C-6F 20 57 6F 72 6C 64 20 -=* Hello World
144B:0030 2A 3D 2D 24 00 00 00 00-00 00 00 00 00 00 00 00 *=-$............
144B:0040 2A 2A 2A 2A 2A 2A 2A 2A-2A 2A 2A 2A 2A 2A 2A 2A ****************
144B:0050 2A 2A 2A 2A 2A 2A 2A 2A-2A 2A 2A 2A 2A 2A 2A 2A ****************
144B:0060 2A 2A 2A 2A 2A 2A 2A 2A-2A 2A 2A 2A 2A 2A 2A 2A ****************
144B:0070 2A 2A 2A 2A 2A 2A 2A 2A-2A 2A 2A 2A 2A 2A 2A 2A ****************
-d ds:0
143B:0000 CD 20 FF 9F 00 9A F0 FE-1D F0 4F 03 34 0E 8A 03 . ........O.4...
143B:0010 34 0E 17 03 34 0E 23 0E-01 01 01 00 02 FF FF FF 4...4.#.........
143B:0020 FF FF FF FF FF FF FF FF-FF FF FF FF F8 13 4C 01 ..............L.
143B:0030 0B 13 14 00 18 00 3B 14-FF FF FF FF 00 00 00 00 ......;.........
143B:0040 05 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
143B:0050 CD 21 CB 00 00 00 00 00-00 00 00 00 00 20 20 20 .!...........
143B:0060 20 20 20 20 20 20 20 20-00 00 00 00 00 20 20 20 .....
143B:0070 20 20 20 20 20 20 20 20-00 00 00 00 00 00 00 00 ........
-t

AX=144D BX=0000 CX=0140 DX=0000 SP=0100 BP=0000 SI=0000 DI=0000
DS=143B ES=143B SS=144F CS=144B IP=0003 NV UP EI PL NZ NA PO NC
144B:0003 8ED8 MOV DS,AX
-t

AX=144D BX=0000 CX=0140 DX=0000 SP=0100 BP=0000 SI=0000 DI=0000
DS=144D ES=143B SS=144F CS=144B IP=0005 NV UP EI PL NZ NA PO NC
144B:0005 B409 MOV AH,09
-d ds:0
144D:0000 2D 3D 2A 20 48 65 6C 6C-6F 20 57 6F 72 6C 64 20 -=* Hello World
144D:0010 2A 3D 2D 24 00 00 00 00-00 00 00 00 00 00 00 00 *=-$............
144D:0020 2A 2A 2A 2A 2A 2A 2A 2A-2A 2A 2A 2A 2A 2A 2A 2A ****************
144D:0030 2A 2A 2A 2A 2A 2A 2A 2A-2A 2A 2A 2A 2A 2A 2A 2A ****************
144D:0040 2A 2A 2A 2A 2A 2A 2A 2A-2A 2A 2A 2A 2A 2A 2A 2A ****************
144D:0050 2A 2A 2A 2A 2A 2A 2A 2A-2A 2A 2A 2A 2A 2A 2A 2A ****************
144D:0060 2A 2A 2A 2A 2A 2A 2A 2A-2A 2A 2A 2A 2A 2A 2A 2A ****************
144D:0070 2A 2A 2A 2A 2A 2A 2A 2A-2A 2A 2A 2A 2A 2A 2A 2A ****************
-t

AX=094D BX=0000 CX=0140 DX=0000 SP=0100 BP=0000 SI=0000 DI=0000
DS=144D ES=143B SS=144F CS=144B IP=0007 NV UP EI PL NZ NA PO NC
144B:0007 BA0000 MOV DX,0000
-t

AX=094D BX=0000 CX=0140 DX=0000 SP=0100 BP=0000 SI=0000 DI=0000
DS=144D ES=143B SS=144F CS=144B IP=000A NV UP EI PL NZ NA PO NC
144B:000A CD21 INT 21
-g 10d
-=* Hello World *=-
Программа завершилась нормально
-

Комментариев нет: