Данные располагаются в памяти непрерывно байт за байтом. Однако ARM-процессор часто требует, что данные были выравнены тем или иным образом, например, по границе слова (4-байтного числа или значения word), либо по другой границе, которая является степенью 2. Обычно при создании исполняемого файла ассемблер генерирует ошибку, если требуется выравнивание. С помощью директивы .align указать ассемблеру, что надо выравнить следующий кусочек данных:
.align N
В качестве значения директиве .align передается степень двойки. Это будет определять выравнивание данных по границе в 2N байт. Это важно не перепутать. То есть выравнивание будет идти не по N байт, а по 2N байт. Например:
.align 2 // выравнивае по 4 байтам .align 3 // выравнивае по 8 байтам .align 4 // выравнивае по 16 байтам
Для чего это нужно? Рассмотрим простейший пример:
.global _start _start: ldr x0, num1 // загружаем код возврата в Х0 mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data num1: .byte 11 num2: .byte 12 num3: .quad 13 num4: .word 14
Здесь в секции .data
последовательно определены 4 переменных. Первые две представляют тип .byte
и соответственно занимают в памяти 1 байт. Далее идет
8-байтовая переменная num3 и затем 4-байтовая переменная num4. Это откровенно плохое расположение данных. Но эта программа работает - в регистр Х0 успешно загружается значение переменной
num1, и программа отрабатывает как надо.
Но теперь попробуем загрузить второе 1-байтовое число:
.global _start _start: ldr x0, num2 // пытаемся загрузить num2 в Х0 mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data num1: .byte 11 num2: .byte 12 num3: .quad 13 num4: .word 14
И если ассемблер as без проблем скомпилирует программу, то компоновщик ld не сможет скомпоновать объектый файл в исполняемый файл и сгенерирует ошибку:
c:\arm>aarch64-none-elf-as hello.s -o hello.o c:\arm>aarch64-none-elf-ld hello.o -o hello hello.o: in function `_start': (.text+0x0): relocation truncated to fit: R_AARCH64_LD_PREL_LO19 against `.data' aarch64-none-elf-ld: (.text+0x0): warning: one possible cause of this error is that the symbol is being referenced in the indicated code as if it had a larger alignment than was declared where it was defined c:\arm>
Ошибка указывает, что у нас проблемы с выравниванием. Изначально секция .data
обычно выравнена по четной границе. Так, посмотрим, что у нас в реальности происходит в секции .data в скомпилированном
объектном файле hello.o. Для дизассемблирования этого раздела утилите objdump надо передать параметры -D --section=.data
c:\arm>aarch64-none-elf-objdump -D --section=.data hello.o hello.o: file format elf64-littleaarch64 Disassembly of section .data: 0000000000000000 <num1>: 0: .inst 0x000d0c0b ; undefined 0000000000000001 <num2>: 1: udf #3340 0000000000000002 <num3>: 2: 0000000d udf #13 6: 00000000 udf #0 000000000000000a <num4>: a: 0000000e udf #14 c:\arm>
Здесь мы видим, что секция .data начинается с адреса 0x0000000000000000
- это адрес переменной num1. Переменная num2 располагается по адресу
0x0000000000000001
(то есть на 1 байт дальше) и т.д. Как видно из дизассемблерного вывода, objdump не совсем корректно интерпертирует значения. Так, значения некоторых переменных
интерпертируется как инструкция udf
, но не это главное. Здесь нас интересуют прежде всего адреса по которым, располагаются переменные.
Теперь изменим код, применив выравнивание. Обычно в качестве выравнивания используется как минимум 4 байта, то есть 22:
.global _start _start: ldr x0, num2 // загружаем num2 в Х0 mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data num1: .byte 11 .align 2 num2: .byte 12 num3: .quad 13 num4: .word 14
Компиляция, компоновка и последующее выполнение данной программы пройдет без проблем. Здесь инструкция .align 2
указывает, что предыдущий байт в реальности будет
занимать 22= 4 байта и будет выровнен по границе слова, хотя в реальности его данные занимают только 1 байт. Так, опять же дизассемблируем скомпилированный объектный файл:
c:\arm>aarch64-none-elf-objdump -D --section=.data hello.o hello.o: file format elf64-littleaarch64 Disassembly of section .data: 0000000000000000 <num1>: 0: 0000000b udf #11 0000000000000004 <num2>: 4: udf #3340 0000000000000005 <num3>: 5: 0000000d udf #13 9: 00000000 udf #0 000000000000000d <num4>: d: 0000000e udf #14 c:\arm>
Здесь мы видим, что переменная num1 формально занимает 4 байта, а число num2 выравнено по 4 байтовой границе - его адрес кратен 4 - 0x0000000000000004
.
Однако, если мы попробуем загрузить в регистр значение следующей переменной - num3:
ldr x0, num3
То опять же мы столкнемся с ошибкой выравнивания, поскольку переменная num3 не выровнена как миниму по 4 байтам. И опять же в этом случае мы можем добавить выравнивание:
.global _start _start: ldr x0, num3 // загружаем num2 в Х0 mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data num1: .byte 11 .align 2 num2: .byte 12 .align 2 num3: .quad 13 num4: .word 14
И здесь важно отметить, что поскольку переменная num3 выровнена по 4-байтной границе, то переменная num4 также будет выровнена по 4-байтной границе, так как если к адресу переменной num3 прибавить ее размер - 8 байт, то получим адрес, который по прежнему кратен 8.
Итак, мы решили проблему с загрузкой данных, но это потребовало увеличения занимаемой памяти. Так, переменные num1 и num2 вместо 2 байт теперь занимают 8 байт. Но как писалось ранее, это
плохое расположение данных, хотя с учетом выравнивания все работает. Для оптимизации программы, как правило, в секции .data
сначала помещаются данные большей разрядности, а потом меньшей:
.global _start _start: ldr x0, num2 // загружаем num2 в Х0 mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data num3: .quad 13 num4: .word 14 num1: .byte 11 .align 2 num2: .byte 12
По сравнению с предыдущим вариантом программы здесь выравнивание применяется один только раз - для последней переменной num2. Поскольку переменная num3 и num4 имеют размеры 8 и 4 байт, то переменная num1 автоматически будет располгаться по 4-байтовой границе, и для нее выравнивание не требуется.
В заключении стоит отметить, что в некоторых ситуациях может потребоваться большее выравнивание, например, для стека, который рассматривается далее, требуется выравнивание в 16 байт. Для инструкций в коде требуется выравнивание в 4 байта.