Работа с данными и памятью

Определение переменных и типы данных. Секция .data

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

Ассемблер 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

Секция данных задается с помощью директивы .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 байт, где переменные будут располагаться следующим образом:

Секция data в ассемблере GNU

Допустим, для переменной 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

Когда надо определить набор байтов, при этом значение каждого байта не важно (например, когда надо просто определить буфер для считывания данных с файла), применяется директива .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
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850