С некоторыми знаниями в собирают инструкции и программы C, я могу визуализировать, как скомпилированная функция была бы похожа, но это забавно, я так тщательно никогда не думал о том, как скомпилированный класс C++ был бы похож.
bash$ cat class.cpp
#include<iostream>
class Base
{
int i;
float f;
};
bash$ g++ -c class.cpp
Я работал:
bash$objdump -d class.o
bash$readelf -a class.o
но то, что я получаю, трудно для меня понять.
Мог кто-то объяснять меня или предлагать некоторые хорошие начальные точки.
Основное отличие от чтения объектных файлов C состоит в том, что имена методов C ++ искажены . Вы можете попробовать использовать опцию -C | --demangle
с objdump
.
«Скомпилированные классы» означают «скомпилированные методы».
Метод - это обычная функция с дополнительным параметром, обычно помещаемая в регистр (я думаю, в основном это% ecx, по крайней мере, это верно для большинства компиляторов Windows, которым приходится создавать COM-объекты с использованием соглашения __thiscall).
Итак, классы C ++ не сильно отличаются от множества обычных функций, за исключением изменения имен и некоторой магии в конструкторах / деструкторах для настройки vtables.
Подобно структуре C и набору функций с дополнительным параметром, который является указателем на структуру.
Самый простой способ проследить за тем, что делал компилятор, - это построить без оптимизации, затем загрузить код в отладчик и выполнить его в смешанном режиме исходного кода / ассемблера.
Однако смысл компилятора в том, что вам не нужно знать это (если, возможно, вы не пишете компилятор).
ок. с скомпилированными классами нет ничего особенного. скомпилированных классов даже не существует. Какие существуют объекты, которые представляют собой плоский кусок памяти с возможными отступами между полями? и автономные функции-члены где-нибудь в коде, которые принимают указатель на объект в качестве первого параметра.
поэтому объект класса Base должен быть чем-то
(* base_address): i (* base_address + sizeof (int)): f
можно ли иметь отступы между полями? но это зависит от оборудования. на основе модели памяти процессоров.
также ... в отладочной версии можно поймать описание класса в отладочных символах. но это зависит от компилятора. вам следует найти программу, которая выводит символы отладки для вашего компилятора.
Попробуйте
g ++ -S class.cpp
. Это даст вам файл сборки 'class.s' (текстовый файл), который вы можете прочитать с помощью текстового редактора. Однако ваш код ничего не делает (объявление класса не генерирует код сам по себе), поэтому в файле сборки у вас будет немного.
Классы (более или менее) построены как обычные структуры. Методы (более или менее ...) преобразуются в функции, первым параметром которых является this. Ссылки на переменные класса делаются как смещение к «this».
Что касается наследования, давайте процитируем C ++ FAQ LITE, который отражен здесь http://www.parashift.com/c++-faq-lite/virtual-functions.html#faq-20.4 . В этой главе показано, как виртуальные функции вызываются на реальном оборудовании (что делает компиляция в машинном коде.
Давайте рассмотрим пример. Предположим, что класс Base имеет 5 виртуальных функций: virt0 ()
- ] virt4 ()
.
// Your original C++ source code
class Base {
public:
virtual arbitrary_return_type virt0(...arbitrary params...);
virtual arbitrary_return_type virt1(...arbitrary params...);
virtual arbitrary_return_type virt2(...arbitrary params...);
virtual arbitrary_return_type virt3(...arbitrary params...);
virtual arbitrary_return_type virt4(...arbitrary params...);
...
};
Шаг № 1 : компилятор создает статическую таблицу, содержащую 5 указателей на функции, закапывая эту таблицу где-нибудь в статической памяти. Многие (не все) компиляторы определяют эту таблицу при компиляции .cpp, который определяет первую не встроенную виртуальную функцию Base. Мы называем эту таблицу v-таблицей; давайте представим, что ее техническое имя - Base :: __ vtable
.Если указатель функции помещается в одно машинное слово на целевой аппаратной платформе, Base :: __ vtable
в конечном итоге потребляет 5 скрытых слов памяти. Не 5 на экземпляр, не 5 на функцию; всего 5. Это может выглядеть примерно как следующий псевдокод:
// Pseudo-code (not C++, not C) for a static table defined within file Base.cpp
// Pretend FunctionPtr is a generic pointer to a generic member function
// (Remember: this is pseudo-code, not C++ code)
FunctionPtr Base::__vtable[5] = {
&Base::virt0, &Base::virt1, &Base::virt2, &Base::virt3, &Base::virt4
};
Шаг №2 : компилятор добавляет скрытый указатель (обычно также машинное слово) к каждому объекту класса Base. Это называется v-указателем. Думайте об этом скрытом указателе как о скрытом элементе данных, как если бы компилятор переписал ваш класс примерно так:
// Your original C++ source code
class Base {
public:
...
FunctionPtr* __vptr; ← supplied by the compiler, hidden from the programmer
...
};
Шаг № 3 : компилятор инициализирует this -> __ vptr
в каждом конструкторе. Идея состоит в том, чтобы заставить v-указатель каждого объекта указывать на v-таблицу его класса, как если бы он добавляет следующую инструкцию в каждый список инициализации конструктора:
Base::Base(...arbitrary params...)
: __vptr(&Base::__vtable[0]) ← supplied by the compiler, hidden from the programmer
...
{
...
}
Теперь давайте разработаем производный класс. Предположим, ваш код C ++ определяет класс Der, который наследуется от класса Base. Компилятор повторяет шаги №1 и №3 (но не №2). На шаге № 1 компилятор создает скрытую v-таблицу, сохраняя те же указатели на функции, что и в Base :: __ vtable
, но заменяя те слоты, которые соответствуют переопределениям. Например, если Der переопределяет virt0 ()
через virt2 ()
и наследует остальные как есть, v-таблица Дера может выглядеть примерно так (притвориться, что Der не добавляет никаких new virtuals):
// Pseudo-code (not C++, not C) for a static table defined within file Der.cpp
// Pretend FunctionPtr is a generic pointer to a generic member function
// (Remember: this is pseudo-code, not C++ code)
FunctionPtr Der::__vtable[5] = {
&Der::virt0, &Der::virt1, &Der::virt2, &Base::virt3, &Base::virt4
}; ^^^^----------^^^^---inherited as-is
На шаге № 3 компилятор добавляет аналогичное присвоение указателя в начало каждого конструктора Der. Идея состоит в том, чтобы изменить v-указатель каждого объекта Der так, чтобы он указывал на v-таблицу своего класса.(Это не второй v-указатель; это тот же v-указатель, который был определен в базовом классе Base; помните, компилятор не повторяет шаг № 2 в классе Der.)
Наконец, давайте посмотрим, как компилятор реализует вызов виртуальной функции. Ваш код может выглядеть так:
// Your original C++ code
void mycode(Base* p)
{
p->virt3();
}
Компилятор не знает, будет ли он вызывать Base :: virt3 ()
или Der :: virt3 ()
или, возможно, virt3 ()
метод другого производного класса, который еще даже не существует. Он только знает наверняка, что вы вызываете virt3 ()
, который является функцией в слоте №3 v-таблицы. Он переписывает этот вызов примерно так:
// Pseudo-code that the compiler generates from your C++
void mycode(Base* p)
{
p->__vptr[3](p);
}
Я настоятельно рекомендую каждому разработчику C ++ прочитать FAQ. Это может занять несколько недель (так как это сложно читать и долго), но это научит вас многому о C ++ и о том, что с ним можно сделать.