Ассемблер Это просто! Учимся программировать

       

Сегментация памяти в DOS


Возьмем следующее предложение: "Изучаем сегменты памяти". Теперь давайте посчитаем, на каком месте стоит буква "ы" в слове "сегменты" от начала предложения, включая пробелы... На шестнадцатом. Подчеркну, что мы считали от начала предложения.

Теперь немного усложним задачу и разобьем предложение следующим образом (символом "_" обозначен пробел):

Пример № 1:

0000: Изучаем_

0010: сегменты_

0020: памяти

0030:

В слове "Изучаем" символ "И" стоит на нулевом месте; символ "з" на первом, "у" на втором и т.д. В данном случае мы считаем буквы начиная с нулевой позиции, используя два числа. Назовем их сегмент и смещение. Тогда, символ "ч" будет иметь следующий адрес: 0000:0003, т.е. сегмент 0000, смещение 0003. Проверьте...

В слове "сегменты" будем считать буквы начиная с десятого сегмента, но с нулевого смещения. Тогда символ "н" будет иметь следующий адрес: 0010:0005, т.е. пятый символ начиная с десятой позиции: 0010 - сегмент, 0005 - смещение. Тоже проверьте...

В слове "память" считаем буквы начиная с 0020 сегмента и также с нулевой позиции. Т.о. символ "а" будет иметь адрес 0020:0001, т.е. сегмент - 0020, смещение - 0001. Опять проверим...

Итак, мы выяснили, что для того, чтобы найти адрес нужного символа, необходимы два числа: сегмент и смещение внутри этого сегмента. В Ассемблере сегменты хранятся в сегментных регистрах: CS, DS, ES, SS (), а смещения могут храниться в других (но не во всех). Не все так сложно, как кажется. Опять-таки, со временем Вы поймете принцип.

Регистр CS служит для хранения сегмента кода программы (Code Segment - сегмент кода);

Регистр DS - для хранения сегмента данных (Data Segment - сегмент данных);

Регистр SS - для хранения сегмента стека (Stack Segment - сегмент стека);

Регистр ES - дополнительный сегментный регистр, который может хранить любой другой сегмент (например, сегмент видеобуфера).

Пример № 2:


Давайте попробуем загрузить в пару регистров ES:DI сегмент и смещение буквы "м" в слове "памяти" из Примера № 1 (см. выше). Вот как это запишется на Ассемблере:

(1) mov ax,0020 (2) mov es,ax (3) mov di,2

Теперь в регистре ES находится сегмент с номером 20, а в регистре DI - смещение к букве (символу) "м" в слове "памяти". Проверьте, пожалуйста...

Здесь стоит отметить, что загрузка числа (т.е. какого-нибудь сегмента) напрямую в сегментный регистр запрещена. Поэтому мы в строке (1) загрузили сегмент в AX, а в строке (2) загрузили в регистр ES число 20, которое находилось в AX:

mov ds,15 // ошибка!

mov ss,34h // ошибка!

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

CS = 5674h

DS = 5674h

SS = 5674h

ES = 5674h

Иначе говоря: CS=DS=SS=ES=5674h

Код программы типа *.com должен начинаться со смещения 100h. Для этого мы, собственно, и ставили в наших прошлых примерах программ оператор ORG 100h, указывая Ассемблеру при ассемблировании использовать смещение 100h от начала сегмента, в который загружена наша программа (позже мы рассмотрим почему так). Сегментные же регистры, как я уже говорил, автоматически принимают значение того сегмента, в который загрузилась наша программа.

Пара регистров CS:IP задает текущий адрес кода. Теперь рассмотрим, как все это происходит на конкретном примере:

Пример № 3.

(01) CSEG segment (02) org 100h (03) _start: (04) mov ah,9 (05) mov dx,offset My_name (06) int 21h (07) int 20h (08) My_name db 'Dima$' (09) CSEG ends (10) end _start

Итак, строки (01) и (09) описывают сегмент:




  • CSEG (даем имя сегменту) segment ( оператор Ассемблера, указывающий, что имя CSEG - это название сегмента);
  • CSEG ends (END Segment - конец сегмента) указывает Ассемблеру на конец сегмента.


Строка (02) сообщает, что код программы (как и смещения внутри сегмента CSEG) необходимо отсчитывать с 100h. По этому адресу в память всегда загружаются программы типа *.com.

Запускаем программу из Примера № 3 в отладчике. Допустим, она загрузилась в свободный сегмент 1234h. Первая команда в строке (04) будет располагаться по такому адресу:

1234h:0100h (т.е. CS = 1234h, а IP = 0100h) (посмотрите в отладчике на регистры CS и IP).

Перейдем к следующей команде (в отладчике CodeView нажмите клавишу F8, в AFD - F1, в другом - посмотрите какая клавиша нужна; будет написано что-то вроде "F8-Step" или "F7-Trace"). Теперь Вы видите, что изменились следующие регистры:


  • AX = 0900h (точнее, AH = 09h, а AL = 0, т.к. мы загрузили командой mov ah,9 число 9 в регистр AH, при этом не трогая AL. Если бы AL был равен, скажем, 15h, то после выполнения данной команды AX бы равнялся 0915h)
  • IP = 102h (т.е. указывает на адрес следующей команды. Из этого можно сделать вывод, что команда mov ah,9 занимает 2 байта: 102h - 100h = 2).


Следующая команда (нажимаем клавишу F8 / F1) изменяет регистры DX и IP. Теперь DX указывает на смещение нашей строки ("Dima$") относительно начала сегмента, т.е. 109h, а IP равняется 105h (т.е. адрес следующей команды). Нетрудно посчитать, что команда mov dx,offset My_name занимает 3 байта (105h - 102h = 3).

Обратите внимание, что в Ассемблере мы пишем:

mov dx,offset My_name

а в отладчике видим следующее:

mov dx,109 (109 - шестнадцатеричное число, но CodeView и многие другие отладчики символ 'h' не ставят. Это надо иметь в виду).

Почему так происходит? Дело в том, что при ассемблировании программы, программа-ассемблер (MASM / TASM) подставляет вместо offset My_name реальный адрес строки с именем My_name в памяти (ее смещение). Можно, конечно, записать сразу:



mov dx,109h

Программа будет работать нормально. Но для этого нам нужно высчитать самим этот адрес. Попробуйте вставить следующие команды, начиная со строки (07) в Примере № 3:

(07) int 20h (08) int 20h (09) My_name db 'Dima$' (10) CSEG ends (11) end _start

Просто продублируем команду int 20h (хотя, как Вы уже знаете, до строки (08) программа не дойдет).

Теперь ассемблируйте программу заново. Запускайте ее под отладчиком. Вы увидите, что в DX загружается не 109h, а другое число. Подумайте, почему так происходит. Это просто!

В окне "Memory" ("Память") отладчика CodeView (у AFD нечто подобное) Вы должны увидеть примерно следующее:

1234:0000CD 20 00 A0 00 9A F0 FE= .a.

№1№2№3№4

Позиция №1 (1234) - сегмент, в который загрузилась наша программа (может быть любым).

Позиция №2 (0000) - смещение в данном сегменте (сегмент и смещение отделяются двоеточием (:)).

Позиция №3 (CD 20 00 ... F0 FE) - код в шестнадцатеричной системе, который располагается с адреса 1234:0000.

Позиция №4 (= .a.) - код в ASCII (ниже рассмотрим), соответствующий шестнадцатеричным числам с правой стороны.

В Позиции №2 (смещение) введите значение, которое находится в регистре DX после выполнения строки (5). После этого в Позиции №4 Вы увидите строку "Dima$", а в Позиции №3 - коды символов "Dima$" в шестнадцатеричной системе... Так вот что загружается в DX! Это не что иное, как АДРЕС (смещение) нашей строки в сегменте!

Но вернемся. Итак, мы загрузили в DX адрес строки в сегменте, который мы назвали CSEG (строки (01) и (09) в Примере № 3). Теперь переходим к следующей команде: int 21h. Вызываем прерывание DOS с функцией 9 (mov ah,9) и адресом строки в DX (mov dx,offset My_name).

Как я уже говорил раньше, для использования прерываний в программах, в AH заносится номер функции. Номера функций желательно запоминать (хотя бы часто используемые) с тем, чтобы постоянно не искать в справочниках, что делает функция.




Содержание раздела