Наследование

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

Наследование - одно из базовых концепций объектно-ориентированного программирования, которая предполагает, что один класс может унаследовать функционал другого класса, а при необходимость переопределять его. Реализовать некое подобие наследования на ассемблере можно различными способами. Рассмотрим простейший пример. Пусть у нас есть условный класс Person, который представляет человека, и который определен в файле person.s:

## условный класс Person
.globl person_new, person_speak, person_info, person_delete, person_alocator
.globl PERSON_SIZE

.globl PERSON_VTABLE_SPEAK_OFFSET, PERSON_VTABLE_INFO_OFFSET
.globl person_vtable

.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 
    addq $8, %rsp   # восстанавливаем стек
person_alocator:                    # помещаем данные в выделенную память
    movq %rdi, PERSON_NAME(%rax)
    movq %rsi, PERSON_AGE(%rax)
    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

# смещения для вирутальных методов в vtable
.equ PERSON_VTABLE_SPEAK_OFFSET, 0
.equ PERSON_VTABLE_INFO_OFFSET, 8
# таблица виртуальных методов условного класса Person
person_vtable:
    .quad person_speak
    .quad person_info

Вкратце пройдемся по основным моментам в коде. В секции .data определяются используемые строки.

Далее в коде определяется набор констант, которые задают размер и расположение данных в памяти:

.equ PERSON_SIZE, 16    # размер объекта Person - 16 байт - для имени (8 байт) и возраста (8 байт)
.equ PERSON_NAME, 0     # смещение, где находится указатель на имя
.equ PERSON_AGE, 8     # смещение, где находится значение возраста

Предположим, что объект Person будет хранить имя и возраст человека. Для создания объекта Person предусмотрена функция конструктора - person_new. В нее через регистр %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 
    addq $8, %rsp   # восстанавливаем стек
person_alocator:                    # помещаем данные в выделенную память
    movq %rdi, PERSON_NAME(%rax)
    movq %rsi, PERSON_AGE(%rax)
    ret

С помощью функции malloc, которая имеется в стандартной библиотеке языка Си, выделяем память. Адрес выделенной памяти помещается в регистр %rax. На метке person_alocator помещаем в эту память переданные имя и возраст человека.

Функции person_speak и person_info получают через регистр %rdi ссылку на объект Person и выводят на экран определенную строку с помощью сишной функции printf. Функция person_delete удаляет объект из памяти с помощью другой сишной функции - free.

Более интересное идет в конце. Мы предполагаем, что от класса Person можно будет унаследовать другой класс и что класс-наследник сможет переопределить некоторые из функций класса Person - это так называемые виртуальные функции или методы. И, как и в случае с примером о полиморфизме в прошлой статье, для подобных виртуальных функций в классе Person определяем таблицу виртуальных функций или vtable:

person_vtable:
    .quad person_speak
    .quad person_info

То есть классы-наследники могут переопределить две функции. Для того, чтобы иметь доступ к определенной функции в этой таблице, определены константы:

.equ PERSON_VTABLE_SPEAK_OFFSET, 0  # смещение, где в vtable находится функция speak
.equ PERSON_VTABLE_INFO_OFFSET, 8  # смещение, где в vtable находится функция info

Также отмечаю, что эти константы и vtable доступны изве:

.globl PERSON_VTABLE_SPEAK_OFFSET, PERSON_VTABLE_INFO_OFFSET
.globl person_vtable

Теперь определим класс-наследник. Для этого создадим файл employee.s с условным классом Employee (класс условного сотрудника):

## условный класс Employee
.globl employee_new, employee_info, employee_delete
.globl employee_vtable
.data
info_text: .asciz "company: %s\n"
.text
.equ EMPLOYEE_SIZE, PERSON_SIZE+8      # размер объекта Person - 16 байт  + 8 байт для компании
.equ EMPLOYEE_COMPANY, PERSON_SIZE   # смещение, где находится указатель на компанию

# %rdi - указатель на имя
# %rsi - возраст
# %rdx - указатель на компанию
employee_new:
    pushq %rdi     
    pushq %rsi      
    pushq %rdx
    movq $EMPLOYEE_SIZE, %rdi # в rdi - количество выделяемых байтов для объекта класса
    call malloc     # выделяем память с помощью инструкции malloc
    popq %rdx   
    popq %rsi       
    popq %rdi     
    call person_alocator    # размещение имени и возраста передаются в родительский класс
    movq %rdx, EMPLOYEE_COMPANY(%rax) # Размещаем компанию после данных родительского класса
    ret

# выводим информацию об объекте
employee_info:
    pushq %rdi
    call person_info
    movq (%rsp), %rdi
    movq EMPLOYEE_COMPANY(%rdi), %rsi    # получаем возраст в %rcx
    movq $info_text, %rdi
    call printf                     # выводим информацию на экран
    addq $8, %rsp
    ret

# удаление объекта из памяти
employee_delete:
     subq $8, %rsp
    # %rdi уже содержит адрес
    call free           # освобождаем память объекта
    addq $8, %rsp
    ret

employee_vtable:
    .quad person_speak
    .quad employee_info

Условный класс Employee к полям, унаследованным от класса Person - то есть к имени и возрасту, добавляет еще одно поле - компанию, где работает сотрудник:

.equ EMPLOYEE_SIZE, PERSON_SIZE+8      # размер объекта Person - 16 байт  + 8 байт для компании
.equ EMPLOYEE_COMPANY, PERSON_SIZE   # смещение, где находится указатель на компанию

Причем класс Employee ничего не знает о расположении полей в объекте Person и лишь добавляет к этим полям свое поле для хранение компании.

Класс определяет свой конструктор. Причем расположение имени и возраста он делегирует на код метки person_alocator, который расположен в файле person.s (эти поля определены в классе Person, поэтому пусть класс Person ими и занимается):

call person_alocator    # размещение имени и возраста передаются в родительский класс
movq %rdx, EMPLOYEE_COMPANY(%rax) # Размещаем компанию после данных родительского класса

Класс Employee лишь добавляет в последние 8 байт памяти адрес строки с компанией.

Класс Employee переопределяет определяет функцию employee_info для вывода информации об объекте, где он вызывает сначала реализацию родительского класса - функцию person_info, а затем дополнительно выводит на консоль данные о компании:

employee_info:
    pushq %rdi
    call person_info    # обращение к реализации родительского класса
    movq (%rsp), %rdi
    movq EMPLOYEE_COMPANY(%rdi), %rsi    # получаем возраст в %rcx
    movq $info_text, %rdi
    call printf                     # выводим информацию на экран
    addq $8, %rsp
    ret

А после деструктора идет определение таблицы vtable для класса Employee:

employee_vtable:
    .quad person_speak
    .quad employee_info

Фактически это переопределение vtable класса Person. То есть класс Employee наследует без изменений функцию person_speak и переопределяет функцию person_info - она заменяется на employee_info.

Протестируем классы. Для этого определим следующий файл hello.s:

.globl main

.data
tom_name: .asciz "Tom"
tom_company: .asciz "METANIT.COM"
bob_name: .asciz "Bob"

.equ tom_var, 0  # смещение переменной tom (тип Employee)
.equ bob_var, 8  # смещение переменной bob (тип Person)

.text
main:
    subq $24, %rsp   # 16 байт для объектов Person и Employee + 8 байт для выравнивания

    # создаем объект Employee, на которую будет указывать переменная tom
    movq $tom_name, %rdi
    movq $39, %rsi 
    movq $tom_company, %rdx
    call employee_new
    movq %rax, tom_var(%rsp)

     # создаем объект [Person, на которую будет указывать переменная bob
    movq $bob_name, %rdi
    movq $43, %rsi
    call person_new
    movq %rax, bob_var(%rsp)

    movq bob_var(%rsp), %rdi    # ссылка на объект Person
    movq $person_vtable, %rsi      # ссылка на VTable для объекта Person
    call testPerson

    movq tom_var(%rsp), %rdi    # ссылка на объект Employee
    movq $employee_vtable, %rsi      # ссылка на VTable для объекта Employee
    call testPerson

    # Удаляем объекты
    movq bob_var(%rsp), %rdi
    call person_delete
    
    movq tom_var(%rsp), %rdi
    call employee_delete

    addq $24, %rsp     # восстанавливаем стек
    ret

# Функция принимает один объект Person
# через %rdi передается указатель на объект
# через %rsi передается ссылка на его vtable
testPerson:
    .equ PERSON_OBJ_OFFSET, 0       # смещение в стеке, где будет храниться ссылка на объект
    .equ PERSON_VTABLE_OFFSET, 8    # смещение в стеке, где будет храниться ссылка на vtable объекта
    subq $24, %rsp   # 16 байт для хранения ссылок на объект и на его vtable + 8 байт для выравнивания

    movq %rdi, PERSON_OBJ_OFFSET(%rsp)
    movq %rsi, PERSON_VTABLE_OFFSET(%rsp)

    # %rdi уже содержит ссылку на объект
    # вызываем функцию speak
    call *PERSON_VTABLE_SPEAK_OFFSET(%rsi)

    movq PERSON_OBJ_OFFSET(%rsp), %rdi
    movq PERSON_VTABLE_OFFSET(%rsp), %rsi
    # вызываем функцию info
    call *PERSON_VTABLE_INFO_OFFSET(%rsi)

    addq $24, %rsp     # восстанавливаем стек
    ret

Непосредственно для тестирования определена функция testPerson. Мы ожидаем, что через регистр %rdi в нее будет передаваться объект Person. Однако класс Employee наследуется от класса Person, соответственно объект Employee также является объектом класса Person. Поэтому в реальности в функцию можно передавать ссылки на объекты обоих типов. И чтобы отразить, объект какого именно типа передается, через регистр %rsi также передаем ссылку на соответствующую таблицу vtable

В самой функции testPerson сохраняем переданные адреса объекта и таблицы vtable по определенным смещениям в стек:

.equ PERSON_OBJ_OFFSET, 0       # смещение в стеке, где будет храниться ссылка на объект
.equ PERSON_VTABLE_OFFSET, 8    # смещение в стеке, где будет храниться ссылка на vtable объекта
subq $24, %rsp   # 16 байт для хранения ссылок на объект и на его vtable + 8 байт для выравнивания

movq %rdi, PERSON_OBJ_OFFSET(%rsp)
movq %rsi, PERSON_VTABLE_OFFSET(%rsp)

Затем вызываем функцию speak (person_speak или employee_speak):

# %rdi уже содержит ссылку на объект
# вызываем функцию speak
call *PERSON_VTABLE_SPEAK_OFFSET(%rsi)

Причем мы не знаем функция какого именно класса вызывается - Person или Employee - мы лишь используем смещение PERSON_VTABLE_SPEAK_OFFSET, чтобы найти адрес нужной функции в переданной vtable и затем вызываем код по этому адресу.

Далее аналогично вызываем функцию info (person_info или employee_info):

movq PERSON_OBJ_OFFSET(%rsp), %rdi
movq PERSON_VTABLE_OFFSET(%rsp), %rsi
# вызываем функцию info
call *PERSON_VTABLE_INFO_OFFSET(%rsi)

Таким образом, мы не знаем, что за объект передается в функцию testPerson. Мы лишь берем переданную с ним vtable и по определенному смещению ищем и вызываем в ней функцию.

Для тестирования в функции main создаем две переменных обоих классов:

.equ tom_var, 0  # смещение переменной tom (тип Employee)
.equ bob_var, 8  # смещение переменной bob (тип Person)

#########################################

# создаем объект Employee, на которую будет указывать переменная tom
movq $tom_name, %rdi
movq $39, %rsi 
movq $tom_company, %rdx
call employee_new
movq %rax, tom_var(%rsp)

 # создаем объект [Person, на которую будет указывать переменная bob
movq $bob_name, %rdi
movq $43, %rsi
call person_new
movq %rax, bob_var(%rsp)

Затем для каждого объекта вызываем функцию testPerson, передавая в нее адрес объекта и адрес соответствующей vtable

movq bob_var(%rsp), %rdi    # ссылка на объект Person
movq $person_vtable, %rsi   # ссылка на VTable для объекта Person
call testPerson

movq tom_var(%rsp), %rdi    # ссылка на объект Employee
movq $employee_vtable, %rsi  # ссылка на VTable для объекта Employee
call testPerson

Консольный вывод программы:

root@Eugene:~/asm# gcc -static -o hello hello.s person.s employee.s
root@Eugene:~/asm# ./hello
Bob says: Hello
Name: Bob   Age: 43
Tom says: Hello
Name: Tom   Age: 39
company: METANIT.COM
root@Eugene:~/asm#
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850