Глава 2. Адресное пространство, структура программ.

Нет, - ответила Маргарита,-более всего меня поражает, где все это помещается.

М.А. Булгаков. Мастер и Маргарита.

Кто может вместить, да вместит.

Евангелие от Матфея.

Программы для компьютера состоят из команд микропроцессора, представляю­щих собой машинные коды. Для выполнения программы она должна быть помещена в память компьютера. Специальный регистр микропроцессора содержит всегда адрес следующей исполняемой команды. В памяти компьютера содержатся и данные, кото­рые могут использоваться исполняемой программой. Нет никакого различия между командами микропроцессора и данными. В определенных ситуациях команды про­граммы могут рассматриваться как данные и, наоборот, данные становятся фрагмен­тами программы. Таким образом, нет никакого препятствия к тому, чтобы программа в процессе выполнения изменяла саму себя. В языках высокого уровня введены пре­поны к самомодификации программ, при программировании же на ассемблере Вы свободны делать любые, втом числе и самомодифицирующиеся программы. "Дурной вкус!" - воскликнут приверженцы "высокого штиля" в программировании. Давайте будем.терпимее - кому что нравится. Нельзя же все время слушать Баха.

Память ЭВМ состоит из однобайтовых ячеек. Каждой ячейке присваивается ад­рес - номер по порядку (от 0 и далее). Такой адрес мы будем называть физическим. Однако применительно к ІВМ РС принято представлять адрес в виде двух компонент. Связано это с тем, что регистры микропроцессора не могут вместить числа длиной большей двух байт (напоминаем читателю, что мы пока рассматриваем микропроцес­сор 8086, другие представители данного семейства будут рассмотрены позднее) - мак­симальное число РТРБН (шестнадцатеричная система!). Поэтому для формирования адреса используются два регистра (см. ниже). Используя же двухкомпонентное пред­ставление, можно адресовать память до одного мегабайта.

Двухкомпонентный (логический) адрес мы будем записывать в виде ЯАОА, где $А - адрес сегмента, ОА - смещение в этом сегменте. Обычный физический адрес ячейки памяти можно получить издвухкомпонентного адреса по формуле: БА* 16+ОА. Умножение на 16 равносильно сдвигу влево на четыре бита. Таким образом мы полу­чаем возможность оперирования 20-битовыми адресами. Использование двухкомпо-нентного адреса с необходимостью приводит нас к разбиению памяти на сегменты (біс!). Размер сегмента не может превышать 64 КЬ. Шестнадцатибайтовую величину принято называть параграфом. Легко видеть, что сегмент должен начинаться на границе параграфа. Отметим также, что если физический адрес у ячейки один, то двух-компонентных адресов у нее может быть несколько, разумеется, все они будут равно­правны. Например, двухкомпонентные адреса FF3AH:2367H и FF38H:2387H указы-ваютнаодну итужеячейку. Лишний раз подчеркнем, что сегментация памяти - след­ствие структуры микропроцессора, а именно размеров его адресных регистров. Начи­ная с микропроцессора 80386,32-битные регистры позволяют адресовать память без использования сегментации. Фактически вся память в такой модели рассматривается как один большой сегмент - микропроцессор в этом случае непосредственно опериру­ет с физическими адресами. Но об этом речь еще впереди.

Используя служебные слова, в программе на ассемблере можно резервировать оп­ределенное количество байт, которыепотом можно использовать дляхранения каких-либо данных. Вотэти служебные слова: DB - резервировать байт, DW- резервировать слово (два байта), DD - резервировать двойное слово (четыре байта), DQ - резервиро­вать восемь байт, DT - резервировать десять байт. Рассмотрим следующий фрагмент:

DB 78Н       ;будет зарезервирован байт, которому присвоится ; значение 76jH

DW 1234Н    /будет зарезервировано два байта,  причем в /младший

/будет помещено число 34Н,   а в  старший  12Н (!) DQ  02f503   /будет зарезервировано  восемь  байт,   причем байты /в памяти будут располагаться в следующем /порядке:

/3,F5h,2,0   (старший байт оказался равным нулю!) DB 12,34     /будет зарезервировано два байта:   младший 12, /старший 34

в программе на языке ассемблера встречается символ или группа символов в ка-

вычках,тотрансляторпоменяетихнабайты,соответствующиеихА8СПкодам.Например:

DB семнадцать байт, куда

/помещаются байты, соответствующие /ASCII кодам записанной строки / см. также Рис .1.1.

Адресом любой из выше указанной цепочки считается адрес младшего байта. Предположим, в Вашей программе будет следующая строка: LAB DW7589H. Тогда команда MOVAL,BYTE PTR LAB загрузит в однобайтный регистр AL микропроцес­сора число 89Н. Как Выуже, наверное, догадались LAB, есть не что иное, как метка7.

7 Точнее было бы назвать ее переменной. Метки в программе на языке ассемблера указывают на команды, переменные - на блоки данных. Однако в языке ассемблера, как ив машинном языке, нет различия между кодом и данными. В любой момент командыможно рассматри­вать как данные^ -а блок данных как фрагмент программы.    . ; , .-

После трансляции всем меткам программы присваивается соответствующеечисловое значение - смещение в сегменте. Служебные слова перед LAB (BYTE PTR) означают, что загружается именно байт. Для того чтобы загрузить старший байт указанного сло­ва, следует выполнить команду MOV AL,BYTE PTR LAB+1. Для загрузки всего сло­ва, скажем, в регистр DX, служит команда MOVDX,IAB.

Чтобы резервировать группу байт, удобно использовать служебное слово DUP. Ниже в программах Вы увидите, как это делается.

Работать с ячейками памяти можно по-разному. С первым способом адресации Вы уже познакомились - можно просто указать метку. Такая адресация называется прямой. К метке можно добавить какое-либо число. Другим способом адресации яв­ляется косвенная адресация. Она использует регистр ВХ. Команда MOVAX,[BX] оз­начает загрузить в регистр АХ слово, адрес которого лежит в регистре ВХ. Адрес в регистр ВХ можно загрузить командой MOVBX,OFFSET LAB или LEA BX,LAB. К регистру ВХ можно добавлять смещение, например, MOVAX,[BX]+2 или просто MOV АХ,[ВХ]2. Такая адресация называется адресацией по базе. Результирующий адрес

получается сложением содержимого ВХ и смещения.

Следующий способ адресации называется прямой адресацией с индексированием. Здесь используют индексные регистры DI и SI. Например, следующие две команды

MOV DI,8

MOV АХ,LAB[DI]

приведут к тому, что в регистр АХ будет загружено слово, находящееся по адресу LAB+8. Последний способ адресации - это адресация по базе с индексированием. Приведем здесь просто команду без комментария: MOVCX,[BX][Di]+2.

Регистр BP очень похож на регистр ВХ, с тем лишь различием, что по умолчанию он адресует ячейки сегмента стека:

MOV АХ, [ВХ] - загрузка из сегмента данных (DS) , MOV АХ, [BP]   - загрузка из сегмента стека    (SS) .

Впрочем, сегмент можно указать явно, и тогда команда MOV AX,SS:[BX] - будет загружать слово из сегмента стека. Аналогично для BP- команда MOVAX,DS:[BP] заг­ружает из сегмента данных (точнее, из сегмента, на который показывает регистр DS).

Описанные способы адресации памяти можно использовать не только в командах загрузки, но при выполнении различных арифметических операций. Например, ко­манда ADD DI,LAB[SI] прибавляет содержимое ячейки, адрес которой получается сложением LAB и содержимого SI, кчислу, которое находится в регистре DI. Резуль­тат сложения помещается в регистр DI.

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

(см. Рис. 1.1 и Рис. 1.3). Минимальное количество сегментов - один. Вот как раз такие программы и могут быть преобразованы к СОМ-виду. Классическая струк­тура программы состоит из трех сегментов: сегмент кода, сегмент данных, сег­мент стека. Данная структура какраз соответствует программе на Рис. 1.3. Но мы необязаныограничиватьсебятолько.тремясегментами - можно, например, сде­лать два сегмента данных, три сегмента кода и т.д. и т.п. Кстати, сейчас Вам долж­но быть понятно, почемудлина СОМ-программыне может превышать 64К (мак­симальный размер сегмента).

Теперь настал черед поговорить о сегментных регистрах - в микропроцессорах 8088/8086 их всего четыре. В них хранятся сегментные адреса: С8 - сегмент кода, - сегмент данных, 88 - сегмент стека, Е8 - дополнительный сегмент. Чтобы не было недоразумений, хочу подчеркнуть, что это не означает, что в программе может быть только четыре сегмента. Например, вначале указывал на один сегмент данных, затем на другой- то же можно сказать и о других регистрах. На Рис. 2,1 представлена программа с двумя сегментами данных.

TITLE PRIMER21 . ; сегмент данных 1 D1SEG SEGMENT

ТЕХТ1  DB   ' ПРОГРАММА НАЧИНАЕТ РАБОТАТЬ. $ '

D1SEG ENDS ;сегмент данных 2 D2SEG SEGMENT

ТЕХТ2   DB    'ПРОГРАММА ЗАКАНЧИВАЕТ  СВОЮ  РАБОТУ.$'

D2SEG ENDS

;сегмент стека /резервируется 3 0 байт

;впрочем в данной программе  стек не используется STSEG   SEGMENT STACK

LB  60   DUP(O) ; 30 байтам присваивается  значение О

STSEG ENDS

CODSEG SEGMENT

ASSUME   CS:CODSEG,    DS:D1SEG,    SS:STSEG BEGIN:

сегмент стека MOV AX,STSEG MOV SS,AX

направляем на первый сегмент данных MOV AX,D1SEG MOV DS,AX

LEA DX,TEXT1      ; DS:DX теперь указывают на TEXT1 MOV AH,9

;ждем нажатие клавиши MOV АН, О

INT 16H

;DS направляем на   второй  сегмент данных MOV AX,D2SEG MOV DS,AX

LEA  DX,TEXT2      ; DS: DX теперь указывают  на TEXT2 MOV   АН, 9 -

INT 21H

;выходим в  операционную систему

MOV

INT 21H CODSEG ENDS

END BEGIN

Рис. 2.1. Пример программы с двумя сегментами данных.

А вот программа с двумя сегментами кода. Возможно, в ней Вам не все понятно, но это дело времени. Обращаю Ваше внимание на то, что переход между сегментами кода осуществляется командой длинного перехода. Фактически команда длинного перехода представляет собой одновременную замену содержимого сразудвух регистров CS и IP.

; сегмент данных DATA SEGMENT

ТЕХТ1 DB 'Я в сегменте 1', 13,10, ' $ * ТЕХТ2 DB 'Я в сегменте 2\13,10,'$'

DATA ENDS

;сегмент стека

STSEG  SEGMENT STACK DW 20 DUP(?)

STSEG ENDS

сегмент кода

SEGMENT

ASSUME   CS:CODE1,    DS:DATA BEGIN:

MOV

MOV DS,AX

MOV

MOV SS,AX

LEA DX,TEXT1 MOV AH, 9

INT   21H .

во  второй сегмент JMP  FAR PTR BEG CODE2 . .

_C,0DE1: . ...

.. MOV AH,4CH

INT 21H CODE1 ENDS

; второй сегмент кода CODE2 SEGMENT

ASSUME   CS:CODE2,    DS:DATA BEG_CODE2:

LEA DX,TEXT2

MOV AH, 9

INT 21H

/возвращаемся в  первый сегмент

.    JMP FAR PTR _CODE1 CODE2 ENDS

END BEGIN

Рис. 2.2. Пример программы с двумя сегментами кода.

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

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

2. В конце программы после служебного слова END нужно указать метку начала программы. Дело в том, что СОМ-программане может содержать какую-либо инфор­мацию для загрузчика, в том числе и точку входа в программу. Для ЕХЕ-программы точка входа может быть в любом месте программы. В принципе, для СОМ-программы метку можно не указывать вообще, но в этом случае придется учитывать сдвиг адре­сов (на 100Н байт).

3. Используядирективу GROUP можно преобразовать к СОМ-формату програм­му, состоящую из нескольких сегментов (см. Приложение 3). Сегмент стека в такой программе, однако, должен отсутствовать.

Служебное слово ASSUME является директивой транслятора. Оно сообщает ему значение сегментных регистров. Для С8такаядирективаобязательна. Для регистров DS.SS и ES эти указания ассемблеру можно не делать. Иногда, однако, могут возник­нуть некоторые затруднения. В частности, может появиться сообщение о фазовой ошибке (Phase error) в случае ссылки вперед: MOVAL.LL, где LL находится в стар­ших адресах (точнее ниже). Ошибка при трансляции возникает из-за неопределенного значения регистра DS для ассемблера. При этом не важно, что программно сегмент­ный регистр определен: MOV AX,DATA/MOVDS,AX.

При запуске операционная система устанавливает вершину стека

в старший адрес сегмента программы (подробнее о стеке см. Главу 3). Стек, таким образом, будет расти в сторону программного кода. Если пространства для стека не достаточно, то следует установить в старшие адреса

-. Для оперативного! управления памятью DOS разбивает ее на блоки, каждый из которых имеет заголовок. Такое деление является недокументированным, т.е. разра­ботчики не гарантируют, что в последующих версиях операционной системы они не откажутся от такого подхода. Однако МСВ (Memory Control Block - контрольный блок

памяти) сохранился вплоть до седьмой версии MS DOS, и, судя по всему, фирма

Microsoft не собирается с ним расставаться.

Итак, вся памятьделится на блоки, а каждый блок имеет заголовок длиной Іббайт. Ниже (Рис. 2.3) приведена структура такого заголовка.

Смещение Длина поля

Содержание

0

1

М - если блок не последний Z - если блок последний.

1

2

сегментный адрес владельца,   если блок памяти не занят.

3

2

длина блока в параграфах (SIZE); если S - сегментный адрес данного заголовка, то заголовок следующего блока будет находиться по адресу S+SIZE+1.

5

3

Резерв

 

 

 

8

_ _ ^

зарезервировано; начиная с пятой версии MS DOS первые восемь байт используются для имени владельца.

Рис.2.3. Структура заголовка блока памяти.

Благодаря использованию блочной структуры имеется возможность осуществлять некоторое подобие многозадачного режима. Программа может быть запущена, а затем остаться в памяти. Причем программы, которые будут запускаться после нее, не смо­гут повредить ее код, если будут пользоваться стандартными ООЗ'овскими процеду­рами. Просто свободной памяти для них станет меньше. Такой подход используется в частности для создания резидентных драйверов, которые, постоянно находясь в памя­ти, осуществляют управление какими-либо внешними устройствами. С резидентны­ми программами мы с Вами еще не раз встретимся. В частности, этому вопросу будут посвящены несколько глав. Кстати, большая часть вирусов имеет как раз резидентный характер.

Ни в коем случае не следует портить заголовок блоков - это может привести толь­ко к катастрофическим результатам, т.к. операционная система постоянно проверяет целостность всей цепочки блоков.

Рассказ наш будет неполным, если не упомянуть еще об одной структуре - векто­ре. Вектором называют адрес некоторой процедуры, расположенной в памяти. При­чем в двух байтах адрес сегмента, а в двух смеще­ние. Предположим, при просмотре памяти обнаружена следующая цепочка байт, ко­торая, как Вы полагаете, является вектором: Е26778ВОН. Адрес процедуры будет тог­да следующий: В078Н:67Р2Н (объяснитепочему). Начало оперативной паМяТИ.Отвс*-дят как раз Под такие вектбры, которые операционной системой во время ее загрузки устанавливаются на свои процедуры. Векторы принято нумеровать, причем если но­мер вектора К, то он располагается по адресу ©:4*М. Команда ДОТ21Н - есть не чтоиное, как вызов процедуры операционной системы, на которую указывает вектор под номером 21Н (он расположен по адресу 0:84Н=2Щ*4). Аналогично INT 16Н- вызов процедуры, на которую направлен вектор с номером 16Н. Такие процедуры мы в даль­нейшем будем называть прерываниями. Умение грамотно работать с векторами пре­рываний особенно необходимо при написании резидентных программ (см. Главы 11,12 и 17). В заключение приведем карту памяти IBM PC (Рис. 2.4.).

Карта, приведеннаянаРис. 2.4,являетсянеслишкомподробной. ВлитературеВы сможете найти более подробную картину. Однако на данном этапе нам вполне доста­точно такой карты. В процессе движения вперед Вы более подробно узнаете те облас­ти, которые приведены на рисунке. Используя утилиту -UTIL. СОМ из антивирусного пакета Е. Касперского (старого), Вы сможете и самостоятельно изучать структуру па­мяти операционной системы MS DOS, что, несомненно, доставит Вам большее удо­вольствие, чем рассматриваниеданногорисунка.

0О0ОН:0000Н

Начало таблицы векторов

0040Н:ООООН

Данные BIOS

0050Н:ООООН

Данные DOS

ХХХХН-.0000Н

Область памяти, куда загружаются файлы и

 

MSDOS.SYS И где операционная система хранит свои данные.

 

Здесь же помещаются загружаемыеданные.

ХХХХН:0000Н

Здесь помещается резидентная часть COMMAND.COM

ХХХХН:0О0ОН

Область, где обычно располагаются резидентные программы.

ХХХХН:0000Н

Область памяти, куда помещается запускаемая программа.

ХХХХН:0000Н

Область памяти, куда помещается транзитная часть

 

COMMAND.COM. Может затираться исполняемой программой.

 

По выходу в MS DOS восстанавливается с диска.

А000Н:0000Н

Начало видеопамяти для VGA адаптера. В текстовом режиме

 

используется только 32 К., начиная с адреса 0В800Н.

сооонюооон

НачалоГОУ.

Рис. 2.4. Карта памяти операционной системы MSDOS. ХХХХЫзначает

переменный адрес.

Прокомментируем Рис. 2.4. Во-первых, область до начала видеопамяти называет­ся базовой. Эту память в основном и использует операционная система MS DOS, здесь помещаютсяисполняемыепрограммыиданные,имииспользуемые. Адресное про­странство, начиная с А000:0000 идо конца ПЗУ, называют UMB - Upper Memory Block, т.е. верхний блок памяти. Этот блок, однако, не полностью используется ПЗУ и видео­памятью. Незанятые блоки используются для эмуляции так называемой дополнитель­ной памяти (expanded). Эмуляция дополнительной памяти на машинах класса ЕХТпроизводилась с помощью специальной аппаратуры и программного обеспечения. На машинах класса AT дополнительная память получается с помощью специальный драй­веров из расширенной памяти (см. ниже). Кроме того, на машинах AT 80386 появи­' лась возможность использовать UMB для переноса части ПЗУ в ОЗУ. Для этого также необходимоналичиерасширеннойпамяти.Расширеннаяпамять-этапамятьсадреса-ми свыше 1 Мб. Если известно, что на Вашем компьютере имеется 8 Мб памяти, то это означает, что часть ее входит в базовую память - 640 Кб, остальная память - расши­ренная (т.е. приблизительно 7.3 Мб). Доступ красширенной памяти можно получить через прерывания 15Н(см. Глава 5) или посредством перехода в защищенный режим (см. Главу 20). Подробно о расширенной и дополнительной памяти см. Главу 22. Под­черкнем лишний раз, что деление памяти на базовую и расширенную память есть пре­рогатива операционной системы MS DOS и является следствием ее несовершенности. Операционная система Windows лишена этого недостатка (см. главы 24,25).

Вопросы структуры программы, затронутые в данной главе, найдут своедальней-шее развитие в Главе

В заключение мне хотелось бы представить Вам (возможно, несколько забегая впе­ред) забавную, намойвзгляд, программу (Рис. 2.5)iОнадемонстрируетследующие сделанные мною утверждения:

1. Нет никаких различий между данными и кодом программы.

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

DATA SEGMENT

ASSUME CS:DATA /строка необходима,   чтобы использовать

/метку в данном сегменте - обман транслятора

команды будут перемещены в  сегмент кода и там выполнены

L1:     /метка и одновременно переменная

/вывод буквы А на экран MOV DL,65 /два байта MOV АН, 2   /два байта-INT 21Н .   /два байта

DATA ENDS

STAC SEGMENT STACK

DB 50 DUP(?) STAC ENDS CODE SEGMENT

ASSUME CS-.CODE,   SS-.STAC,  DS: DATA BEGIN:

: MOV AX,DATA

MOV DS, AX

I I Л SI,LI

LEA 1)1.1 2 , .

/перемещаем данные  из   сегмента  данных  в   сегмент кода

MOV  АХ, WORD   PTR [SI]

MOV WORD PTR CS: [DI] ,AX

MOV AX,WORD PTR [SI]+2

MOV WORD PTR CS:[DI]+2,AX

MOV AX,WORD PTR [SI]+4

MOV WORD PTR CS : [DI) +4, AX ; необходимость такого  перехода будет осмыслена Вами /несколько позднее

JMP L2

/резервная область,    куда   будут   перемещены команды /ИЗ сегмента команд

L2 :      /метка и одновременно переменная NOP NOP NO.P

ЮР

NOP

NOP

MOV АН,4CH

INT 21H CODE ENDS

END BEGIN

Рис. 2.5. Демонстрирует возможность использования команд в качестве данных.

Если программа на Рис. 2.5 покажется Вам непонятной - вернитесь к ней после главы 4.