Показаны сообщения с ярлыком директива SEGMENT. Показать все сообщения
Показаны сообщения с ярлыком директива SEGMENT. Показать все сообщения

четверг, 21 мая 2009 г.

Опять игры с сегментами

При попытках понять как работают компиляторы, отладчики и прочая сказочная живность начинаешь обнаруживать интересные вещи. И становится не понятно, то ли это ошибки компиляторов и отладчиков, то ли это их фишки :)
Итак ближе к монитору. Новая серия "Hello World". Нас ждут невероятные приключения в дебрях кода супер программы.

Итак нового мутанта в студию:

Казалось бы, что тут особенного?! В принципе так оно и есть, но вот TASM и MASM по разному компилируют данную программу, но это в принципе не самая интересная особенность. Интересно, то, что компилятор TASM инициализирует не правильным значением регистр SP (указатель стека). Происходит это видимо потому, что мы указали тип выравнивания для стека DWORD, что привело к тому, что стек начинается не на границе параграфа. Соответственно часть стека потерялась, то есть она ни когда не будет использована. Похоже что это глюк TASM. Смотрим следующий скриншот, и погружаемся в глубокую медитацию.

Замечаем, что отладчик использует стек отлаживаемой программы для своих целей. Это может ему дорого стать :) если со стеком что-нибудь сотворить. Но об этом в следующей серии нашего сериала.
Выпуск серий чуть опаздывает от моего изучения ассемблера и исследования поведения компиляторов и отладчиков.
И так, на что следует обратить внимание на скриншоте. Во первых, TASM абсолютно правильно, скомпилил EXE файл, в соответствии с теми атрибутами выравнивания сегментов что мы задавали, НО вот при загрузке в память программы регистр SP получил не правильное значение, и указывает не на дно стека (вернее не на перове слово за концом стека), а указывает на начало параграфа, считая что стек начинается на границе парагарфа, но у нас он не начинается на границе параграфа! Вот такой вот глюк, ну или фишка :). Второе это, то, что отладчик помещает на дно стека два нулевых байта в начале своей работы. Зачем он это делает? Не знаю... Видимо ему это нужно :)
Из всего этого вывод, что работу компиляторов надо проверять :) Не факт, что они создадут, то что мы задумывали :)
Теперь посмотрим как MASM откомпилил нашу прогу:

MASM просто забил на наш атрибут DWORD для выравнивания стека. Он все равно расположил его, как видим, на границе параграфа. В принципе и правильно сделал :) видимо не захотел мучится с высчитыванием смещения дна стека :)
Смотрим все под отладчиком:

Видим, что сейчас с SP все в порядке.

Теперь повторим наш эксперимент с установкой значений SS и SP вручную. Изучаем внимательно текст программы:

И смотрим какой EXE создал TASM:

Стоит обратить внимание даже на визуальное представление стека. Далее как водится загоняем прогу в отладчик. Далее приведен скриншот еще до выполнения первой команды.

Как видим, наш стек девственно чист, и не осквернен отладчиком :), так как он полагает что стек находится в другом месте :), в конце PSP. Теперь выполним все команды до mov ax,13D5, т.е. команды которые заносят в регистры SS и SP правильные значения.

Теперь, как видно, все в порядке, но в стеке много "мусора", который занес туда отладчик.
MASM тоже отранслировал эту программу. Размер совпал с размером проги отранслированной TASM.

среда, 20 мая 2009 г.

Игры с сегментами

Итак сериал "Hello World" продолжается. Серия уже не помню какая :), но нам же по барабану! :) Мы же фанаты супер гига мега проги всех времен и народов - "Helo World". Итак к монитору! :) Будем воплощать теорию в практику!

Начнем с классики, т.е. со стандартных директив сегментации. И так програ:

Ассемблируем ее:

И получили предупреждение, что стек не определен. И правильно. Вспоминаем описание атрибутов комбинирования сегментов. В частности атрибут stack.

Смотрим расположение сегментов в исполняемом файле:

Все как мы описывали. То есть сегменты располагаются так как мы их описали.

И смотрим расположение сегментов в памяти:

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

Теперь немного поменяем программу вот таким образом:

Транслируем:

Внимательно смотрим листинг:

Смотрим содержимое EXE файлика:

Убеждаемся, что сегменты расположены в порядке, который мы определили в программе. А так же, что сегменты выравнены как мы и указали. Внимательно медитируем на все это, вспоминаем, что было написано про выравнивание и умнеем прямо на глазах :)

Теперь, святое дело, загоняем программу в отладчик:

Скриншот сделан еще до выполнения первой команды программы. Замечаем, что SS установлен на конец PSP, вернее на первое слово за ним, т.е. на начало программы (не путать с точкой входа в программу). Регистр SP вообще не инициализирован. Так же видим, что сегменты располагаются в памяти, в том порядке, как мы их и описали в программе.
Внимательно анализируем значения регистров, углубленно медитируем и просветляемся.
Затем гоняем прогу под отладчиком и становимся еще умнее :)

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

Синтаксис ассемблера. Директива SEGMENT

Директивы

Ассемблер имеет ряд операторов, которые позволяют управлять процессом ассемблирования и формирования листинга. Эти операторы называются псевдокомандами или директивами. Они действуют только в процессе ассемблирования программы и не генерируют машинных кодов. Директивы формирования листинга не очень интересны, так как вывести на печать текст программы не составляет труда для любого современного редактора.
Директивы ассемблирования буду изучать и по ходу дела добавлять их в блог. К сожалению, стройного и полного описания директив ассемблирования и примеров, разъясняющих их назначение и разницу между их параметрами, нет ни в одной из имеющихся у меня книг. Более-менее они описаны опять же у старичка Абеля (но книжка все таки старовата) и не плохое есть описание есть у Юрова, но что тот, что другой не приводят ни каких примеров, которые бы пояснили разницу между параметрами директивы SEGMENT и т.д. Так что придется все исследовать самостоятельно.

Директива SEGMENT

Любые ассемблерные программы содержат, по крайней мере, один сегмент - сегмент кода. В некоторых программах используется сегмент для стековой памяти и сегмент данных для определения данных.
Еще раз вспомним, что физически сегмент представляет собой область памяти, занятую командами и (или) данными, адреса которых вычисляются относительно значения в соответствующем сегментном регистре.

Стандартные Директивы Сегментации

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

Важно отметить, что функциональное назначение сегмента несколько шире, чем простое разбиение программы на блоки кода, данных и стека. Сегментация является частью более общего механизма, связанного с концепцией модульного программирования. Она предполагает унификацию оформления объектных модулей, создаваемых компилятором, в том числе с разных языков программирования. Это позволяет объединять программы, написанные на разных языках. Именно для реализации различных вариантов такого объединения и предназначены операнды в директиве SEGMENT.
Рассмотрим их подробнее.

  • Атрибут выравнивания сегмента (тип выравнивания) сообщает компоновщику о том, что нужно обеспечить размещение начала сегмента на заданной границе. Это важно, поскольку при правильном выравнивании доступ к данным в процессорах i80х86 выполняется быстрее. Допустимые значения этого атрибута следующие:
    • BYTE — выравнивание не выполняется. Сегмент может начинаться с любого адреса памяти;
    • WORD — сегмент начинается по адресу, кратному двум, то есть последний (младший) значащий бит физического адреса равен 0 (выравнивание на границу слова);
    • DWORD — сегмент начинается по адресу, кратному четырем, то есть два последних (младших) значащих бита равны 0 (выравнивание на границу двойного слова);
    • PARA — сегмент начинается по адресу, кратному 16, то есть последняя шестнадцатеричная цифра адреса должна быть 0h (выравнивание на границу параграфа);
    • PAGE — сегмент начинается по адресу, кратному 256, то есть две последние шестнадцатеричные цифры должны быть 00h (выравнивание на границу 256-байтной страницы);
    • MEMPAGE — сегмент начинается по адресу, кратному 4 Кбайт, то есть три последние шестнадцатеричные цифры должны быть 000h (адрес следующей 4-Кбайтной страницы памяти).
    • По умолчанию тип выравнивания имеет значение PARA.


  • Атрибут комбинирования сегментов (комбинаторный тип) сообщает компоновщику, как нужно комбинировать сегменты различных модулей, имеющие одно и то же имя. Значениями атрибута комбинирования сегмента могут быть:
    • PRIVATE — сегмент не будет объединяться с другими сегментами с тем же именем вне данного модуля;
    • PUBLIC — заставляет компоновщик соединить все сегменты с одинаковыми именами. Новый объединенный сегмент будет целым и непрерывным. Все адреса (смещения) объектов, а это могут быть, в зависимости от типа сегмента, команды и данные, будут вычисляться относительно начала этого нового сегмента;
    • COMMON — располагает все сегменты с одним и тем же именем по одному адресу. Все сегменты с данным именем будут перекрываться и совместно использовать память. Размер полученного в результате сегмента будет равен размеру самого большого сегмента;
    • AT xxxx — располагает сегмент по абсолютному адресу параграфа (параграф — объем памяти, кратный 16; поэтому последняя шестнадцатеричная цифра адреса параграфа равна 0). Абсолютный адрес параграфа задается выражением xxx. Компоновщик располагает сегмент по заданному адресу памяти (это можно использовать, например, для доступа к видеопамяти или области ПЗУ), учитывая атрибут комбинирования. Физически это означает, что сегмент при загрузке в память будет расположен, начиная с этого абсолютного адреса параграфа, но для доступа к нему в соответствующий сегментный регистр должно быть загружено заданное в атрибуте значение. Все метки и адреса в определенном таким образом сегменте отсчитываются относительно заданного абсолютного адреса;
    • STACK — определение сегмента стека. Заставляет компоновщик соединить все одноименные сегменты и вычислять адреса в этих сегментах относительно регистра ss. Комбинированный тип STACK (стек) аналогичен комбинированному типу PUBLIC, за исключением того, что регистр ss является стандартным сегментным регистром для сегментов стека. Регистр sp устанавливается на конец объединенного сегмента стека. Если не указано ни одного сегмента стека, компоновщик выдаст предупреждение, что стековый сегмент не найден. Если сегмент стека создан, а комбинированный тип STACK не используется, программист должен явно загрузить в регистр ss адрес сегмента (подобно тому, как это делается для регистра ds), а также установить регистр SP в правильное значение.
    • По умолчанию атрибут комбинирования принимает значение PRIVATE.


  • Атрибут класса сегмента (тип класса) — это заключенная в кавычки строка, помогающая компоновщику определить соответствующий порядок следования сегментов при собирании программы из сегментов нескольких модулей. Компоновщик объединяет вместе в памяти все сегменты с одним и тем же именем класса (имя класса, в общем случае, может быть любым, но лучше, если оно будет отражать функциональное назначение сегмента). Типичным примером использования имени класса является объединение в группу всех сегментов кода программы (обычно для этого используется класс 'code'). С помощью механизма типизации класса можно группировать также сегменты инициализированных и не инициализированных данных.

  • Атрибут размера сегмента. Для процессоров i80386 и выше сегменты могут быть 16 или 32-разрядными. Это влияет, прежде всего, на размер сегмента и порядок формирования физического адреса внутри него. Атрибут может принимать следующие значения:
    • USE16 — это означает, что сегмент допускает 16-разрядную адресацию. При формировании физического адреса может использоваться только 16-разрядное смещение. Соответственно, такой сегмент может содержать до 64 Кбайт кода или данных;
    • USE32 — сегмент будет 32-разрядным. При формирования физического адреса может использоваться 32-разрядное смещение. Поэтому такой сегмент может содержать до 4 Гбайт кода или данных.
Директива ENDS обозначает конец сегмента. Обе директивы SEGMENT и ENDS должны иметь одинаковые имена.

Упрощенные Директивы Сегментации

Стандартные директивы сегментации изначально использовались для оформления программы в трансляторах MASM и TASM. Поэтому их называют стандартными директивами сегментации. Для простых программ, содержащих по одному сегменту для кода, данных и стека, хотелось бы упростить ее описание. Для этого в трансляторы MASM и TASM ввели возможность использования упрощенных директив сегментации. Но здесь возникла проблема, связанная с тем, что необходимо было как-то компенсировать невозможность напрямую управлять размещением и комбинированием сегментов. Для этого совместно с упрощенными директивами сегментации стали использовать директиву указания модели памяти MODEL, которая частично стала управлять размещением сегментов и выполнять функции директивы ASSUME (поэтому при использовании упрощенных директив сегментации директиву ASSUME можно не использовать). Директива MODEL связывает сегменты, которые в случае использования упрощенных директив сегментации имеют предопределенные имена, с сегментными регистрами (хотя явно инициализировать ds все равно придется).
Пример программы с использованием упрощенных директив сегментации:

;----------------------------------------
masm ;режим работы TASM: ideal или masm
model small ;модель памяти
.data ;сегмент данных
message db 'Введите две шестнадцатеричные цифры,$'
.stack ;сегмент стека
db 256 dup ('?') ;сегмент стека
.code ;сегмент кода
main proc ;начало процедуры main
mov ax,@data ;заносим адрес сегмента
;данных в регистр ax
mov ds,ax ;ax в ds
;далее текст программы (см. сегмента кода в листинге 3.1 книги)
mov ax,4c00h ;пересылка 4c00h в регистр ax
int 21h ;вызов прерывания с номером 21h
main endp ;конец процедуры main
end main ;конец программы с точкой входа main

Синтаксис директивы MODEL:


Модификатор модели памяти

Значение модификатора Назначение
use16 Сегменты выбранной модели используются как 16-битные (если соответствующей директивой указан процессор i80386 или i80486)
use32 Сегменты выбранной модели используются как 32-битные (если соответствующей директивой указан процессор i80386 или i80486)
dos Программа будет работать в MS-DOS

Модель памяти является обязательным параметром директивы MODEL. Этот параметр определяет модель сегментации памяти для программного модуля.

Модели памяти
Модель Тип кода Тип данных Назначение модели
TINY near near Код, данные и стек объединены в одну группу с именем DGROUP и размером до 64Кб. Используется для создания программ формата .com. Некоторые языки эту модель не поддерживают.
СS=DS=SS=DGROUP
SMALL near near Код занимает один сегмент, данные и стек объединены в одну группу с именем DGROUP (хотя для описания могут использоваться разные сегменты). Эту модель обычно используют для большинства программ на ассемблере.
CS=_text
DS=SS=DGROUP
MEDIUM far near Код занимает несколько сегментов, по одному на каждый объединяемый программный модуль. Все ссылки на передачу управления — типа far (вызов подпрограмм). Данные и стек объединены в одной группе DGROUP; все ссылки на них — типа near (для доступа к данным испльзуется только смещение).
CS=<модуль>_text
DS=SS=DGROUP
COMPACT near far Код находится в одном сегменте, данные и стек в группе DGROUP и могут занимать несколько сегментов, так что для обращения к данным требуется указывать сегмент и смещение (
ссылка на данные — типа far).
CS=_text
DS=SS=DGROUP
LARGE far far Код может занимать несколько сегментов, по одному на каждый объединяемый программный модуль. Стек и данные находятся в группе DGROUP. Для ссылки на данные используются дальние указатели -far.
CS=<модуль>_text
DS=SS=DGROUP
HUGE far far Тоже что и модель LARGE, что касается TurboAssebmler.
FLAT far far Тоже, что и TINY, но используются 32-битная адресация, так что максимальный размер сегмента, содержащего и данные, и код, и стек - 4Гб.

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

Упрощенные директивы определения сегмента
Формат директивы
(режим MASM)
Формат директивы
(режим IDEAL)
Назначение
.CODE [имя] CODESEG[имя] Начало или продолжение сегмента кода
.DATA DATASEG Начало или продолжение сегмента инициализированных данных. Также используется для определения данных типа near
.CONST CONST Начало или продолжение сегмента постоянных данных (констант) модуля
.DATA? UDATASEG Начало или продолжение сегмента неинициализированных данных. Также используется для определения данных типа near
.STACK [размер] STACK [размер] Начало или продолжение сегмента стека модуля. Параметр [размер] задает размер стека
.FARDATA [имя] FARDATA [имя] Начало или продолжение сегмента инициализированных данных типа far
.FARDATA? [имя] UFARDATA [имя] Начало или продолжение сегмента неинициализированных данных типа far

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

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

Идентификаторы, создаваемые директивой MODEL
Имя идентификатора Значение переменной
@code Физический адрес сегмента кода
@data Физический адрес сегмента данных типа near
@fardata Физический адрес сегмента данных типа far
@fardata? Физический адрес сегмента неинициализированных данных типа far
@curseg Физический адрес сегмента неинициализированных данных типа far
@stack Физический адрес сегмента стека

Если вы посмотрите на текст примера, то увидите пример использования одного из этих идентификаторов. Это @data – с его помощью мы получили значение физического адреса сегмента данных нашей программы.

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

Языкнеобязательный операнд, принимающий значения C, PASCAL, BASIC, FORTRAN, SYSCALL и STDCALL. Если он указан, подразумевается, что процедуры рассчитаны на вызов из программ на соответствующем языке высокого уровня, следовательно, если указан язык C, все имена ассемблерных процедур, объявленных как PUBLIC, будут изменены так, чтобы начинаться с символа подчеркивания, как это принято в C.
Модификаторнеобязательный операнд, принимающий значения NEARSTACK (по умолчанию) или FARSTACK. Во втором случае сегмент стека не будет объединяться в одну группу с сегментами данных.
После того как модель памяти установлена, вступают в силу упрощенные директивы определения сегментов, объединяющие действия директив SEGMENT и ASSUME. Кроме того, сегменты, объявленные упрощенными директивами, не требуется закрывать директивой ENDS — они закрываются автоматически, как только ассемблер обнаруживает новую директиву определения сегмента или конец программы.

Директива .CODE описывает основной сегмент кода

.code имя_сегмента

эквивалентно:

_TEXT segment word public ’CODE’

для моделей TINY, SMALL и COMPACT

name_TEXT segment word public ’CODE’

для моделей MEDIUM, HUGE и LARGE (name — имя модуля, в котором описан данный сегмент). В этих моделях директива .CODE также допускает необязательный операнд — имя определяемого сегмента, но все сегменты кода, описанные так в одном и том же модуле, объединяются в один сегмент с именем NAME_TEXT.

Директива .STACK описывает сегмент стека

.stack размер

эквивалентно:

STACK segment para public ’stack’

Необязательный параметр указывает размер стека. По умолчанию он равен 1 Кб.

Директива .DATA описывает обычный сегмент данных

.data

эквивалентно:

_DATA segment word public ’DATA’

Директива .DATA? описывает сегмент неинициализированных данных

.data?

Эквивалентно:

_BSS segment word public ’BSS’

Этот сегмент обычно не включается в программу, а располагается за концом памяти, так что все описанные в нем переменные на момент загрузки программы имеют неопределенные значения.

Директива .CONST описывает сегмент неизменяемых данных

.const

Эквивалентно:

CONST segment word public ’CONST’

В некоторых операционных системах этот сегмент будет загружен так, что попытка записи в него может привести к ошибке.

Директива .FARDATA описывает сегмент дальних данных

.fardata имя_сегмента

эквивалентно:

имя_сегмента segment para private ’FAR_DATA’

Доступ к данным, описанным в этом сегменте, потребует загрузки сегментного регистра. Если не указан операнд, в качестве имени сегмента используется FAR_DATA.

Директива .FARDATA? описывает сегмент дальних неинициализированных данных

.fardata? имя_сегмента

эквивалентно:

имя_сегмента segment para private ’FAR_BSS’

Как и в случае с FARDATA, доступ к данным из этого сегмента потребует загрузки сегментного регистра. Если имя сегмента не указано, используется FAR_BSS.

Во всех моделях памяти сегменты, представленные директивами .DATA, .DATA?, .CONST, .FARDATA и .FARDATA?, а также сегмент, описанный директивой .STACK, если не был указан модификатор FARSTACK, и сегмент .CODE в модели TINY автоматически объединяются в группу с именем FLAT — для модели памяти FLAT или DGROUPдля всех остальных моделей. При этом сегментный регистр DS (и SS, если не было FARSTACK, и CS в модели TINY) настраивается на всю эту группу, как если бы была выполнена команда ASSUME.

Порядок загрузки сегментов

При использовании стандартных директив сегментации сегменты загружаются в память в том порядке, в котором они описываются в тексте программы.

При использовании упрощенных директив сегментации (по умолчанию) устанавливается порядок загрузки сегментов, существующий в MS DOS и часто требуемый для взаимодействия программ на ассемблере с программами на языках высокого уровня.

Порядок загрузки сегментов:

1. Все сегменты класса 'CODE'.
2. Все сегменты, не принадлежащие группе DGROUP и классу 'CODE'.
3. Группа сегментов DGROUP:
3.1. Все сегменты класса 'BEGDATA'.
3.2. Все сегменты, кроме классов 'BEGDATA', 'BSS' и 'STACK'.
3.3. Все сегменты класса 'BSS'.
3.4. Все сегменты класса 'STACK'.

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

P.S. В книге Зубкова, порядок загрузки сегментов описан несколько иначе, НО практика показала, что его описание не правильное. При использовании упрощенных директив сегментации, директива .seq, которая, по идее должна устанавливать порядок загрузки сегментов, как они описаны в прорамме, этого не делает! А делает это дериктива .alpha, и то, только при использовании TASM, только в модели small, и при этом она добавляет заголовок в exe файл длиной 600h байт! MASM же, при использовании упрощенных директив сегментации всегда размещает сегменты в соответствии с порядком MS DOS, забивая на директивы .seq и .alpha. И еще с выравниванием сегментов, при использовании упрощенных директив сегментации, происходит какая-то свистопляска :). Все это выявлено опытным путем. Все в следующих сериях супер сериала "Hello World". И так, жем вас у экранов мониторов :)

вторник, 12 мая 2009 г.

Здравствуйте, я ваша тётя!

Продолжение сериала с программой hello.asm :)
Сперва медитативная часть. Вспоминаем, что один и тот же физический байт памяти может иметь различные логические адреса, определяемые разными, но при этом эквивалентными парами сегмент-смещение. Скриншот для медитации:

Замечаем, что в выводе команды d cs:0, наши данные располагаются по адресу 144B:0020, а в выводе команды d ds:0, наши данные располагаются по адресу 144D:0000. Дам был сделан после исполнения двух первых команд программы. Медитируем и просветляемся :).
Далее практическая часть.
Справедливости ради, оттранслируем hello.asm при помощи MASM с ключами генерации листинга и карты. Запускаем m.bat и даем команду ml /Fl /Fm hello.asm. Замечаем что для MASM надо указывать расширение – .asm. В результате работы ассемблера получаем четыре файла: hello.obj, hello.lst, hello.map и hello.exe.

Посмотрим содержимое файла hello.lst.

Сгенерированный MASM-ом листинг очень похож на тот, что создал TASM. Есть лишь не большие различия. В листинге MASM отсутствуют номера строк программы. Так же, немного различается, вывод команд по смещениям 0000 и 0007, хотя суть одна и та же.
Теперь начнем эксперименты с выравниваниями сегментов. Изменим немного программу hello.asm добавив атрибут выравнивания byte к командам описаниям сегментов. Cохраним текст программы, как hello1.asm, ассемблируем с генерацией листинга, и посмотрим, что получилось.
Из последнего скриншота видно, что пропали пустые промежутки между сегментом кода и сегментом данных, и сегментом данных и сегментом стека. Значит программа hello1.exe, по идее, должна стать на 27 байт меньше, то есть 805 байт. Так оно и есть.

Если внимательно посмотреть, то немного изменились и заголовки программ. Но с форматом заголовков буду разбираться попозже. Щас играемся с сегментами.
Теперь опять медитация. Будем созерцать под отладчиком обе программы: hello.exe и hello1.exe.
Для вывода дизассемблированного кода программы используется команда – u (unassemble), параметрами которой является диапазон смещений в сегменте. Наша программа имеет размер 11h байт. То есть наши 11h байт располагаются в адресном пространстве с 0h по 10h. Чтобы вывести дизассемблированный листинг этого пространства даем команду u cs:0 10.
Как видно из дампов памяти отладчиков, в программе hello.exe присутствуют пустые промежутки (отмечены зеленым), в программе hello1.exe их нет, так как использовался атрибут выравнивания byte в директивах segment. В результате этого произошло смещение сегмента данных, что отразилось на команде mov dx,offset msg, которая в программе hello.exe имеет вид mov dx,0000, а в программе hello1.exemov dx,0001.
Еще один интересный момент это то, что команда mov ax,data в обеих программах имеет одинаковый машинный код mov ax,144D, не смотря на то, что в программе hello1.exe мы применили выравнивание параграфа. К чему бы это? И почему бы это? Вспоминаем, что сегментная часть адреса всегда кратна 16 или 10h. Наша программа занимает больше одного параграфа на 1 байт. Поэтому ассемблер, видя, что сегмент кода занимает больше одного параграфа, но меньше двух, определил сегментную часть адреса сегмента данных на 20h байт (2 параграфа) отстоящую от начала сегмента кода. Соответственно в программе hello.exe сегмент данных начинается на границе параграфа, то есть имеет смещение 0, а в программе hello1.exe, сегмент данных, из-за атрибута выравнивания, имеет смещение в 1 байт (в данном случае), от начала параграфа. Что мы и видим в коде команды mov dx,offset msg.

Теперь поперемещаем сегменты и посмотрим, как это отразится на листинге программы и ее коде. Создадим «новую» программку – hello2.asm. Определим в ней данные перед сегментом кода.

От сюда видно, что сегменты располагаются в том порядке, как они были объявлены в программе. Теперь помедитируем – посмотрим прогу под дебагером :)

Из дебагера видно, что данных не видно :) … ну при текущем расположении сегментов в программе…. Если внимательно смотрели предыдущие дампы, то данные сразу можно было видеть в выводе команды d cs:0. Теперь их не видно, так как данные располагаются выше в адресном пространстве, чем данные. Но все же при помощи хитрого хака, их можно увидеть еще до инициализации сегментного регистра DS сегментным адресом данных. Для этого поменяем значение сегментного регистра ES (так как его наша программа не использует), на сегментный адрес 144C (от куда взялся этот адрес? … это предмет медитации ;) ). И так даем команду r es и жмем Enter, потом вводим 144C и жмем Enter, потом вводим d es:0 и жмем Enter и видим наши данные.

Обратите внимание, что еще ни одна команда программы не выполнена.
Выполняем две первые команды нашей программы и смотрим сегмент данных.

Далее выполняем остальные команды программы и анализируем это :)..... сами....

понедельник, 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 *=-
Программа завершилась нормально
-