Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Пильщиков

.pdf
Скачиваний:
155
Добавлен:
09.05.2015
Размер:
6.35 Mб
Скачать

180

Программирование на языке ассемблера IBM PC Глава 1

Поскольку мы отводим место не только для символов строки, но для ее текущей длины, то этот дополнительный байт надо учитывать при описании строки. Например, если заранее известно, что в строке не будет более 200 символов, то под нее надо отвести 201 байт:

S DB 201 DUP(?) ; n n m i a ( S ) < - 2 0 0

Отметим, что данное представление используется в языке Турбо Паскаль для строк типа string.

Рассмотрим пример обработки строк переменной длины при данном представлении их: требуется из строки S, длина которой не превосходит 200 и которая «уписана в сегменте данных, удалить первое вхождение символа'+', если такое есть.

Сначала, конечно, ищем '+' в S, для чего воспользуемся командой сканирования строки с подходящим префиксом повторения:

;поиск ' + ' в

с т р о к е

S

 

 

 

 

 

 

 

PDSH DS

 

 

 

 

 

 

 

 

 

POP

ES

 

 

 

 

 

 

 

 

 

LEA

D I f S + l

;ES:DI

-

а д р е с S [ l ]

( в о

не а д р е с S I )

CLD

 

;просмотр

строки

в п е р е д

 

MOV CL,S

 

 

 

 

 

 

 

 

 

MOV

CH,0

;СХ=текущая

 

длина

с т р о к и

( к а к слово)

MOV

A L , ' + '

;искомый

символ

 

 

 

REPNE SCASB

; п о и с к

' + '

в

S

 

 

 

JNE

FIN

; н е т

' + '

в

S

-> на

выход

 

Остановимся на минутку и отметим следующее.

Если V входит в S, тогда в этот момент пара регистров ES:DI будет указывать на символ, следующий за '+', а в регистре СХ будет находиться число символов строки, следующих за'+'.

Теперь надо удалить '+'. Что это значит? Поскольку оставлять пустое место нельзя, то надо сдвинуть "хвост" строки на одну позицию влево. Для этого можно воспользоваться командой пересылки строки MOVS. Перенести нужно СХ элементов, направление просмотра - вперед, поэтому менять регистр СХ и флаг DF не надо. В команде MOVS перенос происходит от адреса DS:SI к адресу ES:DI, поэтому устанавливаем регистр SI на символ, следующий за '+' ("откуда"), а регистр DI - на сам символ '+' ("куда"). Кроме того, надо уменьшить длину строки на 1. В итоге получаем такое окончание решения нашей задачи:

; у д а л е н и е ' + ' из S

 

 

 

MOV

S I , D I

;DS:SI

-

о т к у д а

DEC

DI

;ES:DI

-

куда

 

 

Динамические структуры данных

181

REP

MOVSB

; с д в и г " х в о с т а " строки иа

1

позицию влево

 

DEC

S

;уменьшение длины строки

на

1

 

FIN: . . .

10.3.Списки

Вэтом разделе рассматриваются способы представления и методы работы со списками в машинных программах.

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

Впоследнем звене размещается специальная "пустая" ссылка nil, указывающая на конец списка. Ссылка на первое звено списка хранится в некоторой переменной LIST; если список пуст, то ее значением является nil.

Возможность размещать звенья списков в любых местах памяти обуславливают плюсы и минусы списков (по сравнению с другим способом представления пос-

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

В большинстве ЭВМ, в частности в

ПК, не предусмотрено никакого стандар-

тного способа

представления списков

и нет специальных

команд

для работы

со списками.

Все это приходится реализовывать самим

авторам

программ.

Мы рассмотрим один из возможных вариантов того, как это можно сделать.

10.3.1.Представление списков

Начнем с представления списков в памяти ЭВМ.

Прежде всего рассмотрим, где размещаются звенья списков. Для их хранения обычно отводят специальную область памяти, называемую областью списков (list space) или кучей (heap); мы будем использовать термин "куча". Размер кучи зависит от того, сколь много списков будет использоваться в программе. Договоримся для определенности, что для наших программ достаточно кучи размером до 64 Кб, поэтому на кучу отведем один сегмент памяти (позже будет показано, как можно описать этот сегмент).

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

•ДИАЛОГ-МИФИ"

182

Программирование на языке ассемблера IBM PC Глава 1

Теперь о представлении ссылок. Ссылка на звено - это, конечно, адрес данного звена. Но какой адрес здесь имеется в виду - смещение А или адресная пара Н:А, где Н - начало кучи? Поскольку во всех этих адресных парах первый элемент (Н) один и тот же, то его можно явно не указывать, а лишь подразумевать. Поэтому ради экономии памяти договоримся считать ссылкой на звено лишь смещение А, т. е. адрес этого звена, отсчитанный от начала кучи. В связи с этим ссылками у нас будут 16-разрядные адреса.

Под каждое звено отводим несколько соседних байтов памяти, в первых из которых размещаем соответствующий элемент списка ELEM, а в остальных - ссылку на следующее звено NEXT:

Размер звена зависит от размера элементов списка. Для определенности будем считать, что элементами списка являются данные размером в слово, поэтому на элемент отводим два байта. А поскольку ссылка также занимает два байта, то всего на звено отводим 4 байта, т. е. двойное слово.

При программировании на ЯА звенья списка удобно рассматривать как структуры следующего типа:

NODE STRUC

;тип звена списка

ELEM DW

?

;элемент списка

NEXT DW

?

;ссылка на следующее звено

NODE ENDS

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

ES:A.ELEM - поле с элементом

ESrA.NEXT - поле с адресом следующего звена

Теперь* о пустой ссылке nil. Обычно в этом качестве используется адрес 0. Так будем делать и мы. При этом удобно описать в программе этот адрес как константу:

NIL EQU 0

и далее пользоваться именем NIL, а не нулем. Так более наглядно.

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

LIST DW ?

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

Динамические структуры данных

183

10.3.2.Операции над списками

При работе со списком приходится последовательно просматривать его звенья. Поэтому нам надо знать адрес текущего звена - того, которое мы обрабатываем в данный момент. Для хранения этого адреса будем использовать какой-нибудь регистр-модификатор, например, ВХ. Если текущим является i-e звено списка, тогда имеем следующую картинку:

Если надо сделать текущим какое-то иное звено, то мы обязаны в регистр ВХ занести адрес этого звена.

В регистре ВХ хранится только смещение текущего звена - адрес, отсчитанный от начала кучи. Поэтому, если мы хотим добраться до самого звена, то надо использовать выражение ES:[BX]; писать же просто [ВХ] нельзя, ибо, согласно правилам ПК, такой адрес по умолчанию будет сегментироваться по регистру DS. Что же касается доступа к полям текущего звена, то для этого используются выражения ES:[BX].ELEM и ES:[BX].NEXT.

Теперь рассмотрим, как реализуются операции над списками. Начнем

спростейших операций: анализ текущего элемента, переход к следующему звену

ипроверка на конец списка.

Анализ текущего элемента списка

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

MOV АХ,ES:[ВХ].ELEM

CMP AX,X

 

;if bx?.elem=x then goto eq

JE EQ

 

 

Переход к следующему звену списка

Пусть регистр ВХ

указывает на некоторое звено списка и требуется перейти

к следующему

звену

списка. Согласно нашей договоренности, это означает, что

в регистр ВХ

надо

заслать адрес следующего звена. Адрес же этот находится

в поле NEXT текущего звена, поэтому его надо перенести в ВХ:

MOV BX,ES:[ВХ].NEXT ;bx:=bxT.next

Проверка на конец списка

Может оказаться так, что следующего звена нет, тогда в регистр ВХ попадет пустая ссылка nil. Поэтому сравнением ВХ с nil и можно определить, дошли мы до конца списка или нет:

CMP BX,NIL

;if bx=nil then goto list_end

JE LIST_END

•ДИАЛОГ-МИФИ"

184 Программирование на языке ассемблера IBM PC Глава 1

Так реализуются простейшие, элементарные операции над списками. На их основе можно уже построить более крупные операции - просмотр списка, удаление элемента из списка, вставка нового элемента в список. Рассмотрим, как реализуются эти операции, на конкретных примерах.

Пример 1 (поиск элемента в списке)

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

Идея решения ясна: последовательно просматриваем звенья списка и сравниваем находящиеся в них элементы с X.

MOV AL,О

;искомая величина

MOV

сх,х

MOV

BX,LIST

;Ьх - nil или адрес 1-го звена

L: CMP BX,NIL

;конец списка -> по

JE NO

CMP ES: [ BX] . ELEM, СХ

;bxt.elem=x -> yes

JE YES

MOV

BX,ES:[BX].NEXT

;bx - адрес следующего звена

JMP

L

;В ЦИКЛ

YES: MOV AL,1

NO: . . .

Пример 2 (вставка элемента в список)

Требуется вставить в начало списка LIST новый элемент X.

Здесь прежде всего надо отвести место под новое звено. В языке Паскаль для этого используется стандартная процедура New. К сожалению, в ЯА такой процедуры нет. Но ее можно определить самим. Как это сделать, мы рассмотрим чуть позже, а пока будем считать, что такая процедура уже имеется и мы можем ею пользоваться. Уточним только ее действие.

Обращение к ней выглядит так:

CALL NEW ;new(d±)

У процедуры нет входных параметров. Она отыскивает в куче свободное место (двойное слово) и его адрес возвращает через регистр DI. Это место можно занять новым звеном.

После этого надо заполнить новое звено, Т. е. в поле ELEM записать величину X, а в поле NEXT - ссылку на бывшее первое звено (она берется из LIST), и далее в LIST надо записать адрес нового звена (берется из DI):

Динамические структуры данных

185

С учетом всего сказанного наша задача решается следующим образом:

CALL NEW

;new(di)

MOV AX,X

;dit.elem:«x

MOV ES:[DI]-ELEM,AX

MOV AX,LIST

;dit.next:»list

MOV ES:[DI].NEXT,AX

MOV LIST,DI

;list:=di

Пример 3 (удаление элемента из списка).

Требуется удалить из списка LIST второй элемент, если такой есть.

Пусть в списке есть второй элемент. Тогда сделаем так, чтобы регистр ВХ указывал на первое звено, а регистр DI - на второе звено:

Для исключения второго звена из списка надо изменить ссылку в первом звене - записать сюда ссылку на третье звено. Сделать это просто: ссылка на третье звено находится во втором звене, поэтому ее отсюда надо перенести в первое звено.

Однако лишь поменять ссылку в первом звене мало. Второе звено занимает место в памяти, а теперь оно стало ненужным, поэтому следует освободить это место. В языке Паскаль в этих целях используется стандартная процедура Dispose. В ЯА ее нет, однако ее можно определить самим. Чуть позже мы определим такую процедуру, а пока будем считать, что она уже имеется и мы можем ею пользоваться. Уточним лишь правила обращения к ней:

DI:=адрес ненужного звена

CALL DISPOSE ;dispose(di)

Сначала надо записать в регистр DI адрес звена, ставшего ненужным, а затем обратиться к процедуре, которая каким-то образом учтет, что это место кучи стало свободным.

С учетом всего сказанного наша задача решается так:

CMP LIST,NIL

;list=nil -> fin

JE FIN

MOV BX,LIST

;bx - адрес 1-го звена

MOV DI,ES:[BX].NEXT

;di - nil или адрес 2-го звена

CMP DI,NIL

;dx=nil -> fin

JE FIN

MOV AX,ES:[DI].NEXT

 

MOV ES:[BX].NEXT,AX

;bxT . next:=dil . next

CALL DISPOSE

;dispose(di)

FIN: . . .

 

•ДИАЛОГ-МИФИ"

 

186

Программирование на языке ассемблера IBM PC Глава 1

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

10.3.3. Организация кучи

Теперь займемся организацией кучи и описанием процедур New и Dispose. Напомним, что при программировании на ЯА все это мы обязаны делать сами, никто за нас этого делать не будет. Мы рассмотрим один из возможных вариантов того, как это можно сделать (этот вариант используется в трансляторах языка Лисп).

Список

свободной памяти

 

Как уже

было сказано,

для размещения звеньев всех списков,

используемых

в профамме, необходимо

отвести специальную область памяти,

называемую

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

Для этого следует все свободные ячейки кучи объединить в один список, который принято называть списком свободной памяти (ССП). Начало этого списка указывается в некоторой фиксированной ячейке, скажем, с именем HEAP_PTR (heap pointer, указатель кучи). Значит, помимо списков, с которыми работает программа, заводится еще один список. Используется же ССП так: когда программе нужно место под новое звено, то оно выделяется из этого ССП, а когда программа отказывается от какого-то звена, то это звено добавляется к ССП.

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

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

Где размещать переменную HEAP_PTR? Вообще говоря, где угодно. Но лучше всего отвести ей место в самом начале кучи - в ячейке с относительным адресом 0. Почему? А потому, что тогда ни одно звено не будет размещаться в этой ячейке и не будет иметь адрес 0. Значит, этот адрес остается свободным и его можно использовать для представления пустой ссылки nil, что мы и делали. Если же разместить HEAP_PTR в другом месте, то все равно эту ячейку кучи нельзя было бы занимать, и она пропала бы зря.

Описание сегмента кучи

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

Динамические структуры данных

187

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

х

HEAP_SIZE EQU п

;п - число з в е н ь е в в

к у ч е :

4*п<64К

HEAP

SEGMENT

; с е г м е н т

кучи

 

 

HEAP_PTR DW ?

; я ч е й к а

с начальным

а д р е с о м

ССП

 

DD HEAP_SIZE DUP(?)

; с о б с т в е н н о куча (п

двойных

слов)

HEAP

ENDS

 

 

 

 

Инициализация кучи

Мы договорились, что сегментный регистр ES указывает на начало сегмента кучи. Но, естественно, загрузку этого регистра обязаны сделать мы сами в начале работы программы.

Кроме

того, вначале все ячейки кучи

свободны, однако

они не

объединены

в ССП, и работать с

кучен пока еще нельзя. Поэтому в начале программы надо

связать

все ячейки

(двойные слова)

кучи в единый

список

и записать

вHEAP_PTR ссылку на начало этого списка. При этом совершенно безразлично,

вкаком именно порядке будут объединены эти ячейки. Например, их можно

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

Оба указанных действия - загрузку регистра ES и построение исходного ССП мы опишем в виде одной процедуры:

INIT_HEAP PROC FAR

PUSH SI

PUSH BX

POSH CX

; у с т а н о в к а ES

на начало с е г м е н т а

кучи

 

 

 

 

MOV CX , HEAP

 

 

 

 

 

 

MOV ES,CX

 

 

 

 

 

 

;объединение всех двойных с л о в

кучи в

ССП

 

 

 

MOV

CX,HEAP_SIZE

; ч и с л о

з в е н ь е в в ССП

 

MOV

BX,NIL

;ссылка на построенную

ч а с т ь ССП

MOV SI,4*HEAP_SIZE-2

; а д р е с

нового з в е н а

ССП

 

INIT1: MOV ES:[SI].NEXT,BX

 

; д о б а в и т ь в

начало

п о с т р о е н н о г о

MOV

ВХ,SI

;

ССП

новое

звено

 

 

SUB

S I , 4

; S I -

на двойное с л о в о

"выше"

•ДИАЛОГ-МИФИ"

188 Программирование на языке ассемблера IBM PC Глава 1

LOOP

INIT1

 

MOV

ES:HEAP_PTR,BX

;HEAP_PTR - на начало ССП

POP СХ

 

POP BX

 

POP

SI

 

RET

 

 

INIT_HEAP ENDP

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

Процедура

Dispose

К этой

процедуре программа обращается, когда она отказывается от

некоторого звена. Адрес этого звена, как мы договорились, передается через регистр DI. Это звено становится свободным, поэтому его надо просто присоединить к ССП. Проще всего звено добавить в начало ССП, а как вставлять звено в начало списка, мы уже знаем.

;на входе: DI - адрес ненужного звена DISPOSE PROC FAR

POSH

ES:HEAP_PTR

 

POP

ES:[DI].NEXT

; d i f . n e x t : - h e a p _ p t r

MOV

ES:HEAP_PTR,DI

; h e a p _ p t r : - d i

RET

 

 

DISPOSE ENDP

 

 

Процедура

New

 

К этой процедуре программа обращается, когда нужно место для нового звена какого-то списка. Это место берется из ССП: от этого списка отщепляется одно из звеньев и оно отдается программе. Проще всего отдать первое звено ССП, что и будем делать. Адрес этого звена, согласно договоренности, процедура должна вернуть через регистр DI. Поэтому процедура должна в регистр DI перенести значение HEAP_PTR, а затем в эту переменную записать ссылку на второе звено ССП (она берется из первого звена).

Однако здесь надо учитывать особый случай: ССП может быть пустым. Это может случиться, если программа захватила все ячейки кучи и обратно их не отдает. Что делать в этом случае? Надо просто прекратить выполнение программы, т. к. она все равно не сможет правильно продолжить свою работу (увеличить по ходу дела размер кучи, как правило, сложно). Так мы и сделаем: при пустом ССП сообщим об ошибке и завершим выполнение программы.

С учетом всего сказанного получаем такое описание процедуры New:

 

 

 

 

 

Динамические структуры данных 190

; н а

выходе: DI - а д р е с

с в о б о д н о г о

з в е н а

 

 

 

 

NEW PROC FAR

 

 

 

 

 

 

 

 

MOV

DI,ES:HEAP_PTR

; d i

-

n i l

или

а д р е с 1 - г о

з в е н а ССП

 

CMP

DI,NIL

 

 

 

 

 

 

 

 

JE EMPTY_HEAP

; п у с т о й ССП

->

EMPTY_HEAP

 

POSH ES:[DI].NEXT

 

 

 

 

 

 

 

 

POP

ES:HEAP_PTR

; h e a p _ p t r

-

на

2 - е з в е н о

ССП

 

RET

 

 

 

 

 

 

 

 

EMPTY_HEAP:

; р е а к ц и я

aa

пустую кучу

 

 

LDS

DX,CS:AERR

;DS:DX

-

а д р е с

сообцения

об ошибке

 

OUTSTR

; вывод

э т о г о сообцения

 

 

FINISH

; о с т а н о в

программы

 

AERR

DD

ERR

; а б с .

а д р е с

сообцения

 

ERR

DB

'Ошибка в NEW:

и с ч е р п а н и е

к у ч и ' , ' $ '

 

 

 

NEW ENDP

В заключения рассказа об организации кучи сделаем пару замечаний. Приведенные варианты процедур NEW и DISPOSE рассчитаны только на случай,

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

Поскольку ни в ПК, ни в ЯА не предусмотрено никаких средств для работы со списками и кучей, то в любой программе, где используются списки, приходится явно описывать рассмотренные в этом разделе процедуры, сегмент кучи, тип NODE, константу NIL. И если таких программ много, то, чтобы не повторяться, имеет смысл все эти описания собрать в одном файле, скажем с названием LISTS.ASM, а затем подключать их к программам с помощью директивы INCLUDE LISTS.ASM.

•ДИАЛОГ-МИФИ"

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]