Предыдущий раздел СПИСКИ И ПРОЦЕДУРЫ Следующая глава

7.4. Процедуры

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

1) существенное уменьшение трудоемкости программирования за счет возможности выполнять разработку программы не целиком, а по частям;

2) существенное сокращение памяти для размещения программы, так как выделив в подпрограмму многократно повторяющийся участок кода программы, достаточно выделить память лишь для одной подпрограммы, инициируя ее из различных мест программы;

3) уменьшение трудоемкости программирования за счет того, что одну и ту же подпрограмму можно использовать не в одной, а в нескольких программах.

Машинные подпрограммы бывают двух типов: процедуры и обработчики прерываний. Любой обработчик прерываний можно инициировать (запустить) из своей программы, поместив в нее машинную команду “int n”, где n - номер прерывания. Более подробно прерывания и их обработчики будут рассмотрены во второй части пособия, а сейчас остановимся на процедурах.

Процедура – список машинных команд, который можно вызывать из различных мест программы. Переход к процедуре называется вызовом, а соответствующий переход назад называется возвратом. Вызов процедуры выполняет машинная команда “call b”, где b - адрес, по которому находится первая команда процедуры. Возврат осуществляет команда ret(рис.28). Возврат после каждого вызова осуществляется к команде, которая находится в памяти сразу за командой call.

 

Рис.28. Вызов процедуры и возврат из нее

 

Подобно команде jmp, команды call и ret выполняют или близкие переходы, когда вызывающая программа и процедура находятся в одном сегменте кода, или дальние переходы - программа и процедура находятся в разных сегментах. В первом случае адрес перехода b представляет собой число – смещение первой команды процедуры относительно начала сегмента (новое значение регистра IP). Во втором случае это пара чисел: (CS, IP). Так как пока наши программы небольшие, то далее речь будет идти только о близком вызове процедур.

Допустим, что для вызова процедуры мы будем использовать команду “call 200h”, где 200h – адрес, по которому находится первая команда процедуры. Вспомним, что  первая команда нашей программы находится по адресу l00h. Располагая процедуру по адресу 200h, мы стремимся убрать ее подальше от основной программы. Если мы запишем список команд процедуры (тело процедуры) по другому адресу, то этот адрес следует записать вместо адреса 200h.

Следующая программа вызывает десять раз процедуру, которая каждый раз печатает один символ, начиная от А и заканчивая  J:

 

100                 mov   dl,41

102                 mov   cx,000a

105                 call    0200

108                 loop  0105

10A                 int     20

 

Текст процедуры:

 

200                 mov   ah,02

202                 int     21

204                 inc     dl

206                 ret

 

В тексте процедуры содержится новая команда inc, которая увеличивает содержимое своего операнда (в примере - регистр DL) на единицу. Команда ret возвращает  управление в то место основной программы, откуда процедура была вызвана. Следующей командой, выполняемой после команды ret, будет та команда основной программы, которая следует за командой call. В примере это команда loop.

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

При вызове процедуры и возврате из нее необходимо выполнить следующие три требования:

1) при вызове процедуры необходимо запомнить адрес следующей команды, чтобы можно было впоследствии осуществить возврат в нужное место вызывающей программы;

2) используемые процедурой регистры необходимо запомнить до изменения их содержимого, а перед самым выходом из процедуры восстановить;

3) процедура должна иметь возможность выполнять обмен данными с вызывающей ее программой.

Первое требование реализуют команды call и ret. При выполнении команды вызова процедуры call программа "перескакивает" в другую область ОП (например, по адресу 200h), где находится первая команда тела вызываемой процедуры. Так как при выполнении команды процедуры ret мы возвращаемся в основную программу, то необходимо где-то хранить адрес возврата (адрес команды, следующей за командой call). В качестве места хранения адреса возврата используется программный стек. При близком вызове команда call помещает адрес следующей за ней команды (находится в IP)  в программный стек, а ret извлекает этот адрес из стека и помещает его в указатель команды IP. При дальнем вызове call помещает в стек, а ret извлекает оттуда не одно, а два слова – содержимое регистров CS и IP.

Заметим, что вызов процедуры не требует от нас использования команд push и pop, т.к. работа со стеком в данном случае производится "автоматически" – команда call помещает адрес возврата в стек, а команда ret извлекает его оттуда.

В в е д и т е  в память (если они там не сохранились) предыдущие программу и процедуру. Протестировав программу, наблюдайте за содержимым указателя стека SP до, и после исполнения команд call и ret. Сразу же после любого выполнения команды call найдите в стеке адрес возврата (108), используя команду U  Debug.

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

Допустим, что программа состоит из главной программы и трех процедур. Главная программа имеет вид:

 

 100                call    200

 103                int     20

 

По адресу 200 находится процедура, которая печатает букву А и вызывает вторую процедуру, записанную по адресу 300, которая выводит В. Вторая процедура вызывает третью процедуру, первая команда которой находится по адресу 400. Эта третья процедура выполняет только вывод на экран буквы C.

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

 

Рис. 29. Вложенный вызов процедур

 

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

 

200                 push  cx

201                 push  dx

202                 mov   dl,0a

205                 call    300

208                 inc     dl

20C                pop   dx

20D                pop   cx

20E                 ret

 

Обратите внимание, что команды pop расположены по отношению к командам push в обратном порядке, так как pop удаляет слово, помещенное в стек последним, а старое значение CX находится в стеке "глубже" старого значения DX.

Сохранение и восстановление регистров CX и DX позволяет произвольно изменять их значения внутри процедуры (которая начинается с адреса 200h), в то же время значения регистров, используемых процедурами, вызывающими эту, сохранены. Следовательно, мы можем использовать эти регистры для хранения локальных переменных – переменных,  которые применяются внутри процедур, не влияя на значения переменных, используемых вызывающей программой.

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

Рассмотрим теперь обмен данными между процедурой и вызывающей ее программой. Данные, передаваемые процедуре при ее вызове, называются входными параметрами процедуры. А данные, которые передаются процедурой в вызывающую ее программу, называются выходными параметрами. Информационный обмен между программой и процедурой обеспечивается путем использования областей памяти одного или нескольких следующих видов: 1) регистры данных; 2) программный стек; 3) другие области ОП. Если передаваемых данных немного, то достаточно регистров данных, большее количество данных может быть передано с помощью стека. Если же этих данных слишком много, то они помещаются в область ОП, а начальный адрес этой области записывается или в регистр данных, или в стек.

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

 


Предыдущий раздел В начало Следующая глава