Assembler / P20
.pdf20.Модульное программирование Термин "модульность" применительно к программированию имеет два смысла:
разбиение программы на подпрограммы;
размещение текста программы в различных файлах.
До сих пор мы создавали программы, которые умещались в одном файле. Для
небольших учебных программ это было вполне достаточно. Но большие программы целесообразно хранить в нескольких файлах. Часто наборы служебных подпрограмм (например, для ввода-вывода) хранятся в одном файле, так как там находятся тексты отлаженных, проверенных подпрограмм. Новые программы размещаем в других файлах. Трансляция каждого исходного файла в объектный файл проводится с использованием Ассемблера. Далее эти объектные файлы объединяются в загрузочный файл с помощью компоновщика. Для такого объединения компоновщик должен располагать, в частности, информацией:
какие имена являются общими для модулей;
как объединять логические сегменты (программные секции). В следующих разделах мы подробно изучим эти аспекты.
20.1. Директивы связи Рассмотрим пример.
Из заданной строки получить новую строку, в которой строчные латинские буквы исходной строки преобразованы в прописные. Подпрограмму преобразования разместить
вотдельном файле.
Вглавной программе, размещенной в файле main.asm определены две строки: String — исходная строка, завершенная знаком $, NewStr — преобразованная строка. Подпрограмма ToUpper, размещенная в файле sub.asm, преобразует те символы строки, которые являются строчными латинскими буквами, в соответствующие прописные буквы. Адрес строки String передается в регистре SI. Строка NewStr является глобальным объектом для обоих модулей (файлов). Так сделано в учебных целях. (Было бы логичнее адрес NewStr передавать в регистре DI.) При этом строка NewStr определена в файле main.asm. В этом же файле имеется директива PUBLIC NewStr, которая делает это имя доступным для программ в других файлах. В файле sub.asm с помощью директивы EXTRN NewStr:BYTE:50 транслятору сообщается, что имя NewStr определено в другом файле, имеет тип: байты и размер 50. Тип нужен для правильного кодирования команд (в данном случае указание типа излишне, так как в программе используется только адрес строки). Число 50 в нашем примере также можно было не указывать. Далее мы приведем пример, когда указание такого числа может принести пользу. Аналогичные директивы используются для имени подпрограммы ToUpper.
main.asm
INCLUDE macro.inc
.MODEL small
.STACK 100h
PUBLIC NewStr EXTRN ToUpper:PROC
.DATA
String DB "aB2",CRLFT NewStr DB 50 DUP(?)
.CODE start:mov ax,@data
mov ds,ax
message String ; Вывод исходной строки mov si, OFFSET String
call ToUpper ; Преобразование строки
2
message NewStr ; Вывод новой строки exit
END start
Подпрограмма, описанная в sub.asm, может быть реализована более эффективно с использованием так называемых строковых команд, которые мы изучим позднее. sub.asm
JUMPS
.MODEL small
EXTRN NewStr:BYTE:50 PUBLIC ToUpper
.CODE
; вход si – адрес строки, заканчивающейся $
ToUpper PROC
mov di, OFFSET NewStr
n:mov al,[si] ; Очередной символ строки - в AL
cmp al,'a' ; Если символ входит в диапазон 'a'-'z' jnae copy ; - преобразовать его
cmp al,'z' jnbe copy
sub al,'a' - 'A' ; Перевести символ в верхний регистр copy:mov [di],al ; Поместить символ в новую строку
inc si ; Переместить указатели inc di
cmp al,'$' ; Конец строки ? jne n
ret ToUpper ENDP
END ; Обратите внимание, что метки стартового адреса нет, ; потому что такой метки нет в файле
Вариант с использованием строковых команд:
ToUpper PROC
mov ax, @data mov es,ax cld
mov di, OFFSET NewStr
n:lodsb ; Очередной символ строки - в AL
cmp al,'a' |
; Если символ входит в диапазон 'a'-'z' |
jnae copy |
; - преобразовать его |
cmp al,'z' |
|
jnbe copy
sub al,'a' - 'A' ; Перевести символ в верхний регистр
copy:stosb |
; Поместить символ в новую строку |
cmp al,'$' |
; Конец строки ? |
jne n |
|
ret |
|
ToUpper ENDP |
|
Команды для получения исполняемого файла c:\tasm\bin\tasm /z/zi/la/m main+sub c:\tasm\bin\tlink /v/m main sub
Здесь оба файла с исходными текстами обрабатываются одной командой tasm. При компоновки выполняемый файл получает имя первого из объектных файлов, перечисленных в списке.
3
Итак, в модуле, где имя определено, но должно быть "известно" другим модулям, используется директива PUBLIC имя. В модуле, для которого имя является внешним, используется другая директива: EXTRN имя: тип: количество.
Вдирективе EXTRN для имени указывается его тип:
для имен данных: BYTE, WORD, DWORD…
для имен процедур: NEAR, FAR, либо PROC (тогда NEAR или FAR определяется моделью памяти).
Если тип указан для имен данных, то полезно указывать также и количество
элементов, если количество опущено, то по умолчанию оно полагается равным одному. Упражнение. Посмотрите листинги и карту памяти для main.asm и sub.asm. Что
нового в них появилось благодаря использованию внешних имен? Что произойдет, если закомментировать директивы PUBLIC и EXTRN?
Если нужно указать, что внешними являются несколько имен, то в директивах PUBLIC
иEXTRN они перечисляются через запятую.
Вдирективе PUBLIC тип имени указывать не нужно, он и так известен транслятору из текста модуля. В директиве EXTRN тип имени нужен для генерации правильных кодов команд. Пусть, например, процедура ToUpper должна принудительно менять первый символ строки на символ '*'. Перед ret добавим команду mov NewStr, '*', а директиву
EXTRN NewStr:BYTE:50 заменим на EXTRN NewStr.
Упражнение. Что при этом произойдет?
Ответ. Трансляция пройдет без замечаний, а в отладчике мы увидим команду mov word ptr [0006], 002A. Коды символов в строке NewStr станут такими: 2A 00 32 0D 0A 24.
Если бы Ассемблер "знал", что NewStr — массив байтов, то была бы сгенерирована команда mov byte ptr [0006], 2A.
20.2. Сегментные директивы
До сих пор мы использовали упрощенные сегментные директивы (.CODE, .DATA). При более изощренном программировании может возникнуть потребность свободнее распоряжаться характеристиками логических сегментов. Поэтому нужно наряду с упрощенными сегментными директивами освоить и стандартные сегментные директивы. Также их нужно знать еще по двум причинам:
эти директивы используются в литературе по языку Ассемблера (зачастую без особой необходимости);
при программировании на языках высокого уровня можно получать листинг на языке Ассемблера, при этом используются стандартные сегментные директивы (мы будем работать с ними).
Прежде всего, надо подчеркнуть отличие физического и логического сегмента. Мы уже знаем, что физический сегмент — область памяти размером 64 К, ее начальный адрес лежит на границе параграфа. Логический сегмент (программный сегмент, программная секция) — часть программы. Но связь между этими понятиями есть. Логический сегмент определяет область, адрес которой лежит в сегментном регистре.
Логический сегмент начинается с директивы
имя SEGMENT атрибуты
и завершается директивой
имя ENDS
Пример. |
|
|
data1 |
SEGMENT |
|
p |
DW 2 |
|
q |
DW 3 |
|
data1 |
ENDS |
4
data2 |
SEGMENT |
r DW 0 |
|
data2 |
ENDS |
cseg SEGMENT
s:mov ax, data1 mov ds, ax mov ax, data2 mov es, ax mov ax, p add ax, q mov r,ax
mov ax, 4C00h int 21h
cseg ENDS END s
Здесь атрибуты отсутствуют. Они принимаются по умолчанию (чуть позже, мы узнаем, какие именно). Для ссылки на содержимое сегмента нужно загрузить сегментный регистр
mov ax, data1 mov ds, ax
(Напомню, что команда mov ds, data1 недопустима, поэтому приходится использовать регистр AX как "промежуточное звено".)
Эту программу tasm отказывается транслировать. Для команды mov ax, p и двух следующих он выдает сообщение об ошибке:
**Error** s1.ASM(13) Can't address with currently ASSUMEd segment registers (Не могу адресовать с текущим предполагаемым сегментным регистром)
Исправление:
mov ax, ds:p add ax, ds:q mov es:r,ax
Программа нормально транслируется и работает. В листинге мы видим строку
15 0011 26: A3 0000r mov es:r,ax
В коде команды присутствует префикс переопределения сегмента.
Неудобно перед всеми именами переменных расставлять префиксы. Избежать этого можно, применяя директиву ASSUME. Перед строкой с меткой s вставим директиву:
ASSUME ds:data1, es:data2
Теперь префиксы можно снять. А в листинге мы увидим, что префикс добавляется автоматически:
16 0011 26: A3 0000r mov r,ax
Сама по себе директива ASSUME не загружает регистры DS и ES нужными значениями. Она дает Ассемблеру информацию о связи сегментного регистра с соответствующим сегментом.
Внесем в программу еще одно изменение: после команды mov r,ax добавим команды: cmp r, 4
jg t inc ax
t:
(Не следует искать в этих командах скрытого смысла.)
**Error** s1.ASM(18) Near jump or call to different CS
В сегменте кода появилась ссылка на символическое имя t. Чтобы избежать сообщения об ошибке нужно добавить директиву
ASSUME cs:cseg
а еще проще добавить к уже имеющейся директиве еще один параметр
5
ASSUME ds:data1, es:data2, cs:cseg
Сообщение об ошибке исчезнет.
20.3. Параметры директивы SEGMENT.
Полная форма директивы SEGMENT имеет вид:
имя SEGMENT align combine use ‗class‘
Здесь align — выравнивание, combine — объединение, use — использование, ‗class‘ — класс.
Выравнивание. Этот параметр может принимать значения: BYTE, WORD, DWORD, PARA, PAGE. Может возникает вопрос: как можно выравнивать сегменты на границы байта, если начало сегмента заведомо начинается с границы параграфа, т.е. адрес кратен 16. Ответ следующий: если есть несколько модулей, содержащих сегмент с одинаковым именем, и эти сегменты должны пристыковываться друг к другу последовательно (это определяется параметром combine), то часть сегмента из первого модуля будет выровнена на границу параграфа, а последующие части будут пристыкованы к первой части с «зазором», который определяется параметром align. Например, если align — это BYTE, то «зазора» не будет. Если align — это WORD, то «зазора» не будет или он составит один байт (если предыдущая часть сегмента «закончилась» на нечетном адресе).
Упражнение. В двух файлах с расширением .asm определите сегменты данных с одинаковыми именами и параметром align равным DWORD. Проследите в отладчике эффект выравнивания адреса при объединении сегментов. В одном файле такого эффекта не будет.
Решение.
файл s.asm
n |
SEGMENT dword |
k DB 2,3 |
|
n |
ENDS |
cseg |
SEGMENT |
ASSUME cs: cseg
s:mov ax, 4C00h
int 21h cseg ENDS END s
файл s1.asm
n SEGMENT dword p DB 1
n ENDS END
c:\tasm\bin\tasm /zi s+s1 c:\tasm\bin\tlink /v s s1 c:\tasm\bin\td s.exe
Объединение. Если сегменты с одним именем расположены в одном модуле, то они объединяются последовательно. Если же сегменты находятся в разных модулях, то их объединение регулируется параметром combine. Этот параметр может принимать следующие значения:
PRIVATE — сегмент не должен объединяться с другими сегментами.
COMMON — начальный адрес всех сегментов одинаков, т.е. они перекрываются. Общий размер сегмента — это размер самого большого сегмента с данным именем.
6
PUBLIC — происходит конкатенация сегментов с одним и тем же именем. Общий размер сегмента — сумма размеров всех таких сегментов.
STACK — конкатенация сегментов, причем SS:SP указывает при запуске на конец сегмента.
Использование. Этот параметр принимает два значения: use16 (используются 16-битовые сегменты, которые содержат до 64K кода или данных) и use32 (используются 32-битовые сегменты, которые содержат до 4Г кода или данных).
Класс. Если логическому сегменту назначено имя класса, то компоновщик собирает вместе все сегменты с одинаковым именем класса.
20.4. Модели памяти Это набор правил, по которым компилятор и компоновщик распределяют память для
кода и данных. В частности, определяется, какие адреса будут использованы при доступе к коду или данным сегмента: ближние (если обращение к одному сегменту) или дальние (если обращение к нескольким сегментам).
Tiny — код, данные и стек в одном сегменте.
Small — код в одном сегменте, данные и стек в другом.
Compact — код в одном сегменте, данные в нескольких сегментах. Medium — код в нескольких сегментах, данные в одном сегменте. Large — код в нескольких сегментах, данные в нескольких сегментах.
При программировании в так называемом защищенном режиме используется всего одна модель — плоская.
Flat — то же, что tiny, но сегменты 32-разрядные.
.MODEL модель, язык.
20.5. Пример использования стандартных сегментных директив. Перепишем нашу первую программу.
sseg SEGMENT para stack use16 ‗stack‘ DB 100h DUP(?)
sseg ENDS
dseg SEGMENT word use16 ‗data‘
msg DB ―Hello, world!‖, 0Dh, 0Ah, ‗$‘ dseg ends
cseg SEGMENT word use16 ‗code‘ ASSUME cs:cseg, ds:dseg
start: mov ax, dseg mov ds, ax
mov dx, OFFSET msg mov ah,9h
int 21h mov ah,4Ch int 21h cseg ENDS END start
Группы.
Удобно объединять несколько логических сегментов (с различными именами) в область, адрес которой находится в сегментном регистре. Тогда все эти логические сегменты будут использовать только одно содержимое сегментного регистра. Его не
7
нужно будет переадресовывать. При этом общий объем логических сегментов, входящих в группу не должен превосходить размер физического сегмента.
Директива имеет вид
имя_группы GROUP имя_сегмента1, имя_сегмента2, …
Пример группы мы видели уже в нашей первой программе. Сегмент данных и сегмент стека были объединены в одну группу с именем DGROUP. Встроенная переменная @data содержит адрес группы. Это соответствует директивам
DGROUP GROUP _DATA, STACK
20.6. Ассемблерный код, генерируемый компилятором Си.
До сих пор мы изучали ассемблерный код, соответствующий программе на языке Си, только в отладчике. Но есть возможность получить файл на языке Ассемблера.
bcc –S c31v0.c
Будет получен файл c31v0.asm. Переименуем его в c31v0_16.asm.
_DATA |
segment word public 'DATA' |
|
d@ |
label byte |
|
d@w |
label word |
|
_DATA |
ends |
|
_BSS |
segment word public 'BSS' |
|
b@ |
label byte |
|
b@w |
label word |
|
_BSS |
ends |
|
_DATA |
segment word public 'DATA' |
|
_A |
label word |
|
|
db |
1 |
|
db |
0 |
|
db |
5 |
|
db |
0 |
|
db |
2 |
|
db |
0 |
|
db |
7 |
|
db |
0 |
|
db |
5 |
|
db |
0 |
|
db |
6 |
|
db |
0 |
|
db |
3 |
|
db |
0 |
_DATA ends
_TEXT segment byte public 'CODE' assume cs:_TEXT,ds:DGROUP
_main proc near
;
;int main() {
push bp mov bp,sp sub sp,2 push si push di
;int i, j, k;
;for (i = 0; i < dimA; i++)
xor si,si
jmp short @1@4
@1@2:
;
; printf("%d ", A[i]);
8
; |
|
|
|
mov |
bx,si |
|
add |
bx,bx |
|
push |
word ptr DGROUP:_A[bx] |
|
mov |
ax,offset DGROUP:s@ |
|
push |
ax |
|
call |
near ptr _printf |
|
pop |
cx |
|
pop |
cx |
|
inc |
si |
@1@4: |
|
|
|
cmp |
si,7 |
|
jl |
short @1@2 |
; |
|
|
; |
|
|
; |
i = j = dimA - 1; k = 0; |
|
; |
|
|
|
mov |
ax,6 |
|
mov |
di,ax |
|
mov |
si,ax |
|
mov |
word ptr [bp-2],0 |
|
jmp |
short @1@16 |
@1@6: |
|
|
; |
|
|
;while (j > 0) {
;if (A[j] == 5) {
mov bx,di
add |
bx,bx |
cmp |
word ptr DGROUP:_A[bx],5 |
jne |
short @1@15 |
; |
|
; |
B[k++] = &A[j]; |
; |
|
mov |
ax,di |
add |
ax,ax |
add |
ax,offset DGROUP:_A |
mov |
bx,word ptr [bp-2] |
add |
bx,bx |
mov |
word ptr DGROUP:_B[bx],ax |
inc |
word ptr [bp-2] |
jmp |
short @1@9 |
@1@8: |
|
; |
|
;while (j > 0 && A[--j] != 5) {
;A[i--] = A[j];
; |
|
mov |
bx,di |
add |
bx,bx |
mov |
ax,word ptr DGROUP:_A[bx] |
mov |
bx,si |
add |
bx,bx |
mov |
word ptr DGROUP:_A[bx],ax |
dec |
si |
@1@9: |
|
or |
di,di |
jle |
short @1@11 |
dec |
di |
mov |
bx,di |
add |
bx,bx |
cmp |
word ptr DGROUP:_A[bx],5 |
jne |
short @1@8 |
@1@11: |
|
; |
|
9
;}
;if (j == 0 && A[0] == 5)
or |
di,di |
jne |
short @1@14 |
cmp |
word ptr DGROUP:_A,5 |
jne |
short @1@14 |
; |
|
; |
B[k++] = &A[j]; |
; |
|
mov |
ax,di |
add |
ax,ax |
add |
ax,offset DGROUP:_A |
mov |
bx,word ptr [bp-2] |
add |
bx,bx |
mov |
word ptr DGROUP:_B[bx],ax |
inc |
word ptr [bp-2] |
@1@14: |
|
; |
|
; |
} |
; |
|
jmp |
short @1@16 |
@1@15: |
|
; |
|
;else {
;i--;
dec |
si |
; |
|
; |
j--; |
; |
|
dec |
di |
@1@16: |
|
or |
di,di |
jg |
short @1@6 |
; |
|
;}
;}
;for (j = 0; j <= i; j++)
|
xor |
di,di |
|
jmp |
short @1@20 |
@1@18: |
|
|
; |
|
|
; |
A[j] = -1; |
|
; |
|
|
|
mov |
bx,di |
|
add |
bx,bx |
|
mov |
word ptr DGROUP:_A[bx],-1 |
|
inc |
di |
@1@20: |
|
|
|
cmp |
di,si |
|
jle |
short @1@18 |
; |
|
|
; |
printf("\n\n"); |
|
; |
|
|
|
mov |
ax,offset DGROUP:s@+4 |
|
push |
ax |
|
call |
near ptr _printf |
|
pop |
cx |
; |
|
|
; |
for (i = 0; i < dimA; i++) |
|
; |
|
|
|
xor |
si,si |
10
jmp short @1@24 @1@22:
;
; printf("%d ", A[i]);
;
|
mov |
bx,si |
|
add |
bx,bx |
|
push |
word ptr DGROUP:_A[bx] |
|
mov |
ax,offset DGROUP:s@+7 |
|
push |
ax |
|
call |
near ptr _printf |
|
pop |
cx |
|
pop |
cx |
|
inc |
si |
@1@24: |
|
|
|
cmp |
si,7 |
|
jl |
short @1@22 |
; |
|
|
; |
printf("\n"); |
|
; |
|
|
|
mov |
ax,offset DGROUP:s@+11 |
|
push |
ax |
|
call |
near ptr _printf |
|
pop |
cx |
; |
|
|
; |
for (i = 0; i < k; i++) |
|
; |
|
|
|
xor |
si,si |
|
jmp |
short @1@28 |
@1@26: |
|
|
; |
|
|
; |
|
printf("%p ", B[i]); |
; |
|
|
|
mov |
bx,si |
|
add |
bx,bx |
|
push |
word ptr DGROUP:_B[bx] |
|
mov |
ax,offset DGROUP:s@+13 |
|
push |
ax |
|
call |
near ptr _printf |
|
pop |
cx |
|
pop |
cx |
|
inc |
si |
@1@28: |
|
|
|
cmp |
si,word ptr [bp-2] |
|
jl |
short @1@26 |
; |
|
|
; |
return 0; |
|
; |
|
|
|
xor |
ax,ax |
|
jmp |
short @1@30 |
@1@30: |
|
|
; |
|
|
; |
} |
|
; |
|
|
|
pop |
di |
|
pop |
si |
|
mov |
sp,bp |
|
pop |
bp |
|
ret |
|
_main endp |
|
|
_TEXT ends |
|
|
_BSS |
segment word public 'BSS' |
|
_B |
label word |
|
|
db |
14 dup (?) |