Как сделать объектный файл

Добавил пользователь Алексей Ф.
Обновлено: 10.09.2024

Компилятором языка С является gcc. Для получения объектного файла в процессе компиляции используют опцию -c. Вот как, например, в режиме командной строки компилируется файл main.c:

Полученный объектный файл будет назван main.o.

Компилятором языка C++ является g++. Он работает почти так же, как и gcc. Следующая команда предназначена для компиляции файла reciprocal.cpp:

%g++ -c reciprocal.cpp.

Опция -c говорит компилятору о необходимости получить на выходе объектный файл (он будет называться reciprocal.o). Без нее компилятор g++ попытается скомпоновать программу и создать исполняемый файл a.out.

В процессе написания любой более-менее крупной программы обычно задействуется ряд дополнительных опций. К примеру, опция -I сообщает компилятору о том, где искать файлы заголовков. По умолчанию компиляторы GCC просматривают текущий каталог, а также каталоги, где установлены файлы стандартных библиотек. Предположим, наш проект состоит из двух каталогов: src и include. Следующая команда дает компилятору g++ указание дополнительно искать файл reciprocal.hpp в каталоге ../include:

%g++ -c -I ../include reciprocal.cpp

Компоновка объектных файлов

Если в проект входит хотя бы один файл C++, компоновка всегда осуществляется с помощью компилятора g++. Если же все файлы написаны на языке C, нужно использовать компилятор gcc. В нашем случае имеются файлы обоих типов, поэтому требуемая команда выглядит так:

%g++ -o reciprocal main.o reciprocal.o

Опция -o задает имя файла, создаваемого в процессе компоновки.

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

Запуск программы reciprocal осуществляют следующим образом:

The reciprocal of 7 is 0.142857

Как видите, компилятор g++ автоматически подключил к проекту стандартную библиотеку языка C, содержащую реализацию функции printf().

Автоматизация процесса с помощью GNU-утилиты make

Те, кто программируют в Windows, привыкли работать в той или иной интегрированной среде разработки. Программист добавляет в нее исходные файлы, а среда автоматически создает проект. Аналогичные среды доступны и в Unix системах, но с ними мы будем работать позже. GNU-утилита make позволяет автоматически перекомпилировать программу.

Основная идея утилиты make проста. Ей указываются целевые модули, участвующие в процессе построения исполняемого файла, и правила, по которым протекает этот процесс. Также задаются зависимости, определяющие, когда конкретный целевой модуль должен быть перестроен.

В нашем тестовом проекте reciprocal три очевидных целевых модуля: reciprocal.o, main.o и сама программа reciprocal. Правила нам уже известны: это рассмотренные выше командные строки. А вот над зависимостями нужно немного подумать. Ясно, что файл reciprocal зависит от файлов reciprocal.o и main.o, поскольку нельзя скомпоновать программу, не создав оба объектных файла. Последние должны перестраиваться при изменении соответствующих исходных файлов. Нельзя также забывать о файле reciprocal.hpp: он включается в оба исходных файла, поэтому его изменение тоже затрагивает объектные файлы.

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

Чтобы передать всю эту информацию утилите make, необходимо создать файл Makefile (touch Makefile). Его содержимое будет таким:

reciprocal: main.o reciprocal.o

g++ $(CFLAGS) -o reciprocal main.o reciprocal.o

main.o: main.с reciprocal.hpp

gcc $(CFLAGS) -c main.c

reciprocal.o: reciprocal.cpp reciprocal.hpp

g++ $(CFLAGS) -c reciprocal.cpp

rm -f *.o reciprocal

Целевые модули перечислены слева. За именем модуля следует двоеточие и существующие зависимости. В следующей строке указано правило, по которому создается модуль (назначение записи $(CFLAGS) мы пока проигнорируем). Строка правила должна начинаться с символа табуляции, иначе утилита make проинтерпретирует ее неправильно.

Если удалить созданные нами выше объектные файлы и ввести

будет получен следующий результат:

g++ -c reciprocal.cpp

g++ -o reciprocal main.o reciprocal.o

Утилита make автоматически создала объектные файлы и скомпоновала их. Попробуйте теперь внести какое-нибудь простейшее изменение в файл main.с и снова запустить утилиту. Вот что произойдет:

g++ -o reciprocal main.o reciprocal.o

Как видите, утилита make повторно создала файл main.o и перекомпоновала программу, но не стала перекомпилировать файл reciprocal. cpp, так как в этом не было необходимости.

Запись $(CFLAGS) обозначает переменную утилиты make. Ее можно определить либо в файле Makefile, либо в командной строке. Утилита подставит на место переменной реальное значение во время выполнения правила. Вот как, например, можно осуществить перекомпиляцию с включенной оптимизацией:

rm -f *.o reciprocal

g++ -02 -c reciprocal.cpp

g++ -02 -o reciprocal main.o reciprocal.o

Обратите внимание на то, что вместо записи $(CFLAGS) в правилах появился флаг -02.

В этом разделе мы рассмотрели лишь самые основные возможности утилиты make. Чтобы получить о ней более подробную информацию, обратитесь к интерактивной документации, введя такую команду:

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

GNU-отладчик gdb

Отладчик – это программа, с помощью которой можно узнать, почему написанная вами программа ведет себя не так, как было задумано. Работать с отладчиком приходится очень часто. Большинство Unix-программистов имеет дело с GNU-отладчиком (GNU Debugger, GDB), который позволяет пошагово выполнять программу, создавать точки останова и проверять значения локальных переменных.

Компиляция с включением отладочной информации

Чтобы можно было воспользоваться GNU-отладчиком, необходимо скомпилировать программу с включением в нее отладочной информации. Этой цели служит опция -g компилятора. Если имеется описанный выше файл Makefile, достаточно задать переменную CFLAGS равной -g при запуске утилиты make:

g++ -g -c reciprocal.cpp

g++ -g -o reciprocal main.o reciprocal.o

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

Запуск отладчика

Отладчик gdb запускается следующим образом:

После запуска появится строка приглашения такого вида:

В первую очередь необходимо запустить программу под отладчиком. Для этого введите команду run и требуемые аргументы. Попробуем вызвать программу без аргументов:

Starting program: reciprocal

Program received signal SIGSEGV, Segmentation fault.

__strtol_internal (nptr=0x0, endptr=0x0, base=10, group=0)

287 strtol.c: No such file or directory.

Как нетрудно заметить, функция main() вызвала функцию atoi(), передав ей нулевой указатель, что и стало причиной ошибки.

С помощью команды up можно подняться по стеку на два уровня, дойдя до функции main():

8 i = atoi (argv[l]);

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

(gdb) print argv[l]

Это подтверждает нашу догадку о том, что причина ошибки - передача функции atoi() указателя NULL.

Установка контрольной точки осуществляется посредством команды break:

Breakpoint 1 at 0x804862e: file main.c, line 8.

В данном случае контрольная точка размещена в первой строке функции main(). Давайте теперь заново запустим программу, передав ей один аргумент:

Starting program: reciprocal 7

Breakpoint 1, main (argc=2, argv=0xbffff5e4) at main.c:8

8 i = atoi (argv[l]);

Как видите, отладчик остановился на контрольной точке. Перейти на следующую строку можно с помощью команды next:

9 printf ("The reciprocal of %d is %g\n", i,

Если требуется узнать, что происходит внутри функции reciprocal(), воспользуйтесь командой step:

reciprocal (i=7) at reciprocal.cpp:6

Иногда удобнее запускать отладчик gdb непосредственно из редактора Emacs, а не из командной строки. Для этого следует ввести в редакторе команду M-x gdb. Когда отладчик останавливается в контрольной точке, редактор Emacs автоматически открывает соответствующий исходный файл. Не правда ли, проще разобраться в происходящем, глядя на весь файл, а не на одну его строку?

Работа с qmake

Ни один программист в настоящее время уже не желает каждый раз задавать для компиляции своей программы параметры для компоновки и передавать пути к библиотекам "вручную". Гораздо удобнее создать make-файл (Makefile), который возьмет на себя всю работу по настройке компилятора и компоновщика.

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

Полноценный инструментарий для программирования (контейнеры, протоколы ввода-вывода, протоколы сети, XML, 2D и 3D графика, связка с OpenGL) в среде Unix/Linux предоставляет Qt designer.

Предыдущие версии Qt содержали утилиту tmake, которая позволяла довольно просто создавать make-файлы. Она изначально была реализована на языке Perl, и это накладывало на нее дополнительные ограничения - для использования необходимо было наличие интерпретатора языка Perl на компьютере, что, в свою очередь, снижало способность переносимости данной утилиты. По этой причине и была создана новая утилита qmake, которая вошла в поставку Qt начиная с версии 3.0. Примечательно, что новая утилита так же хорошо переносима, как и сам Qt. Утилита qmake полностью освобождает программиста от использования tmake, перенимая все ее возможности. Благодаря этому старые конфигурационные файлы для tmake могут интерпретироваться ей без проблем. Таким образом, тем, кто уже успел привыкнуть к tmake, вовсе не обязательно изучать новый синтаксис.

Программа qmake интерпретирует файлы проектов, которые имеют расширение .pro и содержат различные параметры. Этот файл создается командой

Создать из .pro файла make-файл совсем не трудно, для этого нужно просто дать команду

%qmake -o Makefile file.pro

В qmake существует возможность создания файлов с расширением dsp (Developer Studio Project), которые могут быть загружены в Visual Studio. Это можно сделать при помощи команды:

%qmake -o file.dsp -t vcapp file.pro

Опция -t является так называемой шаблонной опцией и заставляет qmake использовать параметр vcapp, невзирая на то, что написано в самом файле проекта. Шаблоны нужны для определения типа проекта. Например, при создании make-файла для приложения нужно задать в файле проекта опцию template = app, а при создании библиотеки template должен быть равен lib. Значения vcapp и vclib имеют смысл только для работы в OC Windows.

Допускается передавать опции в командной строке qmake:

%qmake -o Makefile "TEMPLATE=vcapp" file.pro

Табл. 2 содержит некоторые опции файла проекта. Полный список опций можно получить в официальной документации Qt, поставляемой вместе с самой библиотекой.

Опция Назначение
HEADERS Передается список созданных заголовочных файлов
SOURCES Передается список созданных файлов реализации (с расширением срр)
FORMS Передается список файлов с расширением .ui. Эти файлы создаются программой QtDesigner и содержат описание интерфейса пользователя в формате XML.
LEXISOURCES Lex - это программа для синтаксического анализа. Используется для написания компиляторов.
YACCSOURCES Yacc (Yet another Compiler Compiler) - утилита создания компиляторов
TARGET Передается имя приложения. Если данное поле не заполнено, то название программы будет соответствовать имени проектного файла
CONFIG Задает опции, которые должен использовать компилятор
DESTDIR Задает путь, куда будет помещен готовый исполняемый модуль
DEFINES Здесь можно передать опции для компилятора. Например, это может быть опция помещения отладочной информации для debuger в исполняемый модуль
INCLUDEPATH Путь к каталогу,где содержатся заголовочные файлы. Этой опцией можно воспользоваться в случае, если уже есть готовые заголовочные файлы и вы хотите использовать их в текущем проекте
DEPENDPATH В данном разделе указываются зависимости, необходимые для компиляции
DEF_FILE Файл определения модуля, указывает, является ли файл DLL библиотекой или EXE файлом. Используется только для OC Windows.
RCFILE Файл ресурсов в OC Windows. Содержит растровые изображения, меню, диалоговые окна и др. Используется только для OC Windows.
RESFILE Откомпилированный файл ресурсов (присоединяется к исполняемому коду программы). Используется только для OC Windows.

Проектный файл Qt может выглядеть следующим образом:

CONFIG += qt warn_on release

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

Когда мы пишем программу на C/C++ в одном файле, проблем обычно не возникает. Они ждут того момента, когда исходный текст необходимо разбить на несколько файлов. В этой статье я постараюсь рассказать, как это сделать правильно.

Т ермины

Пара слов о терминах. Ниже даны определения терминов так, как они используются в данной статье. В некоторых случаях эти определения имеют более узкий смысл, чем общепринятые. Это сделано намеренно, дабы не утонуть в деталях и лишних уточнениях.

Исходный код — программа, написанная на языке программирования, в текстовом формате. А также текстовый файл, содержащий исходный код.

Компилятор — программа, выполняющая компиляцию (неожиданно! не правда ли?). На данный момент среди начинающих наиболее популярными компиляторами C/C++ являются GNU g++ (и его порты под различные ОС) и MS Visual Studio C++ различных версий. Подробнее см. в Википедии статьи: Компиляторы, Компиляторы C++.

Компиляция — преобразование исходного кода в объектный модуль.

Объектный модуль — двоичный файл, который содержит в себе особым образом подготовленный исполняемый код, который может быть объединён с другими объектными файлами при помощи редактора связей (компоновщика) для получения готового исполняемого модуля, либо библиотеки. (подробности)

Исполняемый модуль (исполняемый файл) — файл, который может быть запущен на исполнение процессором под управлением операционной системы. (подробности)

Препроцессор — программа для обработки текста. Может существовать как отдельная программа, так и быть интегрированной в компилятор. В любом случае, входные и выходные данные для препроцессора имеют текстовый формат. Препроцессор преобразует текст в соответствии с директивами препроцессора. Если текст не содержит директив препроцессора, то текст остаётся без изменений. Подробнее см. в Википедии: Препроцессор и Препроцессор Си.

IDE (англ. Integrated Development Environment) — интегрированная среда разработки. Программа (или комплекс программ), предназначенных для упрощения написания исходного кода, отладки, управления проектом, установки параметров компилятора, линкера, отладчика. Важно не путать IDE и компилятор. Как правило, компилятор самодостаточен. В состав IDE компилятор может не входить. С другой стороны с некоторыми IDE могут быть использованы различные компиляторы. (подробности)

Объявление — описание некой сущности: сигнатура функции, определение типа, описание внешней переменной, шаблон и т.п. Объявление уведомляет компилятор о её существовании и свойствах.

Определение — реализация некой сущности: переменная, функция, метод класса и т.п. При обработке определения компилятор генерирует информацию для объектного модуля: исполняемый код, резервирование памяти под переменную и т.д.

От исходного кода к исполняемому модулю

Создание исполняемого файла издавна производилось в три этапа: (1) обработка исходного кода препроцессором, (2) компиляция в объектный код и (3) компоновка объектных модулей, включая модули из объектных библиотек, в исполняемый файл. Это классическая схема для компилируемых языков. (Сейчас уже используются и другие схемы.)

Часто компиляцией программы называют весь процесс преобразования исходного кода в исполняемы модуль. Что неправильно. Обратите внимание, что в IDE этот процесс называется построение (build) проекта.

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

Затем результат работы препроцессора передаётся компилятору. Компилятор производит весь положенный комплекс работ: от синтаксического разбора и поиска ошибок до создания объектного файла (понятно, что если имеются синтаксические ошибки, то объектный файл не создаётся). В объектном файле обычно имеется таблица внешних ссылок — некая таблица, в которой, в частности, перечислены имена подпрограмм, которые используются в объектном модуле, но код которых отсутствует в данном объектном модуле. Эти подпрограммы внешние по отношению к модулю.

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

Библиотека (объектная библиотека) — это набор откомпилированных подпрограмм, собранных в единый файл определённой структуры. Подключение библиотеки происходит на этапе компоновки исполняемого файла из объектных файлов (т.е. из тех файлов, которые получаются в результате компиляции исходного текста программы).

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

Если исходный код программы разделён на несколько файлов, то процесс компиляции и сборки происходит аналогично. Сначала все единицы компиляции по отдельности компилируются, а затем компоновщик собирает полученные объектные модули (с подключением библиотек) в исполняемый файл. Собственно, этот процесс и называется раздельной компиляцией.

Р азделение текста программы на модули

Разделение исходного текста программы на несколько файлов становится необходимым по многим причинам:

  1. С большим текстом просто неудобно работать.
  2. Разделение программы на отдельные модули, которые решают конкретные подзадачи.
  3. Разделение программы на отдельные модули, с целью повторного использования этих модулей в других программах.
  4. Разделение интерфейса и реализации.

Как только мы решаем разделить исходный текст программы на несколько файлов, возникают две проблемы:

  1. Необходимо от простой компиляции программы перейти к раздельной. Для этого надо внести соответствующие изменения либо в последовательность действий при построении приложения вручную, либо внести изменения в командные или make-файлы, автоматизирующие процесс построения, либо внести изменения в проект IDE.
  2. Необходимо решить каким образом разбить текст программы на отдельные файлы.

Первая проблема — чисто техническая. Она решается чтением руководств по компилятору и/или линкеру, утилите make или IDE. В самом худшем случае просто придётся проштудировать все эти руководства. Поэтому на решении этой проблемы мы останавливаться не будем.

Вторая проблема — требует гораздо более творческого подхода. Хотя и здесь существуют определённые рекомендации, несоблюдение которых приводит либо к невозможности собрать проект, либо к трудностям в дальнейшем развитии проекта.

Во-вторых, нужно определить интерфейсы для модулей. Здесь есть вполне чёткие правила.

Интерфейс и реализация

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

Таким образом, модуль состоит из двух файлов: заголовочного (интерфейс) и файла реализации.

Заголовочный файл, как правило, имеет расширение .h или .hpp, а файл реализации — .cpp для программ на C++ и .c, для программ на языке C. (Хотя в STL включаемые файлы вообще без расширений, но, по сути, они являются заголовочными файлами.)

Заголовочный файл должен содержать все объявления, которые должны быть видны снаружи. Объявления, которые не должны быть видны снаружи, делаются в файле реализации.

Ч то может быть в заголовочном файле

Правило 1.

Заголовочный файл может содержать только объявления. Заголовочный файл не должен содержать определения.

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

Аналогичная ситуация и с объявлением переменных-членов класса: код будет порождаться при создании экземпляра этого класса.

Правило 2.

Заголовочный файл должен иметь механизм защиты от повторного включения.

Защита от повторного включения реализуется директивами препроцессора:

Заголовочный файл сам по себе не является единицей компиляции.

Что может быть в файле реализации

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

Правило 3.

В файле реализации должна быть директива включения соответствующего заголовочного файла.

Понятно, что объявления, которые видны снаружи модуля, должны быть также доступны и внутри.

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

Правило 4.

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

При выполнении Правила 3, нарушение Правила 4 приведёт к ошибкам компиляции.

Практический пример

Допустим, у нас имеется следующая программа:

main.cpp

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

Итак, что у нас имеется?

  1. глобальная константа cint , которая используется и в классе, и в main ;
  2. глобальная переменная global_var , которая используется в функциях func1 , func2 и main ;
  3. глобальная переменная module_var , которая используется только в функциях func1 и func2 ;
  4. функции func1 и func2 ;
  5. класс CClass ;
  6. функция main .

Вроде вырисовываются три единицы компиляции: (1) функция main , (2) класс CClass и (3) функции func1 и func2 с глобальной переменной module_var , которая используется только в них.

Не совсем понятно, что делать с глобальной константой cint и глобальной переменной global_var . Первая тяготеет к классу CClass , вторая — к функциям func1 и func2 . Однако предположим, что планируется и эту константу, и эту переменную использовать ещё в каких-то, пока не написанных, модулях программы. Поэтому прибавится ещё одна единица компиляции.

Теперь пробуем разделить программу на модули.

Сначала, как наиболее связанные сущности (используются во многих местах программы), выносим глобальную константу cint и глобальную переменную global_var в отдельную единицу компиляции.

globals.h

globals.cpp

Обратите внимание, что глобальная переменная в заголовочном файле имеет спецификатор extern . При этом получается объявление переменной, а не её определение. Такое описание означает, что где-то существует переменная с таким именем и указанным типом. А определение этой переменной (с инициализацией) помещено в файл реализации. Константа описана в заголовочном файле.

Также обратите внимание (1) на защиту от повторного включения заголовочного файла и (2) на включение заголовочного файла в файле реализации.

Затем выносим в отдельный модуль функции func1 и func2 с глобальной переменной module_var . Получаем ещё два файла:

funcs.h

funcs.cpp

Наконец выносим в отдельный модуль класс CClass :

CClass.h

CClass.cpp

Обратите внимание на следующие моменты.

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

(2) Класс имеет статический член класса. Т.е. для всех экземпляров класса эта переменная будет общей.

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

Классы практически всегда выделяются в отдельные единицы компиляции.

В файле main.cpp оставляем только функцию main . И добавляем необходимые директивы включения заголовочных файлов.

main.cpp

Типичные ошибки

Ошибка 1. Определение в заголовочном файле.

Ошибка 2. Отсутствие защиты от повторного включения заголовочного файла.

Ошибка 3. Несовпадение объявления в заголовочном файле и определения в файле реализации.

Обычно возникает в процессе редактирования исходного кода, когда в файл реализации вносятся изменения, а про заголовочный файл забывают.

Ошибка 5. Отсутствие необходимого модуля в проекте построения программы.

Ошибка 6. Зависимость от порядка включения заголовочных файлов.

Не совсем ошибка, но таких ситуаций следует избегать. Обычно сигнализирует либо об ошибках в проектировании программы, либо об ошибках при разделении исходного кода на модули.

Заключение

В рамках небольшой статьи невозможно рассмотреть все случаи, возникающие при раздельной компиляции. Бывают ситуации, когда разделение программы или большого модуля на более мелкие кажется невозможным. Обычно это бывает, когда программа плохо спроектирована (в данном случае, части кода имеют сильные взаимные связи). Конечно, можно приложить дополнительные усилия и всё-таки разделить код на модули (или оставить как есть), но эту мозговую энергию лучше потратить более эффективно: на изменение структуры программы. Это принесёт в дальнейшем гораздо большие дивиденды, чем просто силовое решение.

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

Я был бы рад, если бы кто-то мог объяснить на человеческом языке.

Объектный файл - это реальный результат на этапе компиляции. Это в основном машинный код, но есть информация, которая позволяет компоновщику видеть, какие символы в нем, а также символы, которые он требует для работы. (Для справки, "символы" - это в основном имена глобальных объектов, функций и т.д.)

Компонент принимает все эти объектные файлы и объединяет их для формирования одного исполняемого файла (при условии, что он может, т.е. нет каких-либо дубликатов или символов undefined). Многие компиляторы сделают это для вас (читайте: они запускают компоновщик самостоятельно), если вы не скажете им "просто скомпилировать", используя параметры командной строки. ( -c является общей опцией "просто компилировать, не ссылаться".)

Объектный файл - это сам скомпилированный файл. Между ними нет никакой разницы.

Исполняемый файл формируется путем связывания файлов Object.

Файл объекта содержит инструкции низкого уровня, которые могут быть поняты ЦП. Вот почему он также называется машинным кодом.

Этот машинный код нижнего уровня представляет собой двоичное представление инструкций, которые вы также можете написать напрямую с использованием языка ассемблера, а затем обработать код языка ассемблера (представлен на английском языке) на машинный язык (представленный в Hex) с помощью ассемблера.

Здесь типичный поток высокого уровня для этого процесса для кода на языке высокого уровня, например C

- > проходит через предварительный процессор

- > , чтобы дать оптимизированный код, еще в C

- > проходит через компилятор

- > , чтобы получить код сборки

- > проходит через ассемблер

- > , чтобы предоставить код в машинном языке, который хранится в объектных файлах

- > проходит через Linker

- > , чтобы получить исполняемый файл.

Этот поток может иметь некоторые варианты, например, большинство компиляторов могут напрямую генерировать код машинного языка, не проходя через ассемблер. Точно так же они могут сделать предварительную обработку для вас. Тем не менее, приятно разбить составляющие для лучшего понимания.

Существует 3 типа объектных файлов.

Перемещаемые объектные файлы

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

Если у вас есть исходный файл a.c , чтобы создать его объектный файл с GCC, вы должны запустить:
gcc a.c -c

Полный процесс: препроцессор (cpp) будет работать через a.c. Его выход (все еще источник) будет передаваться в компилятор (cc1). Его выход (сборка) будет подаваться на ассемблер (как), который будет генерировать relocatable object file . Этот файл содержит объектный код и связывает (и может отлаживать, если был использован -g ) метаданные, и не является непосредственно исполняемым.

Общие файлы объектов

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

Файлы исполняемых файлов

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

Результатом запуска компоновщика над несколькими relocatable object files является executable object file . Компилятор объединяет все входные объектные файлы из командной строки слева направо, объединяя все секции ввода того же типа (например, .data ) с разделом вывода того же типа. Он использует symbol resolution и relocation .

Бонус:

При связывании с static library функции, на которые ссылаются входные объекты, копируются в окончательный исполняемый файл.
С помощью dynamic libraries вместо этого создается таблица символов, которая позволяет динамически связываться с библиотечными функциями/глобальными. Таким образом, результатом является частично исполняемый объектный файл, так как он зависит от библиотеки. (простыми словами, если библиотека ушла, файл больше не может выполняться).

Процесс связывания может быть выполнен следующим образом:
ld a.o -o myexecutable

Команда: gcc a.c -o myexecutable будет вызывать все команды, упомянутые в точке 1 и в точке 3 (cpp -> cc1 -> as -> ld 1 )

Начало работы с многофайловой компиляцией Linux и make-файлом

Каталог статей


В процессе программирования для нас невозможно поместить все в файл c, что значительно снизит нашу эффективность.Мы можем написать свои собственные файлы c в соответствии с различными функциями и, наконец, вызвать основную программу. Когда я впервые познакомился с vim, я знал только, что gcc можно использовать для компиляции и генерации исполняемых программ.Я всегда думал, что gcc является компилятором, но это не так.


GCC(Коллекция компиляторов GNU, Коллекция компиляторов GNU) - переводчик языков программирования, разработанный GNU. Набор компиляторов GNU включает в себя внешние интерфейсы языков C, C ++, Objective-C, Fortran, Java, Ada и Go, а также библиотеки для этих языков (например, libstdc ++, libgcj и т. Д.).
Хотя мы называем GCC компилятором языка C, процесс использования gcc для создания исполняемого файла из файла исходного кода языка C - это не просто процесс компиляции, а четыре взаимных Связанные шаги: предварительная обработка (также называемая предварительной обработкой, предварительной обработкой), компиляция, сборка и компоновка.

1. Предварительная обработка

2. Скомпилировать

Составление (составление) - это прохождение лексического и грамматического анализа. Убедившись, что все инструкции соответствуют грамматическим правилам, Переведите его в эквивалентное промежуточное представление кода или ассемблерный код. Оптимизационная обработка - относительно сложная технология в системе компиляции. Проблемы, связанные с этим, связаны не только с самой технологией компиляции, но также имеют большое отношение к аппаратной среде машины. Часть оптимизации - это оптимизация промежуточного кода, эта оптимизация не зависит от конкретного компьютера. Другая оптимизация в основном направлена на генерацию целевого кода. Для первой оптимизации основная работа заключается в удалении общих выражений, оптимизации цикла (аутсорсинг кода, ослабление силы, изменение условий управления циклом, слияние известных величин и т. Д.), Распространение копий, удаление бесполезных назначений и т. Д. Последний тип оптимизации тесно связан со структурой аппаратного обеспечения машины.Самое важное соображение состоит в том, как в полной мере использовать значения переменных, хранящихся в аппаратных регистрах машины, для уменьшения количества обращений к памяти. Кроме того, как выполнять инструкции в соответствии с характеристиками аппаратного обеспечения машины (например, конвейер
line, RISC, CISC и т. д.), а также некоторые корректировки инструкций, чтобы сделать целевой код короче и повысить эффективность выполнения, что также является важной темой исследования.
Обычно используйте команду gcc -S hello.i для компиляции и создания файлов сборки, или вы можете выполнить компиляцию с помощью указанного компилятора. Когда вы используете компилятор ПК для компиляции, он создает файлы сборки x86. Компиляция компилятора будет генерировать файлы сборки ARM. Файлы сборки каждой архитектуры могут использоваться только в этой архитектуре. Мы можем выбирать разные компиляторы в соответствии с нашей архитектурой. Этопереносимость. Процесс использования кросс-компилятора ARM на ПК для создания исполняемых программ на платформе ARM называетсяКросс-компиляция?

3. Сборка

Процесс сборки (сборки) фактически относится к Процесс перевода кода на ассемблере в целевые машинные инструкции. Для каждой исходной программы на языке C, обработанной системой перевода, соответствующий целевой файл будет окончательно обработан посредством этого процесса. В целевом файле хранится машинный код целевой программы, эквивалентной исходной программе. Целевой файл состоит из сегментов. Обычно объектный файл состоит как минимум из двух разделов:
1. Сегмент кода (текстовый сегмент): этот сегмент содержит в основном инструкции программы. Этот раздел обычно доступен для чтения и выполнения, но не для записи;
2. Сегмент данных: в основном хранит различные константы, глобальные переменные и статические данные, используемые в программе. Сегменты общих данных доступны для чтения, записи и выполнения;
Мы можем создать файлы сборки с помощью команды gcc -c hello.s.

4. Ссылка

Объектный файл, созданный ассемблером, не может быть запущен сразу, и может быть много нерешенных проблем. Например, функция в исходном файле может ссылаться на символ, определенный в другом исходном файле (например, на переменную или вызов функции и т. Д.); Функция в файле библиотеки может быть вызвана в программе и так далее. Все эти проблемы могут быть решены только обработкой программы ссылки. Основная задача компоновщика - соединить связанные объектные файлы друг с другом, то есть соединить символы, на которые есть ссылки в одном файле, с определением символа в другом файле, чтобы все эти объектные файлы стали такими, которые могут быть установлены операционной системой. Введите единое целое исполнения, то есть исполняемую программу. В соответствии с методом связывания библиотечной функции, указанным разработчиком, обработку ссылок можно разделить на два типа:
1. Статическая ссылка: все функции компилируются в программу во время компиляции.
2. Динамическая ссылка: операционная система помогает переносить динамическую библиотеку в область памяти при запуске программы.
Для вызовов функций в исполняемых файлах можно использовать динамическое связывание или статическое связывание соответственно. Использование динамического связывания может сделать конечный исполняемый файл короче и сэкономить некоторую память, когда общий объект используется несколькими процессами, потому что в памяти должна храниться только одна копия кода общего объекта. Но дело не в том, что использование динамического связывания обязательно лучше статического связывания. В некоторых случаях динамическое связывание может привести к некоторому снижению производительности.

5. Динамические и статические библиотеки

6. Общие параметры компиляции gcc

Параметры Описание
-E Только предварительная обработка, а не компиляция
-S Только компилировать, а не собирать
-c Только компилировать, собирать, а не связывать
-g Скомпилированный исполняемый файл содержит отладочную информацию gdb, которая может быть отлажена с помощью gdb.
-o Укажите имя файла скомпилированного исполняемого файла
-l Укажите каталог поиска для включаемых файлов
-L Укажите путь к библиотеке (динамической или статической), необходимой для компоновки
-ansi Стандарт ANSI
-std=c99 Стандарт C99
-Werror Не различать предупреждения и ошибки и прекращать компиляцию при обнаружении любого предупреждения
-Wall Включите большинство предупреждений
–static Статическая компиляция (по умолчанию - динамическая компиляция)
-static Статическая ссылка (по умолчанию статическая ссылка)

1. Запись в несколько файлов

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

(1)Функция функция .c файл

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

(2)Файл заголовка функции

(3)Записан основной функциональный файл

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

2. Скомпилируйте несколько файлов отдельно

Наша многофайловая компиляция не может напрямую использовать gcc для компиляции каждого файла, но сначала генерирует файлы сборки с помощью gcc -c (только компилировать, собирать, а не связывать), а затем генерировать исполняемые файлы с помощью gcc вместе со всеми файлами сборки. Наконец, удалите файлы .o в этом процессе.


3. Компиляция сценария оболочки

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


4. makefile

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



С помощью make-файла наша рабочая нагрузка может быть значительно уменьшена. Если в приведенном выше содержании есть какие-либо ошибки, пожалуйста, исправьте меня!

Читайте также: