ГЛАВА 9. ПОДПРОГРАММЫ

9.1. ВВЕДЕНИЕ

В предыдущих главах было показано использование простых подпрограмм для выполнения часто повторяющихся операций, таких как ввод или распечатка строк или чисел. Подпрограммы играют очень важную роль в структуре машинных программ. В настоящей главе будет детально рассмотрено, как осуществляются вызов и возврат из подпрограмм. Читатель узнает также, как подпрограммы получают доступ к данным основной программы и как могут быть связаны вместе в сложную программную структуру отдельные подпрограммы и объединены с программами, написанными на языках высокого уровня. Последний вопрос наиболее важен. Одно из самых важных применений языка ассемблера состоит в том, что он расширяет возможности таких языков, как Паскаль или Фортран, позволяя выполнять операции, которые трудно или невозможно реализовать только с помощью языка высокого уровня. Кроме того, в операционной системе VAX/VMS из основных программ, написанных на языке ассемблера, могут вызываться подпрограммы, написанные на языках высокого уровня. Это позволяет ассемблерным программам легко получать доступ к таким функциям языков высокого уровня, как форматный ввод и вывод. В настоящей главе будут обсуждены стандартизированные способы вызова, которые позволяют программам, написанным на различных языках, включая язык ассемблера, взаимодействовать друг с другом.

9.2. ВЫЗОВ ПОДПРОГРАММЫ

ИНСТРУКЦИЯ JSB

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

JSB     ADDR

Возврат из подпрограммы к основной программе выполняется по инструкции

RSB 

В пояснении, данном в гл. 5, говорилось, что инструкция JSB использует указатель стека (регистр 14, или SP) для сохранения программного счётчика (регистр 15, или PC) в области памяти, называемой стеком. Значение программного счётчика, сохраняемое в стеке, есть адрес инструкции, следующей за инструкцией JSB. Это та инструкция, которая будет выполняться после возврата из подпрограммы.

Пояснение верно, но не полно. Например, мы уже встречались с программами, в которых одни подпрограммы вызывали другие. При вложенных вызовах подпрограмм необходимо сохранять весь массив адресов возврата. Тогда возникает вопрос, каким образом каждая инструкция возврата RSB получает из стека правильный адрес возврата?

ИСПОЛЬЗОВАНИЕ СТЕКА

Чтобы можно было использовать массив для хранения адресов возврата, необходим указатель, показывающий, в каком месте этого массива запоминаются данные. Для этой цели всегда применяется регистр общего назначения 14, или SP (Stack Pointer - указатель стека). Теперь читателю должно быть ясно, почему были сделаны предостережения против использования регистра 14. Произвольное применение этого регистра может приводить к тому, что при следующем вызове подпрограммы или возврате из подпрограммы будут происходить очень странные вещи.

Массив, доступ к которому осуществляется путём последовательного добавления и удаления данных описанным ниже способом, называют стеком. Действие стека может быть продемонстрировано на примере стопки тарелок в кафе. Чтобы сохранить элемент информации, откуда-нибудь берут новую тарелку, пишут на ней 32-битовое двоичное число и помещают тарелку на вершину стопки тарелок (стека). Чтобы извлечь элемент из стойки (стека), с вершины стопки удаляют тарелку и смотрят, какое число на ней записано. Обратите внимание, что тарелки добавляются и удаляются всегда на вершине стопки (стека). Поэтому удаляемая тарелка - это всегда та, которая была добавлена последней. Например, ниже показано, как три элемента, помеченные буквами А, В и С, сначала сохраняются в стеке, а затем удаляются из него:

C

В

B

B

А

А

А

А

А

Пустой стек

Добавление элемента

Добавление элемента

Добавление элемента

Удаление
верхнего элемента

Удаление верхнего элемента

Удаление верхнего элемента

Хотя последним в стек был помещён элемент С, он - первый кандидат на удаление из стека. Такая последовательность "последним вошёл - первым вышел" часто обозначается сокращением LIFO (Last In, First Out).

Регистр 14 называется указателем стека, поскольку он указывает на вершину стека. Например, предположим, что первое длинное слово стека хранится в памяти по адресу ^X7FFC8FFC. Поскольку первое длинное слово будет заноситься по адресу ^X7FFC8FFC, то регистр 14 будет содержать адрес ^X7FFC9000. когда стек пуст:

Адрес

Содержимое

^X7FFC8FF4

??????

^X7FFC8FF8

??????

^X7FFC8FFC

??????

регистр 14 ^X7FFC9000

^X7FFC9000

??????

Чтобы сохранить в стеке длинное слово, нужно вычесть число 4 из содержимого регистра 14 и записать элемент по полученному в результате вычитания адресу. Если в стеке сохраняется длинное слово А, то результат будет таким:

Адрес

Содержимое

^X7FFC8FF4

??????

^X7FFC8FF8

??????

регистр 14 ^X7FFC8FFC

^X7FFC8FFC

А

^X7FFC9000

??????

Чтобы сохранить в стеке длинное слово В, вычитаем число 4 из содержимого регистра 14 и помещаем длинное слово В по полученному адресу:

Адрес

Содержимое

^X7FFC8FF4

??????

регистр 14 ^X7FFC8FF8

^X7FFC8FF8

В

^X7FFC8FFC

А

^X7FFC9000

??????

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

Адрес

Содержимое

^X7FFC8FF4

??????

^X7FFC8FF8

??????

регистр 14 ^X7FFC8FFC

^X7FFC8FFC

А

^X7FFC9000

??????

Обратите внимание, что длинное слово с адресом ^X7FFC8FF8, которое использовалось для хранения В, теперь содержит ??????. Ячейки памяти, используемые для хранения помечаемых в стек элементов, после извлечения элемента из стека могут содержать бессмысленную информацию. Программист должен исходить из того, что это отвечает истине на большинстве (если не на всех) вычислительных систем. Конкретная реализация стека может включать аппаратную или программную перезапись ячеек памяти стека после извлечения элементов.

Удаление ещё одного элемента из стека приводит к извлечению элемента А и опустошению стека.

Адрес

Содержимое

^X7FFC8FF4

??????

^X7FFC8FF8

??????

^X7FFC8FFC

??????

регистр 14 ^X7FFC9000

^X7FFC9000

??????

Поскольку адреса возврата сохраняются в стеке, подпрограммы могут вызываться одна из другой без затруднений. Например, основная программа с именем ALPHA может вызывать подпрограмму под названием BETA. В свою очередь, подпрограмма BETA может вызывать подпрограмму GAMMA, а та может вызывать подпрограмму DELTA. К моменту, когда выполнится подпрограмма DELTA, в стеке сохранено три адреса возврата - адреса возврата к ALPHA, BETA и GAMMA. Благодаря организации стека по принципу "последним вошёл - первым вышел" адресом возврата, который будет извлечён из стека после завершения подпрограммы DELTA, окажется адрес возврата к подпрограмме GAMMA. Аналогично подпрограмма GAMMA возвратит управление подпрограмме BETA, а подпрограмма BETA возвратит управление программе ALPHA. Как будет показано далее в этой главе, использование стека для хранения адресов возврата позволяет подпрограмме даже вызывать саму себя. Такие подпрограммы называются рекурсивными.

Длинное слово может быть помещено в стек по инструкции

MOVL    X,-(SP)

Аналогично извлечение длинного слова из стека может быть сделано по инструкции

MOVL   (SP)+,Х

Фактически пользователи могут применять эти две инструкции для помещения данных в стек, используя тем самым стек для сохранения временных данных. Представим себе для примера подпрограмму, которая использует регистры R2, R2 и R4 для внутренних вычислений. Но предположим, что основная программа пользуется теми же регистрами и нежелательно, чтобы подпрограмма изменяла их содержимое. Тогда подпрограмма должна сохранять содержимое регистров R2, R3 и R4 и восстанавливать его при возврате управления. Для этой цели обычно используют стек. Хотя для сохранения регистра можно воспользоваться почти любой доступной ячейкой, применение стека даёт важные преимущества. Когда стек используется в качестве временного хранилища, ячейки памяти в стеке занимаются лишь до удаления элементов из стека, после чего эта память становится доступ ной для использования в других целях. Экономятся также память и время, так как инструкция MOVL R2,-(SP) является короткой, а инструкция MOVL R2,SAVE - длинной, поскольку в неё входит адрес. По этим и другим причинам, которые станут яснее позже, хороший стиль программирования на ЭВМ VAX требует, чтобы для указанных целей использовался стек.

На рис. 9.1 показана подпрограмма, которая сохраняет в стеке содержимое регистров R2, R3 и R4. Обратите внимание на метку RETN. Чтобы осуществить возврат, необходимо либо произвести переход на эту метку, либо выполнить инструкции, дающие такой же эффект. Выполнение инструкции возврата из подпрограммы RSB без восстановления содержимого регистров привело бы к катастрофе. Инструкция RSB берёт в качестве адреса возврата верхний элемент стека. Если же регистры не были восстановлены, то вершина стека будет содержать сохранённое содержимое регистра R4, а не адрес возврата. Общее правило состоит в том, что подпрограммы могут использовать стек для сохранения данных, но всякий раз, когда в стек что-либо добавляется, требуется не больше и не меньше, как удалить это из стека до выполнения инструкции RSB.

START:  MOVL    R2,-(SP)            ; СОХРАНИТЬ СОДЕРЖИМОЕ
        MOVL    R3,-(SP)            ; РЕГИСТРОВ R2,R3 И R4
        MOVL    R4,-(SP)            ; В СТЕКЕ
        .
        .
        .
RETN:   MOVL    (SP)+,R4            ; ВОССТАНОВИТЬ РЕГИСТРЫ
        MOVL    (SP)+,R3            ; ОТМЕТЬТЕ ОБРАТНЫЙ ПОРЯДОК
        MOVL    (SP)+,R2            ; ПОСЛЕДНИЙ ИЗВЛЕКАЕТСЯ ПЕРВЫМ
        RSB                         ; ВОЗВРАТ ИЗ ПОДПРОГРАММЫ

Рис. 9.1. Использование стека для сохранения содержимого регистров

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

PUSHL   X

Фактически эта инструкция даёт тот же самый эффект, что и инструкция

MOVL    Х,-(SP)

Однако инструкция PUSHL яснее, работает несколько более эффективно и занимает на один байт меньше памяти. По этим причинам она является предпочтительной.

Две другие инструкции работы со стеком предназначаются для одновременного сохранения в стеке содержимого сразу нескольких регистров общего назначения. Такие инструкции ссылаются на регистры с помощью 16-битовой маски, осуществляющей выбор подмножества из 16 регистров общего назначения. Это инструкции

PUSHR   MASK

и

POPR    MASK

Операнд, обозначенный как MASK, - 16-битовое слово. Каждый бит в слове определяет некоторый регистр общего назначения. Бит 0 относится к регистру R0, бит 1 - к регистру R1, и т.д. вплоть до бита 14, соответствующего регистру 14 (SP). Поскольку регистр 15 (PC) является программным счётчиком, он обычно в стеке не сохраняется, исключение составляет только инструкция вызова подпрограмм. По этой причине бит 15 слова маски игнорируется и всегда рассматривается как нулевой этими инструкциями. Кроме того, особое внимание следует уделить регистрам AP, FP и SP (регистры 12, 13 и 14 соответственно). Попытка сохранить содержимое этих регистров в стеке и непосредственно восстановить их значения из стека, вероятно, необратимо нарушит поток управления в программе. Эти регистры имеют специальное назначение, и если необходимо их сохранение, то оно должно делаться специально предписанными способами. Более полные пояснения будут даны далее в разделе, посвящённом инструкциям CALLS и CALLG настоящей главы.

Инструкция PUSHR работает таким образом, что в стек помещается содержимое каждого регистра, для которого соответствующий бит в маске равен 1. Например, инструкция

PUSHR   #^X0047

приведёт к сохранению в стеке содержимого регистров R0, R1, R2 и R6, так как число ^X0047 имеет единицы в разрядах 0, 1, 2 и 6, что видно из его двоичного представления:

0000 0000 0100 0111

Следовательно, инструкция PUSHR #^X0047 эквивалентна следующим инструкциям:

PUSHL   R6
PUSHL   R2
PUSHL   R1
PUSHL   R0

Обратите внимание, что содержимое регистров включается в стек в порядке, обратном номерам регистров. Инструкция POPR делает противоположное тому, что делает инструкция PUSHR. Следовательно, инструкция

POPR    #^X0047

эквивалентна совокупности инструкций

MOVL    (SP}+,R0
MOVL    (SP)+,R1
MOVL    (SP)+,R2
MOVL    (SP)+,R6

Очевидная трудность при использовании этой инструкции связана с неудобством задания слова маски. Для упрощения этой процедуры ассемблер VAX MACRO располагает специальным оператором для генерации масок. Это оператор ^M. Он выглядит так:

^M<список регистров>

Список регистров представляет собой просто список имён регистров, разделённых запятыми. При использовании этого оператора инструкция PUSHR #^X0047 может быть переписана так:

PUSHR   #^M<R0,R1,R2,R6>

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

Следует заметить, что до сих пор примеры работы со стеком затрагивали включение и извлечение длинных слов. И хотя возможно использовать стек для хранения данных любого формата, например для байтов или слов, ЭВМ семейства VAX работают значительно эффективнее, если указатель стека устанавливается на границе длинного слова. Иначе говоря, значение указателя стека должно быть кратно числу 4. Поскольку система всегда начинает выполнение программы со значением указателя стека, установленным на границе длинного слова, то это значение будет оставаться на границе длинного слова, если в стек будут включаться или из стека извлекаться только длинные слова. И тем не менее все же существует возможность использования стека для хранения байтов или слов с сохранением выравнивания на границе длинного слова. Осуществить это можно, воспользовавшись инструкциями преобразования формата данных и инструкциями пересылки с добавлением незначащих нулей. Например, байт можно сохранить в стеке с помощью инструкции

MOVZBL  BYTE,-(SP)

Байт может быть восстановлен инструкцией

CVTLB   (SP)+,BYTE

Эти инструкции описаны в гл. 6. Обратите внимание, режимы с автоувеличением и автоуменьшением используются с операндом в формате длинного слова, так что указатель стека увеличивается или уменьшается на четыре.

Поскольку стек - это просто некоторый массив в памяти, то возникает вопрос о том, где же он находится: всё зависит от операционной системы. В ОС VAX/VMS программы обычно загружаются со стартового адреса ^X00000200. Стек начинается с одного из старших адресов, такого как ^X7FFC9000, и располагается в памяти по направлению в сторону уменьшения адресов. Действительный стартовый адрес определяется операционной системой VAX/VMS.

ПЕРЕДАЧА ИНФОРМАЦИИ МЕЖДУ ОСНОВНОЙ ПРОГРАММОЙ И ПОДПРОГРАММАМИ

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

В гл. 5 для подпрограмм RNUM и PNUM использовался очень простой способ связи. Число, передаваемое между подпрограммами, помещалось в регистр R0. Этот способ применим тогда, когда между подпрограммами надо передать лишь несколько (не более чем 12) длинных слов информации. Первое длинное слово помещается в регистр R0, второе - в регистр R1, и т.д. вплоть до регистра R11, если необходимо. Подпрограмма должна составляться так, что ищет информацию в соответствующем регистре. Таким же образом можно вернуть до 12 результатов.

Очевидно, что этот способ передачи информации налагает серьёзные ограничения, когда приходится иметь дело с большим количеством данных или массивами. Ограничение объёма передаваемых данных только 12 регистрами можно преодолеть путём использования специально выделенной области памяти, предназначенной для связи. Однако в действительности при больших объёмах данных такой способ работает не очень хорошо, поскольку требуются загрузка и сохранение содержимого всех ячеек, в которых находятся эти данные при каждом вызове подпрограммы, что может повлечь излишние затраты на вычисления.

SIZE=100
SUMUP:  PUSHR   #^M<R1,R2>          ;СОХРАНИТЬ R1 И R2
        MOVL    #SIZE,R2            ;В R2 - КОЛИЧЕСТВО ЧИСЕЛ
        CLRL    R0                  ;ОБНУЛИТЬ СУММУ
1$:     ADDL2   (R1)+,R0            ;ПРИБАВИТЬ ОЧЕРЕДНОЕ ЧИСЛО
        DECL    R2                  ;УМЕНЬШИТЬ СЧЁТЧИК
        BNEQ    1$                  ;ПОВТОРЯТЬ В ЦИКЛЕ ДО ЗАВЕРШЕНИЯ
        POPR    #^M<R1,R2>          ;ВОССТАНОВИТЬ СОДЕРЖИМОЕ РЕГИСТРОВ
        RSB                         ;ВОЗВРАТ ИЗ ПОДПРОГРАММЫ

Рис. 9.2. Подпрограмма суммирования элементов массива

Одно решение этой проблемы особенно пригодно при работе с массивами. Вместо того чтобы передавать значения всех элементов массива, основная программа передаёт адрес массива. Подпрограмма использует этот адрес для доступа к данным массива. Этот способ применялся в гл. 8 в подпрограммах RLINE и PLINE. В качестве ещё одного примера использования этого способа на рис. 9.2 представлена подпрограмма, в которой выполняется сложение 100 чисел массива. Основная программа передаёт подпрограмме адрес массива через регистр R1. Значение результата возвращается обратно, для чего используется регистр R0. Таким образом, здесь применяется комбинация двух способов. Последовательность действий при вызове этой программы будет следующей:

MOVAL   ARRAY,R1
JSB     SUMUP
MOVL    R0,ANS

В данном случае ARRAY - это метка (символический адрес) массива чисел, подлежащих сложению, ANS - ячейка, в которую в конце концов помещается результат.

Специального замечания здесь заслуживает тот факт, что помимо уже отмеченных преимуществ передача адресов вместо данных обладает ещё одним: она является намного более общим способом связи. Адреса могут использоваться двунаправленно. Другими словами, если подпрограмма располагает адресом ячейки в главной программе, она может воспользоваться этим адресом как для доступа к данным, так и для пересылки результатов назад к главной программе. Так было сделано в случае подпрограмм RLINE и PLINE. Подпрограмма RLINE возвращает данные, тогда как подпрограмма PLINE осуществляет доступ к данным.

Подводя итог, можно сказать, что передача значений - это быстрый и простой способ связи, который весьма пригоден для подпрограмм, имеющих дело только с несколькими данными. Передача адресов необходима для таких подпрограмм, которые работают с большим числом данных. Эти способы называют соответственно вызовом по значению и вызовом по ссылке. Позднее в этой главе читатель познакомится с ещё более общим способом передачи данных, который называют вызовом по дескрипторам.

УПРАЖНЕНИЯ 9.1

Для пп. 1, 2 и 3 следует предположить, что регистр R0 содержит ^X00000256, регистр R1 - ^XFFFFF624, регистр R2 - ^XFFFFFFFC, а следующие ячейки памяти - приведённые ниже числа:

Адрес

Содержимое

^X7FFC8FF0

^X12345678

^X7FFC8FF4

^X000002FC

^X7FFC8FF8

^X00000005

^X7FFC8FFC

^X00004212

^X7FFC9000

^XFFFFFFFE

Также предполагается, что SP содержит ^X7FFC8FF8 и что первоначальное содержимое этого регистра было ^X7FFC9000. Значение PC есть ^X4F3. Адрес SUB равен ^X67F.

  1. Какие значения содержатся в стеке?
  2. Содержимое каких ячеек памяти изменится и каким станет новое содержимое этих ячеек после выполнения каждой из следующих инструкций (используйте значения, определённые ранее для каждой инструкции):
    а.

    PUSHL   R0

    б.

    MOVL    (SP)+,R1

    в.

    CMPL    (SP)+,(SP)+

    г.

    JSB     SUB

    д.

    JSB     @R0

    е.

    RSB

    ж.

    ADDL2   R2,SP

    з.

    MOVL    (SP),-(SP)

    и.

    PUSHR   #^M<R0,R1>

    к.

    POPR    #^M<R0,R1>

  3. Какие из следующих инструкций представляют обычное использование стека? Какие являются неправильными и могут приводить к непредсказуемым результатам из-за того, что операционная система может периодически прерывать выполнение вашей программы и модифицировать ячейки, расположенные "ниже", чем ячейка, адрес которой содержится в указателе стека? Какие инструкции, являются катастрофическими и с большой вероятностью вызовут аварийное завершение программы? Какие инструкции вызовут ошибки при ассемблировании? Поясните ваши ответы. Используйте значения, описанные в п. 1 для каждой инструкции, и покажите результирующие изменения содержимого.
    a.

    MOVL    R0,(SP)+

    б.

    CMPL    -(SP),-(SP)

    в.

    JSB     (SP)+

    г.

    JSB     @(SP)+

    д.

    JSB     SP

    e.

    INCL    SP

    ж.

    DECL    SP

    з.

    MOVL    4(SP),R0

    и.

    MOVL    (SP)+,PC

     

  4. Напишите подпрограмму, аналогичную подпрограмме SUMUP, показанной на рис. 9.2. Но ваша программа будет иметь второй параметр, передаваемый через регистр R2, задающий длину массива, подлежащего суммированию. Затем напишите основную программу, которая вводит 10 чисел, распечатывает их и вызывает вашу подпрограмму для суммирования этих чисел, после чего делает то же самое для 20 чисел.
  5. Напишите программу, которая считывает переменное число чисел (не более 100), сортирует их и распечатывает отсортированный массив. Эту программу следует разбить на различные подпрограммы, по одной подпрограмме на каждую основную функцию. Адреса массивов, их длина и все другие данные должны передаваться через регистры общего назначения при каждом вызове подпрограммы.

9.3. РАЗДЕЛЬНОЕ АССЕМБЛИРОВАНИЕ И ГЛОБАЛЬНЫЕ ИМЕНА

НЕОБХОДИМОСТЬ РАЗДЕЛЬНОГО АССЕМБЛИРОВАНИЯ

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

Чтобы программировать таким образом, важно иметь возможность обработки каждого модуля как отдельной программы. Это означает, что каждая из этих небольших программ должна обеспечивать возможность её самостоятельного ассемблирования. Следовательно, как и в любой другой программе, все используемые программой ячейки должны полностью определяться внутри модуля. Иначе говоря, не должно быть каких-либо неопределённых имён.

С другой стороны, эти программные модули предназначены для того, чтобы, сочетаясь между собой, образовывать единую программу. Это означает, что, хотя модули могут быть весьма независимыми, они не могут быть независимы полностью. Значит, должен существовать некоторый вид связи между программами. Такая связь выполняется с помощью глобальных имён.

ГЛОБАЛЬНЫЕ ИМЕНА

Глобальное имя - это символический адрес, который определяется в одной программе, но доступен и из других раздельно ассемблируемых программ. Хотя глобальные имена могут использоваться почти для всех целей, для которых используются обычные имена, чаще всего они применяются для имён подпрограмм.

Например, программный модуль может содержать подпрограмму с именем READ. Если эта подпрограмма должна вызываться одним из других программных модулей, то имя READ должно быть глобальным. Определение глобальных имён осуществляется посредством директивы .ENTRY, директивы .GLOBAL или при завершении имени, используемого в качестве метки, двумя двоеточиями (::). В директиве .GLOBAL указывается одно или несколько имён, отделённых запятыми. Действие директивы состоит в том, что эти имена помечаются как глобальные в таблице имён, определяемых пользователем, создаваемой при ассемблировании. Например, директива

.GLOBAL START,PRINT,SORT_TABLE

приведёт к тому, что имена START, PRINT и SORT_TABLE будут помечены как глобальные и могут быть доступны другим программным модулям.

Директива .ENTRY, которой мы пользуемся, начиная с гл. 1, автоматически объявляет определяемое в ней имя как глобальное. Помимо этого символические адреса могут определяться как глобальные, если при появлении их в поле метки они заканчиваются не одним, а двумя двоеточиями. Таким образом, следующие строчки программы являются эквивалентными способами задания начала программы и определения метки START как глобального имени, которое может быть доступно из других раздельно ассемблируемых программных модулей:

1.

        .ENTRY  START,0

2.

        .GLOBAL START
START:  .WORD   0

3.

START:: .WORD   0

Если такое имя, как START, используется в некотором модуле, но определяется где-то в другом месте, то оно должно быть объявлено в модуле как внешнее глобальное имя. Это делается с помощью директивы .EXTERNAL. Например, директива

.EXTERNAL PRINT,GET_SYMBOL,REMOVE

отмечает имена PRINT, GET_SYMBOL и REMOVE как внешние глобальные имена, которые используются в текущем модуле, но определены где-то ещё. Это препятствует тому, чтобы ассемблер порождал сообщение об ошибке при обнаружении неопределённого имени. Вместо выдачи сообщения об ошибке ассемблер в файле объектного модуля формирует специальный код, который показывает, что значения этих определённых вне данного модуля адресов должны быть получены тогда, когда все модули будут объединяться компоновщиком для образования единой программы.

ОБРАБОТКА ГЛОБАЛЬНЫХ ИМЁН ПРИ КОМПОНОВКЕ

Процесс объединения всех программных модулей вместе и разрешения глобальных имён выполняется системной программой, называемой компоновщиком[1] (Linker). Компоновщик делает две вещи. Во-первых, он настраивает каждый программный модуль на последующий блок памяти. Отметим, что адрес настройки будет разным для каждого модуля; он зависит от того, как много памяти потребовалось для предыдущих модулей.

Во-вторых, компоновщик разрешает глобальные ссылки. Здесь компоновщик завершает тот процесс, который ассемблер не мог завершить из-за наличия внешних глобальных имён. Как и ассемблеру, компоновщику требуется дважды просмотреть файлы объектных модулей. На первом проходе строится таблица имён для всех определённых глобальных имён. На втором проходе заполняются отсутствующие адреса, соответствующие внешним глобальным именам, путём поиска их в таблице имён, которая была построена на первом проходе. Если глобальные имена остаются неопределёнными, это означает, что произошла одна из нескольких возможных ошибок. Программист мог забыть объявить имя как глобальное; в имени могла быть допущена опечатка; мог быть не включен какой-то модуль и т.п. Компоновщик выдаст сообщение об ошибке для каждого неопределённого имени.

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

.TITLE  MAIN                ОСНОВНОЙ ПРОГРАММНЫЙ МОДУЛЬ
.EXTERNAL READ              ;READ ИСПОЛЬЗУЕТСЯ,НО НЕ ОПРЕДЕЛЁН
.ENTRY  START,0
. . .
JSB     READ                ;ОБРАЩЕНИЕ К READ
. . .
.END    START               ;УКАЗАН НАЧАЛЬНЫЙ АДРЕС

Рис. 9.3. Модуль основной программы

        .TITLE  SUB1                МОДУЛЬ  ПОДПРОГРАММЫ
; ОТМЕТЬТЕ,ЧТО READ:: ОБЪЯВЛЯЕТ СИМВОЛ READ ГЛОБАЛЬНЫМ
READ::  PUSHR   #^M<R0>             ; НАЧАЛО ПОДПРОГРАММЫ READ
        . . .
        POPR    #^M<R0>
        RSB
        .END                        ; ОТМЕТЬТЕ,ЧТО НАЧАЛЬНЫЙ АДРЕС
                                    ; НЕ УКАЗЫВАЕТСЯ

Рис. 9.4. Модуль подпрограммы

Рисунки 9.3 и 9.4 иллюстрируют использование глобальных имён. В подпрограмме (рис. 9.4) два двоеточия используются для объявления метки READ как глобального имени, на которое ссылаются из какого-то другого модуля (в данном случае из основной программы). Если бы метка READ в подпрограмме не была объявлена как глобальное имя, не произошло бы никакой ошибки ассемблирования. Однако, пытаясь разрешить ссылки на метку READ в основной программе, компоновщик мог осуществить просмотр библиотеки подпрограмм в поиске объектного модуля с именем READ. И если библиотека содержит подпрограмму READ, результат при выполнении программы может оказаться непредсказуемым. Ведь кто знает, что может делать модуль по имени READ, написанный совсем для других целей. Программисту найти ошибку такого рода очень трудно. Чтобы избежать неприятности, в библиотеках подпрограмм операционной системы VAX/VMS используются глобальные имена, содержащие знак доллара. Например, подпрограмма вычисления квадратного корня называется MTH$SQRT, а программа, вызываемая с помощью макроинструкции $EXIT_S, называется SYS$EXIT. Вот почему мы предостерегали от использования знака доллара в символических именах, задаваемых пользователем.

В основной программе (рис. 9.3) имя READ объявляется директивой .EXTERNAL как внешнее глобальное имя. Это показывает компоновщику, что он должен подставить числовой адрес READ в инструкцию JSB READ. Если бы директива .EXTERNAL была опущена, ассемблер пометил бы READ как неопределённое имя. Именно по этой причине в предыдущем варианте программы была включена строчка .DISABLE GLOBAL. Без этой строчки ассемблер автоматически объявляет все неопределённые имена внешними глобальными именами. Оставить строчку .DISABLE GLOBAL в вашей программе полезно, ибо автоматическое объявление глобальных имён задержит нахождение символических имён, в которых была допущена опечатка.

Разница между операторами .END на рис. 9.3 и рис. 9.4 довольно значительна. В модуле основной программы в директиве .END должен присутствовать адрес передачи управления или начальный адрес, но в других модулях директива .END должна использоваться без параметров, поскольку во всей программе не может быть более одной начальной точки.

9.4. ВЫЗОВ ПРОЦЕДУРЫ

ИНСТРУКЦИИ CALLG И CALLS

До сих пор вызов подпрограмм осуществлялся с помощью инструкций JSB, BSBW и BSBB, а возврат - с помощью инструкции RSB. Это простой способ вызова подпрограмм, но ЭВМ семейства VAX поддерживают и намного более сложную процедуру вызова подпрограмм, которая позволяет с помощью единственной инструкции вызова и единственной инструкции возврата автоматически выполнять следующие действия:

  1. Обычное управление программным счётчиком, позволяющее осуществлять вызов и возврат так, как это делается с помощью инструкций JSB и RSB.
  2. Передачу списков аргументов вызываемым подпрограммам.
  3. Выборочное сохранение и восстановление регистров и кодов условий.
  4. Сохранение в стеке информации, необходимой для вложенных вызовов подпрограмм.
  5. Отметку критических точек в стеке, которые позволяют выполнять обратную трассировку подпрограммы и другие процедуры обработки ошибок.
  6. Полное восстановление состояния стека при возврате управления из подпрограммы, что в существенной степени снижает опасность, вызванную тем, что в стеке остаются посторонние данные.
  7. Очистку стека таким способом, который позволяет помещать в стек список аргументов при вызове подпрограммы и автоматически удалить его при возврате.

Чтобы все это выполнить, в дополнение к программному счётчику (PC, или регистр 15) и указателю стека (SP, или регистр 14) имеются два специальных регистра. Это указатель аргументов (Argument Pointer - AP, или регистр 12) и указатель блока вызова (Frame Pointer - FP, или регистр 13).

Указатель аргументов используется для указания на массив, который составлен из некоторого числа длинных слов, передаваемых от вызывающей программы к вызываемой. Эти длинные слова могут содержать данные любого типа, но обычно они содержат адреса, которые, в свою очередь, указывают на элементы данных или массивы, передаваемые подпрограмме. Как уже упоминалось, передача адреса обладает тем преимуществом, что позволяет передавать данные как к подпрограмме, так и от неё. Кроме того, по причинам, которые станут ясны в дальнейшем, первое длинное слово списка аргументов обычно представляет число, которое говорит о том, сколько длинных слов содержится в списке. Помимо прочего это позволяет подпрограммам иметь переменное число аргументов как у функции МАХ0 в Фортране или у функции CONCAT в Паскале фирмы UCSD.

Указатель блока вызова используется для сохранения значения указателя стека, чтобы возврат мог быть выполнен, даже если подпрограмма оставит данные в стеке. Вспомним, что подпрограммы, вызванные с помощью инструкции JSB, должны удалять всё, что они включили в стек, до выполнения инструкции RSB. Наличие указателя блока вызова снимает это ограничение для подпрограмм, вызываемых с помощью инструкций CALLS и CALLG. Указатель блока вызова используется также операционной системой для раскручивания (обратной трассировки) списка вызовов подпрограмм, чтобы вернуться обратно к основной программе в случае ошибки.

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

Чтобы использовать рассматриваемый способ вызова подпрограмм, подпрограмму вызывают с помощью инструкции CALLG и CALLS. Возврат из подпрограммы к основной или вызывающей программе осуществляется с помощью инструкции RET. Инструкции CALLG и CALLS сохраняют в стеке содержимое всех нужных регистров, а также устанавливают регистры AP и FP. Вызываемая подпрограмма должна сообщить инструкции CALLG или CALLS, какие регистры из группы R0 - R11 желательно сохранить. Инструкции CALLG и CALLS всегда сохраняют регистры AP, FP, SP и PC (регистры 12, 13, 14 и 15). Подпрограмма показывает, какие регистры желательно сохранить, с помощью битов, устанавливаемых в 16-битовом слове, которое называется маской входа. Эта маска фактически располагается по адресу точки входа в подпрограмму. Первая инструкция подпрограммы, которая будет выполняться, имеет адрес старше на два байта, чем адрес, указанный в инструкции CALLG или CALLS.

Работа с маской входа осуществляется во многом так же, как и работа с маской регистров в инструкциях PUSHR или POPR. Главное различие состоит в том, что могут быть сохранены только регистры R0 - R11. Каждый бит маски соответствует некоторому регистру. Как и в случае инструкций PUSHR и POPR, для создания маски входа может использоваться оператор ^M. Например, оператор ^M<R2,R3> указывает, что должны сохраняться регистры R2 и R3. Поскольку регистры 12-15 сохраняются автоматически, биты 12-15 служат другим целям. Биты 12 и 13 не используются и должны быть нулевыми. Биты 14 и 15 вызывают установку соответственно флагов разрешения прерываний по переполнению целых чисел и по переполнению десятичных чисел. Эти прерывания используются для обнаружения ошибок. Если происходит определённая ошибка и соответствующий бит, управляющий прерыванием, установлен, то управление передаётся операционной системе VAX/VMS. Более полное описание дано в гл. 15. Пока же все четыре бита сбрасываются в 0.

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

        .EXTERNAL   SUBR
        . . .
ARGLST: .LONG       3           ;ИМЕЕТСЯ 3 АРГУМЕНТА
        .ADDRESS    A           ;A - АДРЕС ПЕРВОГО АРГУМЕНТА
        .ADDRESS    B           ;В - АДРЕС ВТОРОГО АРГУМЕНТА
        .ADDRESS    C           ;С - АДРЕС ТРЕТЬЕГО АРГУМЕНТА
        . . .
        CALLG       ARGLST,SUBR
        . . .

Обратите внимание, что директива .LONG используется для помещения константы 3 в длинное слово, а директива .ADDRESS - для помещения в длинные слова адресов A, B и C.

Подпрограмма может быть написана следующим образом:

SUBR::  .WORD   ^M<R2,R3,R4>        ;СОХРАНЯТЬ И ВОССТАНАВЛИВАТЬ R2-R4
        MOVL    4(AP),R2            ;В R2 АДРЕС ПЕРВОГО АРГУМЕНТА
        MOVL    8(AP),R3            ;В R3 АДРЕС ВТОРОГО АРГУМЕНТА
        MOVL    12(AP),R4           ;В R4 АДРЕС ТРЕТЬЕГО АРГУМЕНТА
        . . .
        RET

Обратите внимание, что первое слово подпрограммы является не инструкцией, а словом, в котором содержится маска входа. Нотация ^M<...> действует так же, как и в случае инструкций PUSHR и POPR.

При входе в эту программу регистр AP указывает на список аргументов, а 4(AP) - на первый аргумент (не считая длинного слова со счётчиком аргументов). Аналогично 8(AP) указывает на второй аргумент, а 12(AP) - на третий. Следовательно, инструкция MOVL 4(AP),R2 поместит в регистр R2 первый элемент списка аргументов. Однако следует заметить, что первым элементом списка аргументов является адрес ячейки A. Если желательно получить содержимое A (вероятно, это 16-битовое слово), то можно использовать с регистром R2 косвенную адресацию, например MOVW (R2),..., или можно, не заботясь об адресе, осуществить непосредственный доступ к данным с помощью инструкции MOVW @4(AP),..., вообще не привлекая регистр R2.

Директива .ENTRY, которая применялась для обозначения точки входа основной программы, может быть использована для любой программы или подпрограммы, которая вызывается инструкцией CALLS или CALLG. Например, директива

.ENTRY  START,0

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

  1. Помещать символический адрес START в таблицу имён, определяемых пользователем, как адрес следующей доступной ячейки.
  2. Объявлять метку START глобальным именем.
  3. Порождать слово кода, используя второй аргумент для формирования маски входа. Поскольку операционная система не "полагается" на то, что программа сохранит регистры, мы всегда использовали нуль для маски входа. Следует, однако, отметить, что мы использовали эту директиву в начале программы по той причине, что операционная система VAX/VMS рассматривает нашу основную программу как подпрограмму, которую она вызывает с помощью инструкции CALLS или CALLG.

Можно использовать директиву .ENTRY в предыдущей подпрограмме, чтобы получить следующий эквивалентный текст программы:

.ENTRY  SUBR,^M<R2,R3,R4>
MOVL    4(AP),R2        ;В R2 АДРЕС ПЕРВОГО АРГУМЕНТА
MOVL    8(AP),R3        ;В R3 АДРЕС ВТОРОГО АРГУМЕНТА
MOVL    12(AP),R4       ;В R4 АДРЕС ТРЕТЬЕГО АРГУМЕНТА
. . .
RET

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

. . .
PUSHAL  C
PUSHAL  B
PUSHAL  A
CALLS   #3,SUBR
. . .

Инструкция PUSHAL применяется для помещения в стек адресов A, B и C. Как инструкция

PUSHL   X

эквивалентна по действию инструкции

MOVL    X,-(SP)

так и инструкция

PUSHAL  A

эквивалентна инструкции

MOVAL   A,-(SP)

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

В гл. 7 было показано, что инструкция MOVAL является частью семейства инструкций, включающего инструкции MOVAB, MOVAW, MOVAL, MOVAQ и MOVAO. Аналогично инструкция PUSHAL есть член следующего семейства инструкций:

Мнемоника

Значение

PUSHAB

Включить о стек адрес байта

PUSHAW

Включить в стек адрес слова

PUSHAL

Включить в стек адрес длинного слова

PUSHAQ

Включить в стек адрес квадраслова

PUSHAO

Включить в стек адрес октаслова

9.5. СВЯЗЬ МЕЖДУ ПРОГРАММАМИ НА ЯЗЫКЕ АССЕМБЛЕРА И НА ЯЗЫКАХ
ВЫСОКОГО УРОВНЯ

НЕОБХОДИМОСТЬ ОБЪЕДИНЕНИЯ ПРОГРАММ НА ЯЗЫКЕ АССЕМБЛЕРА
И ЯЗЫКАХ ВЫСОКОГО УРОВНЯ

Языки высокого уровня обычно предусматривают три типа программных модулей. Это основные программы, подпрограммы или процедуры, и функции. В среде операционной системы VAX/VMS такие программные модули могут раздельно компилироваться или транслироваться в объектные модули. Связь между объектными модулями выполняется посредством глобальных имён подпрограмм и функций. Поскольку объектные модули, порождаемые компиляторами операционной системы VAX/VMS, имеют точно тот же формат, что и объектные модули, порождаемые ассемблером, возможно осуществление взаимосвязи основных программ, подпрограмм и функций, написанных на языках высокого уровня, с модулями, написанными на языке ассемблера, при условии соблюдения соглашений по программированию, принятых в операционной системе VAX/VMS.

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

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

СОГЛАШЕНИЯ О СВЯЗЯХ В ОПЕРАЦИОННОЙ СИСТЕМЕ VAX/VMS

Соглашения о связях в операционной системе VAX/VMS - это стандартный способ передачи управления и информации между вызывающей программой и вызываемой. Этот стандарт используется в языках высокого уровня: реализованных как часть операционной системы. Сюда входят языки Бейсик, Блисс, Си, Кобол, Фортран, Паскаль и ПЛ/1. Эти соглашения используются также для получения сервиса, предоставляемого операционной системой.

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

СОГЛАШЕНИЯ О СВЯЗЯХ В ЯЗЫКЕ ФОРТРАН

При написании на языке ассемблера подпрограммы, предназначенной для совместной работы с программами на Фортране, необходимо пользоваться соглашениями о вызове и возврате, которые являются частью стандарта вызова процедур в операционной системе VAX/VMS. Нужно также знать, как передаются данные между подпрограммами. Это делается в основном с помощью списков аргументов, описанных для инструкций CALLS и CALLG.

Простейший случай представляет подпрограмма, не имеющая аргументов. Такая подпрограмма может вызываться в Фортране с помощью такого оператора, как

CALL    XSUB

Объектный код, генерируемый компилятором языка Фортран, в точности совпадает с тем, который ассемблер сгенерировал бы для следующего текста:

.EXTERNAL XSUB
. . .
CALLS   #0,XSUB

В инструкции CALLS присутствует операнд #0 потому, что подпрограмма не имеет аргументов. Это означает, что подпрограмма должна иметь простую структуру, показанную на рис. 9.5.

Следующая тема, которой следует заняться, касается связи по данным между программами на Фортране. Существует два способа, какими основная программа на Фортране может связываться с подпрограммой. Первый - с помощью списка аргументов; второй - через общие блоки. Общие блоки будут рассмотрены в отдельном разделе.

В программах на Фортране списки аргументов передаются путём передачи адресов, как это было описано ранее в разделе данной главы по инструкциям CALLG и CALLS. Например, программа на Фортране, имеющая оператор CALL YSUB(A,B,C), создаёт где-либо в памяти массив из четырёх длинных слов: по одному длинному слову на каждый из трёх адресов и длинное слово в начале массива, в котором задаётся число аргументов.

На рис. 9.6,а представлен эквивалент на языке ассемблера для этого оператора Фортрана. В такой реализации подразумевается, что переменные A, B и C объявляются в программе на Фортране как тип данных, занимающий длинное слово. Например, оператор INTEGER A, B, C объявляет переменные A, B и C целыми со знаком, каждое из которых занимает длинное слово (см. Справочное руководство по Фортрану операционной системы VAX/VMS). На рис. 9.6 показано, как та же самая вызывающая последовательность могла быть написана при использовании инструкции CALLS. Отличие состоит в том, что список аргументов строится в стеке каждый раз, когда вызывается подпрограмма. Число аргументов даётся в списке аргументов, так что подпрограмма может иметь дело с переменным числом аргументов. Это делает вызов с помощью инструкции CALLG совместимым с вызовом с помощью инструкции CALLS. (Другие вычислительные системы используют другие соглашения по определению числа аргументов, например применяют специальное значение для пометки конца списка аргументов.)

Рисунок 9.7 иллюстрирует подпрограмму, которая складывает элементы массива A и помещает сумму в S. Обратите внимание, что программа изменяет содержимое регистров R0 и R1. Однако всё правильно, так как соглашения о связях в операционной системе VAX/VMS позволяют это делать. Стандартные процедуры вызова дают возможность подпрограммам модифицировать содержимое регистров R0 и R1. Причина этого будет разъяснена в разделе, посвящённом функциям в Фортране и Паскале. Все другие регистры должны сохраняться. Следовательно, если подпрограмма использует регистры R2, R3 и R5, в слове маски входа должны быть установлены биты 2, 3 и 5 с помощью оператора .ENTRY SUB,^M<R2, R3, R5>. В программе на рис. 9.7 должно быть сохранено только содержимое регистра R2.

.TITLE  XSUB        ПОДПРОГРАММА-ПРИМЕР
.ENTRY  XSUB,^M<. . .>
. . .
RET
.END

Рис. 9.5. Подпрограмма, вызываемая из программы на языке Фортран

а.

            .EXTERNAL   YSUB        ;ВСЕ ПОДПРОГРАММЫ ГЛОБАЛЬНЫЕ
            . . .
ARGS:       .LONG       3           ;ЧИСЛО АРГУМЕНТОВ
            .ADDRESS    A           ;A - АДРЕС ПЕРВОГО АРГУМЕНТА
            .ADDRESS    B           ;В - АДРЕС ВТОРОГО АРГУМЕНТА
            .ADDRESS    C           ;С - АДРЕС ТРЕТЬЕГО АРГУМЕНТА
            . . .
A:          .BLKL       1           ;ОБЛАСТЬ ПАМЯТИ ДЛЯ ПЕРВОГО АРГУМЕНТА
B:          .BLKL       1           ;ОБЛАСТЬ ПАМЯТИ ДЛЯ ВТОРОГО АРГУМЕНТА
C:          .BLKL       1           ;ОБЛАСТЬ ПАМЯТИ ДЛЯ ТРЕТЬЕГО АРГУМЕНТА
            . . .
            CALLG       ARGS,YSUB   ;ВЫЗОВ ПОДПРОГРАММЫ

б.

            .GLOBAL     YSUB        ;ВСЕ ПОДПРОГРАММЫ ГЛОБАЛЬНЫЕ
            . . .
A:          .BLKL       1           ;ОБЛАСТЬ ПАМЯТИ ДЛЯ ПЕРВОГО АРГУМЕНТА
B:          .BLKL       1           ;ОБЛАСТЬ ПАМЯТИ ДЛЯ ВТОРОГО АРГУМЕНТА
C:          .BLKL       1           ;ОБЛАСТЬ ПАМЯТИ ДЛЯ ТРЕТЬЕГО АРГУМЕНТА
            . . .
            PUSHAL      C           ;ПОСТРОИТЬ СПИСОК АРГУМЕНТОВ
            PUSHAL      B           ;ЗАМЕТЬТЕ, ЧТО ПОСЛЕДНИЙ АРГУМЕНТ
            PUSHAL      A           ;НАХОДИТСЯ НА ВЕРШИНЕ СТЕКА
            CALLS       #3,YSUB     ;ВЫЗОВ ПОДПРОГРАММЫ

Рис. 9.6. Эквивалент на языке ассемблера для оператора CALL YSUB(A, B, C) при использовании инструкции CALLG (а); эквивалент на языке ассемблера для оператора CALL YSUB(A, B, C) при использовании инструкции CALLS (б)

Подпрограмма на Фортране

Эквивалентная программа на ассемблере

        SUBROUTINE SUM(S,A)
        INTEGER S,I,A(10)
        S=0
        DO 10 I=1,10
          S=S+A(I)
10      CONTINUE
        RETURN
        END
        .TITLE  SAMPLE
        .ENTRY  SUM,^M<R2>
        MOVL    8(AP),R0    ;ПЕРЕСЛАТЬ В R0 АДРЕС A
        CLRL    R2          ;В R2 БУДЕТ СУММА
        CLRL    R1          ;ОБНУЛИТЬ ИНДЕКС
10$:    ADDL2  (R0)+,R2     ;R2:=R2+A(I)
        AOBLSS  #10,R1,10$  ;ВЫПОЛНЯТЬ В ЦИКЛЕ
        MOVL    R2,@4(AP)   ;ЗАПОМНИТЬ РЕЗУЛЬТАТ В S
        RET                 ;ВОЗВРАТ ИЗ ПОДПРОГРАММЫ
        .END

Рис. 9.7. Соотношение фортрана и языка ассемблера

Инструкция CALLG для указания на список аргументов использует регистр AP. Следовательно, нотации 4(AP) и 8(AP) ссылаются на вторую и третью ячейки списка. Эти ячейки содержат адреса аргументов (S и A в программе на языке Фортран или Паскаль). Поскольку нотация 4(AP) ссылается на адрес S, запись @4(AP) отсылает к содержимому S. Поэтому при выполнении инструкции MOVL R2,@4(AP) содержимое регистра R2 копируется в ячейку S.

СОГЛАШЕНИЯ О СВЯЗЯХ В ПАСКАЛЕ

Обычный способ объявления процедуры в Паскале заключается в использовании декларации вида

PROCEDURE ИМЯ_ПРОЦЕДУРЫ (ОБЪЯВЛЕНИЕ АРГУМЕНТОВ);
BEGIN
  ТЕЛО ПРОЦЕДУРЫ
END

Эти операторы, конечно, должны располагаться в теле программы, вызывающей процедуру. Чтобы сообщить компилятору Паскаля, что тело процедуры отсутствует, но при этом ожидается, что процедура будет внешней подпрограммой, то всё, что нужно сделать, - это надо заменить описание тела процедуры вместе с его операторными скобками BEGIN и END ключевым словом EXTERNAL, которое является расширением стандартного языка Паскаль. Следовательно, предыдущая процедура может быть переписана так:

PROCEDURE ИМЯ_ПРОГРАММЫ (ОБЪЯВЛЕНИЕ АРГУМЕНТОВ);
EXTERNAL;

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

В качестве примера того, как эго можно использовать, рассмотрим программу на Паскале, которая вызывает внешнюю процедуру, называемую YSUB и имеющую три целочисленных аргумента. Программа может быть написана так, как показано на рис. 9.8. Эта программа будет порождать точно такую же вызывающую последовательность, как и в программе на Фортране, в иллюстрациях, данных на рис. 9.6,а и б[2]. Поэтому программа на языке ассемблера, написанная для этой вызывающей последовательности, будет одинаково хорошо работать и в случае программы на Фортране или Паскале.

PROGRAM MAIN(INPUT,OUTPUT)
VAR
  A,B,C: INTEGER;
. . .
PROCEDURE YSUB(VAR X,Y,Z : INTEGER);
EXTERNAL;
. . .
YSUB(A,B,C)
. . .

Рис. 9.8. Вызов из программы на языке Паскаль внешней процедуры

Важный момент различия, который следует отметить, состоит в том, что Паскаль хранит двумерные массивы по строкам, тогда как Фортран хранит их по столбцам (см. гл. 7). К многомерным массивам применяются те же принципы. В программе на Паскале первым меняется последний индекс массива, в программе же на Фортране первым меняется первый индекс.

ФУНКЦИИ В ЯЗЫКАХ ФОРТРАН И ПАСКАЛЬ

Из описаний, приводимых в большинстве руководств по языкам Фортран и Паскаль, может сложиться представление, что подпрограммы-функции совершенно отличаются от подпрограмм. Фактически же существует только одно реальное отличие - функции возвращают единственное значение, которое доступно для использования в выражениях. Это значение может занимать от одного байта до двух длинных слов, для типов данных двойной точности или комплексных. Эти слова всегда возвращаются в регистрах R0 и R1, причём R1 используется только тогда, когда требуется более одного длинного слова. Списки аргументов для функций реализуются точно так же, как и для подпрограмм или процедур. Таким образом, можно видеть, что подпрограмма SUM на рис. 9.7 или её эквивалент на Паскале может быть переписана в виде функции, как показано на рис. 9.9. Обратите внимание, поскольку функция имеет тип INTEGER и, следовательно, возвращает 32-битовое двоичное число, необходим только регистр R0, чтобы результат стал доступен вызывающей программе. Отличие в основной программе сводится к тому, что вместо оператора CALL SUM(S,A) или SUM(S,A) будет оператор вида S=ISUM(A) или S:=ISUM(A);. В функции на языке Паскаль оператор MODULE является расширением стандартного Паскаля, позволяющим осуществлять раздельную компиляцию функций и процедур. (См. Справочное руководство по языку Паскаль операционной системы VAX/VMS.)

Функция на Фортране

Функция на Паскале

 
 
 
        INTEGER FUNCTION ISUM(A)
        INTEGER A(1:10),J
        ISUM=0
        DO 10 J=1,10
          ISUM=ISUM+A(J)
10      CONTINUE
        RETURN
        END
MODULE SAMPLE(INPUT, OUTPUT)
TYPE
  NMBS = ARRAY[1..10] OF INTEGER;
FUNCTION ISUM( VAR A : NMBS): INTEGER;
VAR
  J : INTEGER;
BEGIN
  ISUM := 0;
  FOR J := 1 TO 10 DO
    ISUM := ISUM + A[J];
  END;
END.

Эквивалентная программа на ассемблере

        .TITLE  SAMPLE          ПРИМЕР ФУНКЦИИ
        .ENTRY  ISUM,^M<R2,R3>
        MOVL    4(AP),R3        ; ПЕРЕСЛАТЬ В R3 АДРЕС А
        CLRL    R2              ; РЕГИСТР R2 СЧЁТЧИК
        CLRL    R0              ; ОБНУЛИТЬ СУММУ
10$:    ADDL2   (R0)+,R2        ; СЛОЖИТЬ А(J) С СУММОЙ
        AOBLSS  #10,R2,10$      ; ПОВТОРИТЬ В ЦИКЛЕ ДЕСЯТЬ РАЗ
        RET                     ; ВОЗВРАТ
        .END

Рис. 9.9. Эквивалент функции в программе на языке Фортран или Паскаль

ОБЩИЕ БЛОКИ ФОРТРАНА

В языке Фортран существует два способа передачи данных между основными программами и подпрограммами. Первый, как мы видели, осуществляется через списки аргументов. Другой способ, который программы на Фортране могут применять для связи, основывается на использовании общих блоков. Если в основной программе и в подпрограмме содержится оператор

COMMON /DBLK/Х,Y,Z

где переменные X, Y и Z ссылаются на одни и те же ячейки в обеих программах[3], то связь может осуществляться без передачи списков аргументов или массивов адресов. Этот способ работает при объявлении имени общего блока как глобального имени и образовании специального массива, достаточно большого для размещения переменных общего блока. Такое пространство устанавливается в каждой программе, но компоновщик разработан так, что перекрывает блоки с одним и тем же глобальным именем (т.е. общим блоком в разных программах назначается одна и та же область памяти). Таким образом гарантируется, что все программы ссылаются на одни и те же ячейки.

На рис. 9.10 показан эквивалент на языке ассемблера для оператора COMMON Фортрана. Директива .PSECT определяет, что все следующие за ней инструкции должны помещаться в определённую программную секцию. Эту определённую секцию идентифицирует имя DBLK. Остальные девять параметров показывают, что секция (блок):

  1. Позиционно независима. Поскольку здесь предполагаются числовые данные, фактически занимаемые адреса не важны.
  2. Перекрываемая, в противоположность размещаемой последовательно. Переменные X, Y и Z в этом блоке должны занимать те же самые ячейки, что и переменные X, Y и Z в блоках с тем же именем в других программах. Секции, размещаемые последовательно, располагались бы одна за другой и использовании раздельные области памяти.
  3. Переместимая, в противоположность абсолютной. В языке ассемблера возможны абсолютные программные секции, но в языке Фортран всё определённое пользователем - переместимо.
  4. Глобальная, в противоположность локальной. Если необходимо, чтобы эта секция была доступна другим программным модулям, имя DBLK должно быть глобальным.
  5. Разделяемая. Эти данные разделяемы с другими процессами.
  6. Неисполняемая. Это данные, а не исполнимый код.
  7. Разрешён доступ к чтению этой секции.
  8. Разрешена запись или модификация.
  9. Выровнена по границе длинного слова. Это может повысить эффективность выполнения.

Фортран

Эквивалентное объявление на ассемблере

INTEGER X,Y(25),Z
COMMON /DBLK/Х,Y,Z
        .PSECT  DBLK,PIC,OVR,REL,GBL,SHR,NOEXE,RD,WRT,LONG
X:      .BLKL   1
Y:      .BLKL   25
Z:      .BLKL   1
        .PSECT

Рис. 9.10. Эквивалент общего блока на языке ассемблера

Хотя некоторые из этих вариантов в настоящее время могут быть и не реализованы (как 6 и 7), они должны быть указаны или будут использоваться значения по умолчанию. Если компоновщик обнаружит конфликтующие атрибуты для программных секций с одним и тем же именем, появятся сообщения об ошибках.

После строки .PSECT идёт последовательность помеченных блоков (вводимых директивой .BLKL) для переменных общего блока. Можно использовать и операторы, в которых задаётся значение данных (такие как .LONG). Это имело бы такое же действие, как и модуль BLOCK DATA Фортрана. В ячейках блока будут загружены данные, но следует избегать перезаписи данных, определённых в другом модуле. Чтобы не сталкиваться с этой проблемой, в стандартном Фортране запрещено использование операторов DATA для переменных в общих блоках, исключая модуль BLOCK DATA.

Блок заканчивается тогда, когда будет встречена другая директива .PSECT или когда появится директива .END в конце программы. Если директива .PSECT не имеет аргументов, это показывает, что снова выполняется ассемблирование обычной программной секции. С другой стороны, последующая директива .PSECT может иметь аргументы, показывающие на то, что определяется другой общий блок.

СИМВОЛЬНЫЙ ТИП ДАННЫХ ФОРТРАНА-77

Поскольку объекты символьного типа данных в языке Фортран имеют произвольную длину, они должны обрабатываться как-то отлично от объектов фиксированной длины, таких как вещественные или целые числа. Чтобы решить проблему получения информации о длине, приходится усложнять процедуру вызова для аргументов символьного типа по сравнению с описанными в предыдущих разделах для обычных переменных. Вместо списка аргументов, содержащего адрес данных, такая процедура вызова содержит адрес дескриптора, Как отмечалось в гл. 8, дескриптор - это блок данных, состоящий из двух длинных слов, который содержит информацию о длине и адрес, по которому расположены данные. Существует ещё код, идентифицирующий объект как символьные данные. Это позволяет использовать дескрипторы и для других типов данных, хотя стандартное применение в Фортране - только для символьных данных.

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

Фортран

Ассемблер

CHARACTER *20 A
. . .
CALL XYZ(A)
A:      .BLKB   20      ;СИМВОЛЬНАЯ СТРОКА
        . . .
ARGS:   .LONG   1       ;СПИСОК АРГУМЕНТОВ
        .ADDRESS ADESC
        . . .
ADESC:  .WORD   20      ;ДЛИНА СТРОКИ
        .BYTE   14      ;ТИП CHARACTER
        .BYTE   1       ;НЕ ПОЯСНЯЕТСЯ
        .ADDRESS A      ;РАСПОЛОЖЕНИЕ СТРОКИ
        . . .
        CALLG   ARGS,XYZ

Рис. 9.11. Передача символьных данных

Дескриптор содержит: 20 в 16-битовом слове - длину символьной строки; 14 в 8-битовом байте - код, говорящий о том, что это дескриптор символьной строки; 1 в 8-битовом байте, которая означает, что это неиндексированная переменная (дескрипторы массивов намного более сложные, они дают все данные о структуре массива); адрес символьной строки в длинном слове.

Очевидно, что регистры R0 и R1 не могут быть использованы для получения результатов функции символьного типа, ведь результатом может быть и очень длинная строка. Поэтому функции символьного типа рассматриваются как подпрограммы. Другими словами, строка-результат добавляется как дополнительный аргумент в начале списка аргументов. Таким образом, вызов символьной функции CFUN, который может выглядеть так:

A=CFUN(B,C,D)

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

CALL    CFUN(A,B,C,D)

9.6. РЕКУРСИВНЫЕ ФУНКЦИИ

Для некоторого типа задач, таких как обработка лингвистической информации и искусственный интеллект, важны рекурсивные функции. Говоря коротко, рекурсивная функция - это такая функция, которая вызывает саму себя. Рекурсивные функции происходят от рекурсивных определений в математике.

В качестве примера рекурсивной функции рассмотрим функцию факториала. Инженер может удовлетвориться определением n факториала (или n!) как произведения целых чисел от 1 до n (случай 0!=1 рассматривается как специальный). Однако, хотя такого определения достаточно для вычисления, в качестве формы математического доказательства оно не очень полезно. Математик предпочёл бы следующее определение:

0! = 1
n! = n * (n - 1)!, если n > 0

Первая строка показывает, что нуль факториал (0!) равен единице, вторая строка говорит, что n-факториал (n!) равен n, умноженному на (n-1) факториал. Такие определения приемлемы как форма доказательства, называемая математической индукцией. Индуктивные доказательства широко используются в информатике для проверки правильности программ.

Хотя в стандартном Фортране не обеспечена поддержка рекурсивных функций, в большинстве более новых языков высокого уровня они допустимы. Например, языки Алгол, АПЛ и Паскаль попадают в число тех, которые разрешают подпрограммам или подпрограммам-функциям вызывать самих себя. На рис. 9.12 показана рекурсивная функция на языке Паскаль, вычисляющая факториалы. Поскольку эта внешняя функция отвечает соглашениям о связях в операционной системе VAX/VMS, она может вызываться из основной программы на Паскале, Фортране или языке ассемблера.

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

MODULE EXTFUN(INPUT,OUTPUT);
  FUNCTION FAC( N : INTEGER) : INTEGER;
  BEGIN
    IF N = 0 THEN
      FAC := 1
    ELSE
      FAC := N * FAC(N-1);
  END;
END.

Рис. 9.12. Программа вычисления факториала на языке Паскаль

        .TITLE  ПРОГРАММА ВЫЧИСЛЕНИЯ ФАКТОРИАЛА
TEMP:   .BLKL   1               ;В TEMP ЗАПОМИНАЕТСЯ N-1
        .ENTRY  FAC,0
        MOVL    @4(AP),R0       ;N ПОМЕЩАЕТСЯ В R0
        BNEQ    10$             ;ПРОПУСТИТЬ,ЕСЛИ НЕ 0
        MOVL    #1,R0           ;ФАКТОРИАЛ НУЛЯ РАВЕН 1
        RET
10$:    PUSHL   R0              ;СОХРАНИТЬ N В СТЕКЕ
        SUBL3   #1,R0,TEMP      ;N-1 ПОМЕСТИТЬ В TEMP
        PUSHAL  TEMP            ;СФОРМИРОВАТЬ СПИСОК АРГУМЕНТОВ
        CALLS   #1,FAC
        MULL2   (SP)+,R0        ;ВЫЧИСЛИТЬ N*(N-1) ФАКТОРИАЛ
        RET                     ;И ВЕРНУТЬ ЕГО ЗНАЧЕНИЕ В R0
        .END

Рис. 9.13. Рекурсивная подпрограмма вычисления факториала на языке ассемблера

Стек ЭВМ VAX даёт пользователям языка ассемблера очень важный инструмент для рекурсивного программирования. Чтобы рекурсивные функции могли работать, в стеке должны сохраняться необходимые данные каждый раз, когда подпрограмма вызывает себя. При возврате подпрограммы к самой себе элементы данных будут удалены из стека. На рис. 9.13 показано, как рекурсивная функция для вычисления факториала, представленная на рис. 9.12, может быть переписана на языке ассемблера. Обратите внимание, что в этой программе имеются два элемента, которые должны сохраняться в стеке. Первый элемент - это аргумент N, второй - адрес возврата и блок вызова, которые, конечно, сохраняются в стеке автоматически.

При входе в эту функцию значение N помещается в регистр R0, а N-1 запоминается во временной ячейке TEMP. Затем программа проверяет, не равно ли N нулю; если это так, результат 0!=1 заносится в R0. Если N не равно нулю, то адрес ячейки TEMP включается в стек, чтобы функция FAC могла вычислить (N-1)!. Здесь требуется проявить осторожность, ибо рекурсивный вызов функции FAC уничтожит значение TEMP. Обычно, рекурсивные программы сохраняют подобные ячейки в стеке и восстанавливают их при возврате. Однако, поскольку ячейка TEMP в дальнейшем не используется, её содержимое сохранять не нужно. После возврата управления значение (N-1)! будет находиться в регистре R0. Оно умножается на значение N, которое было сохранено в стеке, и результат, равный N*(N-1)! или N!, заносится в регистр R0.

УПРАЖНЕНИЯ 9.2

  1. Напишите подпрограмму, аналогичную подпрограмме SUMUP, показанной на рис. 9.2. Но ваша программа будет иметь второй входной аргумент, задающий длину массива, подлежащего суммированию. Затем напишите основную программу, которая считывает 10 чисел, распечатывает их, вызывает вашу подпрограмму для их суммировании и затем проделывает то же самое с 20 числами. Каждая подпрограмма, включая RNUM и PNUM (если используются), должна ассемблироваться раздельно как отдельные модули, которые компонуются вместе с помощью глобальных имён. (см. п. 4 упр. 9.1).
  2. Напишите программу, которая считывает переменное число чисел (до 100 включительно), сортирует их и распечатывает отсортированный массив. Эта программа должна быть разбита на несколько ассемблируемых раздельно подпрограмм, по одной на каждую основную функцию. Отдельные программные модули следует связать, используя глобальные имена. Адреса массивов, их длина и т.п. должны передаваться в регистрах общего назначения при каждом вызове подпрограммы. (См. п. 5 упр. 9.1.)
  3. Ниже предлагается метод генерации положительных случайных чисел в диапазоне 1-32767:
    • а)  Начать с любого положительного числа из указанного диапазона;
    • б)  Для каждого генерируемого числа: 1) сдвинуть первоначальное число один раз влево; 2) если два старших бита оба равны 1 или 0, сбросить младший бит в 0. Если один из битов равен 1, а другой 0, установить младший бит в 1. Другими словами, младший бит получает значение результата операции ИСКЛЮЧАЮЩЕЕ_ИЛИ над знаковым битом и битом 14 сдвинутого слова; 3) затем сбросить знаковый бит в 0.

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

  4. Напишите подпрограмму, вызываемую из программы на Фортране или Паскале, когда вызов осуществляется такими операторами:
    Фортран Паскаль
    CALL    LOCS(A) LOCS(A)

    Здесь A - это массив из трёх элементов типа INTEGER. Ваша подпрограмма заполняет массив A следующим образом:

    A(1) - адрес инструкции, к которой произойдёт возврат при выполнении инструкции RET.

    A(2) - адрес списка параметров.

    A(3) - адрес самого массива A.

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

  5. Рекурсивная функция, известная как функция Аккермана, определяется через неотрицательные целые числа следующим образом:

    А(0,n) = n +1 для n > 0
    А(m,0) = A(m-1,1) для m > 0
    А(m,n) = A [m-1, A (m, n - 1)] для m и n>0

    Напишите рекурсивную подпрограмму на языке ассемблера для вычисления функции Аккермана.

  6. Напишите функцию, которую можно вызывать из программы на Фортране или Паскале, которая вызывает рекурсивную функцию Аккермана из п. 5, и протестируйте её с помощью основной программы, вызывающей эту функцию с различными значениями. (Предостережение: не используйте для аргументов числа больше 3.)
  7. Напишите программу сортировки, такую, как описано в п. 2, за исключением того, что:
    • а)  подпрограммы ввода и вывода и основная программа должны быть написаны на языке Фортран;
    • б)  программа сортировки должна быть написана на языке ассемблера;
    • в)  все аргументы должны передаваться через общие блоки.

 

< НАЗАД ОГЛАВЛЕНИЕ ВПЕРЁД >


[1] Это тот самый компоновщик, который обсуждался в разд. 4.7. По поводу концепции перемещения или настройки модулей отсылаем читателя к этому разделу.

[2] Здесь мы говорим о действии этих инструкций. Фактическая последовательность инструкций может варьироваться, но будет оставаться эквивалентной, как эквивалентны инструкции на рис. 9.6,а и б.

[3] Предполагается, что переменные X, Y и Z в обеих программах объявлены согласованно.