Выравнивание данных

Последнее обновление: 28.07.2023

Данные располагаются в памяти непрерывно байт за байтом. Однако 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 байта.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850