воскресенье, 31 мая 2009 г.

Описание данных

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

Формат описания данных:

[имя] Dn выражение

Имя элемента данных не обязательно (это указывается квадратными скобками), но если в программе имеются ссылки (обращения) на некоторый элемент, то это делается посредством имени.
Имена данных могут включать латинские буквы, цифры (не в качестве первого знака имени) и некоторые специальные знаки, например, знаки подчеркивания (_), доллара ($) и коммерческого at (@). Длину имени некоторые ассемблеры ограничивают (например, ассемблер MASM - 31 символом), другие - нет, но в любом случае слишком длинные имена затрудняют чтение программы. С другой стороны, имена данных следует выбирать таким образом, чтобы они отражали назначение конкретного данного, например counter для счетчика или filename для имени файла:

counter dw 10000
filename db 'a:\myfile.001'

Для определения данных используются, главным образом, три директивы ассемблера: db (define byte, определить байт) для записи байтов, dw (define word, определить слово) для записи слов и dd (define double, определить двойное слово) для записи двойных слов. Кроме перечисленных, имеются и другие директивы, например df (define fanvord, определить поле из 6 байт), dq (define quadword, определить четверное слово) или dt (define tcraword, определить 10-байтовую переменную), но они используются значительно реже.

Выражение может содержать числовое данное, например:

FLD1 DB 25

или знак вопроса для неопределенного значения, например

FLDB DB ?

Выражение может содержать несколько числовых данных, разделенных запятыми и ограниченными только длиной строки:

FLD3 DB 11, 12, 13, 14, 15, 16, ...

Ассемблер определяет эти данные в виде последовательности смежных байт. Ссылка по имени FLD3 указывает на первое число, 11, по FLD3+1 - на второе, 12. (FLD3 можно представить как FLD3+0). Например команда

MOV AL,FLD3+3

загружает в регистр AL значение 14 (шест. 0E).

Выражение допускает также повторение константы в следующем формате:

[имя] Dn число-повторений DUP (выражение)

Следующие три примера иллюстрируют повторение:

DW 10 DUP(?) ;Десять неопределенных слов
DB 5 DUP(14) ;Пять байт, содержащих шест.14
DB 3 DUP(4 DUP(8));Двенадцать восьмерок

В третьем примере сначала генерируется четыре копии десятичной 8 (8888), и затем это значение повторяется три раза, давая в результате двенадцать восьмерок.

Выражение может содержать символьную строку или числовое данное.

Символьная строка используется для описания данных, таких как, например, имена людей или заголовки страниц. Содержимое строки отмечается одиночными кавычками, например, 'PC' или двойными кавычками - "PC". Ассемблер переводит символьные строки в объектный код в обычном формате ASCII. Символьная строка определяется только директивой DB, в которой указывается более два или более символов в нормальной последовательности слева направо. Следовательно, директива DB представляет единственно возможный формат для определения символьных данных.

Числовое данное используются для арифметических величин, для адресов памяти и т.п. Ассемблер преобразует все числовые данные в шестнадцатеричные и записывает байты в объектном коде в обратной последовательности - справа налево.
Значения числовых данных можно записывать в формате различных систем счисления; чаще других используются десятичная и шестнадцатеричная запись:

size dw 256 ;В ячейку size записывается
;десятичное число 256
setb7 db 80h ;В ячейку setb7 записывается
;16-ричное число 80h

Необходимо отметить неточность приведенных выше комментариев. В памяти компьютера могут храниться только двоичные коды. Если мы говорим, что в какой-то ячейке записано десятичное число 128, мы имеем в виду не физическое содержимое ячейки, а лишь форму представления этого числа в исходном тексте программы. В слове с именем size фактически будет записан двоичный код 0000000100000000, являющийся двоичным эквивалентом десятичного числа 256. Во втором случае в байте с именем setb7 будет записан двоичный эквивалент шестнадцатеричного числа 80h, который составляет 10000000 (т.е. байт с установленным битом 7, откуда и получила имя эта ячейка).

Форматы записи числовых данных

Десятичный формат допускает десятичные цифры от 0 до 9 и обозначается последней буквой D, которую можно не указывать, например, 125 или 125D. Несмотря на то, что ассемблер позволяет кодирование в десятичном формате, он преобразует эти значения в шестнадцатеричный объектный код. Например, десятичное число 125 преобразуется в 7Dh.

Шестнадцатеричный формат допускает шестнадцатеричные цифры от 0 до F и бозначается последней буквой H. Так как ассемблер полагает, что с буквы начинаются идентификаторы, то первой цифрой шестнадцатеричного данного должна быть цифра от 0 до 9. Например, 2EH или 0FFFH, которые ассемблер преобразует соответственно в 2E и FF0F (байты во втором примере записываются в объектный код в обратной последовательности).

Двоичный формат допускает двоичные цифры 0 и 1 и обозначается последней буквой B. Двоичный формат обычно используется для более четкого представления битовых значений в логических командах AND, OR, XOR и TEST.

Восьмеричный формат допускает восьмеричные цифры от 0 до 7 и обозначается последней буквой Q или O, например, 253Q. На сегодня восьмеричный формат используется весьма редко.

При записи символьных и числовых данных следует помнить, что, например, символьная строка, определенная как DB '12', представляет символы ASCII и генерирует шестнадцатеричное значение 3132h, а числовое данное, определенное как DB 12, представляет десятичное число и генерирует шестнадцатеричное 0Ch.

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

mov AX,size

занесет в регистр АХ содержимое ячейки size (число 256), независимо от того, в каком месте сегмента данных эта ячейка определена, и в какое место физической памяти она попала. Однако программист, использующий язык ассемблера, должен иметь отчетливое представление о том, каким образом назначаются адреса ячейкам программы, и уметь работать не только с символическими обозначениями, но и со значениями адресов. Для обсуждения этого вопроса рассмотрим пример сегмента данных, в котором определяются данные различных типов. В левой колонке укажем смещения данных (в шестнадцатеричной форме), вычисляемые относительно начала сегмента.

data segment
0000h counter dw 10000
0002h pages db "Страница 1"
000Ch numbers db 0, 1, 2, 3, 4
0011h page_addr dw pages
data ends

Сегмент данных начинается с данного по имени counter, которое описано, как слово (2 байт) и содержит число 10000. Очевидно, что его смещение равно 0. Поскольку это данное занимает 2 байт, следующее за ним данное pages получило смещение 2. Данное pages описывает строку текста длиной 10 символов и занимает в памяти столько же байтов, поэтому следующее данное numbers получило относительный адрес 2 + 10 = 12 = Ch. В поле numbers записаны 5 байтовых чисел, поэтому последнее данное сегмента с именем page_addr размещается по адресу Ch + 5 = 11h.
Ассемблер, начиная трансляцию сегмента (в данном случае сегмента данных) начинает отсчет его относительных адресов. Этот отсчет ведется в специальной переменной транслятора (не программы!), которая называется счетчиком текущего адреса и имеет символическое обозначение знака доллара ($). По мере обработки полей данных, их символические имена сохраняются в создаваемой ассемблером таблице имен вместе с соответствующими им значениями счетчика текущего адреса. Другими словами, введенные нами символические имена получают значения, равные их смещениям. Таким образом, с точки зрения транслятора counter равно 0, pages - 2, numbers - Ch и т.д. Поэтому предложение

page_addr dw pages

трактуется ассемблером, как

page_addr dw 2

и приводит к записи в слово с относительным адресом 11h числа 2 (смещения строки pages).

Приведенные рассуждения приходится использовать при обращении к "внутренностям" объявленных данных. Пусть, например, мы хотим выводить на экран строки "Страница 2", "Страница 3", "Страница 4" и т.д. Можно, конечно, все эти строки описать в сегменте данных по отдельности, но это приведет к напрасному расходу памяти. Экономнее поступить по-другому: выводить на экран одну и ту же строку pages, но модифицировать в ней номер страницы. Модификацию номера можно выполнить с помощью, например, такой команды:

mov pages + 9,'2'

Здесь мы "вручную" определили смещение интересующего нас символа в строке, зная, что все данные размещаются ассемблером друг за другом в порядке их объявления в программе. При этом, какое бы значение не получило имя pages, выражение pages + 9 всегда будет соответствовать байту с номером страницы.
Таким же приемом можно воспользоваться при обращении к данному numbers, которое в сущности представляет собой небольшой массив из 5 чисел. Адрес первого числа в этом массиве равен просто numbers, адрес второго числа - numbers + 1, адрес третьего - numbers + 2 и т.д. Следующая команда прочитает последний элемент этого массива в регистр DL:

mov DL,numbers+4

Какой смысл имело объединение ряда чисел в массив numbers? Да никакого, если к этим числам мы все равно обращаемся по отдельности. Удобнее было объявить этот массив таким образом:

nmb0 db 0
nmbl db 1
nmb2 db 2
nmb3 db 3
nmb4 db 4

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

mov SI,4 ;Индекс элемента в массиве
mov DL,numbers[SI] ;Обращение по адресу
;numbers + содержимое SI

Иногда желательно обращаться к элементам массива (обычно небольшого размера) то с помощью индексов, то по их именам. Для этого надо к описанию массива, как последовательности отдельных данных, добавить дополнительное символическое описание адреса начала массива с помощью директивы ассемблера label (метка):

numbers label byte
nmb0 db 0
nmbl db 1
nmb2 db 2
nmb3 db 3
nmb4 db 4

Метка numbers должна быть объявлена в данном случае с описателем byte, так как данные, следующие за этой меткой, описаны как байты и мы планируем работать с ними именно как с байтами. Если нам нужно иметь массив слов, то отдельные элементы массива следует объявить с помощью директивы dw, а метке numbers придать описатель word:

numbers label word
nmb0 dw 0
nmbl dw 1
nmb2 dw 2
nmb3 dw 3
nmb4 dw 4

В чем состоит различие двух последних описаний данных? Различие есть, и весьма существенное. Хотя в обоих случаях в память записывается натуральный ряд чисел от 0 до 4, однако в первом варианте под каждое число в памяти отводится один байт, а во втором - слово. Если мы в дальнейшем будем изменять значения элементов нашего массива, то в первом варианте каждому числу можно будет задавать значения от 0 до 255, а во втором - от 0 до 65535.
Выбирая для данных способ их описания, необходимо иметь в виду, что ассемблер выполняет проверку размеров используемых данных и не пропускает команды, в которых делается попытка обратиться к байтам, как к словам, или к словам - как к байтам. Рассмотрим последний вариант описания массива numbers. Хотя под каждый элемент выделено целое слово, однако реальные числа невелики и вполне поместятся в байт. Может возникнуть искушение поработать с ними, как с байтами, перенеся предварительно в байтовые регистры:

mov AL,nmb0 ;Переносим nmb0 в AL
mov DL,nmbl ;Переносим nmb1 в AL
mov CL,nmb2 ;Переносим nmb2 в AL

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

mov AL,byte ptr nmb0
mov DL,byte ptr nmbl
mov CL,byte ptr nmb2

Эти команды транслятор рассматривает, как правильные. Но следует заметить, что эта команда будет указывать на младший байт в слове (nmb0, nmb1 и т.д.).
Часто возникает необходимость выполнить обратную операцию - к паре байтов обратиться, как к слову. Для этого надо использовать оператор word ptr:

okey db 'OK'

mov AX,word ptr okey

Здесь оба байта из байтовой переменной okey переносятся в регистр АХ. При этом первый по порядку байт, т.е. байт с меньшим адресом, содержащий букву "О", отправится в младшую половину АХ - регистр AL, а второй по порядку байт, с буквой "К", займет регистр АН.
До сих пор речь шла о данных, которые, в сущности, являлись переменными, в том смысле, что под них выделялась память и их можно было модифицировать. Язык ассемблера позволяет также использовать константы, которые являются символическими обозначениями чисел и могут использоваться всюду в тексте программы, как наглядные эквиваленты этих чисел:

maxsize = 0FFFFh
......
mov CX,maxsize
mov CX,0FFFFh


Последние две команды полностью эквивалентны.
При определении констант допустимо выполнение арифметических операций. Пусть нам надо задать позицию символа (или строки символов) на экране. Учитывая, что каждый символ записывается в видеопамяти в двух байтах (в первом - код ASCII символа, а во втором - его атрибут), строка экрана имеет длину 80 символов, а высота экрана составляет 25 строк, то для вывода некоторого символа в середину экрана его смещение в видеопамяти от начала видеостраницы можно определить следующим образом:

position=80*2*12+40*2

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

mes db 'Ждите'
mes_len = $-mes

В этом примере константа mes_len получает значение длины строки mes (в данном случае 5 байт), которая вычисляется как разность значения счетчика текущего адреса после определения строки и ее начального адреса mes. Такой способ удобен тем, что при изменении содержимого строки достаточно перетранслировать программу, и та же константа mes_len автоматически получит новое значение.

ДИРЕКТИВА ОПРЕДЕЛЕНИЯ БАЙТА (DB)

Из различных директив, определяющих элементы данных, наиболее полезной является DB (определить байт). Символьное выражение в директиве DB может содержать строку символов любой длины. Числовое выражение в директиве DB может содержать одну или более однобайтовых констант. Один байт выражается двумя шестнадцатеричными цифрами. Если интерпретировать числовые значения, описанные директивой DB, как содержащие знак, то наибольшее положительное шестнадцатеричное число в одном байте это 7F, все "большие" числа от 80 до FF представляют отрицательные значения. В десятичном исчислении эти пределы выражаются числами +127 и -128.

ДИРЕКТИВА ОПРЕДЕЛЕНИЯ СЛОВА (DW)

Директива DW определяет элементы, которые имеют длину в одно слово (два байта). Символьное выражение в DW ограничено двумя символами, которые ассемблер представляет в объектном коде так, что, например, 'PC' становится 'CP'. Для определения символьных строк директива DW имеет ограниченное применение.
Два байта представляются четырьмя шестнадцатеричными цифрами. Если интерпретировать числовые значения, описанные директивой DW, как содержащие знак, то наибольшее положительное шестнадцатеричное число в двух байтах это 7FFF; все "большие" числа от 8000 до FFFF представляют отрицательные значения. В десятичном исчислении эти пределы выражаются числами +32767 и -32768.
Для форматов директив DW, DD и DQ ассемблер преобразует константы в шестнадцатеричный объектный код, но записывает его в обратной последовательности.
Таким образом десятичное значение 12345 преобразуется в шестнадцатеричное 3039, но
записывается в объектном коде как 3930.

ДИРЕКТИВА ОПРЕДЕЛЕНИЯ ДВОЙНОГО СЛОВА (DD)

Директива DD определяет элементы, которые имеют длину в два слова (четыре байта). Если интерпретировать числовые значения, описанные директивой DD , как содержащие знак, то, наибольшее положительное шестнадцатеричное число в четырех байтах это 7FFFFFFF. Все "большие" числа от 80000000 до FFFFFFFF представляют отрицательные
значения. В десятичном исчислении эти пределы выражаются числами +2147483647 и -2147483648.
Ассемблер преобразует все числовые константы в директиве DD в шестнадцатеричное представление, но записывает объектный код в обратной последовательности. Таким образом десятичное значение 12345 преобразуется в шестнадцатеричное 00003039, но
записывается в объектном коде как 39300000.
Символьное выражение директивы DD ограничено двумя символами.

ДИРЕКТИВА ОПРЕДЕЛЕНИЯ УЧЕТВЕРЕННОГО СЛОВА (DQ)

Директива DQ определяет элементы, имеющие длину четыре слова (восемь байт). Наибольшее без знаковое значение может быть FFFFFFFFFFFFFFFF, что равно десятичному 18446744073709551615. Отрицательные и положительные значения считайте сами :).

Двоично-десятичный

ДИРЕКТИВА ОПРЕДЕЛЕНИЯ ДЕСЯТИ БАЙТ (DT)

Директива DT определяет элементы данных, имеющие длину в десять байт.
Назначение этой директивы связано с упакованным двоично-десятичным форматом данных.

Ну и на последок, кусок кода с моими экспериментами:

суббота, 30 мая 2009 г.

Представление данных

Теперь, опять, немного матчасти :) Надо бы поподробней разобраться с представлением данных...

В языке ассемблера имеются средства записи целых и вещественных чисел, а также символьных строк и отдельных символов. Целые числа могут быть со знаком и без знака, а также записанными в двоично-десятичном формате. Для целых чисел и символов в составе команд микропроцессора и, соответственно, в языке ассемблера, есть средства обработки - анализа, сравнения, поиска и проч. Для вещественных чисел таких средств в самом микропроцессоре нет, они содержатся в арифметическом сопроцессоре.
Рассмотрим сначала целые числа без знака и со знаком. Числа без знака получили свое название потому, что среди этих чисел нет отрицательных. Это самый простой вид чисел: они представляют собой весь диапазон двоичных чисел, которые можно записать в байте, слове или двойном слове. Для байта числа без знака могут принимать значения от 00h (0) до FFh (255); для слова - от 0000h (0) до FFFFh (65535); для двойного слова - от 00000000h (0) до FFFFFFFFh (4294967295).
В огромном количестве приложений вычислительной техники для чисел нет понятия знака. Это справедливо, например, для адресов ячеек памяти, кодов ASCII символов, результатов измерений многих физических величин, кодов управления устройствами, подключаемыми к компьютеру. Для таких чисел естественно использовать весь диапазон чисел, записываемых в ячейку того или иного размера. Если, однако, мы хотим работать как с положительными, так и с отрицательными числами, нам придется половину чисел из их полного диапазона считать положительными, а другую половину - отрицательными. В результате диапазон изменения числа уменьшается в два раза. Кроме того, необходимо предусмотреть систему кодирования, чтобы положительные и отрицательные числа не перекрывались.
В вычислительной технике принято записывать отрицательные числа в так называемом дополнительном коде, который образуется из прямого путем замены всех двоичных нулей единицами и наоборот (обратный код) и прибавления к полученному числу единицы. Это справедливо как для байтовых (8-битовых) чисел, так и для чисел размером в слово или в двойное слово (рис. 1)

Рис. 1. Образование отрицательных чисел различного размера.
Такой способ образования отрицательных чисел удобен тем, что позволяет выполнять над ними арифметические операции по общим правилам с получением правильного результата. Так, сложение чисел +5 и -5 дает 0; в результате вычитания 3 из 5 получается 2; вычитание -3 из -5 дает -2 и т.д.
Анализируя алгоритм образования отрицательного числа, можно заметить, что для всех отрицательных чисел характерно наличие двоичной единицы в старшем бите. Положительные числа, наоборот, имеют в старшем бите 0. Это справедливо для чисел любого размера. Кроме того, из рис. 1 видно, что для преобразования отрицательного 8-битового числа в слово достаточно дополнить его слева восемью двоичными единицами. Легко сообразить, что для преобразования положительного 8-битового числа в слово его надо дополнить восемью двоичными нулями. То же справедливо и для преобразования слова со знаком в двойное слово со знаком, только добавить придется уже не 8, а 16 единиц или нулей. В системе команд МП 86 и, соответственно, в языке ассемблера, для этих операций предусмотрены специальные команды cbw и cwd.
Следует подчеркнуть, что знак числа условен. Одно и то же число, например, изображенное на рис. 1 8-битовое число FBh можно в одном контексте рассматривать, как отрицательное (-5), а в другом - как положительное, или, правильнее, число без знака (FBh=251). Знак числа является характеристикой не самого числа, а нашего представления о его смысле.
На рис. 2 представлена выборочная таблица 16-битовых чисел с указанием их машинного представления, а также значений без знака и со знаком. Из таблицы видно, что для чисел со знаком размером в слово диапазон положительных значений простирается от 0 до 32767, а диапазон отрицательных значений - от -1 до -32768.

Рис. 2 Представление 16-битовых чисел без знака и со знаком
На рис. 3 представлена аналогичная таблица для 8-битовых чисел. Из таблицы видно, что для чисел со знаком размером в байт диапазон положительных значений простирается от 0 до 127, а диапазон отрицательных значений - от -1 до -128.

Рис. 3 Представление 8-битовых чисел без знака и со знаком
Среди команд процессора, выполняющих ту или иную обработку чисел, можно выделить команды, безразличные к знаку числа (например, inc, add, test), команды, предназначенные для обработки чисел без знака (mul, div, ja, jb и др.), а также команды, специально рассчитанные на обработку чисел со знаком (imul, idiv, jg, jl и т.д.).
Рассмотрим теперь другой вид представления чисел - двоично-десятичный формат (binary-coded decimal , BCD), используемый в ряде прикладных областей. В таком формате выдают данные некоторые измерительные приборы; он же используется КМОП-часами реального времени компьютеров IBM PC для хранения информации о текущем времени. В МП 86 предусмотрен ряд команд для обработки таких чисел.
Двоично-десятичный формат существует в двух разновидностях: упакованный и распакованный. В первом случае в байте записывается двухразрядное десятичное число от 00 до 99. Каждая цифра числа занимает половину байта и хранится в двоичной форме. Из рис. 4 можно заметить, что для записи в байт десятичного числа в двоично-десятичном формате достаточно сопроводить записываемое десятичное число символом h.

Рис. 4 Упакованный двоично-десятичный формат
В машинном слове или в 16-разрядном регистре можно хранить в двоично-десятичном формате четырехразрядные десятичные числа от 0000 до 9999 (рис.5).

Рис. 5 Запись десятичного числа 9604 в слове
Распакованный формат отличается от упакованного тем, что в каждом байте записывается лишь одна десятичная цифра (по-прежнему в двоичной форме). В этом случае в слове можно записать десятичные числа от 00 до 99 (см. рис. 6)

Рис. 6 Запись десятичного числа 98 в распакованном виде
При хранении десятичных чисел в аппаратуре обычно используется более экономный упакованный формат; умножение и деление выполняются только с распакованными числами, операции же сложения и вычитания применимы и к тем, и к другим.

пятница, 29 мая 2009 г.

Опять играем со стеком

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

Теперь смотрим листинг:

Погружаемся в медитацию...
Команда call subp теперь имеет код E8FFE4. Складываем FFE4h+1Ch получаем 0 (проверьте в калькуляторе Windows предварительно выставив длину 2 байта). Команда jmp finish имеет вид EBF7, где собственно F7 отрицательное (в данном случае) смещение равное -9. То есть эта команда вычитает 9 из следующего после нее адреса (вернее она складывает шестнадцатеричные значения). Проверяем 25h+F7=1C (проверьте в калькуляторе Windows, предварительно выставив длину 1 байт). Почему выставляем длину, то один, то два байта – это тема для медитации :)
Теперь посмотрим прогу в hiew...

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

Скриншот сделан после выполнения команды call subp (call 0000). Обращаем внимание, что адрес возврата из подпрограммы помещен в стек и равен 001C, как и должно быть. Дебагим нашу прогу дальше до команды ret (ее не выполняем):

Как видим, наш код поменял непосредственно в стеке адрес возврата из подпрограммы, поэтому команда ret извлечет из стека уже не адрес 001С, а адрес 0021, передав таким образом управление уже на этот адрес.

Ну и приведем вывод нашей программы:

C:\ASM\HELLO\TASM>hello10
-=* Hello World *=--=* Hello World *=--=* Hello World *=-
C:\ASM\HELLO\TASM>

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

Игры со стеком

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

Как видно из скрина, мы переместили подпрограмму в начало сегмента кода. Посмотрим теперь на листинг:

Как видим, машинный код команды call subp изменился, теперь он имеет вид - E8FFEE, где FFEE является смещением на начало подпрограммы. Складываем адрес следующей команды, после команды call с этим смещением 12h+FFEEh=0. Можете проверить это в калькуляторе Windows, только не забудьте выставить длину в ДВА БАЙТА (Почему именно в два байта? Ну помедитируйте на это :) ). Здесь мы опять встретилсь с явлением циклического возврата или оборачивания адреса. Помедитируем на это немного и дальше в путь... (Подсказка для медитирующих: 12h=18d и это как раз количество команд отделяющих адрсе 12h до начала нашей подпрограммы. Вобщем медитируйте...)
Далее на нашем пути hiew...

Hiew, надо отметить, преполезнейшая вещь.
Ну и теперь отладчик :)

Этот скрин сделан еще до выполнения первой команды. Обращаем внимание на точку входа в программу и на состояние регистров SS и SP. Следующий скрин сделан уже после входа в подпрограмму, но еще до выполнения первой ее команды. Смотрим, медитируем, просветляемся...

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

Стек и подпрограммы

В своем изучении ассемблера, я немного отклонился от линии начертанной великим Питерем Аблем, но я думаю он на меня не обидется :) Еще немного поэкспериментируем со стеком и подпрограммами. Немного практики ни когда не повредит, а потом вернемся к генеральной линии партии... ммм.... вернее Питера.... Абеля....

Итак, наша програ продолжает мутировать:

Оператор CALL вызывает подпрограмму subp, которую мы определили с помощью соответствующих директив PROC и ENDP. Оператор RET осуществляет возврат из подпрограммы в головную программу.
Взглянем на листинг:

Внимательно медитируем на листинг...
Команда call subp имеет машинный код E80005, где собственно E8 – это сама команда, а 0005 – это смещение, указывающее на первую команду нашей подпрограммы. Команда call уменьшает значение SP на два, помещает в стек адрес, следующей за ней команды (в нашем случае адрес команды mov ax,4c00h равный 000F). Затем вычисляет адрес команды подпрограммы, на которую надо передать управление, прибавляя к адресу следующей за ней команды смещение (положительное или отрицательное) равное количеству байт программы, до первой команды вызываемой подпрограммы – в нашем случае 5 байт (000F+5=0014) и помещает это число (в нашем случае 0014) в регистр IP, передавая таким образом управление на подпрограмму. Подпрограмма выполняется до команды RET, которая извлекает из стека адрес возврата из подпрограммы (в нашем случае 000F) и помещает его в регистр IP (передавая, таким образом, управление вызвавшей ее программе) и уменьшает значение SP на два.
И, традиционно, помедитируем на код сгенерированный ассемблером:

Посмотреть на программу в hiew, так же не повредит:

Ну и, святое дело, дебагер:

Скриншот сделан еще до выполнения первой команды. Внимательно медитируем и жмем пять раз F8, то есть останавливаемся на команде call (ее не выполняем).

Из скриншота видно, что отладчик не плохо поорудовал в стеке, но при этом значение регистра SP осталось прежним, и он по прежнему указывает на дно стека (вернее на первое слово под ним). Делаем выводы и далее жмем один раз F7, чтобы войти в подпрограмму.

Как, уже говорилось, команда call уменьшила содержимое регистра SP на два (0040-2=003E), поместила в стек адрес следующей за ней команды (000F), прибавила к адресу следующей за ней команды смещение до первой исполняемой команды подпрограммы (000F+5=14h) и поместила это значение (14h) в регистр IP.
Далее жмем два раза F8.

Команда ret, извлекла из стека адрес возврата из подпрограммы (000F) и поместила его в регистр IP, а затем увеличила значение регистра SP на два, вернув указатель стека, опять на его дно.
Ну и покажем вывод нашей мега программы. Она два раза выводит знаменитую фразочку, но без перевода каретки на новую строку (так как мы это не запрограммировали.

C:\ASM\HELLO\TASM>hello8
-=* Hello World *=--=* Hello World *=-
C:\ASM\HELLO\TASM>

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

Директивы END, PROC, ENDP

Директива END

end start_label

Этой директивой завершается любая программа на ассемблере. В роли необязательного операнда здесь выступает метка (или выражение), определяющая адрес, с которого начинается выполнение программы (точка входа в программу). Если программа состоит из нескольких модулей, только один файл может содержать начальный адрес, так же как в языке C только один файл может содержать функцию main(). Операнд может быть опущен, если программа не предназначена для выполнения, например, если ассемблируются только определения данных, или эта программа должна быть скомпанована с другим (главным) модулем.

Директива PROC

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

метка proc язык тип USES регистры ; TASM

или

метка proc тип язык USES регистры ; MASM/WASM
...
ret
метка endp

Все операнды PROC необязательны.

Тип может принимать значения NEAR и FAR, и если он указан, все команды RET в теле процедуры будут заменены соответственно на RETN и RETF. По умолчанию подразумевается, что процедура имеет тип NEAR в моделях памяти TINY, SMALL и COMPACT.

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

USES — список регистров, значения которых изменяет процедура. Ассемблер помещает в начало процедуры набор команд PUSH, а перед командой RET — набор команд POP, так что значения перечисленных регистров будут восстановлены.

Директива ENDP определяет конец процедуры и имеет имя, аналогичное имени в директиве PROC.

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

Стек

Стеком называют область программы для временного хранения произвольных данных. Разумеется, данные можно сохранять и в сегменте данных, однако в этом случае для каждого сохраняемого на время данного надо заводить отдельную именованную ячейку памяти, что увеличивает размер программы и количество используемых имен. Удобство стека заключается в том, что его область используется многократно, причем сохранение в стеке данных и выборка их оттуда выполняется с помощью эффективных команд push и pop без указания каких-либо имен.
Стек традиционно используется, например, для сохранения содержимого регистров, используемых программой, перед вызовом подпрограммы, которая, в свою очередь, будет использовать регистры процессора "в своих личных целях". Исходное содержимое регистров извлекается из стека после возврата из подпрограммы. Другой распространенный прием - передача подпрограмме требуемых ею параметров через стек. Подпрограмма, зная, в каком порядке помещены в стек параметры, может забрать их оттуда и использовать при своем выполнении.
Отличительной особенностью стека является своеобразный порядок выборки содержащихся в нем данных: в любой момент времени в стеке доступен только верхний элемент, т.е. элемент, загруженный в стек последним. Выгрузка из стека верхнего элемента делает доступным следующий элемент.
Элементы стека располагаются в области памяти, отведенной под стек, начиная со дна стека (т.е. с его максимального адреса) по последовательно уменьшающимся адресам. Адрес верхнего, доступного элемента хранится в регистре-указателе стека SP. Как и любая другая область памяти программы, стек должен входить в какой-то сегмент или образовывать отдельный сегмент. В любом случае сегментный адрес этого сегмента помещается в сегментный регистр стека SS. Таким образом, пара регистров SS:SP описывают адрес доступной ячейки стека: в SS хранится сегментный адрес стека, а в SP - смещение последнего сохраненного в стеке данного (смотрим рисунок). Обратите внимание на то, что в исходном состоянии указатель стека SP указывает на ячейку, лежащую под дном стека и не входящую в него.

а - исходное состояние, б - после загрузки одного элемента (в данном примере - содержимого регистра АХ), в - после загрузки второго элемента (содержимого регистра DS), г - после выгрузки одного элемента, д - после выгрузки двух элементов и возврата в исходное состояние.

Загрузка в стек осуществляется специальной командой работы со стеком push (протолкнуть). Эта команда сначала уменьшает на 2 содержимое указателя стека (SP), а затем помещает операнд по адресу в SP. Если, например, мы хотим временно сохранить в стеке содержимое регистра АХ, следует выполнить команду

push АХ

Стек переходит в состояние, показанное на рис. б. Видно, что указатель стека смещается на два байта вверх (в сторону меньших адресов) и по этому адресу записывается указанный в команде проталкивания операнд. Следующая команда загрузки в стек, например,

push DS

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

pop DS
pop AX

Состояние стека после выполнения первой команды показано на рис. г, а после второй - на рис. д. Для правильного восстановления содержимого регистров выгрузка из стека должна выполняться в порядке, строго противоположном загрузке - сначала выгружается элемент, загруженный последним, затем предыдущий элемент и т.д.
Совсем не обязательно при восстановлении данных помещать их туда, где они были перед сохранением. Например, можно поместить в стек содержимое DS, а извлечь его оттуда в другой сегментный регистр - ES;

push DS
pop ES ; Теперь ES=DS, а стек пуст

Это распространенный прием для перенесения содержимого одного регистра в другой, особенно, если второй регистр - сегментный.
Обратите внимание (см. рисунок) на то, что после выгрузки сохраненных в стеке данных они физически не стерлись, а остались в области стека на своих местах. Правда, при "стандартной" работе со стеком они оказываются недоступными. Действительно, поскольку указатель стека SP указывает под дно стека, стек считается пустым; очередная команда push поместит новое данное на место сохраненного ранее содержимого АХ, затерев его. Однако пока стек физически не затерт, сохраненными и уже выбранными из него данными можно пользоваться, если помнить, в каком порядке они расположены в стеке. Этот прием часто используется при работе с подпрограммами.
Какого размера должен быть стек? Это зависит от того, насколько интенсивно он используется в программе. Если, например, планируется хранить в стеке массив объемом 10 000 байт, то стек должен быть не меньше этого размера. При этом надо иметь в виду, что в ряде случаев стек автоматически используется системой, в частности, при выполнении команды прерывания int 21h. По этой команде сначала процессор помещает в стек адрес возврата, а затем DOS отправляет туда же содержимое регистров и другую информацию, относящуюся к прерванной программе. Поэтому, даже если программа совсем не использует стек, он все же должен присутствовать в программе и иметь размер не менее нескольких десятков слов.
Что произойдет, если программист по ошибке или умышленно не опишет стек в своей программе? Изменим нашу мега программу, удалив из нее описание стека.

В регистр DX помещен код двух знаков @. Это сделано для наглядности примера.
Запустим программу под отладчиком.

Видим, что в SS находится тот же адрес, что и в CS; от сюда можно сделать вывод, что сегменты команд и стека совпадают. Однако содержимое SP равно нулю. Первая же команда push уменьшит содержимое SP на 2, то есть поместит в SP число -2, значит ли это, что стек будет расти как ему и положено, ввех, но не внутри сегмента команд, а над ним, по адресам -2, -4, -6 и т.д. относительно верхней границы сегмента команд. Оказывается это не так.
Если взять 16-разрядный двоичный счетчик, в котором записн 0, и послать в него два вычитающих импульса, то после первого в нем окажется число FFFFh, а после второго – FFFEh. При желании мы можем рассматривать число FFFEh как -2 (что имеет место при работе со знаковыми числами, о которых поговорим чуть позже), однако процессор при вычислении адресов рассматривает содержимое регистров как целые числа без знака и число FFFEh оказывается эквивалентным не -2, а 65534. В результате первая же команда занесения данного в стек (в нашем случае push dx) поместит это данное не над сегментом команд, а в самый его конец, в последнее слово по адресу CS:FFFEh (что, в данном примере, эквивалентно SS:FFFEh). При дальнейшем использовании стека его указатель будет смещаться в сторону меньших адресов, проходя значения FFFCh, FFFAh и т.д.
Таким образом, если в программе отсутствует явное определение стека, система сама создает стек по умолчанию в конце сегмента команд или, точнее, по адресу FFFEh относительно начала сегмента команд (см. рисунок).

Выполним программу дальше и убедимся в этом. По ходу выполнения программы с первой команды до седьмой, мы сможем видеть, что в стек помещаются какие-то значения, но при этом регистр SP не изменяется (предположительно, все это самодеятельность отладчика), и только после выполнения седьмой команды – push dx, SP принимает значение FFFEh, и на дне стека мы видим два символа @.

Рассмотренное явление, когда при уменьшении адреса после адреса 0 у нас получается адрес FFFEh, т.е. от начала сегмента мы прыгнули сразу в его конец, носит название циклического возврата или оборачивания адреса. С этим явлением приходится сталкиваться довольно часто.
Расположение стека в конце сегмента команд не приводит к каким-либо неприятностям, пока размер программы далек от граничной величины 64Кбайт. В этом случае начало сегмента команд занимают коды команд, конец – стек, а между ними располагаются данные (если сегмент данных описан в программе после сегмента команд). Если, однако, размер сегментов команд или данных приближается к 64Кбайт, то фактически стек будет наложен на тот или иной сегмент программы и возникнет опасность затирания данных или кода. Очевидно, что этого нельзя допускать. В тоже время, система не проверяет, что происходит со стеком и ни как не реагирует на затирание команд или данных. Таким образом, оценка размеров собственно программы, данных и стека является важным этапом разработки программы.

воскресенье, 24 мая 2009 г.

И снова "Здравствуй МИР!"

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

Как уже не раз говорилось, обращение к памяти осуществляется исключительно посредством сегментов - логических образований, накладываемых на любые участки физического адресного пространства. Начальный адрес сегмента, деленный на 16, т.е. без младшей шестнадцатеричной цифры, заносится в один из сегментных регистров; после этого мы получаем доступ к участку памяти, начинающегося с заданного сегментного адреса.
Каким образом понятие сегментов памяти отражается на структуре программы? Следует заметить, что структура программы определяется, с одной стороны, архитектурой процессора (если обращение к памяти возможно только с помощью сегментов, то и программа, видимо, должна состоять из сегментов), а с другой - особенностями той операционной системы, под управлением которой эта программа будет выполняться. Наконец, на структуру программы влияют также и правила работы выбранного транслятора - разные трансляторы (например MASM и TASM в режиме IDEAL) предъявляют несколько различающиеся требования к исходному тексту программы (хотя TASM по умолчанию работает в режиме совместимости с MASM ).
Следует заметить, что при вводе исходного текста программы с клавиатуры можно использовать как прописные, так и строчные буквы; транслятор воспринимает, например, строки mov ax,data и mov ax.data одинаково. Однако с помощью соответствующих ключей можно заставить транслятор различать прописные и строчные буквы в отдельных элементах предложений. Предложения языка ассемблера могут содержать комментарии, которые отделяются от предложения языка знаком точки с запятой (;). при необходимости комментарий может занимать целую строку (тоже, естественно, начинающуюся со знака ";"). Поскольку в языке ассемблера нет знака завершения комментария, комментарий нельзя вставлять внутрь предложения языка, как это допустимо делать во многих языках высокого уровня. Каждое предложение языка ассемблера, даже самое короткое, должно занимать отдельную строку текста.
В нашей программе описаны три сегмента: сегмент команд с именем code, сегмент данных с именем data и сегмент стека с именем stk. Описание каждого сегмента начинается с ключевого слова segment, предваряемого некоторым именем, и заканчивается ключевым словом ends, перед которым указывается то же имя, чтобы транслятор знал, какой именно сегмент мы хотим закончить. Имена сегментов выбираются вполне произвольно. текст программы заканчивается директивой ассемблера end, завершающей трансляцию. В качества операнда этой директивы указывается точка входа в программу; в нашем случае это метка begin.
Порядок описания сегментов в программе, как правило, не имеет значения. Часто программу начинают с сегмента данных, это несколько облегчает чтение программы, и в некоторых случаях устраняет возможные неоднозначности в интерпретации команд, ссылающиеся на данные, которые еще не описаны. Мы в начале программы расположили сегмент команд, за ним - сегмент данных и в конце - сегмент стека; такой порядок предоставляет некоторые удобства при отладке программы. Важно только понимать, что в оперативную память компьютера сегменты попадут в том же порядке, в каком они описаны в программе (если специальными средствами ассемблера не задать иной порядок загрузки сегментов в память).
Сегменты вводятся в программу с помощью директив ассемблера segment и ends. Что такое директива ассемблера? В тексте программы встречаются ключевые слова двух типов: команды процессора (mov, int) и директивы транслятора (в данном случае термины "транслятор" и "ассемблер" являются синонимами, обозначая программу, преобразующую исходный текст, написанный на языке ассемблера, в коды, которые будут при выполнении программы восприниматься процессором). К директивам ассемблера относятся обозначения начала и конца сегментов segment и ends; ключевые слова, описывающие тип используемых данных (db, dup); специальные описатели сегментов вроде stack и т. д. Директивы служат для передачи транслятору служебной информации, которой он пользуется в процессе трансляции программы. Однако в состав выполнимой программы, состоящей из машинных кодов, эти строки не попадут, так как процессору, выполняющему программу, они не нужны. Другими словами, операторы типа segment и ends не транслируются в машинные коды, а используются лишь самим ассемблером на этапе трансляции программы.
Еще одна директива ассемблера используется в первом предложении программы:

assume cs:code,ds:data

здесь устанавливается соответствие сегмента code сегментному регистру cs и сегмента data сегментному регистру ds. Первое объявление говорит о том, что сегмент code является сегментом команд, и встречающиеся в этом сегменте метки принадлежат именно этому сегменту, что помогает ассемблеру правильно транслировать команды переходов (при использовании транслятора masm эта часть объявления необходима в любой, даже самой простой программе).
Второе объявление помогает транслятору правильно обрабатывать предложения, в которых производится обращение к полям данных сегмента data. Ранее уже отмечалось, что для обращения к памяти процессору необходимо иметь две составляющие адреса: сегментный адрес и смещение. Сегментный адрес всегда находится в сегментном регистре. Однако в процессоре два сегментных регистра данных, ds и es, и для обращения к памяти можно использовать любой из них. Разумеется, процессор при выполнении команды должен знать, из какого именно регистра он должен извлечь сегментный адрес, поэтому команды обращения к памяти через регистры ds или es кодируются по-разному. Объявляя соответствие сегмента data регистру ds, мы предлагаем транслятору использовать вариант кодирования через регистр ds.
Однако отсюда совсем не следует, что к моменту выполнения команды с обращением к памяти в регистре ds будет содержаться сегментный адрес требуемого сегмента. Более того, можно гарантировать, что нужного адреса в сегментном регистре не будет. Директива assume влияет только на кодирование команд, но отнюдь не на содержимое сегментных регистров. Поэтому практически любая программа должна начинаться с предложений, в которых в сегментный регистр, используемый для адресации к сегменту данных (как правило, это регистр ds) заносится сегментный адрес этого сегмента. Так сделано и в нашем примере с помощью двух команд

mov ax,data ;настроим ds
mov ds,ax ;на сегмент данных

с которых начинается наша программа. Сначала значение имени data (т.е. адрес сегмента data) загружается командой mov в регистр общего назначения процессора ах, а затем из регистра ах переносится в регистр ds. Такая двухступенчатая операция нужна потому, что процессор в силу некоторых особенностей своей архитектуры не может выполнить команду непосредственной загрузки адреса в сегментный регистр. Приходится пользоваться регистром ах в качестве "перевалочного пункта".
Поместив в регистр ds сегментный адрес сегмента данных, мы получили возможность обращаться к полям этого сегмента. поскольку в программе может быть несколько сегментов данных, операционная система не может самостоятельно определить требуемое значение ds, и инициализировать его приходится "вручную".
Назначением нашей программы является вывод на экран текстовой строки " -=* Hello World *=-", описанной в сегменте данных.
Следующие предложения программы как раз и выполняют эту операцию. Делается это не непосредственно, а путем обращения к служебным программам операционной системы MS-DOS, которую мы для краткости будем в дальнейшем называть просто DOS. Дело в том, что в составе команд процессора и, соответственно, операторов языка ассемблера нет команд вывода данных на экран (как и команд ввода с клавиатуры, записи в файл на диске и т.д.). Вывод даже одного символа на экран в действительности представляет собой довольно сложную операцию, для выполнения которой требуется длинная последовательность команд процессора. Конечно, эту последовательность команд можно было бы включить в нашу программу, однако гораздо проще обратиться за помощью к операционной системе. В состав DOS входит большое количество программ, осуществляющих стандартные и часто требуемые функции - вывод на экран и ввод с клавиатуры, запись в файл и чтение из файла, чтение или установка текущего времени, выделение или освобождение памяти и многие другие.
Для того, чтобы обратиться к DOS, надо загрузить в регистр общего назначения ah номер требуемой функции, в другие регистры - исходные данные для выполнения этой функции, после чего выполнить команду hit 21h (int - от interrupt, прерывание), которая передаст управление DOS. Вывод на экран строки текста можно осуществить функцией 09h, которая требует, чтобы в регистрах ds:dx содержался полный адрес выводимой строки. регистр ds мы уже инициализировали, осталось поместить в регистр dx относительный адрес строки, который ассоциируется с именем поля данных msg. Длину выводимой строки указывать нет необходимости, так как функция 09h DOS выводит на экран строку от указанного адреса до символа доллара, который мы предусмотрительно включили в выводимую строку. Заполнив все требуемые, для конкретной функции регистры, можно выполнить команду int 21h, которая осуществит вызов DOS.
Как завершить выполняемую программу? В действительности завершение программы - это довольно сложная последовательность операций, в которую входит, в частности, освобождение памяти, занятой завершившейся программой, а также вызов той системной программы (конкретно - командного процессора command.com), которая выведет на экран запрос DOS, и будет ожидать ввода следующих команд оператора. Все эти действия выполняет функция DOS с номером 4ch. Эта функция предполагает, что в регистре al находится код завершения нашей программы, который она передаст DOS. Если программа завершилась успешно, код завершения должен быть равен 0, поэтому мы в одном предложении mov ax,4c00h загружаем в ан 4ch, а в al - 0, и вызываем DOS уже знакомой нам командой int 21h.

При загрузке программы сегменты размещаются в памяти, как показано на рисунке.

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

Это еще раз подчеркивает важнейшую особенность архитектуры процессоров intel: адрес любой ячейки памяти состоит из двух слов, одно из которых определяет расположение в памяти соответствующего сегмента, а другое - смещение в пределах этого сегмента. Смысл сегментной части адреса, хранящейся всегда в одном из сегментных регистров, в реальном и защищенном режиме различен; в МП 8086 сегментная часть адреса, после умножения ее на 16, определяет физический адрес начала сегмента в памяти.
Отсюда следует, что сегмент всегда начинается с адреса, кратного 16, т.е. на границе 16-байтового блока памяти (параграфа). Сегментный адрес можно рассматривать, как номер параграфа, с которого начинается данный сегмент. Размер сегмента определяется объемом содержащихся в нем данных, но никогда не может превышать величину 64 кбайт, что определяется максимально возможной величиной смещения.
Сегментный адрес сегмента команд хранится в регистре cs, а смещение к адресуемому байту - в указателе команд ip. Как уже отмечалось, после загрузки программы в ip заносится смещение первой команды программы; процессор, считав ее из памяти, увеличивает содержимое ip точно на длину этой команды (команды процессоров intel могут иметь длину от 1 до 6 байт), в результате чего ip указывает на вторую команду программы. Выполнив первую команду, процессор считывает из памяти вторую, опять увеличивая значение ip. в результате в ip всегда находится смещение очередной команды, т. е. команды, следующей за выполняемой. Описанный алгоритм нарушается только при выполнении команд переходов, вызовов подпрограмм и обслуживания прерываний.
Сегментный адрес сегмента данных обычно хранится в регистре ds, a смещение может находится в одном из регистров общего назначения, например, в dх или si. Однако в МП 8086 два сегментных регистра данных - ds и es. Дополнительный сегментный регистр es часто используется для обращения к полям данных, не входящим в программу, например к видеобуферу или системным ячейкам. Однако при необходимости его можно настроить и на один из сегментов программы. в частности, если программа работает с большим объемом данных, для них можно предусмотреть два сегмента и обращаться к одному из них через регистр ds, а к другому - через es.

суббота, 23 мая 2009 г.

Goodbye debugger :)

В результате экспериментов с сегментами, программа "Hello World", мутировала в программу "Goodbye debugger" :)
Вывел два штамма, далее было лень, так как надо изучать ассемблер. А то получилось, что язык еще не изучил, а сносить крышу отладчикам уже научился. В общем, случайным образом, «открыл» пару антиотладочных приемов.
Итак первого мутанта в студию :)

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

Этот скриншот еще до выполнения первой команды:

А вот что получаем по ходу выполнения:

А это то, что выводит програ вместо сообщения Hello World, под отладчиком:

Забавненько, да?!

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


Исполнение третье команды приводит вот к такому результату:

После чего отладчик вываливается в командную строку :)

Вот так и свернули голову отладчику, даже не научившись еще программировать :)

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

пятница, 22 мая 2009 г.

Директива MODEL

Сегодня поиграем с упрощенными директивами сегментации. Итак прога:

Стек хоть и не используется в нашей программе, определен для наглядности, размер стека так же выбран маленький чтобы его можно было увидеть в отладчике.
Смотрим листинг и EXE, созданные TASM:

Обращаем внимание на то что выделено красным, а так же что хотя мы определили данные и стек перед кодом, упрощенные директивы TASM, расположили сперва код, потом данные, потом стек. Причем хотя для данных в листинге видим выравнивание типа WORD, по факту TASM сделал выравнивание типа PARA, что видно на скрине экзешника. Так же стоит помедитировать на идентификаторы (Symbol Name) созданые TASM.

Теперь посмотрим как эту прогру оттранслировал MASM:

MASM обошёлся с атрибутами выравнивания более праведным образом. Из скрина EXE видно, что данные выравнены в соответствии с атрибутом WORD, что соответствует листингу программы. Соответственно размер программы оттранслированной MASM меньше на 14 байт, чем то, что сделал TASM.
Теперь побыстрому проги в отладчик и медитируем :)

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

Ну и на последок, так же прога но в режиме IDEAL TASM.

TASM оттранслировал ее с тем же размером, что и в режиме совместимости с MASM. В режиме IDEAL, как помним, чуток изменены названия директив определения сегментов.