Ассемблер GAS позволяет определять в программе объекты, которые хранят некоторые данные, и на протяжении программы мы можем использовать эти данные, изменять их. Подобные данные еще называются переменными. Объявления данных имеют следующую форму:
label: directive value
label
представляет название переменной, которая может представлять произвольный идентифкатор. После названия переменной идет directive
-
директива, которая устанавливает тип данных. Это может быть одна из следующих директив:
.ascii: строка в двойных кавычках
.asciz: строка ascii, которая заканчивается 0-вым байтом
.byte: целое число размером в 1 байт
.short (.word): целое число размером в 2 байта (слово)
.long: целое число размером в 4 байта (так называемое двойное слово)
.quad: целое число размером в 8 байт (четверное слово)
.octa: целое число размером в 16 байт (восьмеричное слово)
.float: число с плавающей точкой одинарной точности
.double: число с плавающей точкой двойной точности
После директивы данных идет собственно значение. Например:
number: .byte 22
Здесь определена переменная number, которая имеет тип .byte
, то есть представляет 1 байт, и которая равна 22.
С каждой переменной ассемблер будет ассоциировать некоторый свободный участок памяти. Например, переменная number имеет тип .byte
и занимает 1 байт,
соответственно ассемблер найдет в памяти свободный 1 байт и ассоциирует их с этой переменной.
Где определяются данные? В программе на ассемблере можно опредлять данные в различных секциях. Например, в секции .text
:
.globl _start .section .text number: .byte 123 # определяем объект number внутри секции .text _start: movq $1, %rdi mov $60, %rax syscall
После определения переменные можно использовать в программе как обычные данные. Например, можно помещать в регистр или, наоборот, сохранять значение из регистра в переменную. Как правило, для переменной лучше использовть инструкцию, которая соответствует ее типу. Например, для копирования однобайтовых чисел в GAS предназначена инструкция movb. И мы могли бы ее использовать для помещения в регистр значения переменной размером 1 байт:
.globl _start .section .text number: .byte 123 # определяем объект number внутри секции .text _start: movb number, %dl # RDX = number movq %rdx, %rdi # RDI = RDX = number movq $60, %rax syscall
Здесь переменная number имеет размер 1 байт. Для ее копирования в регистр RDX (точнее его младшие 8 бит - регистр DL) применяется инструкция movb. Затем с помощью инструкции movq значение из регистра RDX помещается в регистр RDI. Но проблема в том, что регистр RDI не имеет 8-разрядного вложенного регистра, как RDX - DL. Соответственно для копирования в RDI мы не можем применить movb. Тем не менее GAS позволяет использовать инструкции, которые предназначены для работы с данными большей разрядности, например:
.globl _start .section .text number: .byte 123 _start: movq number, %rdi # RDI = number movq $60, %rax syscall
Либо можно использовать общую форму инструкции без суффикса типа данных - mov:
.globl _start .section .text number: .byte 123 _start: mov number, %rdi # RDI = number movq $60, %rax syscall
Однако секция .text
больше подходит для инструкций, которые выполняют некоторые действия. Кроме того, по умолчанию, когда GAS компонует программу,
он сообщает системе, что программа может выполнять инструкции и считывать данные из секции .code
, но не может записывать
данные в эту секцию. Поэтому операционная система сгенерирует общую ошибку защиты (general protection fault), если мы попытаемся сохранить какие-либо данные в секции кода.
Например:
.globl _start .text number: .byte 123 _start: movb $67, %al # помещаем в AL число 67 movb %al, number # помещаем число из AL в переменную number movq number, %rdi # помещаем число значение переменной number в RDI movq $60, %rax syscall
Здесь мы пытаемся поместить значение регистра AL в переменную number:
movb %al, number
Однако объект number определен в секции .text, поэтому он не изменяем. И хотя ассемблер может успешно скомпилировать программу:
eugene@Eugene:~/asm# as hello.s -o hello.o eugene@Eugene:~/asm# ld hello.o -o hello eugene@Eugene:~/asm# ./hello Segmentation fault eugene@Eugene:~/asm#
Собственно при определении объектов в секции .text у нас получаются константы - объекты, значения которых нельзя изменить. Поэтому для определения переменных и в целом для определения данных больше подходят другие секции, в частности, секция данных.
Секция данных задается с помощью директивы .data. Эта директива сообщает ассемблеру, что дальше
(до следующей директивы, которая определяет секцию, например, .text
) идут объявления данных.
.globl _start .data number: .byte 123 # переменная number в секции .data .text _start: movb $67, %al # помещаем в AL число 67 movb %al, number # помещаем число из AL в переменную number movq number, %rdi # помещаем число значение переменной number в RDI movq $60, %rax syscall
Если секция .data
содержит несколько переменных, то ассемблер с каждой из этих переменной ассоциирует некоторый участок памяти. Причем
в памяти все переменные будут расположены друг за другом. Например, возьмем следующую секцию данных
.globl _start .data i64: .quad 8 i32: .long 4 i16: .word 2 i8: .byte 1 .text _start: movq i64, %rdi # RDI = i64 movq $60, %rax syscall
Для переменной i8 выделяется 1 байт, для i16 - 2 байта, для i32 - 4 байта и для i64 - 8 байт. В итоге для секции .data будет выделен блок памяти, который занимает не менее 15 байт, где переменные будут располагаться следующим образом:
Допустим, для переменной i64 выделено 8 байт по адресу 0x0000, тогда i32 располагается по адресу 0x0008, а i16 - по адресу 0x000С и i8 по адресу 0x000E.
Ассемблер GAS позволяет определить набор данных. Если нам известны все элементы набора, то мы их можем перечислить через запятую:
nums: .byte 1, 2, 3, 4, 5, 10, 0
Здесь переменная nums представляет набор из 7 байтов. При при обращении к этому набору по имени переменной мы фактически обращаемся к первому элементу этого набора:
.globl _start .data nums: .byte 15, 16, 17, 18, 19, 20, 0 .text _start: movb nums, %al # AL = 15 movq $60, %rax syscall
Для упрошения определения наборов большего размера с определенным значением можно использовать директиву .fill, которая имеет следующую форму:
.fill repeat, size, value
Эта директива повторяет значение (value) определенного размера (size) определенное количество раз (repeat):
.data zeros: .fill 10, 4, 0
Эта инструкция создает блок памяти из 10-ти 4-байтовых чисел (слов - тип .long), каждое из которых равно нулю. При обращении к этому набору по имени мы получаем первый его элемент:
.globl _start .data nums: .fill 10, 8, 5 .text _start: movq nums, %rdi # RDI = 5 movq $60, %rax syscall
Еще одна конструкция - .rept:
.rept count ... .endr
Повторяет выражения между .rept
и .endr
столько раз, сколько указано в параметре count. Например:
nums: .rept 3 .quad 0, 1, 2 .endr
Здесь создается 3 раза по 3 числа quad - 0, 1, 2. То есть этот код будет эквивалентен следующему:
nums: .quad 0, 1, 2 .quad 0, 1, 2 .quad 0, 1, 2
Когда надо определить набор байтов, при этом значение каждого байта не важно (например, когда надо просто определить буфер для считывания данных с файла), применяется директива .skip. Она также имеет псевдонимы .space и .zero.
Директива .skip принимает 1-2 аргумента. Первый аргумент представляет количество резервируемых байтов. Второй, необязательный аргумент представляет значение, которым инициализируется каждый байт. Если второй аргумент не указан, то числа в наборе инициализируются нулем. Например:
.data buffer1: .skip 1024, 22 # каждый из 1024 байтов равен 22 buffer2: .space 512 # каждый из 512 байтов равен 0
Физическая организация памяти, шин данных и архитектура процессора позволяют извлекать данные из некоторых адресов в памяти быстрее, чем из других адресов. В случае некоторых расширенных инструкций загрузка с адреса, который не кратен правильному числу, фактически завершится неудачей и вызовет ошибку или исключение. Например, некоторые векторные инструкции используют 16-байтовые (128-битные) числа и требуют выравнивания по 16 байтам.
Для решения проблем с выравниванием ассемблер предоставляет ряд директив:
.balign: эта директива выравнивает следующий адрес по значению, который кратен заданному. Например, выражение:
.balign 8
Позволяет сделать так, что следующий используемый адрес памяти будет кратен 8.
.p2align: эта директива очень похожа на .balign
, за исключением того, что вместо количества байтов директиве передается число, степень двойки которого
будет применяться для выравнивая. Например, чтобы выровнять по 8 байтам, применяется команда
.p2align 3
так как 23 = 8.
.align: в некоторыхконфигурациях он работает как .p2align
, а в других — как
.balign
.
Кроме собственно секции .data
в программе на ассемблере может использоваться еще ряд секций:
.rodata: содержит данные, которые нельзя изменить (то есть по сути константы). Он загружается в память при загрузке приложения и помечается как доступный только для чтения. Попытки записи в эту память приведут к остановке программы.
.bss: содержит неинициализированные данные, для которых известен размер, но неизвестно значение. Это экономит место в исполняемом файле, особенно если здесь большой объем данных. Операционная система инициализирует раздел .bss всеми нулями. Можно зарезервировать данные в разделе .bss, используя директивы .zero или .skip/.space.
Пример определения секций:
.bss buffer: .zero 1000 .section .rodata const: .quad 6