Pull to refresh

ARM ассемблер (продолжение)

Reading time7 min
Views46K
Доброго времени суток, хабражители. Вдохновившись статьёй ARM аccемблер, решил для интересующихся и таких же начинающих, как я, продолжить эту статью. Исходя из названия становится понятно, что перед тем, как читать эту статью, желательно прочесть вышеуказанную. Итак, «продолжим».

Мой случай будет отличаться от предыдущего следующим:
  • у меня на машине ubuntu 12.04
  • arm toolchain я брал от сюда(выбрать ARM Processors — Download the GNU/Linux Release). На момент написания статьи появились более свежие версии, но я использовал arm-2012.09(arm-none-linux-gnueabi toolchain)
  • устанавливал так:
    $ mkdir ~/toolchains
    $ cd ~/toolchains
    $ tar -jxf ~/arm-2012.09-64-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2
  • добавлял для упрощения дальнейших действий наш тулчейн в PATH
    $ PATH=$HOME/toolchains/arm-2012.09/bin:$PATH
  • установка qemu в ubuntu
    $ sudo apt-get install qemu
    $ sudo apt-get install qemu-system

В принципе, никаких критических изменений относительно случая в статье-«родителе» нет.
Флэш-память, в которой хранилась программа из предыдущей статьи, является своего рода EEPROM (перепрограммируемое ПЗУ с электрическим стиранием). Это полезная «вторичная» память, применяемая обычно как жесткий диск, но неудобная для хранения переменных. Переменные должны быть сохранены в ОЗУ, чтобы их можно было легко изменять.
Эмулируемая пакетом QEMU плата имеет 64 МБ оперативной памяти, начинающейся с адреса 0xA000 0000, в которую можно сохранять переменные. Карту памяти эмулируемой платы можно изобразить на рисунке
image
Чтобы разместить переменные начиная с этого адреса, нужно предпринять специальные меры. Чтобы понять, что именно требуется сделать, нужно понимать роль, которую играет компоновщик (линкер).

Компоновщик


Во время трансляции программы, состоящей из нескольких файлов исходного текста, каждый такой файл преобразовывается в объектный. Компоновщик объединяет эти объектные файлы в конечный исполняемый файл.
image
Во время компоновки линкер выполняет следующие операции:
  1. Разрешение символов
  2. Перемещение

Разрешение символов


В ходе преобразования исходного файла в объектный код транслятор заменяет все ссылки на метки соответствующими адресами. В многофайловой программе, если в модуле есть какие-либо ссылки на внешние метки, определенные в другом файле, ассемблер помечает их как «нерешённые». Когда эти объектные файлы передаются компоновщику, он определяет значения адресов таких ссылок из других объектных файлов и исправляет код на правильные значения.
Рассмотрим пример, вычисляющий сумму элементов массива – специально разделенный на два файла, чтобы было наглядно видно выполняемое компоновщиком разрешение символов. Для демонстрации наличия нерешенных ссылок соберем оба файла и проверим их таблицы символов.
Файл 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, в расчете на то, что позже компоновщик преобразует символьные ссылки и создаст на выходе исполняемый файл.

Перемещение


Перемещение – процесс изменения адреса, уже заданного метке ранее, а также исправления всех ссылок для отражения вновь назначенных адресов. В первую очередь, перемещение осуществляется по следующим двум причинам:
  1. Слияние секций
  2. Размещение секций в исполняемом файле

Для понимания процесса перемещения важно понимать, что такое секции.
В момент выполнения программы код и данные могут обрабатываться по-разному: если, код можно разместить в ПЗУ (ROM, read-only memory), то для данных может потребоваться как чтение из памяти, так и запись. Удобнее всего, если код и данные не чередуются, и именно поэтому программы разделены на секции. Большинство программ имеют хотя бы две секции: .text для кода и .data для работы с данными. Для переключения между двумя секциями используются директивы ассемблера .text и .data.
Когда ассемблер встречает какую-нибудь директиву секции, он кладёт код или данные, следующие за ней, в соответствующую область памяти. Таким образом, код и данные, которые относятся к одной секции, оказываются в смежных ячейках. Процесс наглядно показан на следующем рисунке
image

Слияние секций

В многофайловых программах секции с одинаковыми именами (например .text) могут оказаться в разных файлах. Компоновщик отвечает за слияние секций из входных файлов в секции выходного файла. По умолчанию секции с одинаковым именем из каждого файла размещаются по-порядку, а ссылки на метки корректируются значением нового адреса.
Результат слияния секций можно наблюдать с помощью таблицы символов объектных файлов и соответствующего исполняемого файла. Ниже результат слияния показан на примере программы вычисления суммы массива:

$ 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) секций показан на схеме.
image

Скрипт-файлы компоновщика


Как упоминалось ранее, объединение и размещение секций выполняет компоновщик. Тем, как объединяются и в какой области памяти размещаются секции, можно управлять через скрипт-файл компоновщика. Ниже приведён пример очень простого скрипта, ключевые места которого помечены цифровыми метками.
SECTIONS { ❶
      . = 0x00000000; ❷
      .text : { ❸
          abc.o (.text);
          def.o (.text);
      } ❹
}
❶ SECTIONS – самая важная команда компоновщика, она определяет, как будут объединены секции, и куда они должны быть помещены.
❷ В блоке, следующем после команды SECTIONS, указываетсячч число – счётчик расположений. По умолчанию расположение всегда инициализируется значением 0x0, но указанием другого значения можно изменить инициализацию. В данном случае установка нами значения в ноль — излишнее действие.
❸-❹ Эта часть скрипта определяет, что секции .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
eoa:

      .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. Инструкция для перепрыгивания через данные теперь не нужна, т. к. скрипт корректно размещает секции.
Когда программа компонуется, скрипт передаётся в качестве входных данных компоновщику:

$ 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» указывает в качестве скрипта компоновщика файл sum-data.lds. Размещение секций в памяти, как обычно, можно проследить по таблице символов:

$ arm-none-linux-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.

Так как это мой первый пост, то не хотелось бы сильно грузить и делать его огромным. Поэтому на данном этапе закончу. Если будет интересно и будут просьбы, то продолжу эту статью новой, в которой затрону такие вопросы как
  • более подробное рассмотрение директив ассемблера (естественно, полезных)
  • работа с оперативной памятью
  • обработка прерываний
  • запуск кода, написанного на языке более высокого уровня на процессоре ARM
Tags:
Hubs:
+35
Comments29

Articles

Change theme settings