Ассемблер в отличие от ряда языков высокого уровня не поддерживает напрямую парадигму объектно-ориентированного программирования, тем не менее и на уровне ассемблера мы можем имитировать работу с классами и объектами, тем более что те же классы в языках высокого уровня непосредственно или опосредовано компилируются в тот же самый код. Но естественно такое ООП в ассемблере будет довольно условным.
Так, пусть у нас есть файл person.s со следующим кодом:
## условный класс Person .globl person_new, person_speak, person_info, person_delete .data speak_text: .asciz "%s says: Hello\n" info_text: .asciz "Name: %s Age: %d\n" .text .equ PERSON_SIZE, 16 # размер объекта Person - 16 байт - для имени (8 байт) и возраста (8 байт) .equ PERSON_NAME, 0 # смещение, где находится указатель на имя .equ PERSON_AGE, 8 # смещение, где находится значение возраста # %rdi - указатель на имя # %rsi - возраст person_new: pushq $0 # для выравнивания стека по 16-байтной границе pushq %rdi # поскольку функция malloc использует rdi, сохраняем регистр в стек pushq %rsi # поскольку при вызове функции malloc может использоваться rsi, сохраняем регистр в стек movq $PERSON_SIZE, %rdi # в rdi - количество выделяемых байтов для объекта класса call malloc # выделяем память с помощью инструкции malloc popq %rsi # восстанавливаем возраст в rsi popq %rdi # восстанавливаем имя в rdi movq %rdi, PERSON_NAME(%rax) movq %rsi, PERSON_AGE(%rax) addq $8, %rsp # восстанавливаем стек ret # условная функция говорения person_speak: subq $8, %rsp movq PERSON_NAME(%rdi), %rsi # получаем имя в %rsi movq $speak_text, %rdi call printf addq $8, %rsp ret # выводим информацию об объекте person_info: subq $8, %rsp movq PERSON_NAME(%rdi), %rsi # получаем имя в %rsi movq PERSON_AGE(%rdi), %rdx # получаем возраст в %rdi movq $info_text, %rdi call printf # выводим информацию на экран addq $8, %rsp ret # удаление объекта из памяти person_delete: subq $8, %rsp # %rdi уже содержит адрес call free # освобождаем память объекта addq $8, %rsp ret
Данный файл представляет условный класс Person. В секции .data
определяются используемые в коде выводимые на экран сообщения - speak_text и info_text. Для вывода сообщений
для упрощения будем использовать встроенную функцию языка Си - printf.
Затем идут константы, которые определяют внутреннее строение объекта Person:
.equ PERSON_SIZE, 16 # размер объекта Person - 16 байт - для имени (8 байт) и возраста (8 байт) .equ PERSON_NAME, 0 # смещение, где находится указатель на имя .equ PERSON_AGE, 8 # смещение, где находится значение возраста
Пусть наш объект Person, который представляет человека, будет иметь два атрибута - имя и возраст. Для хранения имени нам потребуется 8 байт - размер указателя на строку. Для хранения возраста можно использовать различные целочисленные типы, но в данном случае также возьмем 8-байтное число. В итоге для одного объекта Person потребуется 8+8=16 байт. Это значение зафиксируем в константе PERSON_SIZE. То есть на уровне ассемблера объект Person - это будет просто некоторый участок в памяти длиной 16 байт.
Чтобы знать, где в этом участке памяти что находится, определяются две константы. PERSON_NAME указывает на смещение в этом участке, по которому располагается указатель на имя, а константа PERSON_AGE определяет смещение для возраста. Пусть с самого начала участка памяти находится имя (PERSON_NAME = 0), а через 8 байт (длина указателя на имя) хранится возраст (PERSON_AGE = 8).
В секции кода определен собственно функционал нашего псевдокласса. Прежде всего нам надо выделить память под объект. Для этого предназначен условный конструктор - функция
person_new
. В нее через регистры %rdi и %rsi передаем соответственно значения имени и возраста. В самой функции вначале выделяем память с помощью библиотечной функции языка Си malloc
:
movq $PERSON_SIZE, %rdi # в rdi - количество выделяемых байтов для объекта класса call malloc
После этого в регистр %rax помещается адрес выделенной памяти (в данном случае опустим ситуацию, когда операционной системе не удается выделить память). Стоит учитывать, что функция malloc задействует регистры %rdi и %rsi, поэтому они сохраняются в стек. После выделения памяти, используя смещения, сохраняем в выделенную память имя и возраст:
movq %rdi, PERSON_NAME(%rax) movq %rsi, PERSON_AGE(%rax)
Таким образом, после вызова этой функции в регистре %rax будет содержаться указатель на память (по сути объект Person), а в выделенной памяти будут храниться значения имени и возраста.
Для теста определена функция person_speak, в которой с помощью функции printf
просто выводится некоторое сообщение.
# условная функция говорения person_speak: subq $8, %rsp movq PERSON_NAME(%rdi), %rsi # получаем имя в %rsi movq $speak_text, %rdi call printf addq $8, %rsp ret
Функция person_info выводит информацию об объекте на консоль. Для этого в функцию через регистр %rdi передается указатель на область памяти, где содержится объект.
person_info: subq $8, %rsp movq PERSON_NAME(%rdi), %rsi # получаем имя в %rsi movq PERSON_AGE(%rdi), %rdx # получаем возраст в %rdi movq $info_text, %rdi call printf # выводим информацию на экран addq $8, %rsp ret
Поскольку нам известны смещения, по которым располагаются данные - смещения PERSON_NAME и PERSON_AGE, то мы легко можем получить эти данные, имея адрес участка памяти в %rdi.
Для удаления объекта и освобождения памяти предназначена функция person_delete:
person_delete: subq $8, %rsp # %rdi уже содержит адрес call free # освобождаем память объекта addq $8, %rsp ret
Здесь мы предполагаем, что в функцию через регистр %rdi передается адрес удаляемого объекта. Вызываемая сишная функция free
получает этот адрес и освобождает по нему память.
Стоит отметить, что все функции доступны извне с помощью директивы .globl
.globl person_new, person_speak, person_info, person_delete
Остальные данные, например, смещения, извне не доступны.
Протестируем этот условный класс. Для этого определим файл hello.s со следующим кодом:
.globl main .data name: .asciz "Tom" age: .quad 39 .equ PERSON_VAR, 0 # смещение в стеке, где будет располагаться адрес на переменную Person .text main: subq $8, %rsp # резервируем место для ссылки на объект Person # параметры для конструктора person movq $name, %rdi # %rdi - указатель на имя movq age, %rsi # %rsi - возраст call person_new # создаем один объект person с помощью конструктора movq %rax, PERSON_VAR(%rsp) # сохраняем ссылку на объект в стек movq %rax, %rdi # в %rdi - ссылку на объект person call person_speak movq PERSON_VAR(%rsp), %rdi # в %rdi - ссылку на объект person call person_info movq PERSON_VAR(%rsp), %rdi # в %rdi - ссылку на объект person call person_delete # удаляем объект person addq $8, %rsp ret
Здесь мы создаем один объект нашего псевдокласса. Значения для объекта определены в глобальных переменных name и age:
.data name: .asciz "Tom" age: .quad 39
Адрес объекта будет сохраняться в стеке - условная локальная переменная, и для хранения ее смещения в стеке определена константа PERSON_VAR
.equ PERSON_VAR, 0 # смещение в стеке, где будет располагаться адрес на переменную Person
В коде вначале резервируем место в стеке - 8 байт для хранения указателя на память объекта:
subq $8, %rsp # резервируем место для ссылки на объект Person
Также отмечу, что это также позволит нам выровнять стек по 16-байтной границе. Соответственно вызываемые функции будут иметь стек, выровненным по 8-байтной границе (так как в него помещается адрес возврата функции).
Затем вызываем конструктор, передавая в него данные из переменных name и age:
# параметры для конструктора person movq $name, %rdi # %rdi - указатель на имя movq age, %rsi # %rsi - возраст call person_new # создаем один объект person с помощью конструктора movq %rax, PERSON_VAR(%rsp) # сохраняем ссылку на объект в стек
Полученный адрес объекта из %rax сохраняем в стек.
Стоит отметить, что здесь используется смещение относительно верхушки стека - адреса в %rsp. При усложнении программы при увеличении операций со стеком, возможно, будет оптимальнее использовать регистр %rbp - указатель на фрейм стека.
Затем используя сохраненный адрес объекта, можно вызывать его функции. Например, при вызове функции person_info передаем сохраненный указатель из стека в функцию через регистр %rdi
movq PERSON_VAR(%rsp), %rdi # в %rdi - ссылку на объект person call person_info
Пример компиляции и работы программы:
root@Eugene:~/asm# gcc -static -o hello hello.s person.s root@Eugene:~/asm# ./hello Tom says: Hello Name: Tom Age: 39 root@Eugene:~/asm#
Подобным образом можно создать несколько объектов Person:
.globl main .data tom_name: .asciz "Tom" bob_name: .asciz "Bob" .equ tom_var, 8 # смещение переменной tom .equ bob_var, 0 # смещение переменной bob .text main: subq $24, %rsp # 24 байта - место для 2 объектов Person + 8 байт для выравнивания # создаем объект, на которую будет указывать переменная tom movq $tom_name, %rdi # %rdi - указатель на имя movq $39, %rsi # %rsi - возраст call person_new # создаем один объект person с помощью конструктора movq %rax, tom_var(%rsp) # сохраняем ссылку на объект tom в стек # создаем объект, на которую будет указывать переменная bob movq $bob_name, %rdi # %rdi - указатель на имя movq $43, %rsi # %rsi - возраст call person_new # создаем один объект person с помощью конструктора movq %rax, bob_var(%rsp) # сохраняем ссылку на объект bob в стек movq tom_var(%rsp), %rdi # в %rdi - ссылку на объект tom call person_info movq bob_var(%rsp), %rdi # в %rdi - ссылку на объект bob call person_info movq tom_var(%rsp), %rdi # в %rdi - ссылку на объект tom call person_delete # удаляем объект tom movq bob_var(%rsp), %rdi # в %rdi - ссылку на объект bob call person_delete # удаляем объект bob addq $24, %rsp # восстанавливаем стек ret
Здесь в стек помещаются две условных переменных нашего класса - tom_var и bob_var. Консольный вывод:
root@Eugene:~/asm# gcc -static -o hello hello.s person.s root@Eugene:~/asm# ./hello Name: Tom Age: 39 Name: Bob Age: 43 root@Eugene:~/asm#