Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Флоренсов А.Н. УП Системное программное обеспечение.docx
Скачиваний:
46
Добавлен:
28.06.2021
Размер:
148.95 Кб
Скачать

3.4. Организация процедур

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

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

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

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

Важнейшим регистром процессора является регистр, в котором автоматически поддерживается адрес следующей для выполнения команды. Его называют счетчиком команд или подобным образом. В архитектуре Intel этот регистр назван указателем инструкций (instruction pointer) и обозначается EIP. Он имеет разрядность 32 бита. В 16-битной архитектуре используется младшая половина этого регистра, обозначаемая IP. Следует заметить, что в 16-битной архитектуре этого регистра в большинстве случаев недостаточно для указания места следующей команды, так как с помощью 16-битного кода можно указать только смещение в 64-кило-байтном сегменте, а это мало даже для компьютеров 80-х годов XX века. Поэтому для указания начала самого сегмента в архитектуре служит еще регистр CS (сокращение от code segment), который имеет 16 бит. В 32-бит-ной архитектуре этот регистр также используется, но довольно сложным образом, и, главное, манипуляции над ним практически всегда поручаются операционной системе. Поэтому понимание роли этого специального регистра в 32-битных ОС не существенно для начинающих программистов. Мы для начального знакомства ограничимся рассмотрением использования регистра EIP.

Может быть начинающим интересно узнать, каким образом в регистре EIP автоматически поддерживается адрес следующей для выполнения команды. Делает это аппаратура и в большинстве случаев достаточно просто. В самом начале выполнения любой команды к содержимому регистра EIP прибавляется длина этой команды (которая взята из сегмента команд для выполнения; это делается еще до анализа того, что должна делать текущая команда и как). При выполнении команд управления – условных и безусловных переходов, циклов и некоторых других – в регистр EIP заносится адрес команды, на которую нужно перейти, если этот переход действительно реализуется. В результате таких организационных решений для выборки следующей команды из памяти (из сегмента команд) аппаратуре необходимо только прочитать машинный код, начиная с адреса, задаваемого регистром EIP!

Объяснений, приведенных в начале раздела для описания существа запоминаемой информации при вызове подпрограмм, должно быть достаточно, чтобы понять – запоминать при вызове следует как раз содержимое регистра EIP. Поэтому аппаратура как бы выполняет команду PUSH EIP – в действительности не нужно записывать в программе что-нибудь подобное, это действие выполняется в ходе реализации команды CALL. Команда CALL в простейшей форме используется в виде

CALL имя_подпрограммы

Заметим, что в архитектуре Intel для обозначения подпрограмм принята более частная терминология – они обозначаются термином процедура. Напомним, что в терминологии языка Си все подпрограммы называются функциями, но в языке Паскаль используются оба термина: процедурами в нем называются подпрограммы, не возвращающие собственного значения, а функциями называются подпрограммы, обязательно возвращающие собственное значение. Из-за несогласованного многообразия использования этих терминов применяется более общее понятие подпрограммы. Далее перейдем на использование термина «процедура» в архитектуре Intel, пока же будем использовать термин «подпрограмма».

Работа аппаратно программного стека в архитектуре Intel также использует специальные регистры. Наиболее важным для начального знакомства построения этого стека является регистр ESP – расширенный регистр указателя стека (Stack Pointer). Он также 32-битный, а его младшая половина с обозначением SP используется в 16-битной архитектуре с этими же целями. Регистр ESP своим содержимым – относительным адресом в сегменте стека – указывает на верхнее запомненное поле в стеке. При выполнении команды PUSH для двойного слова регистр EIP автоматически уменьшается на 4. Стек растет от старших адресов к младшим, заполняясь со дна, которым является самый старший адрес в сегменте стека. При выполнении команды POP, которая снимает из стека 4-байтовое значение двойного слова, регистр EIP автоматически увеличивается на 4. При выполнении команд PUSH и POP для слов одинарной длины (16-битных операндов) указатель стека в EIP уменьшается или увеличивается соответственно на 2. (Заметим, что адрес начала сегмента стека задается с помощью специального регистра SS – Stack Segment, но нам особенности использования этого регистра сейчас не нужны.)

Наиболее важной командой в составе подпрограммы является команда, задаваемая мнемокодом RET. Ее действие соответствует действию оператора return в языке программирования Си. Детальные действия команды RET заключаются в том, что она снимает из стека верхнее значение и помещает его в регистр EIP (в 16-битной архитектуре снимает слово и помещает его в IP). Как следствие, следующей автоматически будет выполняться команда, расположенная по адресу возврата, т. е. расположенная сразу за той командой CALL вызова подпрограммы, которая и обеспечила перед этим обращение к подпрограмме. В отличие от языков высокого уровня, в частности языка Си, присутствие команды RET совершенно необходимо в подпрограмме. При отсутствии этой команды в конце подпрограммы будет автоматически выполняться двоичный код команды, записанный в памяти за последней командой, предусмотренной программистом в исходном коде. За последней командой подпрограмм в машинном коде не может быть ничего, какие-то коды, оставшиеся от других программ, или коды команд других подпрограмм там будут обязательно (редким, но возможным явлением может оказаться нарушение защиты памяти).

В ассемблерах MASM и TASM, подражающих языкам высокого уровня, для выделения команд подпрограммы из остальной части программного текста служат специализированные директивы, задаваемые ключевыми словами PROC и ENDP. Их используют согласно следующей схеме:

имя_подпрограммы PROC

команды подпрограммы

ret

имя_подпрограммы ENDP

Таким образом, директива с ключевым словом PROC служит для именования подпрограммы, а вспомогательная директива с ключевым словом ENDP – для обозначения конца подпрограммы, причем в обеих директивах должно быть использовано одно и то же имя, иначе диагностируется ошибка.

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

имя_подпрограммы:

команды подпрограммы

ret

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

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

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

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

имя_подпрограммы:

PUSH регистр1

PUSH регистр2

. . .

PUSH регистрN

команды подпрограммы

POP регистрN

. . .

POP регистр2

POP регистр1

ret

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

Когда используемых в подпрограмме регистров много, целесообразно для их запоминания применять специальную команду PUSHA, а для их восстановления – команду POPA. Эти команды запоминают и восстанавливают соответственно регистры EAX, EBX, ECX, EDX, ESP, EBP, ESI, EDI.