Администратор информационной безопасности

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

Показывать лекцию целиком

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

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

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

  • Изучить примеры простейших ассемблерных программ.
  • Ознакомиться с основными командами для компиляции ассемблерных программ.
  • На примере 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. Задание к лабораторной работе

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

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

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

  • Какую задачу решает утилита ld из пакета binutils?
  • Какие задачи решают команды ассемблера li и auipc?
  • Для чего предназначена секция .global?
  • Что такое метки?
  • Вернуться к учебному плану