Опубликован: 19.01.2025 | Доступ: свободный | Студентов: 0 / 0 | Длительность: 05:57:00
Лекция 3:

Лабораторная работа №2. Компиляция и запуск ассемблерной программы

< Лекция 2 || Лекция 3 || Лекция 4 >

2.1 Цель и задачи

Целью работы является освоение способов компиляции программ на ассемблере RISC-V.

Для достижения поставленной цели требуется решить следующие задачи:

  1. Изучить примеры простейших ассемблерных программ.
  2. Ознакомиться с основными командами для компиляции ассемблерных программ.
  3. На примере HelloWorld приложений изучить основные структурные элементы ассемблерной программы.

Презентация к блоку "Введение и инструменты сборки"

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

Язык ассемблера представляет максимально близкий к аппаратному обеспечению способ для создания прикладных и системных программ. В таких программах практически отсутствуют привычные абстракции и программист вынужден оперировать реальными аппаратными структурами процессора и ОЗУ, а также учитывать особенности аппаратной архитектуры конкретной системы. Это приводит к тому, что языки ассемблера для разных процессорных архитектур не совместимы между собой, однако, общая логика построения программ и манипуляций с данными сохраняется. Ниже мы поясним необходимые элементы простейшей ассемблерной программы, подробное введение в ассемблер для RISC-V дается в книге, синтаксис команд описан в мануале.

Ассемблерные программы представляют собой последовательность простейших команд для процессора, которые оперируют минимальными элементами данных и простейшими операциями с ними или другими устройствами. Для создания базового комфорта программисту, в большинстве языков ассемблера (в том числе и для RISC-V) предусмотрены такие структурные единицы как псевдооператоры и метки:

  • Псевдооператоры позволяют разделить программу по секциям на статические данные (строковые и числовые константы) и исполняемый код, а также задать точку входа в программу (строку, с которой начинается выполнение программы). Для нашего примера нам потребуется две секции - .global (задает точку входа) и .data (задает секцию данных). Это не единственное назначение псевдооператоров, подробнее можно прочитать в мануале.
  • Метки помогают разделить программу на элементы, которые в ближайшем приближении напоминают функции в языке Си. Метки представляют собой именованные позиции в рамках исходного кода, к которым можно выполнить различные виды переходов в программе. Самый лучший аналог меток из языка Си это оператор GOTO, однако в отличии от Си, в ассемблере можно выполнять и условные переходы по меткам.

2.2.1 Структура ассемблерных программ

В отличии от языков программирования высокого уровня, в ассемблере вместо переменных используются регистры - небольшие ячейки памяти, расположенные на микросхеме процессора. В каждой архитектуре процессора свой ограниченный набор регистров. Малый объем памяти, отводимый на регистры, компенсируется высокой скоростью работы с ними - она превосходит аналогичный показатель для ОЗУ и ПЗУ. Регистры могут иметь специальное назначение - так в примере простейшей программы ниже мы будем использовать четыре регистра (a0, a1, a2, a7), назначение которых подразумевает передачу аргументов для вызываемой функции системного вызова. В остальном работа с регистрами с некоторыми оговорками похожа на работу с переменными в языках программирования высокого уровня.

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

  • la - Load Address (загрузить адрес в регистр),
  • addi - Add an Immediate value to register (сложить константу со значением из регистра),
  • ecall - Environment Call (произвести вызов системной процедуры). В случае ОС Linux (в данном случае RISC-V ОС) это будет означать вызов функции стандартной библиотеки.

Команды могут иметь от нуля до трех операндов (аргументов) для базового набора инструкций. В расширениях RISC-V может встречаться и большее число операндов.

  • la - два операнда (регистр назначения и адрес области данных),
  • addi - три операнда (регистр назначения для результата сложения, слагаемое №1, слагаемое №2),
  • ecall - без операндов.

Простейший helloworld на ассемблере для RISC-V будет выглядеть так:

.global _start      # Точка входа для программы

# Настройка вызова функции write так, чтобы он вывел строчку в терминал
# В регистры a0-a2 записываются аргументы write
# В регистре a7 указывается номер write

_start: addi  a0, x0, 1      # 1 = Поток стандартного выовда (StdOut)
        la    a1, helloworld # Загрузка адреса контсанты helloworld в a1
        addi  a2, x0, 13     # Запись длины строки в a2
        addi  a7, x0, 64     # Запись номера функции write 64
        ecall                # Выполнение функции write

# Настройка вызова exit и вызов функции

        addi    a0, x0, 0   # Указание кода возврата для exit
        addi    a7, x0, 93  # Запись номера функции exit 93
        ecall               # Выполнение функции exit 


.data
helloworld:      .ascii "Hello World!\n"

Необходимо отдельно рассказать о разнице между системным вызовом exit и одноименной функцией стандартной библиотеки:

  • Системный вызов только выходит из текущего потока управления, т.е. нити. За завершение процесса отвечает системный вызов exit_group.
  • В стандартной библиотеке функции exit(), _Exit(), _exit() используют именно вызов exit_group.

2.2.2 Компиляция ассемблерных программ

Для компиляции и запуска нам потребуется выполнить следующие команды в терминале RISC-V ОС (предполагаем, что исходный код сохранен в файле hello.s):

$ as -o hello.o hello.s # Запуск компилятора ассемблерных программ 
$ ld -o hello hello.o    # Запуск линковщика
$ ./hello

В случае успеха, в терминал будет выведена строка "Hello World!".

Аналогичные операции можно проделать и из гостевой ОС, выполняя кросс-компиляцию, при этом потребуется использовать утилиты riscv64-linux-gnu-as и riscv64-linux-gnu-ld соответственно.

Для исследования скомпилированных ассемблерных программ удобно использовать утилиту objdump. Она позволяет выполнить обратное преобразование - из бинарного файла получить код на языке ассемблера:

 $ objdump -d ./hello

Флаг -d в данном случае обозначает "дизассемблировать секции файла, в которых ожидается наличие кода". Результат выполнения:

./hello:     file format elf64-littleriscv

Disassembly of section .text:

00000000000100e8 <_start>:
   100e8:	00100513          	li	a0,1
   100ec:	00001597          	auipc	a1,0x1
   100f0:	02058593          	addi	a1,a1,32 # 1110c <__DATA_BEGIN__>
   100f4:	00d00613          	li	a2,13
   100f8:	04000893          	li	a7,64
   100fc:	00000073          	ecall
   10100:	00000513          	li	a0,0
   10104:	05d00893          	li	a7,93
   10108:	00000073          	ecall

Вывод objdump имеет следующую структуру:

hex-код операции     код операции     ассемблерный код

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

2.3. Задание к лабораторной работе

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

  1. Вывести строку "Hello, I am <ваше ФИО> from group <номер вашей группы>".
  2. Вывести с помощью псевдографики изображение фигуры (размер фигуры по одной из сторон - 3 строки), составленное по следующему алгоритму:
    • a. Если у вас четный номер в списке - вы выводите изображение квадрата, если нечетный - равносторонний прямоугольный треугольник.
    • b. Символы, из которых необходимо составить фигуру, определяются остатком от деления последней цифры в номере вашей группы на 4:
      • 0 - @
      • 1 - #
      • 2 - $
      • 3 - %
  3. Завершить работу программы.

Для вывода изображения используйте несколько последовательных вызовов write.

Программу необходимо скомпилировать и убедится в ее работоспособности на RISC-V ОС. С помощью утилиты objdump (в RISC-V ОС) необходимо дизассемблировать бинарный файл программы и зафиксировать в отчете общее количество строк, а также hex-код последней команды.

2.3.1. Описание последовательности выполнения работы

Для выполнения поставленной задачи необходимо понять, как выглядит текст, выводимый вашей программой и фигура, которую вам необходимо нарисовать в вашей программе. Для определенности будем считать, что это квадрат из символов *, размером 2х2. Для наглядности лучше всего изобразить эту фигуру:

**
**

Далее необходимо составить программу, которая будет выводить указанные строки. Учитывая пример фигуры выше, всего программа должна вывести три строки. Запишите эти строки в секцию .data. Соответственно, необходимо сделать три вызова write.

Далее необходимо скомпилировать и запустить программу в RISC-V ОС. Для этого можно использовать утилиты as и ld, либо применить кросс-компиляцию и запуск из гостевой ОС. После компиляции и запуска, программу необходимо проанализровать через objdump.

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

Напишите программу, которая выводит две строки "I am an example string\n" и "Another string example\n".

.global _start     
_start: addi  a0, x0, 1    
        la    a1, string1 
        addi  a2, x0, 25     
        addi  a7, x0, 64    
        ecall    

        addi  a0, x0, 1    
        la    a1, string2 
        addi  a2, x0, 25     
        addi  a7, x0, 64    
        ecall             

        addi    a0, x0, 0  
        addi    a7, x0, 93 
        ecall              

.data
string1:      .ascii "I am example string\n"
string2:      .ascii "Another string example\n" 

Вопросы для контроля

  1. Какую задачу решает утилита ld из пакета binutils?
  2. Какие задачи решают команды ассемблера li и auipc?
  3. Для чего предназначена секция .global?
  4. Что такое метки?
< Лекция 2 || Лекция 3 || Лекция 4 >