Нужна помощь в понимании компиляции программ на C ++ - PullRequest
2 голосов
/ 09 июля 2010

Я не правильно понимаю компиляцию и компоновку программ на C ++. Есть ли способ, я могу посмотреть на объектные файлы, сгенерированные путем компиляции программы C ++ (в понятном формате). Это должно помочь мне понять формат объектных файлов, как скомпилированы классы C ++, какая информация нужна компилятору для генерации объектных файлов и помочь мне понять такие выражения, как:

если класс используется только в качестве входных параметров и возвращаемого типа, нам не нужно включать весь заголовочный файл класса. Предварительного объявления достаточно, но если производный класс является производным от базового класса, нам нужно включить файл, содержащий определение базового класса (взято из «Exceptional C ++»).

Я читаю книгу «Связывание и загрузка», чтобы понять формат объектных файлов, но я бы предпочел что-то специально разработанное для исходного кода C ++.

Спасибо

Jagrati

Edit:

Я знаю, что с помощью nm я могу просматривать символы, присутствующие в объектных файлах, но мне интересно узнать больше об объектных файлах.

Ответы [ 5 ]

1 голос
/ 09 июля 2010

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

По конкретным вопросам, почему определение base требуется, когда вы объявляете его базовым классом derived, есть несколько разных причин (и, вероятно, больше, которые я забыл):

  1. Когда объект типа derivedСозданный, компилятор должен зарезервировать память для полного экземпляра и всех подклассов: он должен знать размер base
  2. Когда вы обращаетесь к атрибуту-члену, компилятор должен знать смещение от неявного указателя this,и это смещение требует знания размера, взятого подобъектом base.
  3. Когда идентификатор анализируется в контексте derived и идентификатор не найден в классе derived, компилятор должен знатьопределено ли оно в base перед поиском идентификатора во вложенных пространствах имен.Компилятор не может знать, является ли foo(); допустимым вызовом внутри derived::function(), если в классе base объявлено foo().
  4. Число и подписи всех виртуальных функций, определенных в base, должны бытьизвестно, когда компилятор определяет класс derived.Эта информация необходима для построения механизма динамической отправки - обычно vtable-- и даже для того, чтобы узнать, связана ли функция-член в derived для динамической отправки или нет - если base::f() является виртуальной, то derived::f() будетбыть виртуальным независимо от того, имеет ли объявление в derived ключевое слово virtual.
  5. Множественное наследование добавляет несколько других требований - например, относительные смещения от каждого baseX, которые должны быть переписаны перед окончательными переопределениями длявызываются методы (указатель типа base2, который указывает на объект multiplyderived, указывает не на начало экземпляра, а на начало подобъекта base2 в экземпляре, который может быть смещен другимбазы, объявленные до base2 в списке наследования.

К последнему вопросу в комментариях:

Так не происходит создание экземпляров объектов (кроме глобальных)может ждать до времени выполнения и, следовательно, размер, смещение и т. д. могут ждать до времени ссылки, и нам не обязательно иметь дело сс этим в то время мы генерируем объектные файлы?

void f() {
   derived d;
   //...
}

Предыдущий код выделяет и объект типа derived в стеке.Компилятор добавит инструкции ассемблера, чтобы зарезервировать некоторый объем памяти для объекта в стеке.После того, как компилятор проанализировал и сгенерировал сборку, объекта не осталось, в частности (при условии тривиального конструктора для типа POD: т.е. ничего не инициализировано), этот код и void f() { char array[ sizeof(derived) ]; } создадут точно такой же ассемблер.Когда компилятор генерирует инструкцию, которая зарезервирует место, он должен знать, сколько.

0 голосов
/ 09 июля 2010

Я читаю «http://www.network -theory.co.uk / docs / gccintro / » - «Введение в GCC». Это дало мне хорошее представление о связывании и компиляции. На начальном уровне, но мне все равно.

0 голосов
/ 09 июля 2010

Обычно вам не нужно подробно знать внутренний формат файлов Obj, поскольку они созданы для вас. Все, что вам нужно знать, - это то, что для каждого создаваемого вами класса компилятор генерирует и файл Obj, представляющий собой двоичный байт-код вашего класса, подходящий для ОС, для которой вы компилируете. Затем следующий шаг - связывание - соберет объектные файлы для всех классов, которые вам нужны для вашей программы, в один EXE или DLL (или любой другой формат для ОС, отличных от Windows). Может быть также EXE + несколько DLL, в зависимости от ваших пожеланий.

Наиболее важным является то, что вы разделяете интерфейс (объявление) и реализацию (определение) вашего класса.

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

Все реализации методов класса или других функций должны быть в файле CPP. Это гарантирует, что файл Obj, сгенерированный компилятором, не понадобится, если кто-то включит ваш заголовок, а вы можете включить его от других только в файлы CPP.

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

Если вы включили в заголовок, это означает, что когда компилятор генерирует файл Obj для вашего класса, он должен сначала сгенерировать файл Obj для других классов, включенных в ваш заголовок, что может потребовать снова другие файлы Obj и т. Д. Может быть даже круговая зависимость, а затем вы не можете скомпилировать! Или, если вы что-то измените в своем классе, то компилятору потребуется перегенерировать множество других файлов Obj, потому что через некоторое время они станут очень зависимыми, если вы не разделитесь.

0 голосов
/ 09 июля 2010

nm - инструмент Unix, который покажет вам имена символов в объектном файле.

objdump - инструмент GNU, который покажет вам больше информации.Но оба инструмента покажут вам довольно сырую информацию, которая используется компоновщиком, но не предназначена для чтения людьми.Это, вероятно, не поможет вам лучше понять, что происходит на уровне C ++.

0 голосов
/ 09 июля 2010

Вы пытались проверить свои двоичные файлы с помощью readelf (при условии, что вы работаете на платформе Linux)? Это обеспечивает довольно полную информацию об объектных файлах ELF.

Честно говоря, я не уверен, насколько это поможет понять компиляцию и компоновку. Я думаю, что правильный путь, вероятно, заключается в том, чтобы понять, как код C ++ отображается на сборку до и после связывания.

...