Кадр вызова функции, системные вызовы и макрокоманды

Лекция оказалась простой, но трудно поверить, что это всё уместилось в 2 часа. Тем не менее!

Кадр стека и промышленные конвенции

Зачем нужен кадр

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

Если подпрограмма использует много локальных переменных, описанная ранее конвенция оказывается довольно неудобной (

   1 # подпрограмма, вычисляющая сумму и произведение двух чисел,
   2 # и хранящая их все на стеке без использования кадра, ПОТОМУ ЧТО ТАК НАДО
   3 main:   li      $v0 5
   4         syscall
   5         move    $a0 $v0
   6         li      $v0 5
   7         syscall
   8         move    $a1 $v0
   9         jal     addmul
  10         
  11         move    $t0 $v0
  12         move    $t1 $v1
  13         move    $a0 $t0
  14         li      $v0 1
  15         syscall
  16         li      $a0 ' '
  17         li      $v0 11
  18         syscall
  19         move    $a0 $t1
  20         li      $v0 1
  21         syscall
  22         li      $a0 '\n'
  23         li      $v0 11
  24         syscall
  25         li      $v0 10
  26         syscall
  27         
  28 addmul: addiu   $sp $sp -4
  29         sw      $ra ($sp)
  30         addiu   $sp $sp -4
  31         sw      $a0 ($sp)       # первая переменная
  32         addiu   $sp $sp -4      
  33         sw      $a1 ($sp)       # вторая переменная
  34         # какой-то код, портящий регистры
  35         lw      $t0 4($sp)      # первая переменная
  36         lw      $t1 ($sp)       # вторая переменная
  37         addu    $t2 $t1 $t0
  38         addiu   $sp $sp -4      
  39         sw      $t2 ($sp)       # сумма
  40         # какой-то код, портящий регистры
  41         lw      $t0 8($sp)      # внезапно первая переменная
  42         lw      $t1 4($sp)      # соответственно вторая переменная
  43         mul     $t2 $t1 $t0
  44         addiu   $sp $sp -4      
  45         sw      $t2 ($sp)       # произведение
  46         # какой-то код, портящий регистры
  47         lw      $v1 ($sp)       # произведение
  48         lw      $v0 4($sp)      # сумма
  49         # жэээсть, что я тут в стек понадобавлял
  50         # сколько сбрасывать-то??
  51         addiu   $sp $sp 16
  52         lw      $ra ($sp)
  53         addiu   $sp $sp 4
  54         jr      $ra
  55         
  56         
  57         
  58         
вот такая подпрограмма, зацените):

Возникает идея хранить в специально отведённом регистре (он называется Frame Pointer, $fp) состояние $sp на момент вызова функции. Тогда для восстановления $sp перед возвратом из функции достаточно будет переложить туда значение $fp. Предыдущее значение $fp при этом тоже надо будет сохранять на стеке, т. к. кадр локален для каждого вызова функции.

Пример конвенции с использованием кадра

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

  1. (преамбула) Передаваемые подпрограмме значения надо заносить в регистры $a

  2. (преамбула) Если передаваемых параметров больше 4, все остальные необходимо положить в стек в обратном порядке (последний, предпоследний, … шестой, пятый)
  3. Вызов подпрограммы должен производиться командой jal или jalr

  4. Никакая вызываемая подпрограмма не модифицирует содержимое стека выше того указателя, который она получила в момент вызова
  5. (пролог) Подпрограмма обязана сохранить на стеке значение $fp (старый кадр) в первую очередь

  6. (пролог) Значение $sp, каким оно было на момент вызова подпрограммы, она обязана записать в $fp

  7. (пролог) Подпрограмма обязана сохранить на стеке значения $ra и всех используемых ею регистров типа $s

  8. (пролог) Подпрограмма обязана выделить на стеке память для всех локальных переменных
  9. Подпрограмма может хранить на стеке произвольное количество временных данных. Количество этих данных и занимаемого ими места на стеке не оговаривается и может меняться в процессе работы подпрограммы. Такие данные, например, могут образоваться в преамбуле вызова подпрограммы при размещении параметров в стеке. Локальные переменные рекомендуется размещать в пределах кадра.

  10. Возвращаемое подпрограммой значение надо заносить в регистры $v

  11. (эпилог) Подпрограмма обязана сбросить кадр путём восстановления $sp из $fp

  12. (эпилог) Подпрограмма обязана восстановить из стека сохранённые значения $s, $fp и $ra

  13. Возврат из подпрограммы должен производиться командой jr $ra

  14. (очистка стека) Если передаваемых параметров было больше 4, необходимо очистить стек от 5-го, 6-го и т. д. параметров

Согласно такой конвенции:

Стоит заметить, что и эта конвенция

Например, мы можем потребовать, чтобы дополнительные параметры подпрограммы также входили в кадр (тогда инициализация кадра войдёт в преамбулу, зато не будет нужды в восстановлении стека после возврата), или, наоборот, чтобы в кадр входили только локальные переменные и временные данные (тогда сброс кадра становится слегка сложнее — к восстановленному $sp надо добавлять размер области сохраняемых регистров, зато адресация локальных переменных относительно начала кадра начинается прямо с 0).

Подпрограмма с использованием кадра

Это подпрограмма, сортирующая методом обмена массив целых размером $a0 байтов (т. е. $a0/4 элементов), лежащий по адресу ($a1). Подпрограмма не концевая — она вызывает ещё одну подпрограмму, поиск минимального элемента в массиве.

   1 function:
   2         # общая подпрограмма
   3         # a0 — размер массива
   4         # a1 — начало массива
   5         # v0 — размер выходного массива (того же самого :)
   6         # v1 — начало выходного массива
   7 .eqv    FP      -4              # Смещения в кадре
   8 .eqv    .RA     -8($fp)         #   для сохраняемых регистров
   9 .eqv    .S0     -12($fp)
  10 .eqv    .S1     -16($fp)
  11 .eqv    .ARR    -20($fp)        # Смещения в кадре
  12 .eqv    .SIZE   -24($fp)        #   для локальных переменных
  13 .eqv    FSIZE   -24             # Размер кадра
  14         sw      $fp FP($sp)     # Сохраняем кадр
  15         move    $fp $sp         # Переставляем кадр
  16         addiu   $sp $sp FSIZE   # Переставляем стек
  17         sw      $ra .RA         # Сохраняем регистры
  18         sw      $s0 .S0     
  19         sw      $s1 .S1     
  20 
  21         sw      $a0 .SIZE       # Размер массива
  22         sw      $a1 .ARR        # Адрес массива
  23 
  24         move    $s0 $a0         # Регистры типа $s* не меняются
  25         move    $s1 $a1         # при вызовах функций
  26 fnext:  move    $a0 $s0
  27         move    $a1 $s1
  28         jal     getmin
  29         lw      $t0 ($v0)
  30         lw      $t1 ($s1)
  31         sw      $t0 ($s1)
  32         sw      $t1 ($v0)
  33         addiu   $s1 $s1 4
  34         addiu   $s0 $s0 -4
  35         bleu    $a0 4 fnext ### TODO ??? A0 или s0?
  36 
  37         lw      $v0 .SIZE       # Обращаемся к локальным переменным
  38         lw      $v1 .ARR
  39 
  40         lw      $ra .RA         # Восстанавливаем регистры
  41         lw      $s0 .S0     
  42         lw      $s1 .S1     
  43         move    $sp $fp         # Восстанавливаем стек
  44         lw      $fp FP($sp)     # Восстанавливаем предыдущий кадр
  45         jr      $ra

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

Впрочем, наша подпрограмма и стек не использует:

   1 getmin: # концевая подпрограмма
   2         # a0 — размер массива
   3         # a1 — начало массива
   4         # v0 — адрес минимального элемента
   5         lw      $t0 ($a1)
   6         move    $v0 $a1
   7 gmnext: addiu   $a0 $a0 -4
   8         addiu   $a1 $a1 4
   9         blez    $a0 gmfin
  10         lw      $t1 ($a1)
  11         bge     $t1 $t0 gmnext
  12         move    $t0 $t1
  13         move    $v0 $a1
  14         j       gmnext
  15 gmfin:  jr      $ra

   1 .data
   2 .eqv    SZ 20
   3 Arr:    .space  SZ
   4 EndArr: .align  2
   5 .text
   6 
   7 main:   la      $t1 Arr
   8         la      $t2 EndArr
   9 input:  li      $v0 5
  10         syscall
  11         sw      $v0 ($t1)
  12         addiu   $t1 $t1 4
  13         bne     $t2 $t1 input
  14         
  15         li      $a0 SZ
  16         la      $a1 Arr
  17         jal     function
  18         
  19         la      $t1 Arr
  20         la      $t2 EndArr
  21 output: li      $v0 1
  22         lw      $a0 ($t1)
  23         syscall
  24         li	$a0 '\n'
  25         li	$v0 11
  26         syscall
  27         sw      $v0 ($t1)
  28         addiu   $t1 $t1 4
  29         bne     $t2 $t1 output
  30         
  31         li      $v0 10
  32         syscall
  33 
  34 function:
  35         # общая подпрограмма
  36         # a0 — размер массива
  37         # a1 — начало массива
  38         # v0 — размер выходного массива (того же самого :)
  39         # v1 — начало выходного массива
  40 .eqv    FP      -4              # Смещения в кадре
  41 .eqv    .RA     -8($fp)         #   для сохраняемых регистров
  42 .eqv    .S0     -12($fp)
  43 .eqv    .S1     -16($fp)
  44 .eqv    .ARR    -20($fp)        # Смещения в кадре
  45 .eqv    .SIZE   -24($fp)        #   для локальных переменных
  46 .eqv    FSIZE   -24             # Размер кадра
  47         sw      $fp FP($sp)     # Сохраняем кадр
  48         move    $fp $sp         # Переставляем кадр
  49         addiu   $sp $sp FSIZE   # Переставляем стек
  50         sw      $ra .RA         # Сохраняем регистры
  51         sw      $s0 .S0     
  52         sw      $s1 .S1     
  53 
  54         sw      $a0 .SIZE       # Размер массива
  55         sw      $a1 .ARR        # Адрес массива
  56 
  57         move    $s0 $a0         # Регистры типа $s* не меняются
  58         move    $s1 $a1         # при вызовах функций
  59 fnext:  move    $a0 $s0
  60         move    $a1 $s1
  61         jal     getmin
  62         lw      $t0 ($v0)
  63         lw      $t1 ($s1)
  64         sw      $t0 ($s1)
  65         sw      $t1 ($v0)
  66         addiu   $s1 $s1 4
  67         addiu   $s0 $s0 -4
  68         bleu    $a0 4 fnext ### TODO ??? A0 или s0?
  69 
  70         lw      $v0 .SIZE       # Обращаемся к локальным переменным
  71         lw      $v1 .ARR
  72 
  73         lw      $ra .RA         # Восстанавливаем регистры
  74         lw      $s0 .S0     
  75         lw      $s1 .S1     
  76         move    $sp $fp         # Восстанавливаем стек
  77         lw      $fp FP($sp)     # Восстанавливаем предыдущий кадр
  78         jr      $ra
  79         
  80 getmin: # концевая подпрограмма
  81         # a0 — размер массива
  82         # a1 — начало массива
  83         # v0 — адрес минимального элемента
  84         lw      $t0 ($a1)
  85         move    $v0 $a1
  86 gmnext: addiu   $a0 $a0 -4
  87         addiu   $a1 $a1 4
  88         blez    $a0 gmfin
  89         lw      $t1 ($a1)
  90         bge     $t1 $t0 gmnext
  91         move    $t0 $t1
  92         move    $v0 $a1
  93         j       gmnext
  94 gmfin:  jr      $ra
Вся программа

Промышленные конвенции

Описанные нами конвенции достаточно удобны, чтобы программировать «вручную»: весь код программы и подпрограммы пишется на языке ассемблера (возможно, разными программистами), при этом учёт сохраняемых на стеке данных и размер кадра программист «держит в голове».

Правда, даже для таких условий эти конвенции неполны:

Чем сложнее описываемый конвенцией случай, тем сложнее изобрести универсальную договорённость. Ниже пример нескольких документов, содержащий несколько версий «живых» ABI (application binary inverface), что соответствует части конвенций по вызову подпрограмм и запуску программ.

Наконец, полноценный ABI, в отличие от «договорённостей» предназначен не только и не столько для людей-программистов, сколько для системных программ, занимающихся исходным и машинным кодом

Системные вызовы

Базовая лекция

Операционная система

Задачи операционной системы: унификация, разделение и учёт ресурсов компьютера. Для взаимодействия с ОС программа следует некоторым соглашениям, называемым ABI (Application Binary Interface).

Под ресурсами поминаются оперативная память, процессорное время и разнообразные внешние устройства.

Программный интерфейс использования ресурсов — системные вызовы — предоставляет т. н. ядро ОС

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

Аппаратная поддержка

ВНИМАНИЕ! Большая часть системных вызовов в MARS реализована аппаратно!

Системные вызовы MARS

Описаны документация MARS

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

Конвенции по вызову syscall в MIPS

  1. В регистр $v0 помещается номер системной функции (service number)

  2. В регистры $a0-$a3 или $f12 (для вещественных чисел) помещаются параметры системного вызова, если есть

  3. Инструкция syscall передаёт управление операционной системе (обычно ядру, но, например, в MARS это может быть сразу сам MARS)

  4. Возврат из системного вызова — по аналогии с возвратом из подпрограммы, на следующую после syscall инструкцию

  5. Возвращаемые значения (если есть) помещаются в $v0, $a0-$a1 или в $f0 в соответствии с работой вызванной системной функции

Механизм работы syscall со стороны ядра ОС может быть разным.

Пример: вывести на консоль число, лежащее в регистре $t0

   1     li  $v0, 1           # Функция 1 — вывод числа
   2     add $a0, $t0, $zero  # Это была псевдоинструкция move $a0 $t0
   3     syscall

Некоторые системные вызовы MARS

Функция

Тип
$v0

Параметры

Возвращаемое значение

вывод целого

1

$a0 = целое

вывод вещественного

2

$f12 = вещественное

вывод вещественного двойной длины

3

$f12 = двойное вещественное

вывод строки

4

$a0 = адрес ASCIIZ-строки

ввод целого

5

$v0 — введённое целое (word)

ввод вещественного

6

$f0 — введённое вещественное (float)

ввод двойного вещественного

7

$f0 — введенное двойное вещественное (double)

ввод строки

8

$a0 = адрес памяти для размещения строки
$a1 = размер буфера

Если строка короче буфера, все её символы записываются в буфер, после чего туда дописывается нулевой байт Если строка не короче буфера, в него записывается первые $a1-1 символов, а в последний оставшийся байт записывается 0 Таким образом формируется ASCIIZ-строка

выделение памяти в Куче (sbrk)

9

$a0 = размер требуемой памяти в байтах

$v0 ­— адрес требуемой памяти

останов (exit)

10

вывод символа

11

$a0 = символ

Некоторые управляющие символы, например, перевод строки (код 10) тоже имеет смысл выводить

ввод символа

12

$v0 — считанный символ (в командной строке всё равно придётся нажать ещё и Enter :( )

системное время (time)

30

Время выдаётся в милисекундах с начала мира (полночь на 1 января 1970 года), так что помещается как длинное целое в два слова $a0 = старшее слово $a1 = младшее слово

приостановка выполнения (sleep)

32

$a0 = время простоя в милисекундах.

вывод шестнадцатеричного

34

$a0 = целое

вывод двоичного

35

$a0 = целое

вывод беззнакового

36

$a0 = целое

Замечания

Пример работы со строками: подсчёт количества пробелов во введённой строке.

   1 .eqv    SIZE 100                # размер буфера
   2 .data
   3 str:    .space SIZE             # буфер
   4 .text
   5         la      $a0 str         # считаем строку в буфер
   6         li      $a1 SIZE
   7         li      $v0 8
   8         syscall
   9         move    $t0 $zero       # счётчик пробелов
  10         li      $t2 0x20        # пробел
  11 loop:   lb      $t1 ($a0)       # очередной символ
  12         beqz    $t1 fin         # нулевой — конец строки
  13         bne     $t1 $t2 nosp    # не пробел — обойдём счётчик
  14         addi    $t0 $t0 1       # пробел — увеличим счётчик
  15 nosp:   addi    $a0 $a0 1       # следующий символ
  16         b       loop
  17 fin:    move    $a0 $t0         # выведем счётчик
  18         li      $v0 1
  19         syscall
  20         li      $v0 10
  21         syscall

Заказ памяти у системы

Статическая память выделяется при трансляции секций .data . В результате трансляции в определённых областях оперативной памяти (по умолчанию — начиная с 0x10010000) резервируются области, которые затем доступны с помощью меток или непосредственно по адресам. Области могут быть инициализированы начальными значениям (как в .word, .asciiz и т. п.), а могут остаться без изменения (как в .space). Добавление новых директив резервирования памяти в секцию .data приведёт к тому, что соответствующие ячейки окажутся после уже отведённых.

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

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

В MARS этим занимается операционная система. Системный вызов 9, которому в $a0 передаётся объём заказываемой памяти, отведёт на куче соответствующую область и вернёт в $v0 её адрес. Следующий вызов syscall 9 вернёт адрес после уже заказанной области и т. д.

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

Подпрограмма принимает в $a0 объём памяти, а возвращает в $v0 выделенный адрес.

   1 newmem: li      $v0 9           # Закажем память (объём уже в $a0)
   2         syscall
   3         addi    $sp $sp -4
   4         sw      $v0 ($sp)       # Сохраним адрес
   5         move    $a0 $v0         # Выведем адрес
   6         li      $v0 34          # как 16-ричное число
   7         syscall
   8         li      $a0 10          # Выведем перевод строки
   9         li      $v0 11
  10         syscall
  11         lw      $v0 ($sp)       # Вспомним адрес
  12         addi    $sp $sp 4
  13         jr $ra

Напишем программу, которая трижды просит у системы память, причём один раз — нечётного объёма в байтах:

   1 .globl main
   2 main:   li      $a0 0x100       # Закажем 256 байтов
   3         jal     newmem
   4         li      $a0 0x101       # закажем 257 байтов
   5         jal     newmem
   6         li      $a0 0x100       # Закажем опять 256 байтов
   7         jal     newmem

Несмотря на кажущуюся простоту, освобождать в куче память для повторного использования намного сложнее. Рассмотрим общую задачу выделения/освобождения памяти (т. н. memory management).

Один из простейших вариантов решения — собственно Куча, как алгоритм работы с динамической памятью (описан в литературе, не путать со структурой данных «куча»): все когда-либо запрошенные области памяти (занятые и уже освобождённые) составляют список, и когда приходит запрос на выделение памяти, сначала просматривается этот список (точнее, его часть, содержащая освобождённые области), а если подходящей освобождённой области нет, заводится новая. У Кучи есть масса модификаций, но это уже предмет курся «Операционные системы».

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

Макроподстановка и макрокоманды

базоваыя лекция

(документация)

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

Псевдоннимы

Примитивный макрос — директива .eqv имя строка, которая добавляет имя в список распознаваемых ассемблером лексем, а в результате препроцессинга (обработки текста перед трансляцией) происходит замена этой лексемы на строку

Макроподстановка и простые макросы

(Да, я знаю, что правильно «макро», а «макрос» — это множественное число но ничего не могу с собой поделать).

Механизм макроподстановки может быть и посложнее:

   1 .macro    exit
   2     li    $v0 10
   3     syscall
   4 .end_macro
   5 
   6 .text
   7     nop
   8     exit

Первые 4 строчки — задание макроса exit, оно же макроопределение, последняя — использование этого макроса, оно же макрокоманда. (Не «вызов макроса», потому что на месте макрокоманды не будет никакой инструкции вызова, только то, что составляло тело макроса).

Добрый Mars даже распишет номера строк, в которых находилось макросово тело:

snap-0411-214109.png

Здесь 7 и 8 — номера строк исходного текста, а <2> и <3> — номера строк, на которых располагалось тело макроса.

Сам макрос (в отличие от подпрограммы), конечно, ни во что не странслировался, потому что он — всего лишь задание нового правила для трансляции каждой макрокоманды.

Самое удобное в макроподстановке — параметризация макрокоманд. Общий вид макроопределения:

   1 .macro имя %параметр1 %параметр2
   2        тело макроса, в строках которого
   3        могут встречаться %параметр1, %параметр2 и т. д.
   4 .end_macro

Например:

   1 .macro        print    %reg
   2     move      $a0 %reg
   3     li        $v0 1
   4     syscall
   5 .end_macro
   6 
   7 .text
   8     li       $t0 100
   9     li       $t1 -20
  10     print    $t0
  11     print    $t1

Здесь макрокоманда print дважды раскрывается в три инструкции, причём первая из них (move) в первом случае подставится в виде move $a0 $t0, а во втором — в виде move $a0 $t1:

snap-0411-220601.png

Обратите внимание на то, как отмечает Mars номера строк исходного кода и строк в теле макроса.

Макроподстановка, вообще говоря, может не иметь никакого отношения к синтаксису того текста, в котором встречаются макросы (например, универсальные макропроцессоры m4 или cpp). В ассемблере MARS присутствует ограничение: параметрами макрокоманды могут быть только лексемы самого ассемблера, а не произвольные строки.

Такая реализация проще (для препроцессора и для последующей трансляции используется один и тот же анализатор), но не такая гибкая. Подставить «--» вместо $t0 в макрокоманде из примера не удастся ещё на этапе макорподстановки (ошибка «mips2.asm line 10 column 2: forward reference or invalid parameters for macro "print"» в строке с макрокомандой). А вот 100500 вместо $t0 пройдёт макроподстановку (потому что 100500 — это хорошее годное целое число), но полученный текст не пройдёт трансляцию с сообщением «mips2.asm line 10->2 column 11: "100500": operand is of incorrect type». Ошибка возникнет, с точки зрения ассемблера Mars, всё в той же строке 10, но по вине строки 2 макроопределения.

Кстати, print-ы в примере слились в одну строку, потому что никто не вывел между ними ещё и разделителя. Чтобы исправить это положение, не надо модифицировать основную программу! Достаточно добавить в макроопеделение print такой вывод:

   1 .macro  print   %reg
   2         move    $a0 %reg
   3         li      $v0 1
   4         syscall
   5         li      $a0 10
   6         li      $v0 11
   7         syscall
   8 .end_macro

Сама программа при этом разрастётся чуть ли не в два раза:

snap-0411-224453.png

Макровзрыв

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

Метки и макроподстановка

Мы уже знаем, что процесс макроподстановки достаточно умён, чтобы находить в макроопределении формальные параметры и подставлять вместо них фактические. Не меньше (а может быть, и больше) интеллекта ему требуется, чтобы отслеживать метки.

Свойства макроассемблера MARS

Замечания авторов MARS относительно их макроассемблера:

Чего нет в Mars

Макроассемблер Mars вполне достаточен для учебных целей, но не реализует много из того, что есть в промышленных средствах программирования на ассемблере

Д/З

  1. Прочесть всю лекцию (сложное задание, я знаю :) )

  2. Написать библиотеку нужных макроопределений. Эту библиотеку при реальной работе можно включать директивой .include, но при сдаче Д/З в EJudge надо вставлять непосредственно в код. Дальнейшие формулировки Д/З исходят из того, что такая библиотека есть, и ею можно пользоваться (соответственно уменьшается объём написанного кода). Например, вывод чисел теперь будет в столбик :) . Предполагаю, что в библиотеке будут разные макросы ввода и вывода, push и pop, а также пролог и эпилог хорошей конвенциальной подпрограммы. На самом деле такой инклюдник можно писать по ходу: подвернётся подходящий макро — добавить.

TODO Примерные задания.

  1. EJudge: CrtDraw 'ASCII-арт'

    Написать полную программу. Вводятся два целых числа, M и N, выводится «решётка» из символов «+», «-», «|» и « », в которой содержится M×N клеток.

    Input:

    3
    4
    Output:

    +-+-+-+
    | | | |
    +-+-+-+
    | | | |
    +-+-+-+
    | | | |
    +-+-+-+
    | | | |
    +-+-+-+
  2. EJudge: TacStr 'Задом наперёд'

    Написать полную программу, которая вводит строки до тех пор, пока не будет введена пустая (в действительности она состоит из двух символов — перевода строки и нулевого байта в конце), а затем выводит все непустые строки в обратном порядке — сначала последнюю, потом предпоследнюю, и т. д. до первой.

    Input:

    404. Thats an error.
    The requested URL /sea was not found on this server.
    Thats all we know.
    Output:

    Thats all we know.
    The requested URL /sea was not found on this server.
    404. Thats an error.
  3. EJudge: KeySort 'Сортировка по ключу'

    Сортировка по ключу. Написать полную программу, в которой будет подпрограмма сортировки массива по адресу $a0 и $a1 элементов, причем в $a2 передаётся размер одного элемента (пока что всегда 4), а в $a3 — адрес подпрограммы сравнения двух элементов в памяти (эта подпрограмма, в свою очередь, принимает два параметра, $a0 и $a1). Программа вводит натуральное число N, затем — 0 или 1, затем — N штук целых чисел, сортирует их, и выдаёт в столбик. Написать две подпрограммы сравнения: если был введён 0, числа упорядочиваются по возрастанию, если 1 — по убыванию остатка от деления на 10 (сортировка должна быть устойчивой, например, пузырьком).

    Input:

    9
    0
    34
    456
    2
    5
    567
    2
    2
    0
    42
    Output:

    0
    2
    2
    2
    5
    34
    42
    456
    567

LecturesCMC/ArchitectureAssembler2019/06_FrameSyscallsMacros (last edited 2019-05-17 13:40:49 by ArsenyMaslennikov)