Наследование - одно из базовых концепций объектно-ориентированного программирования, которая предполагает, что один класс может унаследовать функционал другого класса, а при необходимость переопределять его. Реализовать некое подобие наследования на ассемблере можно различными способами. Рассмотрим простейший пример. Пусть у нас есть условный класс 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#