Полиморфизм предполагает, что для некоторого типа может быть определено многообразие форм. Cущность полиморфизма заключается в определении некоторого интерфейса - набора функций/методов, а различные классы могут по-разному реализовать эти методы/функции, обеспечивая для объектов интерфейса многообразие форм поведения. Например, у нас может быть сущность Животное, которая описывает некоторый общий функционал для всех животных или интерфейс животных. И также могут быть сущности Кошка или Собака или другие типы животных, которые могут по-разному реализовать интерфейс Животное.
Чтобы было понятно, о чем идет речь, возьмем следующий условный псевдокод:
void playWithAnimal(Animal myAnimal) { myAnimal.speak(); myAnimal.eat(); myAnimal.speak(); } interface Animal { void eat(); void speak(); } class Cat : Animal { string _name; public Cat(string name) { _name = name; } public void eat(){ // реализация метода eat } public void speak(){ // реализация метода eat } } class Dog : Animal { string _name; public Dog(string name) { _name = name; } public void eat(){ // реализация метода eat } public void speak(){ // реализация метода eat } }
На высокоуровневом языке программирования (C#, Java и т.д.) функция playWithAnimal получает в качестве параметра объект Animal. Причем на момент определения функции мы не знаем, что это за объект - это может быть и объект Cat, и объект Dog. Затем внутри функции вызываем методы интерфейса. То есть один интерфейс - Animal и есть многобразие форм его реализации - классы Cat и Dog.
Полиморфизм обычно реализуется с помощью специальных записей, которые еще называются vtable и которые представляют собой просто список указателей на каждую функцию в интерфейсе, который реализует класс. А когда вызывается функция, которая принимает указатель на объект, то вместе с ним также передается и указатель на vtable таблицу. Поскольку интерфейс известен заранее, также известны смещения в виртуальной таблице, и функция просто ищет в виртуальной таблице функцию, которую она хочет вызвать. Такую комбинацию из двух указателей — указателя объекта и указателя виртуальной таблицы — часто называют fat pointer (толстый указатель).
Так, определим файл cat.s, который будет хранить условный класс Cat (класс "Кошка") и будет иметь следующий код:
## Условный класс Cat .globl cat_new, cat_eat, cat_speak, cat_delete .data speak_text: .asciz "% says: Meow\n" eat_text: .asciz "Cats like fish\n" .text .equ CAT_SIZE, 8 # размер объекта .equ CAT_NAME, 0 # смещение имени кота в объекте # условный конструктор # через %rdi функция получает имя кота cat_new: pushq %rdi movq $CAT_SIZE, %rdi call malloc popq %rdi movq %rdi, CAT_NAME(%rax) ret # через %rdi функция получает ссылку на объект cat_speak: subq $8, %rsp movq CAT_NAME(%rdi), %rsi # получаем сохраненное имя movq $speak_text, %rdi call printf addq $8, %rsp ret cat_eat: subq $8, %rsp movq $eat_text, %rdi call printf addq $8, %rsp ret # освобождение памяти объекта # через %rdi функция получает ссылку на объект cat_delete: subq $8, %rsp call free addq $8, %rsp ret
Класс будет определять объект длиной всего в 8 байт, которые будут хранить имя кошки. Для создания объекта определена функция cat_new, в которую передается ссылка на строку -имя кошки и
которая с помощью функции malloc
из библиотеки языка Си выделяет память. Получив адрес выделенной памяти через регистр %rax, помещаем в нее указатель на имя кошки.
Функция cat_delete удаляет объект из памяти. Через регистр %rdi она принимает адрес объекта и передает его в библиотечную функцию free
, которая собственно и
освобождает память.
Две остальные функции - cat_speak и cat_eat представляют поведение кошки и просто выводят некоторую строку с помощью функции printf
Далее также определим файл dog.s, который будет представлять класс Dog ("Собака") и который будет определять следующий код:
## Условный класс Dog .globl dog_new, dog_eat, dog_speak, dog_delete .data speak_text: .asciz "%s says: Gaw\n" eat_text: .asciz "Dogs like meat\n" .text .equ DOG_SIZE, 8 # размер объекта .equ DOG_NAME, 0 # смещение имени собаки в объекте # условный конструктор # через %rdi функция получает имя собаки dog_new: pushq %rdi movq $DOG_SIZE, %rdi call malloc popq %rdi movq %rdi, DOG_NAME(%rax) ret # через %rdi функция получает ссылку на объект dog_speak: subq $8, %rsp movq DOG_NAME(%rdi), %rsi # получаем сохраненное имя movq $speak_text, %rdi call printf addq $8, %rsp ret dog_eat: subq $8, %rsp movq $eat_text, %rdi call printf addq $8, %rsp ret # освобождение памяти объекта # через %rdi функция получает ссылку на объект dog_delete: subq $8, %rsp call free addq $8, %rsp ret
Здесь весь код однотипен и аналогичен классу Cat. Таким образом, в обоих классах у нас есть две функции, которые определяют поведение животного - cat_speak/dog_speak и cat_eat/dog_eat. Эти функции имеют разную реализацию - выводят разные сообщения на консоль, но они принимают один и тот же параметр - ссылку на объект через регистр %rdi. И в принципе они представляют общий интерфейс обоих классов.
Теперь определим файл animal.s, который будет определять интерфейс Animal ("Животное") с общим поведением для обоих классов:
.globl VTABLE_SPEAK_OFFSET, VTABLE_EAT_OFFSET .globl dog_vtable .globl cat_vtable # смещения функций в vtable .equ VTABLE_SPEAK_OFFSET, 0 .equ VTABLE_EAT_OFFSET, 8 # vtable для класса Dog dog_vtable: .quad dog_speak .quad dog_eat # vtable для класса Cat cat_vtable: .quad cat_speak .quad cat_eat
Для каждого класса требуется отдельная таблица vtable - cat_vtable и dog_vtable. В каждой таблице определяется ссылка на реализацию функции. Чтобы найти в vtable функцию, определены смещения VTABLE_SPEAK_OFFSET и VTABLE_EAT_OFFSET.
Протестируем вышеопределенный функционал. Для этого определим следующий файл hello.s:
.globl main .data # имена для животных cat_name: .asciz "Murzik" dog_name: .asciz "Barbos" # смещение переменных типа Animal в стеке .equ CAT_VAR, 0 # смещение переменной типа Cat .equ DOG_VAR, 8 # смещение переменной типа Dog .text main: subq $24, %rsp # 16 байт для одного объекта Cat и Dog + 8 байт для выравнивания # создаем один объект Cat, через %rdi передаем ссылку на имя movq $cat_name, %rdi call cat_new movq %rax, CAT_VAR(%rsp) # создаем один объект Dog, через %rdi передаем ссылку на имя movq $dog_name, %rdi call dog_new movq %rax, DOG_VAR(%rsp) movq CAT_VAR(%rsp), %rdi # ссылка на объект Cat movq $cat_vtable, %rsi # ссылка на VTable для объекта Cat call playWithAnimal movq DOG_VAR(%rsp), %rdi # ссылка на объект Dog movq $dog_vtable, %rsi # ссылка на VTable для объекта Dog call playWithAnimal # Удаляем объекты movq CAT_VAR(%rsp), %rdi call cat_delete movq DOG_VAR(%rsp), %rdi call dog_delete addq $24, %rsp # восстанавливаем стек ret # Функция принимает один объект Animal # через %rdi передается указатель на объект # через %rsi передается ссылка на его vtable playWithAnimal: .equ ANIMAL_OBJ_OFFSET, 0 # смещение в стеке, где будет храниться ссылка на объект .equ ANIMAL_VTABLE_OFFSET, 8 # смещение в стеке, где будет храниться ссылка на vtable объекта subq $24, %rsp # 16 байт для хранения ссылок на объект и на его vtable + 8 байт для выравнивания movq %rdi, ANIMAL_OBJ_OFFSET(%rsp) movq %rsi, ANIMAL_VTABLE_OFFSET(%rsp) # %rdi уже содержит ссылку на объект # вызываем функцию speak call *VTABLE_SPEAK_OFFSET(%rsi) movq ANIMAL_OBJ_OFFSET(%rsp), %rdi movq ANIMAL_VTABLE_OFFSET(%rsp), %rsi # вызываем функцию eat call *VTABLE_EAT_OFFSET(%rsi) movq ANIMAL_OBJ_OFFSET(%rsp), %rdi movq ANIMAL_VTABLE_OFFSET(%rsp), %rsi call *VTABLE_SPEAK_OFFSET(%rsi) addq $24, %rsp # восстанавливаем стек ret
Главные действия тут происходят в функции playWithAnimal. Через регистр %rdi она получает объект Animal - это может быть и Cat, и Dog, а через регистр %rsi - ссылку на vtable объекта. Для последующего использования эти ссылки сохраняются в стеке по смещениям ANIMAL_OBJ_OFFSET и ANIMAL_VTABLE_OFFSET:
playWithAnimal: .equ ANIMAL_OBJ_OFFSET, 0 # смещение в стеке, где будет храниться ссылка на объект .equ ANIMAL_VTABLE_OFFSET, 8 # смещение в стеке, где будет храниться ссылка на vtable объекта subq $24, %rsp # 16 байт для хранения ссылок на объект и на его vtable + 8 байт для выравнивания movq %rdi, ANIMAL_OBJ_OFFSET(%rsp) movq %rsi, ANIMAL_VTABLE_OFFSET(%rsp)
Далее мы вызываем функцию speak объекта:
# %rdi уже содержит ссылку на объект # вызываем функцию speak call *VTABLE_SPEAK_OFFSET(%rsi)
То есть %rsi
содержит адрес таблицы vtable для текущего объекта из %rdi. Выражение VTABLE_SPEAK_OFFSET(%rsi)
позволяет найти в этой vtable адрес
функции speak (cat_speak или dog_speak). И чтобы вызвать эту функцию по адресу, перед адресом указывается звездочка: *VTABLE_SPEAK_OFFSET(%rsi)
. Таким образом,
будет вызываться указанная функция.
Подобным образом вызывается функция eat (cat_eat или dog_eat). Единственное, что нам надо восстановить значения в %rdi и %rsi, так как при вызове функции speak они изменяются:
movq ANIMAL_OBJ_OFFSET(%rsp), %rdi movq ANIMAL_VTABLE_OFFSET(%rsp), %rsi # вызываем функцию eat call *VTABLE_EAT_OFFSET(%rsi)
В функции main для теста создаем пару объектов Animal - по одному объекту Cat и Dog и сохраняем их адреса в стек:
# создаем один объект Cat, через %rdi передаем ссылку на имя movq $cat_name, %rdi call cat_new movq %rax, CAT_VAR(%rsp) # создаем один объект Dog, через %rdi передаем ссылку на имя movq $dog_name, %rdi call dog_new movq %rax, DOG_VAR(%rsp)
Далее для каждого созданного объекта вызываем функцию playWithAnimal, передавая в нее через регистр %rdi адрес объекта, а через регистр %rsi - адрес vtable:
movq CAT_VAR(%rsp), %rdi # ссылка на объект Cat movq $cat_vtable, %rsi # ссылка на VTable для объекта Cat call playWithAnimal movq DOG_VAR(%rsp), %rdi # ссылка на объект Dog movq $dog_vtable, %rsi # ссылка на VTable для объекта Dog call playWithAnimal
Компиляция и пример работы программы:
root@Eugene:~/asm# gcc -static -o hello hello.s cat.s dog.s animal.s root@Eugene:~/asm# ./hello Murzik says: Meow Cats like fish Murzik says: Meow Barbos says: Gaw Dogs like meat Barbos says: Gaw root@Eugene:~/asm#