Skip to content

Latest commit

 

History

History
330 lines (199 loc) · 27.3 KB

lab3.textile

File metadata and controls

330 lines (199 loc) · 27.3 KB

ЛАБОРАТОРНАЯ РАБОТА 3

Цель: Научиться работать с файлом листинга; изучить дополнительные приёмы компоновки и использования директив объявления данных; научиться программировать ветвления в ассемблерной программе и вести простой диалог через устройство ввода вывода.

Краткие теоретические сведения

Файл листинга

Назначение листинга

Листинг – это один из выходных файлов,  создаваемых транслятором. Он имеет текстовый вид и нужен при  отладке  программы,  т.к.  кроме строк самой программы содержит дополнительную информацию.

Обычно as  создает в результате ассемблирования только объектный файл. Получить файл листинга можно, указав ключ -a и задав имя файла листинга в командной строке. Например:

$ arm-none-linux-gnueabi-as -a main.lst main.asm

Структура листинга

Строки в первой части листинга имеют следующую структуру:

  17   00000014  CD80  add r1, r2, r3   @сложение чисел

└──┬─┘└───┬────┘└──┬─┘└────────────────┬─────────────────┘
   │      │        │                   └── исходный текст программы
   │      │        └── машинный код
   │      └── адрес
   └── номер строки

Все ошибки и предупреждения, обнаруженные при ассемблировании, транслятор выводит на экран, и файл листинга не создается.

Номер строки представляет  собой номер строки файла листинга. Номера строк особенно полезны при работе с перекрестными ссылками.

Важно понимать, что номера строк в поле “номер строки” — это не номера строк исходного модуля.  Например,  при расширении макрокоманды  или  включении файла отсчет строк продолжается, хотя текущая строка в исходном файле остается той же.  Чтобы перевести номер строки (сгенерированный,  например, при создании перекрестных ссылок), вы должны найти соответствующую строку в листинге, а затем (по номеру или на глаз) найти ее в исходном файле.

Адрес — это смещение машинного кода от начала текущего сегмента.

Машинный код представляет  собой действительную последовательность шестнадцатеричного значения байт и слов,  которые ассемблируются из соответствующей исходной строки программы. Информация  справа  от  данной инструкции — это машинный код,  в который ассемблируется инструкция.       

Исходный текст программы — это просто строка исходной программы вместе с комментариями. Некоторые строки на языке ассемблера (например,  строки, содержащие только комментарии) не генерируют никакого машинного кода, и поля “смещение” и “исходный текст программы” в таких строках отсутствуют. Тем не менее номер строки им присваивается.

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

Описание инструкций. Команды условного перехода

Команда перехода по адресу, имеющаяся в наборе инструкций процессоров ARM, выглядит следующим образом: b <метка>, где аргумент – метка, поставленная выше или ниже в коде программы – указывает, по какому именно адресу должен быть выполнен переход.

Как и многие другие команды ARM, команда перехода может быть снабжена кодом условия, и будет выполняться, только если это условие истинно. Добавив к команде b код условия, мы получаем целое семейство команд условного перехода.

Команда условного перехода выполняет или не выполняет переход по заданному адресу в зависимости от флагов состояния процессора, хранящихся в специальном регистре cpsr. Флаги – это биты специального регистра, отражающие состояние процессора в текущий момент времени. Наиболее часто используются следующие: флаг отрицательного результата N (установлен в единицу, если реузльтат последней арифметической операции был отрицательным), флаг нуля Z (единица, если в результатом последней операции был ноль), флаг переноса С (устанавливается, если в результате последней операции случился перенос бита из старшего разряда) и флаг переполнения V (в результате последней операции произошло переполнение разрядной сетки).

Чаще всего программисты формируют флаги, проверяя отношение между двумя операндами op1 <отношение> op2, для чего выполняется команда вычитания или команда сравнения. Команда сравнения имеет мнемонический код операции cmp и такой же формат, как и команда вычитания:

cmp op1,op2

Она и выполняется точно так же, как команда вычитания за исключением того, что разность не записывается на место первого операнда. Таким образом, единственным результатом команды сравнения является формирование флагов, которые устанавливаются так же, как и при выполнении команды вычитания. Команды этой группы выполняют условный переход в зависимости от состояния флагов регистра cpsr. Синтаксис этих команд в ассемблере приведен в таблице.

В результате, ветвления в программе организуются в два этапа:

  1. проверка условия командой cmp или формирование флагов каким-то другим способом, например, арифметической инструкцией;
  2. использование инструкции условного перехода, т.е. одной из разновидностей команды b, приведенной в таблице.

Скрипты компоновщика

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


SECTIONS { ❶
        . = 0x00000000; ❷
        .text : { ❸
                abc.o (.text);
                def.o (.text);
        } ❹
}
  1. Команда SECTIONS определяет, как будут объединены секции и куда они должны быть помещены.
  2. В блоке, следующем после команды SECTIONS, приводится численное значение — счётчик размещений. Размещение всегда инициализируется значением 0x0. Его можно проинициализировать каким-либо другим значением. В данном случае установка нами значения в ноль избыточное действие.
  3. и 4. Эта часть скрипта определяет, что секции .text из исходных файлов abc.o и def.o должны перейти в секцию .text выходного файла.

Скрипт компоновщика можно упростить указанием символа * вместо имён файлов:


SECTIONS {
        . = 0x00000000;
        .text : { * (.text); }
}

Если программа содержит обе секции (.text и .data), то объединение и размещение секции .data можно выполнить следующим образом:


SECTIONS {
         . = 0x00000000;
         .text : { * (.text); }
         . = 0x00000400;
         .data : { * (.data); }
}

Здесь секция .text помещается по адресу 0x0, а секция .data по адресу 0x400. Если же счётчику размещений не были присвоены конкретные значения, то секции помещаются в соседних областях памяти.

Пример скрипта компоновщика

Используем последний пример скрипта для управления расположением программных секций .text и .data. Для этой цели воспользуемся слегка модифицированной версией программы для вычисления суммы элементов массива:


        .data
arr: 	.byte 10, 20, 25 	@ Read-only array of bytes
eoa:     			@ Address of end of array + 1
        .text
start:
	ldr   r0, =eoa          @ r0 = &eoa
	ldr   r1, =arr          @ r1 = &arr
	mov   r3, #0            @ r3 = 0
loop:       
	ldrb  r2, [r1], #1      @ r2 = *r1++
	add   r3, r2, r3        @ r3 += r2
	cmp   r1, r0            @ if (r1 != r2)
	bne   loop              @ goto loop
stop:   b stop

Единственное отличие — то, что массив теперь находится в секции .data. Также стоит отметить, что в этой программе не нужна инструкция для перепрыгивания через данные, т. к. скрипт корректно размещает секции .text и .data. В результате объявление данных может быть расположено в программе в любом удобном месте, а скрипт компоновщика позаботится о правильном размещении секций а памяти.

Когда программа компонуется, скрипт передаётся в качестве входных данных компоновщику, как показано в следующих командах:

$ arm-none-linux-gnueabi-as -o sum-data.o sum-data.s
$ arm-none-linux-gnueabi-ld -T sum-data.lds -o sum-data.elf sum-data.o

Опция -T определяет, что файл sum-data.lds должен быть использован в качестве скрипта компоновщика. Сброс таблицы символов даст понимание того, как секции помещаются в памяти

$ arm-none-gnueabi-nm -n sum-data.elf
00000000 t start
0000000c t loop
0000001c t stop
00000400 d arr
00000403 d eoa

Из таблицы символов становится очевидным, что секция .text размещается с адреса 0x0, а секция .data с 0x400.

Больше об ассемблерных директивах

Рассмотрим еще несколько часто используемых директив на примере двух программ:

  1. Программа суммы элементов массива
  2. Программа, считающая длину строки

Сумма массива

Следующий код суммирует массив байт и сохраняет  результат в r3:


	.text
entry:  b start 
arr:    .byte 10, 20, 25
eoa: 
	.align
start:
	ldr   r0, =eoa  	@ r0 = &eoa
        ldr   r1, =arr  	@ r1 = &arr
        mov   r3, #0   		@ r3 = 0
loop:
	ldrb  r2, [r1], #1   	@ r2 = *r1++
        add   r3, r2, r3  	@ r3 += r2
        cmp   r1, r0            @ if (r1 != r2)
        bne   loop              @ goto loop
stop:   b stop

В коде представлены две новые ассемблерные директивы .byte и .align. Эти директивы описаны ниже.

Директива .byte

 

Аргументы директивы .byte имеют размер 1 байт и собраны в последовательность байт в памяти. Существуют аналогичные директивы .2byte и .4byte для хранения 16- и 32-битных значений соответственно. Общий синтаксис приведён ниже

.byte   exp1, exp2, ...
.2byte  exp1, exp2, ...
.4byte  exp1, exp2, ...

Аргумент может быть простым целым числом представленным в двоичной, восьмеричной, десятичной или шестнадцатеричной формах. Целые числа могут также быть представлены как символьные константы (обозначены одинарными кавычками), в этом случае будет использоваться ASCII значение символа.

Аргументом могут также служить выражения составленные их букв и других символов. Например:


.byte 0b01010101, 0b00110011, 0b00001111
.byte npattern - pattern
.byte 'A', 'B', 'C', 'D', 'E', 'F'
.4byte 0xDEADBEEF
.byte 'Z' - 'A' + 1

Директива .align

Процессору ARM требуется, чтобы инструкции были представлены в 32-битных ячейках памяти. Адрес первого из четырёх байт должен быть кратным 4. Чтобы придерживаться этого, директива .align используется для заполнения недостающими битами до тех пор, пока адрес не будет кратным 4. Это требуется только когда данные байт или полуслов вставлены в код.

Длина строки

Следующий код считает длину строки и сохраняет результат в регистр r1:


	.text
	b start
str:	.asciz "Hello World"
	.equ   nul, 0
	.align
start:       
	ldr   r0, =str  	@ r0 = &str
	mov   r1, #0
loop:       
	ldrb  r2, [r0], #1  	@ r2 = *(r0++)
	add   r1, r1, #1  	@ r1 += 1
	cmp   r2, #nul  	@ if (r1 != nul)
        bne   loop              @ goto loop
        sub   r1, r1, #1        @ r1 -= 1
stop:       
b stop

В коде представлены две новые ассемблерные директивы .asciz и .equ.

Директива .asciz

Директива .asciz в качестве аргументов принимает строковые литералы. Строковые литералы представляют собой последовательность символов в двойных кавычках. Строковые литералы собраны в ячейки памяти последовательно. Ассемблер автоматически вставляет нули после каждой строки.

Директива .ascii аналогична .asciz, но ассемблер не вставляет нули после каждой строки.

Директива .equ

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

Использование директивы .equ также возможно для ручной вставки записей в таблицу символов для сопоставления имён и значений, которые не обязательно являются адресами. Эти имена и имена меток вместе называются символьными именами.

Общий синтаксис этой директивы представлен ниже.

.equ имя, выражение

Имя является символьным именем и имеет те же ограничения, что и имя метки. Выражение может быть простым литералом или выражением, описанным в директиве .byte.

Организация обмена информации

Для простейшего обмена информацией программы с окружающим миром мы воспользуемся последовательной консолью. Для использования ее в эмуляторе QEMU необходимо выполнить проброс порта UART. UART (универсальный асинхронный приемопередатчик) – устройство и одноимённый протокол, обеспечивающие передачу данных по последовательному интерфейсу. Наиболее широко распространенным примером является последовательный порт персонального компьютера, однако последовательный интерфейс широко используется и в микроконтроллерных устройствах. UART эмулируется системой qemu-system-arm, и мы соединим его c консолью хоста – то есть выполним проброс из хост-системы (снаружи эмулятора) в гостевую систему (внутрь).

Чтобы было удобно вести диалог с ассемблерной программой, мы соединим последовательную консоль эмулятора с одним из графических окон терминала, запущенных на хост-системе. Доступ к терминалу на хост-системе будет осуществляться через виртуальный файл устройства /dev/pts/?, где знак вопроса заменяется номером консоли. Узнать, какой именно файл устройства соответствует запущенному треминалу можно, выполнив в нём команду tty. Так, например, для использования первого терминала запуск эмулятора будет иметь вид:

qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/pts/1

       
Таким образом, информация будет отправляться и приниматься с двух сторон: из окружающей среды, в роли котороый выступает ОС GNU/Linux на хост-системе, и изнутри эмулятора QEMU. В эмулируемой qemu-system-arm модели connex в качестве  последовательного порта с нулевым номером выступает порт по адресу 0x40100000. Поэтому для того чтобы почесть или передать байт, нужно почесть или записать байт по адресу 0x101f1000:


ldr                r0, =0x40100000        @ запись адреса в регитр r0
...
ldrb                r1, [r0]               @ чтение байта данных в r1 из UART
...
strb                r1, [r0]               @ запись данных из регистра r1 в UART

Чтобы иметь возможность читать через UART то, что отвечает пользователь, может понадобиться отключить перехват текстового ввода (по умолчанию ввод с клавиатуры, выполненный в окне терминала, получает запущенная в нём программа-облочка командной строки). Самый простой способ это сделать – поставить запущенную в терминале оболочку на большую паузу, например “усыпить” на целый день командой sleep 1d.

Также заметим, что для корректной работы этой программы необходимо использовать скрипт компоновщика, размещающий по отдельным адресам секции кода и данных, как было показано в примере выше.

Последнее замечание будет касаться точности эмуляции UART. Реализованная модель в QEMU не воспроизводит в точности передачу байтов с точки зрения синхронизации и задержек, существующих в реальном устройстве: данные в эмулируемом UART просто мгновенно “появляются” по соответствующему адресу. В реальном устройстве его использование несколько сложнее (например, требуется проверка флага “Transmit FIFO Full” по дополнительному порту, прежде чем выполнять вывод очередного байта, и т.д.).

Задание для выполнения

  1. Написать программу, работающую по следующему алгоритму:
    1. вывести на экран запрос о времени дня, например, “Полдень прошел?”;
    2. принять с клавиатуры ответ (Y/N);
    3. если было введено N, выдать сообщение “Доброе утро”,  в противном случае – “Добрый день”.
  2. Получить файл листинга и внимательно ознакомиться с его  форматом  и  содержимым.
  3. Подробно объяснить содержимое трех строк файла листинга по выбору.

Контрольные вопросы

  1. Для чего нужен файл листинга? В чем его отличие от текста программы?
  2. Каков формат файла листинга? Из каких частей он состоит? Каково назначение первой части?
  3. Каким образом в Unix-подобных ОС определяются права доступа к файлу?
  4. Как ОС определяет, является ли файл исполняемым? Как регулировать права на чтение и запись?
  5. Как разграничить права доступа для различных категорий пользователей?