Как выглядит скомпилированный класс С++?

С некоторым опытом в сборке инструкций и программ на 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

но то, что я получаю, трудно понять.

Может кто-нибудь объяснить мне или предложить хорошие отправные точки.

Ответ 1

Классы (более или менее) построены как регулярные структуры. Методы (более или менее...) преобразуются в функции, первый параметр которых равен "this". Ссылки на переменные класса выполняются как смещение на "this".

Что касается наследования, процитируйте цитату из С++ 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, которая определяет базовую первую не встроенную виртуальную функцию. Мы называем эту таблицу 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-table, как если бы он добавлял следующую инструкцию в каждый init-list конструктора:

 Base::Base(...arbitrary params...)
   : __vptr(&Base::__vtable[0])  ← supplied by the compiler, hidden from the programmer
   ...
 {
   ...
 }

Теперь давайте обработаем производный класс. Предположим, что ваш код на С++ определяет класс Der, который наследуется от класса Base. Компилятор повторяет шаги №1 и №3 (но не # 2). На шаге 1 компилятор создает скрытую v-таблицу, сохраняя те же функции-указатели, что и в Base::__vtable, но заменяя те слоты, которые соответствуют переопределениям. Например, если Der переопределяет virt0() через virt2() и наследует другие as-is, Der v-table может выглядеть примерно так (притвориться, что Der не добавляет никаких новых виртуальных машин):

 // 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-table. (Это не второй 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);
 } 

Я настоятельно рекомендую всем разработчикам С++ прочитать FAQ. Это может занять несколько недель (так как трудно читать и долго), но это научит вас многому о С++ и что с ним можно сделать.

Ответ 2

ок. нет ничего особенного в компилируемых классах. скомпилированных классов даже не существует. что существуют объекты, которые являются плоской частью памяти с возможными прокладками между полями? и автономные функции-члены где-то в коде, которые принимают указатель на объект в качестве первого параметра.

поэтому объект класса Base должен быть чем-то

(* base_address): i (* base_address + sizeof (int)): f

возможно иметь paddings между полями? но это аппаратное обеспечение. основанный на модели памяти процессоров.

также... в отладочной версии можно уловить описание класса в символах отладки. но это специфический компилятор. вам следует искать программу, которая отбрасывает символы отладки для вашего компилятора.

Ответ 3

"Скомпилированные классы" означают "скомпилированные методы".

Метод является обычной функцией с дополнительным параметром, обычно помещенным в регистр (в основном,% ecx, я считаю, это, по крайней мере, верно для большинства компиляторов Windows, которые должны создавать COM-объекты, используя соглашение __thiscall).

Итак, классы С++ не сильно отличаются от кучи обычных функций, за исключением манипуляции с именами и некоторой магии в конструкторах/деструкторах для настройки vtables.

Ответ 4

Основное отличие от чтения объектных файлов C состоит в том, что имена методов С++ искалечены. Вы можете попробовать использовать опцию -C|--demangle с objdump.

Ответ 5

Попробуйте

g++ -S class.cpp

Это даст вам файл сборки "class.s" (текстовый файл), который вы можете прочитать с помощью текстового редактора. Тем не менее, ваш код ничего не делает (объявление класса не генерирует код самостоятельно), поэтому у вас не будет много в файле сборки.

Ответ 6

Как структура C и набор функций с дополнительным параметром, который является указателем на структуру.

Самый простой способ следить за тем, что сделал компилятор, - это построить без оптимизации, затем загрузить код в отладчик и выполнить его в режиме смешанного источника/ассемблера.

Однако точка компилятора заключается в том, что вам не нужно знать этот материал (если, возможно, вы не пишете компилятор).