Полный вариант командной строки as выглядит следующим образом:
arm-none-linux-gnueabi-as [-@ косвенный_файл_настроек] [-o объектный_файл] [-a файл листинга] [параметры...] исходный_файл
Для более подробной информации см. man as
.
Пример arm-none-linux-gnueabi-as -o add.o -a=add.lst add.s
скомпилирует исходный файл add.s
в add.o
, вдобавок будет создан файл листинга add.lst
.
make
— утилита из набора GNU для поддержки групп программ. Она управляет трансляцией, компоновкой. Поведение make описывается формирующим файлом Makefile
, который должен присутствовать в текущем каталоге. Это текстовый файл, который может содержать объекты трех видов:
- комментарии;
- правила;
- макроопределения.
Пример:
build:
arm-none-linux-gnueabi-as -o add.o -a=add.lst add.s
arm-none-linux-gnueabi-ld -Ttext=0x0 -o add.elf add.o
clean:
rm -f add.s add.o add.lst add.elf
В примере созданы два правила с именами («целями») build
и clean
. Первое из них выполняет трансляцию и сборку программы, а второе удаляет все созданные предыдущим правилом файлы.
Таким образом, если набрать в командной строке make build
, то будет создан исполняемый файл из add.s
. Если набрать make clean
, то проект будет очищен: будут удалены все файлы, создаваемые as
и ld
.
Для более подробной информации см. man make
.
ПРИМЕЧАНИЕ: утилита make
необычно требовательна к содержимому make-файла. Ей необходимо, чтобы команды в правилах (в отличие от целей) начинались с отступа, и отступ обязательно должен создаваться символом табуляции, а не пробелами. Например, редактор mcedit по умолчанию заменяет табуляции пробелами. Чтобы он создавал настоящую табуляцию, нужно в меню (F9) выбрать подпункт «Общая» пункта «Настройка» и убрать галочку в пункте «Симулировать неполную табуляцию».
Флэш-память, в которой хранилась программа из предыдущей работы, является своего рода EEPROM (англ. Electrically Erasable Programmable Read-Only Memory — перепрограммируемое ПЗУ с электрическим стиранием). Это полезная «вторичная» память, применяемая обычно как жесткий диск, но неудобная для хранения переменных. Переменные должны быть сохранены в ОЗУ, чтобы их можно было легко изменять.
Эмулируемая пакетом QEMU плата Connex имеет 64 МБ оперативной памяти, начинающейся с адреса 0xA000 0000
, в которую можно сохранять переменные. Карту памяти платы Connex можно изобразить на рисунке:
Чтобы разместить переменные начиная с этого адреса, нужно предпринять специальные меры. Чтобы понять, что именно требуется сделать, нужно понимать роль, которую играет компоновщик (линкер).
Во время трансляции программы, состоящей из нескольких файлов исходного текста, каждый такой файл преобразовывается в объектный. Компоновщик объединяет эти объектные файлы в конечный исполняемый файл:
Во время компоновки линкер выполняет следующие операции:
- Разрешение символов
- Перемещение
В ходе преобразования исходного файла в объектный код транслятор заменяет все ссылки на метки соответствующими адресами. В многофайловой программе, если в модуле есть какие-либо ссылки на внешние метки, определенные в другом файле, ассемблер помечает их как «нерешённые». Когда эти объектные файлы передаются компоновщику, он определяет значения адресов таких ссылок из других объектных файлов и исправляет код на правильные значения.
Рассмотрим пример, вычисляющий сумму элементов массива — специально разделенный на два файла, чтобы было наглядно видно выполняемое компоновщиком разрешение символов. Для демонстрации наличия нерешенных ссылок соберем оба файла и проверим их таблицы символов.
Файл sum-sub.s
содержит подпрограмму sum
, а файл main.s
вызывает подпрограмму с требуемым аргументом. Исходные файлы приведены ниже.
main.s:
.text
b start
arr: .byte 10, 20, 25 @ Массив байт (только для чтения)
eoa: @ Адрес конца массива + 1
.align
start:
ldr r0, =arr @ r0 = &arr
ldr r1, =eoa @ r1 = &eoa
bl sum @ Вызов подпрограммы sum
stop: b stop
sum-sub.s:
@ Аргументы:
@ r0: Начальный адрес массива
@ r1: Конечный адрес массива
@ Результат:
@ r3: Сумма элементов массива
.global sum
sum: mov r3, #0 @ r3 = 0
loop: ldrb r2, [r0], #1 @ r2 = *r0++; Загрузка элемента массива
add r3, r2, r3 @ r3 += r2; Вычисление суммы
cmp r0, r1 @ if(r0 != r1); Проверка на конец массива
bne loop @ Цикл, аналог «goto loop» архитектуры х86
mov pc, lr @ pc = lr; Возврат по окончании
С помощью директивы .global
мы задали видимость объявленных в функции переменных для других файлов. Теперь компилируем фалы и просмотрим таблицу символов с помощью команды nm
.
$ arm-none-linux-gnueabi-nm main.o
00000004 t arr
00000007 t eoa
00000008 t start
00000014 t stop
U sum
$ arm-none-linux-gnueabi-nm sum-sub.o
00000004 t loop
00000000 T sum
Одиночная буква во второй колонке определяет тип символа. Тип t
означает, что символ определён в секции .text
. Тип u
определяет, что символ не определён. Заглавная буква определяет принадлежность к типу доступа .global
. Очевидно, что символ sum
определён в sum-sub.o
и не описан в main.o
, в расчете на то, что позже компоновщик преобразует символьные ссылки и создаст на выходе исполняемый файл.
Перемещение — процесс изменения адреса, уже заданного метке ранее, а также исправления всех ссылок для отражения вновь назначенных адресов. В первую очередь, перемещение осуществляется по следующим двум причинам:
- Слияние секций
- Размещение секций в исполняемом файле
Для понимания процесса перемещения важно понимать, что такое секции.
В момент выполнения программы код и данные могут обрабатываться по-разному: если, код можно разместить в ПЗУ (ROM, read-only memory), то для данных может потребоваться как чтение из памяти, так и запись. Удобнее всего, если код и данные не чередуются, и именно поэтому программы разделены на секции. Большинство программ имеют хотя бы две секции: .text
для кода и .data
для работы с данными. Для переключения между двумя секциями используются директивы ассемблера .text
и .data
.
Когда ассемблер встречает какую-нибудь директиву секции, он кладёт код или данные, следующие за ней, в соответствующую область памяти. Таким образом, код и данные, которые относятся к одной секции, оказываются в смежных ячейках. Процесс наглядно показан на следующем рисунке.
Использованная в примере директива .skip</skip> резервирует в памяти место, равное указанному количеству байт, без их инициализации какими-либо данными.
В многофайловых программах секции с одинаковыми именами (например .text
) могут оказаться в разных файлах. Компоновщик отвечает за слияние секций из входных файлов в секции выходного файла. По умолчанию секции с одинаковым именем из каждого файла размещаются по-порядку, а ссылки на метки корректируются значением нового адреса.
Результат слияния секций можно наблюдать с помощью таблицы символов объектных файлов и соответствующего исполняемого файла. Ниже результат слияния показан на примере программы вычисления суммы массива:
$ arm-none-linux-gnueabi-nm main.o
00000004 t arr
00000007 t eoa
00000008 t start
00000014 t stop
U sum
$ arm-none-linux-gnueabi-nm sum-sub.o
00000004 t loop
00000000 T sum
$ arm-none-linux-gnueabi-ld -Ttext=0x0 -o sum.elf main.o sum-sub.o
$ arm-none-linux-gnueabi-nm sum.elf
00000004 t arr
...
00000007 t eoa
00000024 t loop
00000008 t start
U _start
00000014 t stop
00000020 T sum
Символ loop
имеет адрес 0x4
в файле sum-sub.o
и 0x24
в sum.elf
, так как секция .text
файла sum-sub.o
переместилась и располагается сразу после секции .text
файла main.o
.
Когда программа скомпилирована, предполагается, что каждая секция начинается с адреса 0
, а меткам приписываются значения относительно начала секции. При создании исполняемого файла секции помещаются по некоторому адресу X
, а затем ссылки на метки, определённые в секции, увеличиваются на величину X
.
Размещение каждой секции в конкретной области памяти и исправление всех ссылок на метки в секции производятся компоновщиком.
Результат размещения секций можно наблюдать из таблиц символов объектных и исполняемого файлов. Для лучшего понимания разместим секцию .text
по адресу 0x100
. В результате адрес секции .text
будет в исполняемом файле на 100 больше. Процесс объединения (section merging) и размещения (section placement) секций показан на схеме:
- Создайте два файла с ихсходными кодами программы, как указано в методичке.
- Откомпилируйте и запустите программу. Убедитесь в ее работоспособности, посмотрев содержимое регистров в мониторе QEMU.
- Используйте утилиту make для автоматизации сборки программы. Измените что-либо в одном из исходных файлов и убедитесь, что пересборка проекта выполняется успешно.
- Каково назначение и формат косвенных командных файлов для NASM?
- Каково назначение и формат файлов подсказки для LD?.
- Каково назначение утилиты MAKE?
- Где задаются правила поведения MAKE?
- Расскажите о размещении секций в исполняемом файле.
- Расскажите о слиянии секций в исполняемом файле.