Профессиональное веб-программирование

Лабораторная работа №7. Вызов подпрограммы, работа со стеком

Разбить на страницы
Показывать лекцию целиком

7.1. Цель и задачи

Целью работы является разработка простой программы преобразования данных для приобретения практических навыков программирования на языке ассемблера и закрепления знаний по работе с подпрограммами и стеком. Задачи:

  • Изучение соглашений о вызовах подпрограмм.
  • Освоение принципов работы со стеком.
  • Презентация к блоку "Ассемблер RISC-V"

    7.2. Основные теоретические сведения

    7.2.1 Директивы размещения данных в памяти

    Определения

  • TEXT - область для инструкций программы. Теоретически никто не мешает иметь несколько директив .text адрес, размещающих код по различным адресам
  • DATA - область для всевозможных данных программы (глобальных переменных, статических локальных переменных, кучи и стека)
  • extern base address - область для внешних данных (нужна для взаимодействия с ОС). В этой области размещает данные директива .extern
  • .data base address - начало области, в которую обычно раскладываются данные директивами .data. Именно там лежат переменные, объявленные массивы и прочее. Традиционно имеется зазор между началом области данных и непосредственно статическими данными. Обычно в процессе работы программы нельзя переходить по адресам из области данных и декодировать их как инструкции.
  • В программе на языке ассемблера возникает необходимость описать содержимое сегмента памяти. Для этого код программы помечается .text, а данные - .data.

    В секции .data помещают директивы (указания ассемблеру) по размещению данных в памяти.

  • .word число - одно или несколько 4-байтовых чисел
  • .dword число - одно или несколько 8-байтовых чисел
  • .half число - одно или несколько 2-байтовых чисел
  • .byte число - одно или несколько однобайтовых чисел
  • .ascii "строка" - последовательность символов в кодировке ASCII
  • .asciz "строка" - то же, только после последнего символа обязательно записывается нулевой байт (конец строки, договорённость, например, для языка Си). Пример размещения данных различного размера:
  • .data
    .word   0xdeadbeef
    .dword  0xacebad0feeded
    .half   0x1234, 0x5678
    .byte   12, 13, 14, 15
    .half   0x3344
    .byte   0x66, 0x77
    

    Результат трансляции пословно (допустим секция .data начинается с адреса 0x10010000). Используется little endian: младший байт в слове имеет меньший адрес:

    10010000: deafbeef d0feeded 000aceba 56781234 0f0e0d0c 77663344
    

    7.2.2 Инструкция ecall

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

    Важно отметить, что в случае bare metal программирования на RISC-V (то есть работа с устройствами напрямую, без ОС) процедуры из ecall могут быть не поддержаны, так как будет отсутствовать необходимый уровень абстракции для его работы (ядро ОС).

    Рассмотрим соглашение о вызовах ecall (ABI) на примере RISC-V ОС:

  • В регистр a7 помещается номер системной функции (service number).
  • Если есть параметры системного вызова, то они помещаются в регистры a0-a6.
  • Инструкция ecall передаёт управление операционной системе (в данном случае - ядру RISC-V ОС).
  • Возврат из системного вызова происходит по аналогии с возвратом из подпрограммы, на следующую после ecall инструкцию
  • Возвращаемое значение (если есть) помещаются в a0.
  • Пример: вывести на консоль число, лежащее в регистре t0

    li  a7 64         # Функция 64 - вывод числа
    mv  a0 t0
    ecall             # Выведённое число помещается в a0
    

    Для некоторых вызовов вам могут потребоваться строковые аргументы. С точки зрения ассемблера, строки - это последовательности ненулевых байтов, заканчивающихся символом с кодом 0 (однобайтным нулём) согласно формату ASCIZ. Ввод заканчивается символом перевода строки (ASCII-код 10, соответствует нажатию Enter). Этот символ тоже попадает в строку! Так что, если требуется ввести слово не более, чем из 10 букв, размер буфера должен быть на 2 больше - 12. 11-м символом в нём будет '\n', а 12-м - 0.

    Полный список поддерживаемых системных вызовов для RISC-V ОС можно найти в таблице (необходимо найти колонку для архитектуры riscv64).

    7.2.3 Подпрограммы

    Для разработки комплексных приложений может потребоваться не только применение условных переходов, но и разбиение исходного кода на повторно используемые фрагменты (подпрограммы). Подпрограмма - часть программного кода, оформленная таким образом, что

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

       .text
    main:
       # Подготовка к вызову функции - сохранение аргументов и
       # сохранение адреса возврата
       # Аргументы (первые 8) - в a0-a7
      # Адрес возврата в ra (x1) 
       ..
       # Вызов функции - выполнение перехода к метке func
       j func # Для упрощения мы использовали j, в реальности 
       # используется lalr или jar
       # Продолжение работы программы
       ..
    func:
       # Код функции стартует с метки func
       ..  
       # Получение аргументов и действия с ними (если они подразумевались)
       ..
       # Формирование возвращаемого значения в a0
       ..
       # выход из функции - переход к адресу возврата
       jalr   zero, ra,0  
    

    Показанный выше пример является крайне упрощенным и не иллюстрирует всех сценариев разработки подпрограмм и организации кода. Для того, чтобы такой ценный и ограниченный ресурс как регистры использовался разумно и безопасно для исполняемого кода, в application binary interface (ABI) есть явный набор соглашений о том, как необходимо поступать со значениями регистров при вызовах и возврате из подпрограмм. Одно из соглашений заключается в сохранении значений регистра, когда команда перехода вызывает подпрограмму. Это означает, что обратный адрес инструкции после команды перехода в памяти сохраняется в регистре, а программный счетчик является началом подпрограммы. Подпрограмма состоит из инструкций, которые обрабатываются и возвращаются обратно к инструкции, следующей за ее вызовом. Для возврата подпрограмма использует сохраненный обратный адрес. В таблице 7.1 обобщены псевдонимы и соглашение о хранении, например, регистр x1 имеет псевдоним ra, который обозначает адрес возврата, и вызывающий функцию должен сохранить значение регистра x1 перед вызовом функции и восстановить его после возврата функции.

    Соглашение об использовании регистров
    Номер Название Описание Сохраняется?
    x0 zero Константа нуля (zero register) n/a
    x1 ra Адрес возврата (return address) нет
    x2 sp Указатель стека (stack pointer) да
    x3 gp Глобальный указатель (global pointer) n/a
    x4 tp Указатель потока (thread pointer) n/a
    x5-x7 t0-t2 Временные переменные (temporary registers) нет
    x8 s0 / fp Сохраняемая переменная /Указатель фрейма стека (saved register / frame pointer) да
    x9 s1 Сохраняемая переменная (saved register) да
    x10-x11 a0-a1 Аргументы функций/Возвращаемые значения (function arguments / return values) нет
    x12-x17 a2-a7 Аргументы функций (function arguments) нет
    x18-x27 s2-s11 Сохраняемые переменные (saved registers) дп
    x28-x31 t3-t6 Временные переменные (temporary registers) нет

    Как правило, сохраненные регистры s0-s11 сохраняются при вызовах функций, в то время как регистры аргументов a0-a7 и временные регистры t0-t6 - нет.

    В RISC-V для передачи аргументов в подпрограмме используются 8 регистров аргументов, а именно a0-a7. Перед выполнением вызова подпрограммы аргументы подпрограммы копируются в регистры аргументов а0-а7. Стек используется в том случае, если количество аргументов превышает 8.

    Для вызова подпрограммы используются команды jal (типа U) или jalr (типа S). Эти команды работают позиционно-независимо - в одной (jal) чётко формируется смещение относительно адреса текущей инструкции (но этот переход ограничен 20-битным адресом), в другой (jalr) - полный адрес перехода формируется в s0 с помощью команды auipc (адрес 32-битный). auipc выполняет формирование адреса при помощи адреса текущей инструкции и корректирующего сложения. Таким образом, в зависимости от величины необходимого смещения относительно счетчика команд (PC) для вызова подпрограмм применяется или jal или комбинация auipc и jalr.

    Пример, в котором подпрограмма сначала вызывается с помощью jal, а затем - с помощью jalr:

    jal ra,0x00000018   # jal   subr
    auipc s1,0      # la    s1 subr
    addi  s1, s1,20
    jalr  ra, s1,0      # jalr  s1
    addi  x17, zero,10      # li    a7 10
    ecall                   # ecall
    auipc a0,0x0000fc10   # subr: la   a0 ping
    addi  a0, a0,0xffffffe8
    addi  a7, zero,4       # li   a7 4
    ecall                   # ecall
    jalr  zero, ra,0      # ret
    

    Примечание: справа в комментариях приведен текст с использованием псевдоинструкций.

    В качестве команды возврата используется jalr. Как это работает:

  • в качестве регистра перехода используется ra
  • в качестве регистра возврата используется zero (это позволяет разработчику сразу определять, что перед нами инструкция не перехода, а именно возврата из подпрограммы)
  • 7.2.4 Стек

    Для динамического хранения локальных переменных и адресов возврата нужен стек. Стек регулируется соответствующим соглашением ABI:

  • выделенный регистр sp (x2)
  • определено дно стека и его начальное значение
  • начальное значение отделено от дна буфером
  • стек растёт вниз по одному слову (4 байта)
  • операции добавления и снятия:
  • при добавлении сначала уменьшается указатель, затем записывается значение
  • при снятии сначала считывается значение, затем увеличивается указатель
  • при такой организации не используется исходная ячейка стека (начальное значение)
  • Пример

    li t1 123       # какое-то значение
    addi sp sp -4   # положить на стек - 1
    sw t1 (sp)      # положить на стек - 2
    addi t2 t1 100  # какое-то ещё значение
    addi sp sp -4   # положить на стек - 1
    sw t2 (sp)      # положить на стек - 2
    lw t3 (sp)      # доступ к вершине стека
    lw t4 4(sp)     # доступ ко 2-му элементу стека
    lw t0 (sp)      # снятие со стека - 1
    addi sp sp 4    # снятие со стека - 2
    addi sp sp -4   # положить на стек - 1
    sw zero (sp)    # положить на стек - 2
    

    Особенности хранения данных в стеке

  • Несколько более эффективно, чем в произвольном месте памяти (lw/sw не превращаются в псевдоинструкции).
  • Использует адресацию относительно постоянно меняющегося sp. Как следствие, требует аккуратного подсчёта текущей глубины стека.
  • Не требует явного указания адреса и заведения метки в программе на языке ассемблера.
  • Может привести к сбоям в работе при переполнении/исчерпании/неаккуратном использовании стека.
  • 7.3 Задание к лабораторной работе

  • Создать программу согласно варианту, которая
  • выполняет заданные действия,
  • выводит на экран сообщения о результатах/ошибках.
  • Откомпилировать программу и запустить на исполнение.
  • Отладить программу, проследить изменения в регистрах и ОП.
  • 7.3.1 Описание последовательности выполнения работы

    Скомпилируйте исходную программу и убедитесь в ее работоспособности в RISC-V ОС, либо через qemu-riscv64.

    7.3.2 Пример выполнения задания на защиту

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

    function gcd(a, b)
                    while a ? b
                  if a > b
                  a := a ? b
                  else
                  b := b ? a
                    return a
    

    Фрагмент решения:

    main:
      # read_int (t0)
      # read_int (t1)
                       
      mv  a0, t0
      mv  a1, t1
      jal euclid
     
      li  a7, 1
      ecall
      li a7, 10
      ecall
     
    euclid:
      beq a0, a1, finish
      blt a0, a1, if_less
      sub a0, a0, a1
      j   euclid
    if_less:
      sub a1, a1, a0
      j   euclid
    finish:
      jr ra
    
    

    ВАРИАНТЫ ЗАДАНИЙ

  • Напишите программу, которая вводит целые числа M и N и выводит сетку M x N, составленную с помощью + и -. Вы должны написать подпрограмму, который принимает два параметра: M и N, и выводит строку, подобную этой: +-+-+-+-+
    Input:
    3
    4
    Output:
    +-+-+-+
    | | | |
    +-+-+-+
    | | | |
    +-+-+-+
    | | | |
    +-+-+-+
    | | | |
    +-+-+-+
     
  • Написать функцию, которая меняет местами два значения, переданные в стек.
  • Написать функцию, которая находит минимальное и максимальное значение в заданном массиве целых чисел.
  • 7.4 Вопросы для контроля

  • В чем состоит основная идея использования подпрограмм?
  • Какими командами можно реализовать вызов подпрограммы в RISC-V?
  • Для чего используется стек?
  • Страницы:

    7.1. Цель и задачи

    Целью работы является разработка простой программы преобразования данных для приобретения практических навыков программирования на языке ассемблера и закрепления знаний по работе с подпрограммами и стеком. Задачи:

  • Изучение соглашений о вызовах подпрограмм.
  • Освоение принципов работы со стеком.
  • Презентация к блоку "Ассемблер RISC-V"

    7.2. Основные теоретические сведения

    7.2.1 Директивы размещения данных в памяти

    Определения

  • TEXT - область для инструкций программы. Теоретически никто не мешает иметь несколько директив .text адрес, размещающих код по различным адресам
  • DATA - область для всевозможных данных программы (глобальных переменных, статических локальных переменных, кучи и стека)
  • extern base address - область для внешних данных (нужна для взаимодействия с ОС). В этой области размещает данные директива .extern
  • .data base address - начало области, в которую обычно раскладываются данные директивами .data. Именно там лежат переменные, объявленные массивы и прочее. Традиционно имеется зазор между началом области данных и непосредственно статическими данными. Обычно в процессе работы программы нельзя переходить по адресам из области данных и декодировать их как инструкции.
  • В программе на языке ассемблера возникает необходимость описать содержимое сегмента памяти. Для этого код программы помечается .text, а данные - .data.

    В секции .data помещают директивы (указания ассемблеру) по размещению данных в памяти.

  • .word число - одно или несколько 4-байтовых чисел
  • .dword число - одно или несколько 8-байтовых чисел
  • .half число - одно или несколько 2-байтовых чисел
  • .byte число - одно или несколько однобайтовых чисел
  • .ascii "строка" - последовательность символов в кодировке ASCII
  • .asciz "строка" - то же, только после последнего символа обязательно записывается нулевой байт (конец строки, договорённость, например, для языка Си). Пример размещения данных различного размера:
  • .data
    .word   0xdeadbeef
    .dword  0xacebad0feeded
    .half   0x1234, 0x5678
    .byte   12, 13, 14, 15
    .half   0x3344
    .byte   0x66, 0x77
    

    Результат трансляции пословно (допустим секция .data начинается с адреса 0x10010000). Используется little endian: младший байт в слове имеет меньший адрес:

    10010000: deafbeef d0feeded 000aceba 56781234 0f0e0d0c 77663344
    

    7.2.2 Инструкция ecall

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

    Важно отметить, что в случае bare metal программирования на RISC-V (то есть работа с устройствами напрямую, без ОС) процедуры из ecall могут быть не поддержаны, так как будет отсутствовать необходимый уровень абстракции для его работы (ядро ОС).

    Рассмотрим соглашение о вызовах ecall (ABI) на примере RISC-V ОС:

  • В регистр a7 помещается номер системной функции (service number).
  • Если есть параметры системного вызова, то они помещаются в регистры a0-a6.
  • Инструкция ecall передаёт управление операционной системе (в данном случае - ядру RISC-V ОС).
  • Возврат из системного вызова происходит по аналогии с возвратом из подпрограммы, на следующую после ecall инструкцию
  • Возвращаемое значение (если есть) помещаются в a0.
  • Пример: вывести на консоль число, лежащее в регистре t0

    li  a7 64         # Функция 64 - вывод числа
    mv  a0 t0
    ecall             # Выведённое число помещается в a0
    

    Для некоторых вызовов вам могут потребоваться строковые аргументы. С точки зрения ассемблера, строки - это последовательности ненулевых байтов, заканчивающихся символом с кодом 0 (однобайтным нулём) согласно формату ASCIZ. Ввод заканчивается символом перевода строки (ASCII-код 10, соответствует нажатию Enter). Этот символ тоже попадает в строку! Так что, если требуется ввести слово не более, чем из 10 букв, размер буфера должен быть на 2 больше - 12. 11-м символом в нём будет '\n', а 12-м - 0.

    Полный список поддерживаемых системных вызовов для RISC-V ОС можно найти в таблице (необходимо найти колонку для архитектуры riscv64).

    7.2.3 Подпрограммы

    Для разработки комплексных приложений может потребоваться не только применение условных переходов, но и разбиение исходного кода на повторно используемые фрагменты (подпрограммы). Подпрограмма - часть программного кода, оформленная таким образом, что

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

       .text
    main:
       # Подготовка к вызову функции - сохранение аргументов и
       # сохранение адреса возврата
       # Аргументы (первые 8) - в a0-a7
      # Адрес возврата в ra (x1) 
       ..
       # Вызов функции - выполнение перехода к метке func
       j func # Для упрощения мы использовали j, в реальности 
       # используется lalr или jar
       # Продолжение работы программы
       ..
    func:
       # Код функции стартует с метки func
       ..  
       # Получение аргументов и действия с ними (если они подразумевались)
       ..
       # Формирование возвращаемого значения в a0
       ..
       # выход из функции - переход к адресу возврата
       jalr   zero, ra,0  
    

    Показанный выше пример является крайне упрощенным и не иллюстрирует всех сценариев разработки подпрограмм и организации кода. Для того, чтобы такой ценный и ограниченный ресурс как регистры использовался разумно и безопасно для исполняемого кода, в application binary interface (ABI) есть явный набор соглашений о том, как необходимо поступать со значениями регистров при вызовах и возврате из подпрограмм. Одно из соглашений заключается в сохранении значений регистра, когда команда перехода вызывает подпрограмму. Это означает, что обратный адрес инструкции после команды перехода в памяти сохраняется в регистре, а программный счетчик является началом подпрограммы. Подпрограмма состоит из инструкций, которые обрабатываются и возвращаются обратно к инструкции, следующей за ее вызовом. Для возврата подпрограмма использует сохраненный обратный адрес. В таблице 7.1 обобщены псевдонимы и соглашение о хранении, например, регистр x1 имеет псевдоним ra, который обозначает адрес возврата, и вызывающий функцию должен сохранить значение регистра x1 перед вызовом функции и восстановить его после возврата функции.

    Соглашение об использовании регистров
    Номер Название Описание Сохраняется?
    x0 zero Константа нуля (zero register) n/a
    x1 ra Адрес возврата (return address) нет
    x2 sp Указатель стека (stack pointer) да
    x3 gp Глобальный указатель (global pointer) n/a
    x4 tp Указатель потока (thread pointer) n/a
    x5-x7 t0-t2 Временные переменные (temporary registers) нет
    x8 s0 / fp Сохраняемая переменная /Указатель фрейма стека (saved register / frame pointer) да
    x9 s1 Сохраняемая переменная (saved register) да
    x10-x11 a0-a1 Аргументы функций/Возвращаемые значения (function arguments / return values) нет
    x12-x17 a2-a7 Аргументы функций (function arguments) нет
    x18-x27 s2-s11 Сохраняемые переменные (saved registers) дп
    x28-x31 t3-t6 Временные переменные (temporary registers) нет

    Как правило, сохраненные регистры s0-s11 сохраняются при вызовах функций, в то время как регистры аргументов a0-a7 и временные регистры t0-t6 - нет.

    В RISC-V для передачи аргументов в подпрограмме используются 8 регистров аргументов, а именно a0-a7. Перед выполнением вызова подпрограммы аргументы подпрограммы копируются в регистры аргументов а0-а7. Стек используется в том случае, если количество аргументов превышает 8.

    Для вызова подпрограммы используются команды jal (типа U) или jalr (типа S). Эти команды работают позиционно-независимо - в одной (jal) чётко формируется смещение относительно адреса текущей инструкции (но этот переход ограничен 20-битным адресом), в другой (jalr) - полный адрес перехода формируется в s0 с помощью команды auipc (адрес 32-битный). auipc выполняет формирование адреса при помощи адреса текущей инструкции и корректирующего сложения. Таким образом, в зависимости от величины необходимого смещения относительно счетчика команд (PC) для вызова подпрограмм применяется или jal или комбинация auipc и jalr.

    Пример, в котором подпрограмма сначала вызывается с помощью jal, а затем - с помощью jalr:

    jal ra,0x00000018   # jal   subr
    auipc s1,0      # la    s1 subr
    addi  s1, s1,20
    jalr  ra, s1,0      # jalr  s1
    addi  x17, zero,10      # li    a7 10
    ecall                   # ecall
    auipc a0,0x0000fc10   # subr: la   a0 ping
    addi  a0, a0,0xffffffe8
    addi  a7, zero,4       # li   a7 4
    ecall                   # ecall
    jalr  zero, ra,0      # ret
    

    Примечание: справа в комментариях приведен текст с использованием псевдоинструкций.

    В качестве команды возврата используется jalr. Как это работает:

  • в качестве регистра перехода используется ra
  • в качестве регистра возврата используется zero (это позволяет разработчику сразу определять, что перед нами инструкция не перехода, а именно возврата из подпрограммы)
  • 7.2.4 Стек

    Для динамического хранения локальных переменных и адресов возврата нужен стек. Стек регулируется соответствующим соглашением ABI:

  • выделенный регистр sp (x2)
  • определено дно стека и его начальное значение
  • начальное значение отделено от дна буфером
  • стек растёт вниз по одному слову (4 байта)
  • операции добавления и снятия:
  • при добавлении сначала уменьшается указатель, затем записывается значение
  • при снятии сначала считывается значение, затем увеличивается указатель
  • при такой организации не используется исходная ячейка стека (начальное значение)
  • Пример

    li t1 123       # какое-то значение
    addi sp sp -4   # положить на стек - 1
    sw t1 (sp)      # положить на стек - 2
    addi t2 t1 100  # какое-то ещё значение
    addi sp sp -4   # положить на стек - 1
    sw t2 (sp)      # положить на стек - 2
    lw t3 (sp)      # доступ к вершине стека
    lw t4 4(sp)     # доступ ко 2-му элементу стека
    lw t0 (sp)      # снятие со стека - 1
    addi sp sp 4    # снятие со стека - 2
    addi sp sp -4   # положить на стек - 1
    sw zero (sp)    # положить на стек - 2
    

    Особенности хранения данных в стеке

  • Несколько более эффективно, чем в произвольном месте памяти (lw/sw не превращаются в псевдоинструкции).
  • Использует адресацию относительно постоянно меняющегося sp. Как следствие, требует аккуратного подсчёта текущей глубины стека.
  • Не требует явного указания адреса и заведения метки в программе на языке ассемблера.
  • Может привести к сбоям в работе при переполнении/исчерпании/неаккуратном использовании стека.
  • 7.3 Задание к лабораторной работе

  • Создать программу согласно варианту, которая
  • выполняет заданные действия,
  • выводит на экран сообщения о результатах/ошибках.
  • Откомпилировать программу и запустить на исполнение.
  • Отладить программу, проследить изменения в регистрах и ОП.
  • 7.3.1 Описание последовательности выполнения работы

    Скомпилируйте исходную программу и убедитесь в ее работоспособности в RISC-V ОС, либо через qemu-riscv64.

    7.3.2 Пример выполнения задания на защиту

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

    function gcd(a, b)
                    while a ? b
                  if a > b
                  a := a ? b
                  else
                  b := b ? a
                    return a
    

    Фрагмент решения:

    main:
      # read_int (t0)
      # read_int (t1)
                       
      mv  a0, t0
      mv  a1, t1
      jal euclid
     
      li  a7, 1
      ecall
      li a7, 10
      ecall
     
    euclid:
      beq a0, a1, finish
      blt a0, a1, if_less
      sub a0, a0, a1
      j   euclid
    if_less:
      sub a1, a1, a0
      j   euclid
    finish:
      jr ra
    
    

    ВАРИАНТЫ ЗАДАНИЙ

  • Напишите программу, которая вводит целые числа M и N и выводит сетку M x N, составленную с помощью + и -. Вы должны написать подпрограмму, который принимает два параметра: M и N, и выводит строку, подобную этой: +-+-+-+-+
    Input:
    3
    4
    Output:
    +-+-+-+
    | | | |
    +-+-+-+
    | | | |
    +-+-+-+
    | | | |
    +-+-+-+
    | | | |
    +-+-+-+
     
  • Написать функцию, которая меняет местами два значения, переданные в стек.
  • Написать функцию, которая находит минимальное и максимальное значение в заданном массиве целых чисел.
  • 7.4 Вопросы для контроля

  • В чем состоит основная идея использования подпрограмм?
  • Какими командами можно реализовать вызов подпрограммы в RISC-V?
  • Для чего используется стек?
  • Вернуться к учебному плану