Уровень 56: Вызов и возврат

Открыть уровень →

Задание

Цель: запрограммировать вызов функции: jal (J-формат) прыгает на метку и пишет в rd адрес следующей команды (PC + 4); jalr прыгает по регистру — цель (rs1 + imm) с очисткой бита 0, младший бит адреса всегда 0. Соберите sum_to_n: вызов с аргументом в x5, функция суммирует 5+4+3+2+1 в x6 и возвращается через jalr x0, 0(ra). Останов .end стоит на месте возврата — в середине программы; парсер собирает и код ниже него.

Первая программа, которая умеет вызывать себя по частям: функция с аргументом, телом и возвратом. Схема по-прежнему не меняется — исполнение идёт на эталонном ядре, как на уровнях 54 и 55. Новые команды — jal (J-формат) и jalr:

  • jal rd, label прыгает на метку и одновременно пишет в rd адрес следующей команды (PC + 4) — обратный адрес. jal ra, label — вызов; ra — стандартное имя x1, регистра возврата.
  • jalr rd, imm(rs1) прыгает по регистру: цель = rs1 + imm, причём бит 0 суммы очищается аппаратно, и только затем проверяется кратность 4. jalr x0, 0(ra) — возврат из функции.

Каноническая программа sum_to_n — функция, суммирующая числа от 1 до n (аргумент в x5, результат в x6):

        addi x5, x0, 5      # 0:  аргумент
        jal  ra, sum_to_n   # 4:  вызов (ra = 8)
        .end                # 8:  останов на месте возврата — функция ниже
sum_to_n:
        addi x6, x0, 0      # 12
again:  beq  x5, x0, back   # 16
        add  x6, x6, x5     # 20
        addi x5, x5, -1     # 24
        beq  x0, x0, again  # 28
back:   jalr x0, 0(ra)      # 32

Обратите внимание на .end: здесь он стоит в середине программы. Останов — это место, куда вернётся jalr; функция пишется ниже него, и парсер спокойно собирает код после директивы. Ассемблер испускает слово jal x0, 0 на адресе 8, а метки и команды дальше получают свои адреса как обычно.

Задания уровня:

  1. Соберите слово jal в «Слове из полей» (J-формат появился на 56-м) и разберите разрезку смещения: imm[20]=w31, imm[10:1]=w30:21, imm[11]=w20, imm[19:12]=w19:12 — база смещения, как у beq, это адрес самой команды.
  2. Соберите sum_to_n во вкладке «Ассемблер». Псевдо-команды j label (→ jal x0, label) и ret (→ jalr x0, 0(ra)) тоже доступны с 56-го.
  3. Перед первым «Проверить» ответьте на вопрос про ra: jal стоит по адресу 4 — посчитайте PC + 4 сами.
  4. Поэкспериментируйте в блоке «Исполнение» с целями jalr: jalr x0, 1(x1) при x1 = 0x10 — переход состоится на 0x10 (бит 0 очищен); jalr x0, 2(x1) — цель 0x12, не кратна 4, ошибка ERR_INSTR_ADDRESS_MISALIGNED.

Решение для самопроверки

Сначала посчитайте ra и такты на бумаге. Решение ниже — контрольные точки.

Что должно получиться: sum_to_n возвращается корректно: ra = 8 (адрес .end), x6 = 15, x5 = 0; предсказанные 25 тактов (3 + 6 + 5 × 3 + 1) совпадают с трассой, jalr на PC 32 возвращает PC на адрес 8.

Шаг 1 — обратный адрес. jal по адресу 4 пишет в ra значение 4 + 4 = 8: после возврата исполнение продолжится с адреса 8, где стоит .end. Значит, в конце прогона ra = 8, PC = 8, статус — останов. Это же число спрашивает предикт перед первой проверкой.

Шаг 2 — посчитайте такты. Вход — 3 команды (addi, jal; останов .end такта не добавляет). Функция: страж beq исполняется 6 раз (5 раз не берётся, шестой — берётся и ведёт на back), тело — 5 итераций × (add + addi + возвратный beq), и финальный jalr. Итого 3 + 6 + 5 × 3 + 1 = 25 тактов. Результат: x6 = 1+2+3+4+5 = 15, x5 = 0.

Шаг 3 — разберите jalr. Цель = (rs1 + imm) & ~1: бит 0 очищается всегда, поэтому jalr x0, 1(x1) при x1 = 0x10 прыгает на 0x10 (сумма 0x11 стала 0x10). А вот нечётный бит 1 не исправляется: jalr x0, 2(x1) даёт цель 0x12 — ядро останавливается с ERR_INSTR_ADDRESS_MISALIGNED. Запись в x0 переход не блокирует: rd = x0 лишь отменяет сохранение обратного адреса.

Шаг 4 — сверьте с трассой. Найдите в трассе: jal на PC 4 пишет x1 = 8 и уводит PC на 12; пять возвратов beq на again; jalr на PC 32 возвращает PC на 8; запись останова .end без счётчика тактов. Дальше курса — стек: у функции появятся свои сохраняемые регистры, и ra начнёт жить в памяти.

Проверь себя

Почему jalr очищает бит 0 цели?

Чтобы нечётная сумма rs1 + imm не создала невыровненный адрес: бит 0 обнуляется аппаратно ещё до проверки кратности 4. А вот бит 1 не исправляется — jalr x0, 2(x1) при x1 = 0x10 даёт цель 0x12 и ERR_INSTR_ADDRESS_MISALIGNED.