Объектно-ориентированное программирование

Определение классов

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

Ассемблер в отличие от ряда языков высокого уровня не поддерживает напрямую парадигму объектно-ориентированного программирования, тем не менее и на уровне ассемблера мы можем имитировать работу с классами и объектами, тем более что те же классы в языках высокого уровня непосредственно или опосредовано компилируются в тот же самый код. Но естественно такое ООП в ассемблере будет довольно условным.

Так, пусть у нас есть файл 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#
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850