Подготовка №3. Интерфейс системных вызовов. Простейшие подпрограммы.
1 Интерфейс системных вызовов, вывод на консоль
Программы на компьютерах обычно запускаются поверх операционной системы. Это позволяет осуществлять ряд операций (в первую очередь, ввод-вывод), используя интерфейс ОС и не прибегая к более низкоуровневому взаимодействию с устройствами (BIOS, прямая запись в память устройств и т. п.).
Средства ACK (as88/s88/t88) поддерживают 7 системных вызовов и 5 функций, вместе моделирующих базовые средства ввода-вывода ОС UNIX и подобных (полное перечисление см. в Приложении к книге Таненбаума). Для обращения к ним применяется одна инструкция SYS. При этом для указания конкретного системного вызова (функции), а также её аргументов, используется стек.
Пример системного вызова для корректного завершения программы (с явным сообщением ОС о том, что программа завершена успешно).
_EXIT = 1
.SECT .TEXT
PUSH 0 ! return code
PUSH _EXIT
SYS(Секции DATA и BSS опущены для краткости). Системный вызов для завершения программы имеет номер 1 (для наглядности используется именованная константа _EXIT). Он имеет один аргумент: код возврата (return code); в ОС UNIX каждая программа по завершении должна сообщать ОС целое число, описывающее результат работы программы. Обычно это ноль, если программа завершилась удачно, иначе число обозначает «код ошибки», который следовало расшифровывать с помощью документации к программе. Далее во всех сдаваемых заданиях обязательно корректно завершать программу, указывая системный вызов из приведенного выше примера.
Для вывода на консоль целых чисел следует использовать системный вызов с кодом 127. Под ним скрывается известная из языка C функция форматного вывода printf, которая требует передачи строки, содержащей формат вывода, и аргументов, которые будут подставлены в нужные места форматной строки (если таковые имеются). Для примера смоделируем следующий вызов функции printf из языка C:
printf("%d\n", 42);на языке ассемблера as88:
_PRINTF = 127
.SECT .TEXT
PUSH 42
PUSH fmt
PUSH _PRINTF
SYS
.SECT .DATA
fmt: .ASCIZ "%d\n"В этом примере показан способ объявления строковых литералов, завершающихся нулём (в стиле C) — с помощью псевдокоманды ассемблера .ASCIZ. При использовании _PRINTF следует помнить о том, что это функция: она возвращает количество выведенных символов в регистре AX (который, таким образом, «портится»).
Задачи
[
task-1.s] Составьте программу, которая печатает на консоль числа от n до k через пробел, а затем печатает символ перехода на новую строку (\n). Значения n и k заданы в секции инициализированных данных.Указания
- Для организации цикла можно использовать и условные переходы
J**и циклыLOOP. - В конце программы добавить код завершения программы (как в первом примере с
_EXIT). - Выполнить программу с помощью
t88, а затемs88.
- Для организации цикла можно использовать и условные переходы
[
task-2.s] При выполнении прошлой программы под отладчикомt88видно, что стек сильно захламляется в цикле — это некорректное использование стека: считается, что после того, как данные, помещённые на стек, использованы, их следует удалить со стека. Простой и неэффективный способ очистки стека — использование инструкцииPOP. Более разумный и широко используемый способ очистки стека — непосредственное манипулирование адресом вершины стека, который хранится в регистреSP. Для этого следует знать одну особенность устройства стека в Intel-процессорах: «стек растёт вниз». Это означает, что при добавлении элементов на стек адрес вершины стека уменьшается. И наоборот: при снятии элементов со стека адрес вершины стека увеличивается. В данном случае для снятия трёх двухбайтовых значений следует увеличить значение адреса вершины стека (регистрSP) на 6 (= 3x2).Преобразуйте программу
task-1.sвtask-2.s, добавив инструкцию увеличенияSPна 6 сразу после системного вызова форматного вывода, выполняемого в цикле для печати чисел. Проследите с помощьюt88, что захламления стека теперь не происходит.[
task-3.s] Постоянное добавление-снятие элементов с вершины стека, как в прошлой программе, не вполне эффективно, если учесть, что меняется лишь третий аргумент системного вызова (печатаемое число). Этот аргумент можно менять, используя косвенную адресацию на стеке — для неё используется регистрBP(в отличие от косвенной адресации в сегменте данных, для которой используютсяBX/SI/DI).- Скопируйте решение прошлой задачи, вынесите код добавления на стек аргументов и код очистки стека за границы цикла (до и после, соответственно).
- Перед циклом занесите значение вершины стека в регистр
BPдля последующей косвенной адресации на стеке с целью изменения третьего аргумента системного вызова. - На каждом шаге цикла записывайте в третий аргумент на стеке очередное число для печати. Для этого используйте косвенную адресацию со сдвигом (знакомимся с пятым режимом адресации):
N(BP)вместо обычной косвенной адресации(BP). ЗдесьNозначает необходимый сдвиг относительно вершины стека. Первый аргумент лежит на вершине стека —(BP), второй аргумент — со сдвигом 2, то есть обратиться к нему можно так:2(BP), третий аргумент — со сдвигом 4, обращение:4(BP).
[
task-4.s] В прошлой задаче происходило обращение к одному и тому же (третьему с вершины) элементу на стеке. Постоянно указывать сдвиг в этом случае не вполне эффективно: проще сразу же, до начала цикла, увеличить значениеBPна 4, а внутри цикла использовать обычную косвенную адресацию(BP)без сдвига. Выполните это преобразование программы.
2 Простейшие подпрограммы
Чтобы создать программу, использующую подпрограмму (процедуру или функцию), нужно должным образом организовать две части программы: код вызова подпрограммы (из основной программы) и код самой подпрограммы.
Код вызова очень похож на код осуществления системного вызова: необходимые параметры (обычно) складываются на стек, а затем используется инструкция
CALL <имя_метки_подпрограммы>В этом месте происходит непосредственно вызов подпрограммы. После её окончания продолжается выполнение инструкций, следующих за CALL. Обычно первая такая инструкция состоит в очистке стека от аргументов (например, ADD SP, <количество аргументов * 2>). Для понимания того, как должна быть устроена подпрограмма, следует разобраться в механизме работы CALL. По сути CALL совершает безусловный переход по заданной метке, как JMP. Однако если бы этим действие CALL ограничивалось, то невозможно было бы вернуться из подпрограммы обратно в место вызова («сделать обратный J(u)MP»). Чтобы было ясно, куда возвращаться, CALL кладёт на стек адрес следующей за ней инструкции. Это необходимо учитывать при доступе к аргументам подпрограммы: они оказываются на 2 байта глубже, чем хотелось бы, из-за лежащего на вершине стека адреса возврата (который разместила там CALL).
Теперь разберёмся как определяется подпрограмма. В самом простом случае она размещается в том же файле, что и основная программа, после инструкций, составляющих основную программу, которые завершаются системным вызовом _EXIT.
Подпрограмма начинается с метки, которая будет использоваться при вызове (CALL). Далее в подпрограмме могут идти любые инструкции, но обычно первым делом выполняются два действия, предназначенные для организации доступа к аргументам подпрограммы. Этот доступ организуется с помощью косвенной адресации со сдвигом через регистр BP (как в задаче 3). Для этого в BP заносится текущий указатель на вершину стека (тоже аналогично задаче 3). Однако сделать это первым же действием нельзя, так как процедура может быть вызвана из другой процедуры, которая использует BP для доступа к своим аргументам. Так что первым действием следует сохранить старое значение BP на стеке и только после этого заносить в BP текущее значение SP.
MY_PROC:
PUSH BP
MOV BP, SPТакая последовательность действий называется прологом подпрограммы. В конце подпрограммы следует восстановить старое значение BP и выполнить инструкцию RET, которая снимет с вершины стека адрес возврата и записывает его в регистр PC: программа продолжится с инструкции, следующей за CALL в основной программе. Если подпрограмме требуются какие-либо локальные переменные, они размещаются также на стеке, в таком случае перед завершением вершину стека следует переместить на то место, где она была после первой инструкции (PUSH BP). В этом случае завершение или эпилог подпрограммы выглядит так:
MOV SP, BP ! можно опустить, если вершина стека
! и так указывает на старое значение BP
POP BP
RETОбратите внимание на зеркальное подобие пролога и эпилога. Очень важно представлять, что происходит со стеком перед вызовом подпрограммы, во время вызова и по её завершении. В течение работы подпрограммы стек выглядит примерно так (ссылка, если картинка не отображается):

Здесь стек изображён растущим вниз, что соответствует указанному ранее принципу «стек растёт вниз». Однако следует учесть, что в окне t88 стек показан растущим вверх.
Задачи
[
task-5.s] На основе решения задачи 2 напишите подпрограммуPRINT_N, которая печатает числа от n до k . Числа n и k передаются как параметры на стеке. Следуя рисунку выше, при правильной организации подпрограммы доступ к ним осуществляется так:4(BP)и6(BP)(в зависимости от последовательности, в которой вы их разместите на стеке). При этом основная программа должна состоять из четырёх действий:- Положить на стек числа n и k.
- Вызвать подпрограмму (
CALL). - Очистить стек от чисел n и k.
- Завершить программу системным вызовом
_EXIT.
После основной программы идёт определение подпрограммы: метка с именем, пролог, цикл печати из задачи 2 (обратите внимание, что условие окончания цикла меняется), эпилог.
[
task-6.s] В этой задаче вам предлагается создать простейшую функцию. Функция отличается от процедуры наличием возвращаемого значения. Есть несколько вариантов размещения возвращаемого значения подпрограммы. Самый простой и одновременно широко используемый вариант (который примем и мы) — помещать возвращаемое значение в регистрAX. Создайте функцию возведения числа k в степень n (аргументы передаются через стек). В основной программе вызовите функцию и распечатайте её результат.