GNU ассемблер по умолчанию не поддерживает структуры в виде отдельного типа данных, как это реализовано в более высокоуровневых языках. Тем не менее мы можем эмулировать структуры данных с помощью расположения в памяти набора отдельных данных. Например:
section .data ; условная структура person: db "Alice",10 ; имя dq 34 ; возраст
Здесь переменная person представляет два подряд расположенных объекта. Во-первых, это строка. Во-вторых, это 64-разрядное число. Условно будем считать, что эта переменная представляется структуру, которая, в свою очередь, представляет человека. Строка представляет имя, а 64-разрядное число - возраст человека.
Напишем простейшую программу по работе с этой структурой. Допустим, мы хотим выводить на консоль имя и возраст человека:
global _start section .data ; условная структура person: db "Alice",10 ; имя dq 34 ; возраст ; смещение компонентов в структуре NAME_OFFSET equ 0 AGE_OFFSET equ 6 section .text _start: mov rsi, person ; в RSI - адрес строки mov rdi, 1 ; в RDI - дексриптор вывода в стандартный поток (консоль) mov rdx, AGE_OFFSET ; в RDX - длина строки mov rax, 1 ; в RAX - номер функции для вывода в поток syscall ; вызываем функцию Linux mov rdi, [rsi + AGE_OFFSET] ; в RDI - возраст mov rax, 60 syscall
Важный момент здесь заключается в установке смещений для каждого компонента структуры:
.equ NAME_OFFSET, 0 .equ AGE_OFFSET, 6
С нулевого байта в структуре начинается имя, а с 6-го байта - возраст. И константа AGE_OFFSET хранит смещение возраста в структуре. При этом AGE_OFFSET также представляет количество символов в строке, где символы занимают 1 байт.
Для вывода имени на консоль мы помещаем строку с именем в регистр RSI:
mov rsi, person
Поскольку имя находится по нулевому смещению, то адресом имени в структуре является адрес самой структуры.
Далее в регистр RDI помещается дескриптор консольного вывода - 1:
mov rdi, 1
Затем в регистр RDX помещается количество символов строки - в нашем случае это смещение в структуре компонента возраста - константа AGE_OFFSET:
mov rdx, AGE_OFFSET
Затем вызываем системную функцию Linux номер 1 - функцию write:
mov rax, 1 syscall
Для простоты возраст выводится в виде статуса результата в регистр RDI:
mov rdi, [rsi + AGE_OFFSET] ; в RDI - возраст
Поскольку выше в программе в RSI был загружен адрес структуры person, то мы можем применить к нему смещение AGE_OFFSET и получить адрес компоненты возраста в структуре.
Пример компиляции и консольный вывод программы:
eugene@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o eugene@Eugene:~/asm# ld hello.o -o hello eugene@Eugene:~/asm# ./hello Alice eugene@Eugene:~/asm# echo $? 34 eugene@Eugene:~/asm#
Это доврльно простой способ представления структуры. Но здесь есть недостатки. Если нам потребуется создать набор подобных структур, то в каждом случае у нас могут оказаться имена с разной длиной. Соответственно для каждой отдельной структуры придется прописывать отдельные смещения. При этом компонентов, в том числе строк, в структуре может быть множество. Кроме того, при перемещении по набору структур нам может потребоваться знать точный размер структуры. В условиях, когда имена могут различаться по длине, точный размер каждой структуры также будет отличаться. Мы можем выйти из этой проблемы, вынеся строку из структуру, а в структуру поместив адрес этой строки:
global _start section .data ; условная структура person: dq alice ; адрес строки dq alice_name_size ; размер имени dq 34 ; возраст ; смещение компонентов в структуре NAME_OFFSET equ 0 NAME_SIZE_OFFSET equ 8 AGE_OFFSET equ 16 alice db "Alice", 10 ; имя alice_name_size equ $-alice ; размер имени section .text _start: mov rbx, person mov rsi, [rbx + NAME_OFFSET] ; в RSI - адрес строки mov rdi, 1 ; в RDI - дексриптор вывода в стандартный поток (консоль) mov rdx, [rbx + NAME_SIZE_OFFSET] ; в RDX - длина строки mov rax, 1 ; в RAX - номер функции для вывода в поток syscall ; вызываем функцию Linux mov rdi, [rbx + AGE_OFFSET] ; в RDI - возраст mov rax, 60 syscall
Теперь структура представляет набор из трех компонентов - 3 чисел типа .quad
. Строка имени вынесена в отдельную переенную alice, а в структуру в качестве первого компонента помещается
адрес этой строки. Адреса в х86-64 всегда имеют размер 8 байт, а в качестве адреса служит само название переменной.
; условная структура person: dq alice ; адрес строки dq alice_name_size ; размер имени dq 34 ; возраст ; смещение компонентов в структуре NAME_OFFSET equ 0 NAME_SIZE_OFFSET equ 8 AGE_OFFSET equ 16 alice db "Alice", 10 ; имя alice_name_size equ $-alice ; размер имени
Второй компонент структуры для удобства определяет размер имени, который вычисляется автоматически и сохраняется в константу alice_name_size.
Третий компонент структуры - возраст. В связи с новым строением структуры применяются новые смещения:
NAME_OFFSET equ 0 NAME_SIZE_OFFSET equ 8 AGE_OFFSET equ 16
Использование подобных смещений позволит быстро перейти к определенному компоненту структуры, зная ее адрес:
mov rbx, person mov rsi, [rbx + NAME_OFFSET] ; в RSI - адрес строки mov rdx, [rbx + NAME_SIZE_OFFSET] ; в RDX - длина строки mov rdi, [rbx + AGE_OFFSET] ; в RDI - возраст
Подобный подход упрощает работу с набором структур. Например, определим массив структур-пользователей и выведем на консоль имя каждого пользователя:
global _start section .data ; массив структур people: dq tom, tom_name_size, 39 dq sam, sam_name_size, 29 dq bob, bob_name_size, 43 dq kate, kate_name_size, 25 dq alice, alice_name_size, 34 count equ ($ - people)/PERSON_SIZE ; количество данных в массиве tom db "Tom",10 tom_name_size equ $-tom sam db "Sam",10 sam_name_size equ $-sam bob db "Bob",10 bob_name_size equ $-bob kate db "Kate",10 kate_name_size equ $-kate alice db "Alice", 10 alice_name_size equ $-alice ; смещение компонентов в структуре NAME_OFFSET equ 0 NAME_SIZE_OFFSET equ 8 AGE_OFFSET equ 16 PERSON_SIZE equ 24 ; размер одной структуры в байтах section .text _start: mov r9, 0 mov rbx, people jmp condition mainloop: mov rbx, people ; получаем смещение для имени и длины имени mov rax, PERSON_SIZE mul r9 add rbx, rax mov rsi, [rbx + NAME_OFFSET] ; в RSI - адрес строки mov rdi, 1 ; в RDI - дексриптор вывода в стандартный поток (консоль) mov rdx, [rbx + NAME_SIZE_OFFSET] ; в RDX - длина строки mov rax, 1 ; в RAX - номер функции для вывода в поток syscall ; вызываем функцию Linux inc r9 ; инкрементируем значение RCX для перехода к новой структуре condition: cmp r9, count jne mainloop mov rdi, r9 mov rax, 60 syscall
Здесь для перебора всех элементов структуры применяется счетчик-регистр R9, который хранит индекс текущей структуры в наборе people.
Компиляция и консольный вывод программы:
eugene@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o eugene@Eugene:~/asm# ld hello.o -o hello eugene@Eugene:~/asm# ./hello Tom Sam Bob Kate Alice eugene@Eugene:~/asm#