На что похож скомпилированный класс C++?

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

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

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

12
задан CharlesB 29 August 2017 в 13:10
поделиться

6 ответов

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

1
ответ дан 2 December 2019 в 05:53
поделиться

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

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

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

2
ответ дан 2 December 2019 в 05:53
поделиться

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

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

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

0
ответ дан 2 December 2019 в 05:53
поделиться

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

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

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

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

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

2
ответ дан 2 December 2019 в 05:53
поделиться

Попробуйте

g ++ -S class.cpp

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

0
ответ дан 2 December 2019 в 05:53
поделиться

Классы (более или менее) построены как обычные структуры. Методы (более или менее ...) преобразуются в функции, первым параметром которых является 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 ++ и о том, что с ним можно сделать.

21
ответ дан 2 December 2019 в 05:53
поделиться
Другие вопросы по тегам:

Похожие вопросы: