C
cpp
@cplusplustudy47 подп.
313просмотров
13 февраля 2024 г.
Score: 344
Продолжение граблей со static_cast В прошлый раз мы выяснили что при касте к братскому классу компилятор вставит вызов метода того типа которым помечен объект. Рассмотрим данный код: struct Base {}; struct ActualChild : Base { int foo() { return 1; } }; struct OtherChild : Base { int foo() { return 2; } }; int main() { ActualChild actual; OtherChild other_ptr = static_cast<OtherChild>((Base*)&actual); return other_ptr->foo(); } https://godbolt.org/z/E1z194zf6 Результатом программы будет 2 и в окне ассемблера мы видим что компилятор вставил напрямую вызов OtherChild::foo() (9я строка). Давайте теперь посмотрим что будет в случае если метод станет виртуальным. Для этого нам надо поменять только структуру Base: struct Base { virtual int foo() = 0; }; https://godbolt.org/z/1Kh8qfcfs Теперь результатом будет 1. Но каким образом компилятор определил какой метод вызвать? Ответ на это мы найдем в окне ассемблера в следующих строках (7-12 строки): call ActualChild::ActualChild() [base object constructor] lea rax, [rbp - 16] … call qword ptr [rax] Последняя строка собственно является вызовом метода ActualChild::foo, однако как видно метод вызывается не напрямую, а через указатель на область памяти который лежит в регистре rax. Давайте попробуем понять какой именно адрес лежит в этой строке. Как видно из второй строчки – адрес достается из регистра rbp со смещением в 16. На всякий напомню что регистр rbp используется для хранения указателя на текущую голову стека. То есть конструктор ActualChild положил в стек указатель на соответствующую функцию foo. Давайте найдем как он это сделал. Для этого посмотрим на ассемблер данного конструктора (строки 16-30): ActualChild::ActualChild() [base object constructor]: … call Base::Base() [base object constructor] … lea rcx, [rip + vtable for ActualChild] add rcx, 16 mov qword ptr [rax], rcx add rsp, 16 pop rbp ret Мы видим до 23й строки определенные операции которые нас не сильно интересуют. Далее мы видим вызов конструктора базового класса (строка 23). Затем в инструкции lea rcx, [rip + vtable for ActualChild] мы видимо, что в регистр rcx кладется указатель на глобальный объект vtable_for_ActualChild (rip это специальная инструкция конкретно для Intel которая позволяет брать адреса объектов в глобальной области). После этого к адресу добавляется смещение в 16 (26 строка) и получившийся адрес собственно и кладется на стек, откуда мы его далее и получаем. Зачем компилятору нужно смещение в 16? Для ответа на этот вопрос давайте собственно посмотрим что такое виртуальная таблица vtable for ActualChild в глобальной области. Для этого прокрутим ассемблер вниз до 48й строки где видим следующее: vtable for ActualChild: .quad 0 .quad typeinfo for ActualChild .quad ActualChild::foo() Итак в первых 8 битах лежит 0 (честно говоря не знаю зачем, возможно это результат отсутствия оптимизаций, я собирал с O0). Далее лежит некоторая RTTI информация о классе, и после этого на расстоянии в 16 бит начинается список указателей на непосредственно виртуальные методы. Таким образом мы получаем указатель на ActualChild::foo() который конструктор ActualChild положит в сам объект и который будет далее вызван компилятором. Дописав эту заметку, я вдруг понял, что по сути она не имеет ничего общего со static_cast. Я просто разобрал как работают таблицы виртуальных методов. Извиняюсь за небольшое вранье в названии.
313
просмотров
3575
символов
Нет
эмодзи
Нет
медиа

Другие посты @cplusplustudy

Все посты канала →
Продолжение граблей со static_cast В прошлый раз мы выяснили — @cplusplustudy | PostSniper