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

Доброго времени суток, хабражители. Вдохновившись статьёй 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
Share post

Comments 29

    +3
    Пишите еще! Тема очень интересна и востребована.
      0
      Благодарю за такой интересный пост. Пишите ещё. =)
        0
        Есть где-нибудь таблица команд процессоров ARM?
        Я помню, когда впервые прочитал книгу Зубкова по Ассемблеру (x86), там в конце был алфавитный список команд процессора с кодами, и я решил составить шестнадцатеричную таблицу опкодов — и так обнаружил недокументированный опкод F1, а после уже стал искать в интернете подробную информацию про этот код:)
        0
        В убунте тулчейн можно поставить очень и очень просто:

        sudo apt-get install g++-arm-linux-gnueabihf

        Если поковыряться в пакетах то можно даже найти разные версии GCC.
          +2
          arm-linux-gnueabi — это для компиляции проектов для ос линукс. Для bare metall нужен arm-none-eabi.

          В gentoo все куда проще. Ставится crossdev, он просто настраивает тулчейн, собирая самую последнюю версию. Причем хоть arm, хоть avr, хоть mips.
            –1
            совсем необязательно. -linux- указывает на наличие libc, но никто не мешает этим компилятором для армов компилировать загрузчик bootstrap (без всего) и ядро линукса (которое тоже с libc не линкуется, как понимаете).
              0
              none-eabi идет дефолтно с newlib, которая как бы совсем не libc.
                0
                дефолтно он ни с чем не обязан идти, это как соберёшь.
                  0
                  Да ладно. Дефолтно в code sourcery none-eabi идет именно с newlib.

                  Собрать можно, конечно, как угодно. Только с newlib мне сомнительно, что можно собрать ядро. С glibc, в теории, собрать bare metall embedded проект можно, но он получится слишком распухшим.
                    0
                    У компилятора есть опции «не линковать никакие библиотеки по умолчанию». с ними bare metal и собирают.

                    newlib/gnu libc/uclibc — это прослойки между ядром и юзерспейсом, обеспечивающие определённый интерфейс. Если нет ни ядра, ни юзерспейса — откуда им взяться в bare metal? Уж от пухлости библиотеки поддержки С никакой загрузчик не вырастет, ему что надо, у него всё внутри есть
                      0
                      >«не линковать никакие библиотеки по умолчанию»
                      А можно чуть специфичней быть? Их там как минимум три разных опции. Я просто не знаю, на какую из глупостей вынужден отвечать.

                      >newlib/gnu libc/uclibc — это прослойки между ядром и юзерспейсом, обеспечивающие определённый интерфейс.
                      Угу. memcpy или функция деления.
                        0
                        Ну например, -nodefaultlibs -nostdlib для начала. -nostartfiles для совсему уж чистоты хочется. Бареметальнее не бывает. Если того требует историческая справедливость, могу вам мейкфайлы загрузчиков разбирать, которые, будучи собраны компилятором со словом -linux- в названии, безупречно работают в отсутствие оного.

                        Про memcpy уже увы, демагогия. Я не сказал, что libc _только_ сопрягает то, что нужно С с тем, что есть у ядра ОС, но тем не менее в этом его (libc) основная задача.
                          0
                          Пичаль. no-hosted есть только в мане. А та гугловая ссылка, по который Вы зашли его не выдает. Я не сомневаюсь, что Вы активно искали через гугл и понятия не имеете о чем вообще речь — эти три параметра там рядом идут. Но Вы на верном пути, читайте внимательно, даже там все описано очень неплохо. Если хотите исторической справедливости — купите наиболее продаваемый на рынке микроконтроллер — SMT32F1 c 32к флеш. И напишите программу вида: sprintf(s, «Hello, World!»); Попробуйте сделать это с glibc. Удачи, может в 32кб она и влезет.

                          Демагогия? Нет, Вы просто не знаете из чего состоит стандартная библиотека. Хоть бы википедию открыли, прежде чем глупости писать.

                          Я так могу очень долго писать. Но Вы ведь понимаете, что с каждым новым ответом выставляете себя еще большим идиотом?
                            0
                            Для особо одарённых.

                            Загрузчик at91bootstrap:

                            CCFLAGS=-g -mcpu=arm926ej-s -O2 -Wall -D$(TARGET) -I$(INCL)
                            ASFLAGS=-g -mcpu=arm926ej-s -c -O2 -Wall -D$(TARGET) -I$(INCL) -DTOP_OF_MEM=$(TOP_OF_MEMORY)

                            # Linker flags.
                            # -Wl,...: tell GCC to pass this to linker.
                            # -Map: create map file
                            # --cref: add cross reference to map file
                            LDFLAGS+=-nostartfiles -nostdlib -Wl,-Map=$(BOOT_NAME).map,--cref
                            LDFLAGS+=-T $(BOOTSTRAP_PATH)/elf32-littlearm.lds -Ttext $(LINK_ADDR)

                            — Лично я здесь вижу -nostartfiles -nostdlib. Для вас это поиск в Гугле, для меня — код из рабочего проекта.

                            Собирается, работает. С этим проектом я работал. Если в какой-то другой версии GCC (у меня для армов уже много лет 4.4) есть некий no-hosted — увы, это вне моего поля деятельности.

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

                            Контроллеров разнообразных у меня лежит полный шкаф и есть пяток поднятых с нуля версий железок.

                            snprintf если и будет вообще для данного приложения, то только самостоятельно написанный. Объём, соответственно, будет зависеть от глубины детальности написанного. Можно утащить попроще, можно посложнее.

                            У меня как-то в голове не укладывается использование GNU Libc без ОС вообще. Может, я не такую кашу какую-то ем? libc.so.6 для STM без операционок бывает? со всеми pthread, nss, так что ли?
                              0
                              Я ведь предупреждал.

                              >libc.so.6 для STM без операционок бывает?

                              Господи, да Вы даже не имеете представления о динамической и статической линковке библиотек.
                                0
                                Отлично, статически слинкованная gnu libc реализует вызов open.

                                Который syscall с небольшой библиотечной обвязкой. Который исполняется ядром.

                                Кому управление «статически слинкованная» libc передавать будет?

                                System calls are generally not invoked directly, but rather via wrapper functions in glibc (or perhaps some other library). For details of direct invocation of a system call, see intro(2). Often, but not always, the name of the wrapper function is the same as the name of the system call that it invokes. For example, glibc contains a function truncate() which invokes the underlying «truncate» system call.
                                  0
                                  .
                                    0
                                    savok.name/uploads/bukvar/1.jpg

                                    Наконец-то. Это вторая причина по которой необходимо использовать newlib вместо glibc. И именно поэтому же необходим отдельный тулчейн: в лучшем случае проект попросту не соберется, в худшем вызовется stub. Но не undefined behaviour, как при использовании glibc.
                                      0
                                      мне к сожалению совершенно неясно, чем использование одной библиотеки CRT вместо другой помогает в отсутствие ядра линукса (а мы вроде именно эту проблему здесь обсуждаем).

                                      Если его (ядра ОС) нет, не надо жаловаться на библиотеку, что она от грабль не спасла. Не будет и не должна работать хоть с какой линковкой.

                                      Или мы какой-то другой use case здесь рассматриваем? В чём вообще вопрос? Мой bare-metal собирается arm-(название конторы)-linux-gnueabi-* и не испытывает никаких проблем (в том числе ограничение на размер загрузчика в 4 килобайта для at91sam9260). Это я и попытался раскрыть в этом треде.

                                      Если вы считаете, что мы обсуждаем нечто другое, уточните пожалуйста.
                                        0
                                        при этом, само собой есть такие реализации libc, которым и на ядро всё равно, но функционал у них конечно же ограничен. Такие гадости несутся подарками во всяких SDK.
                                        0
                                        и да, конечно же, snprintfу и всему его семейству ядро линукса не помогает. Только это не всё, что должна уметь libc
                                          0
                                          я последний аргумент вспомнил. Без хидеров линукса gnu libc даже не соберётся, т.к. из него берёт всё платформозависимое. Осталось выяснить, как можно собрать под ядро, а запускать без ядра :-D
                                            0
                                            А как собиралась glibc до первого релиза линукса? :)
                                              0
                                              Под хидеры UNIX- (и не только) ядер тех платформ, под которые она тогда была :-)
                0
                Если нужен именно ассемблер, то GCC — далеко не лучший выбор. Процесс сборки сильно переусложнён, требуется куча шаманства на всех стадиях, а ключи вообще приходится брутфорсить, т.к. с логикой там всегда было туго.
                Гораздо лучше воспользоваться чисто ассемблерными пакетами вроде FASM for ARM.
                  0
                  А можно поподробнее про проблемы с GCC? Я вот писал на ассемблере для Cortex-M3 (STM32), но никаких особых сложностей не было. Почитал доки, пощупал чужие примеры в интернете — стандартная процедура при изучении чего-то нового.

                  Ах да, GCC имеет смысл использовать хотя бы потому, что это стандарт де-факто для разработки низкоуровневого софта, в отличие от сторонних продуктов. Как минимум, проще будет использовать асму в связке с C и C++.
                    0
                    Не шаманства, а просто умения читать документацию.

                    Вы это Торвальдсу отпишите, а то он не в курсе.

                  Only users with full accounts can post comments. Log in, please.