Что такое объектный файл
Я понимаю, основы компиляции. Исходные файлы, скомпилированные в объектные файлы, которые компоновщик затем связывает в исполняемые файлы. Эти объектные файлы состоят из исходных файлов, содержащих определения.
Итак, мои вопросы:
- почему у нас есть отдельная реализация для библиотеки? .ля. движение за освобождение, .файл DLL.
- Я, наверное, ошибаюсь, но мне кажется .o сами файлы вроде то же самое, что библиотеки?
- не мог кто-нибудь дать вам их .о реализации определенного декларация.( h) и вы можете заменить это и связать его с стать исполняемым файлом, выполняющим те же функции, но использующим разные операции?
исторически объектный файл связывается либо полностью, либо вообще не в исполняемый файл (в настоящее время существуют исключения, такие как связывание уровней функций или оптимизация всей программы становится более популярным), поэтому, если используется одна функция объектного файла, исполняемый файл получает их все.
чтобы сохранить исполняемые файлы небольшими и свободными от мертвого кода, стандартная библиотека разделена на множество небольших объектных файлов (обычно в порядке сотни.) Наличие сотен небольших файлов очень нежелательно по причинам эффективности: открытие многих файлов неэффективно, и каждый файл имеет некоторый провис (неиспользуемое дисковое пространство в конце файла). Вот почему объектные файлы группируются в библиотеки, что похоже на ZIP-файл без сжатия. Во время ссылки читается вся библиотека и все объектные файлы из этой библиотеки, которые разрешают символы, уже известные как неразрешенные, когда компоновщик начал читать библиотеку или необходимые объектные файлы по ним включаются в выход. Это, вероятно, означает, что вся библиотека должна быть в памяти сразу, чтобы рекурсивно решать зависимости. Поскольку объем памяти был довольно ограничен, компоновщик загружает только одну библиотеку за раз, поэтому библиотека, упомянутая позже в командной строке компоновщика, не может использовать функции из библиотеки, упомянутой ранее в командной строке.
для повышения производительности (загрузка всей библиотеки занимает некоторое время, особенно с медленных носителей, таких как дискета диски), библиотеки часто содержат индекс это сообщает компоновщику, какие объектные файлы предоставляют какие символы. Индексы создаются такими инструментами, как ranlib или инструмент управления библиотекой (Borland's tlib имеет переключатель для создания индекса). Как только есть индекс, библиотеки определенно более эффективны для связи, чем отдельные объектные файлы, даже если все объектные файлы находятся в кэше диска и загрузка файлов из кэша диска бесплатна.
вы совершенно правы что я могу заменить .o или .a файлы при сохранении файлов заголовков и изменении того, что делают функции (или как они это делают). Это используется LPGL-license , для чего требуется автор программы, которая использует LGPL-licensed библиотека, чтобы дать пользователю возможность заменить эту библиотеку исправленной, улучшенной или альтернативной реализацией. Доставка объектных файлов собственного приложения (возможно, сгруппированных как файлы библиотеки) достаточно, чтобы дать пользователю необходимую свободу; нет необходимости грузить исходный код (например, с GPL ).
если два набора библиотек (или объектных файлов) могут быть успешно использованы с теми же файлами заголовков, они называются совместимость с ABI, где ABI означает Двоичный Интерфейс Приложения. Это более узко, чем просто иметь два набора библиотек (или объектных файлов), сопровождаемых их соответствующими заголовками, и гарантировать, что вы можете использовать каждую библиотеку, если вы используете заголовки для этой конкретной библиотеки. Это называться совместимость API, где API означает Приложения, Программы. В качестве примера разницы посмотрите на следующие три файла заголовка:
первые два файла не идентичны, но они предоставляют сменные определения, которые (насколько я ожидаю) не нарушают "правило одного определения", поэтому библиотека, предоставляющая файл 1 Как файл заголовка может использоваться также с файлом 2 в качестве файла заголовка. С другой стороны, файл 3 предоставляет очень похожий интерфейс программисту (который может быть идентичен во всем, что автор библиотеки обещает пользователю библиотеки), но код, скомпилированный с файлом 3, не связывается с библиотекой, предназначенной для использования с файлом 1 или файлом 2, поскольку библиотека, предназначенная для файла 3, не будет экспортировать calculate , но только do_calculate . Кроме того, структура имеет другой макет элемента, поэтому с помощью файла 1 или файла 2 вместо файла 3 не будет доступа к b правильно. Библиотеки, предоставляющие файл 1 и файл 2, совместимы с ABI, но все три библиотеки совместимы с API (при условии, что c и более способная функция do_calculate не учитывайте этот API).
для динамических библиотек (.dll, .Итак) вещи совершенно разные: они начали появляться в системах, где несколько (прикладных) программ могут быть загружены одновременно (что не относится к DOS, но это относится к Windows). Это расточительно иметь одну и ту же реализацию библиотечной функции в памяти несколько раз, поэтому загрузка ее только один раз в память имеет другое применение, оно сохраняет память. Для динамических библиотек код ссылочной функции не включается в исполняемый файл, а включается только ссылка на функцию внутри динамической библиотеки (для Windows NE/PE указывается, какая DLL должна предоставлять какую функцию; для Unix .таким образом, файлы, только имена функций и набор библиотек указанный.) Операционная система содержит погрузчик ака динамический линкер это разрешает эти ссылки и загружает динамические библиотеки, если они еще не находятся в памяти во время запуска программы.
Хорошо, давайте начнем с начала.
программист (вы) создает некоторые исходные файлы .cpp и .h . Разница между этими двумя файлами - это просто соглашение:
- .cpp предназначены для компиляции
- .h предназначены для включения в другие исходные файлы
но ничто (кроме страха, что unmaintanable вещь) запрещает импорт cpp файлы в другое .cpp файлы.
в C++ с шаблонами, вы также должны добавить .h реализация классов шаблонов, поскольку C++ использует шаблоны, а не дженерики, такие как Java, каждый экземпляр шаблона отличается класс.
теперь с ответом на ваш вопрос:
каждого .cpp файл является единицей компиляции. Компилятор будет:
этот формат объекта содержит :
- перемещаемый код (то есть адреса в коде или переменные родственники для экспортируемых символов)
- экспортировать символы: символы, которые могут быть использованы из других единиц компиляции (функции, классы, глобальные переменные)
- импортированные символы: символы, используемые в этой единице компиляции и определенные в других единицах компиляции
затем (давайте пока забудем о библиотеках) компоновщик возьмет все единицы компиляции вместе и разрешит символы для создания исполняемого файла файл.
еще один шаг со статическими библиотеками.
статическая библиотека (обычно .a или .lib ) - это более или менее куча объектных файлов, собранных вместе. Он существует, чтобы не перечислять отдельно каждый файл объекта, который вам нужен, те, из которых вы используете экспортированные символы. Связывание библиотеки, содержащей используемые объектные файлы, и связывание самих объектных файлов-это одно и то же. Просто добавив -lc , -lm или -lx11 короче их добавлять сто .o файлы. Но, по крайней мере в Unix-подобных системах, статическая библиотека-это архив, и вы можете извлечь отдельные объектные файлы, если вы хотите.
динамические библиотеки совершенно разные. Динамическую библиотеку следует рассматривать как специальный исполняемый файл. Как правило, они построены с тем же компоновщиком, который создает обычные исполняемые файлы (но с разными параметрами). Но вместо того, чтобы просто объявлять точку входа (в windows a .dll файл объявляет запись точка, которую можно использовать для инициализации .dll ), они объявляют список экспортированных (и импортированных) символов. Во время выполнения есть системные вызовы, которые позволяют получить адреса этих символов и использовать их почти нормально. Но на самом деле, когда вы вызываете процедуру в динамической загруженной библиотеке, код находится вне того, что загрузчик изначально загружает из вашего собственного исполняемого файла. Как правило, операция загрузки всех используемых символов из динамической библиотеки выполняется непосредственно во время загрузки загрузчик (в Unix подобных системах) или с библиотеками импорта в Windows.
- во-первых, объявите функции или классы-сделано путем включения .h файл из вашего источника, чтобы компилятор знал, что они такое
- далее свяжите модуль объекта, статическую библиотеку или динамическую библиотеку, чтобы фактически получить доступ к коду
объектные файлы содержат определения функций, статические переменные, используемые этими функциями, и другую информацию, выводимую компилятором. Это в форме, которая может быть связана компоновщиком (например, точки связывания, где функции вызываются с точками входа функции).
файлы библиотеки обычно упаковываются, чтобы содержать один или несколько объектных файлов (и, следовательно, всю информацию в них). Это дает преимущества, которые легче распределить по одному библиотека, чем куча объектных файлов (например, при распространении скомпилированных объектов другому разработчику для использования в своих программах), а также упрощает связывание (компоновщик должен быть направлен на доступ к меньшему количеству файлов, что упрощает создание скриптов для связывания). Кроме того, как правило, есть небольшие преимущества производительности для компоновщика-открытие одного большого файла библиотеки и интерпретация его содержимого более эффективна, чем открытие и интерпретация содержимого множества небольших объектных файлов, особенно, если компоновщику нужно сделать несколько проходов через них. Есть также небольшие преимущества, которые, в зависимости от того, как жесткие диски отформатированы и управляются, что несколько больших файлов потребляет меньше места на диске, чем много меньших.
часто стоит упаковывать объектные файлы в библиотеки, потому что это операция, которая может быть выполнена один раз, и преимущества реализуются многократно (каждый раз, когда библиотека используется компоновщиком для создания выполнимый.)
поскольку люди лучше понимают исходный код и, следовательно, имеют больше шансов на его правильную работу, когда он состоит из небольших кусков, большинство крупных проектов состоят из значительного количества (относительно) небольших исходных файлов, которые компилируются в объекты. Сборка объектных файлов в библиотеки - за один шаг-дает все преимущества, о которых я упоминал выше, позволяя людям управлять своим исходным кодом таким образом, который имеет смысл для людей, а не компоновщики.
тем не менее, это выбор разработчика для использования библиотек. Компоновщику все равно, и для настройки библиотеки и ее использования может потребоваться больше усилий, чем для связывания множества объектных файлов. Таким образом, ничто не мешает разработчику использовать сочетание объектных файлов и библиотек (за исключением очевидной необходимости избегать дублирования функций и других вещей в нескольких объектах или библиотеках, что приводит к сбою процесса связи). Это, в конце концов, работа разработчика разработать стратегию управления созданием и распространением своего программного обеспечения.
на самом деле (по крайней мере) существует два типа библиотеки.
статически связанные библиотеки используются компоновщиком для создания исполняемого файла, а скомпилированный из них код копируется компоновщиком в исполняемый файл. Примеры .lib файлы под windows и .а файлы под unix. Сами библиотеки (как правило) не должны распространяться отдельно с исполняемым файлом программы, поскольку необходимые части находятся в исполняемом файле.
программы могут быть разработаны, чтобы использовать сочетание статических и динамических библиотек, а также - опять-таки на усмотрение разработчика. Статическая библиотека также может быть связана с программой и заботиться обо всех книгах, связанных с использованием динамически загружаемой библиотеки.
В данной статье я хочу рассказать о том, как происходит компиляция программ, написанных на языке C++, и описать каждый этап компиляции. Я не преследую цель рассказать обо всем подробно в деталях, а только дать общее видение. Также данная статья — это необходимое введение перед следующей статьей про статические и динамические библиотеки, так как процесс компиляции крайне важен для понимания перед дальнейшим повествованием о библиотеках.
Все действия будут производиться на Ubuntu версии 16.04.
Используя компилятор g++ версии:
Определения символов
Символы, с точки зрения компоновщика, это и переменные, и процедуры. Нет никакой разницы между именем процедуры и именем переменной или другого агрегата данных, важны лишь размер и адрес размещения. На уровне компоновщика, подчеркиваю еще раз. Все дополнительные синтаксические и семантические смыслы остаются на уровне компилятора.
И это является очень наглядной демонстрацией семантического разрыва между языком высокого уровня и машиной. При этом компоновщик работает немного выше уровня машины, но гораздо ниже уровня языка высокого уровня.
Каждый символ размещается, еще компилятором, в одной из программных секций. Адрес размещения символа отсчитывается от начала программной секции внутри данного конкретного объектного файла. Причем символ должен именно определяться, размещаться, а не просто описываться в исходном файле, который и был преобразован компилятором в объектный.
И вот тут возникает несколько интересных моментов. Во первых, какие именно символы должны попадать в объектный файл в виде определений?
Локальные переменные, которые размещаются в стеке, не требуют внимания со стороны компоновщика. С их обработкой отлично справляется компилятор, поэтому дополнительная информация в объектном файле о таких переменных отсутствует. Параметры передаваемые функции, как и возвращаемое значение, тоже размещаются в стеке. И тоже не требуют внимания компоновщика.
Глобальные переменные, которые в языке С определяются вне функций, доступны из других объектных файлов. А значит, информация о них должна быть в объектном файле. И компоновщик будет этой информацией пользоваться.
А вот что можно сказать о переменных с модификатором static? Нужна ли информация о них в объектном файле? Если нужна, то о каких именно, определенных только вне функций, или об определенных и внутри функций? Ведь эти переменные из других объектных файлов не доступны.
Нужна, обязательно нужна! Вспомните, что компилятор не знает, как и по каким адресам будут размещаться программные секции, а переменные static, не смотря на ограничения области видимости, должны существовать все время выполнения программы. Они размещаются не в стеке, не относительно указателя стека, а в области данных программы, в одной из секций данных. И в момент компиляции полный адрес такой переменной неизвестен. Уточнить этот адрес может только компоновщик.
Попадают в объектный в виде записей определения символов и все процедуры, не только глобальные, но и локальные. Исключением могут являться локальные метки, переходы на которые выполняются только командами относительного перехода.
Во вторых, возникает вопрос с формой определения сложных агрегатов данных. С простой переменной проблем нет, а что делать с массивами, структурами, объединениями? Давайте немного подробнее рассмотрим этот вопрос.
Итак, компилятор размещает агрегат данных задав его начальный адрес (относительно начального адреса программной секции) и указав размер. Но агрегат имеет и внутреннюю структуру. Например, у структуры есть поля
которые и сами могут быть агрегатами данных
person.name[20]
Как поступать в подобных случаях? Ведь обращение к person.age требует обращения по адресу равному адресу размещения person плюс смещение до поля age внутри person. А если person определена в другом файле, то даже начальный адрес внутри программной секции будет неизвестен.
Здесь возможны различные варианты, но основных два. Первый вариант, компилятор размещает в программной секции не структуру, а отдельные поля структуры, как независимые переменные. Особых сложностей при этом не возникает, все равно с точки зрения машины семантической сущности "структура" не существует. Равно как и сущности, например, "объект класса". Машина, процессор, оперирует лишь с ячейками памяти.
Да, в природе существуют машины с более высокими уровнями абстракции, когда единицей информации может быть не ячейка памяти, а структурированная область памяти. Но эти машины являются экзотическими (во всяком случае, для большинства читателей) и мы их не рассматриваем.
Таким образом, вся высокоуровневая семантика остается на уровне компилятора, а компоновщик работает с обычными переменными в ячейках памяти.
Второй вариант, когда агрегат данных рассматривается как специальная виртуальная программная секция. Такая секция не размещается компоновщиком в памяти, она размещается компилятором внутри одной из реальных программных секций. А для компоновщика появляются два вида адресов: адреса обычные и адреса относительные. Первые относятся к адресам в программных секциях, а вторые к адресам в виртуальных программных секциях.
Это более гибкий способ, но он порождает новые проблемы. Поскольку, например, структура может включать в себя другие структуры в виде полей, то глубина вложенности может оказаться большой. А компоновщику надо будет с этой вложенностью разбираться. При том, для большинства процессоров такая детализация просто излишня.
Поэтому я буду рассматривать лишь вариант размещения полей структур как отдельных переменных. Для нас это будет достаточным. Теперь мы модем составить список основных атрибутов символов:
3) Ассемблирование
Так как x86 процессоры исполняют команды на бинарном коде, необходимо перевести ассемблерный код в машинный с помощью ассемблера.
Ассемблер преобразовывает ассемблерный код в машинный код, сохраняя его в объектном файле.
Объектный файл — это созданный ассемблером промежуточный файл, хранящий кусок машинного кода. Этот кусок машинного кода, который еще не был связан вместе с другими кусками машинного кода в конечную выполняемую программу, называется объектным кодом.
Далее возможно сохранение данного объектного кода в статические библиотеки для того, чтобы не компилировать данный код снова.
Получим машинный код с помощью ассемблера (as) в выходной объектный файл driver.o:
Но на данном шаге еще ничего не закончено, ведь объектных файлов может быть много и нужно их всех соединить в единый исполняемый файл с помощью компоновщика (линкера). Поэтому мы переходим к следующей стадии.
Объектный файл
Документация по объектному файлу хорошо объясняет его содержимое и формат:
Объектный файл (объектный модуль, object file) состоит из зависимостей, отладочной информации (DWARF), списка проиндексированных символов, раздела данных и, наконец, списка символов, в котором можно найти релокации. Вот его формат:
Каждый символ начинается с байта fe в шестнадцатеричном формате. Итак, давайте откроем наш объектный файл main.o с помощью шестнадцатеричного редактора, например xxd на Mac. Вот часть содержимого с выделенными символами:
Символ main.main - это первый символ в списке:
Первые байты 0102 00dc 0100 dc01 0a представляют первые атрибуты, охарактеризованные в определении: тип (type), флаг (flag), размер (size), данные (data), и количество релокаций.
Байты хранятся в формате zigzag (формат переменной длины varint). zigzag кодирует беззнаковые целые числа, используя младший бит для знака, делая их меньше по размеру.
Таким образом, релокация Println представляет собой последовательность байтов b201 0810 0008 :
b201 - это закодированное значение смещения (offset) - 89 . Это смещение является int32 , а благодаря формату varint оно может уместиться в двух байтах.
08 - количество байтов для перезаписи. Декодированное значение 4.
10 - это тип релокации, закодированное значение 8 представляет R_CALL , релокацию вызова функции.
08 - это ссылка на индексированные символы.
Загрузчик теперь имеет всю информацию, необходимую для выполнения релокаций и создания исполняемого бинарника.
исполняемый объект файлы
они содержат машинный код, который может быть непосредственно загружен в память (загрузчиком, e.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 )
1: на самом деле это collect2, который является оболочкой над ld.
объектный файл-это то, что вы получаете при компиляции одного (или нескольких) исходных файлов.
Это может быть либо полностью завершенный исполняемый файл, либо библиотека, либо промежуточные файлы.
объектные файлы обычно содержат собственный код, информацию компоновщика, отладочные символы и т. д.
объектный код-это коды, которые зависят от функций, символов, текста для запуска машины. Просто как старый телекс машин которые требовали teletyping посылают сигналы другим телекс машина. Таким же образом процессор требует двоичного кода для запуска машины. Объектный файл похож на двоичный код, но не связан. Связывание создает дополнительные файлы, так что пользователь не должен иметь компилятор языка Си. Пользователь может напрямую открыть exe-файл, как только объектный файл связан с некоторым компилятором, таким как c язык, или vb etc.
Сегодня мы рассмотрим структуру абстрактного объектного файла. То есть, фактически результат работы компилятора. Именно эта информация и попадает на вход компоновщика.
Рассматривать будем некий абстрактный формат для не менее абстрактной машины. По той простой причине, что кроме привычных всем x86 процессоров существует и множество других архитектур, включая великое множество микроконтроллеров. Да и компиляторы бывают разные, для не менее разных языков высокого уровня.
Еще раз отмечу, что в данном цикле статей рассматриваются, причем очень упрощенно, лишь самые общие моменты связанные с объектными файлами и работой компоновщика.
не является строго обязательной к прочтению, но все таки рекомендуется с ней познакомится, если вы этого еще не сделали.
Объектный файл состоит из записей различного типа и различной длины. В общем и целом, можно выделить такие типы записей:
- Определения. Сюда относятся определения программных секций, символов, и прочих сущностей.
- Машинный код. Это то, во что превратилась программа на языке высокого уровня.
- Корректировки и перемещения. Это инструкции для компоновщика по изменению машинного кода и работе с определенными в других файлах и библиотеках сущностями.
Именно записи этих типов мы сегодня будем рассматривать. Все прочие типы, например, информация для отладчика, номера строк, различная текстовая и служебная информация, сегодня остаются "за кадром".
2) Компиляция
На данном шаге g++ выполняет свою главную задачу — компилирует, то есть преобразует полученный на прошлом шаге код без директив в ассемблерный код. Это промежуточный шаг между высокоуровневым языком и машинным (бинарным) кодом.
Ассемблерный код — это доступное для понимания человеком представление машинного кода.
Используя флаг -S, который сообщает компилятору остановиться после стадии компиляции, получим ассемблерный код в выходном файле driver.s:
Мы можем все также посмотреть и прочесть полученный результат. Но для того, чтобы машина поняла наш код, требуется преобразовать его в машинный код, который мы и получим на следующем шаге.
Заключение
В данной статье были рассмотрены основы процесса компиляции, понимание которых будет довольно полезно каждому начинающему программисту. В скором времени будет опубликована вторая статья про статические и динамические библиотеки.
Релокация — это этап процесса линковки, в рамках которого каждому внешнему символу присваиваются соответствующие адреса. Поскольку пакеты компилируются отдельно, они не имеют понятия, где функции или переменные из других пакетов находятся фактически. Начнем с тривиального примера, когда нам потребуется релокация.
Этапы компиляции:
Перед тем, как приступать, давайте создадим исходный .cpp файл, с которым и будем работать в дальнейшем.
driver.cpp:
Определения программных секций
В предыдущей статье я рассказывал, что программные секции используются компилятором для размещения кода и данных. Ни одна процедура, ни одна переменная, не могут быть размещены вне программной секции.
Программная секция имеет несколько атрибутов:
- Имя секции . Фактически, это произвольный текст с ограничением на длину. Практически, компиляторы используют свои внутренние соглашения для именования секций. Например, секции с машинным кодом могут именоваться "TEXT" или "имя_функции_CODE". Зачастую программист может определить и свои собственные секции с любым именем.
- Тип данных . Секция, как область памяти, могут содержать информацию разного типа. Наиболее известное деление это код и данные. Со строгой точки зрения невозможно прочитать или модифицировать информацию в секции кода при выполнении программы. И невозможно передать управление в секцию данных. Дополнительно можно определить секцию констант, которая отличается от секции данных тем, что запись в нее не возможна. А для микроконтроллеров может иметь смысл выделить секцию данных для энергонезависимой памяти.
- Тип размещения и объединения . Одноименные секции из разных файлов можно размещать последовательно, друг за другом. Общий размер итоговой секции будет равен сумме размеров одноименных секций из всех файлов. А можно размещать с одного и того же адреса, накладывая одну на другую. И размер итоговой секции будет равен размеру наибольшей секции из всех файлов. При этом размещение объектов в секциях обычно идет в направлении увеличения адресов. Но есть еще и стек, в котором объекты размещаются в направлении уменьшения адресов.
- Адрес размещения секции . Если необходимо разместить секцию по строго определенному адресу в памяти машины, то используют специальный тип размещения - абсолютный. Для других типов размещения этот атрибут заполняется не компилятором, а компоновщиков. Мы рассмотрим это в следующей статье.
- Размер секции . Имеется ввиду размер секции в данном объектном файле. Этот размер вычисляется компилятором.
Теперь мы можем представить запись объектного файла о программной секции примерно в таком виде:
У программных секция могут быть и другие атрибуты, например, выравнивание по границам слов или двойных слов. Я не буду в данном цикле статей рассматривать эти атрибуты.
5) Загрузка
Последний этап, который предстоит пройти нашей программе — вызвать загрузчик для загрузки нашей программы в память. На данной стадии также возможна подгрузка динамических библиотек.
Запустим нашу программу:
4) Компоновка
Компоновщик (линкер) связывает все объектные файлы и статические библиотеки в единый исполняемый файл, который мы и сможем запустить в дальнейшем. Для того, чтобы понять как происходит связка, следует рассказать о таблице символов.
Таблица символов — это структура данных, создаваемая самим компилятором и хранящаяся в самих объектных файлах. Таблица символов хранит имена переменных, функций, классов, объектов и т.д., где каждому идентификатору (символу) соотносится его тип, область видимости. Также таблица символов хранит адреса ссылок на данные и процедуры в других объектных файлах.
Именно с помощью таблицы символов и хранящихся в них ссылок линкер будет способен в дальнейшем построить связи между данными среди множества других объектных файлов и создать единый исполняемый файл из них.
Получим исполняемый файл driver:
перемещаемые объектные файлы
содержит машинный код в форме, которая может быть объединена с другими перемещаемыми объектными файлами во время ссылки, чтобы сформировать исполняемый объектный файл.
если у вас a.c исходный файл, чтобы создать его объектный файл с помощью GCC, вы должны запустить: gcc a.c -c
полный процесс будет: препроцессор (cpp) будет работать над a.c. Его выход (все еще источник) будет подаваться в компилятор (СС1). Его выход (сборка) будет подаваться в ассемблер (as), который будет производить relocatable object file . Этот файл содержит объектный код и ссылку (и может отлаживать, если -g был использован) метаданные, и не является непосредственно исполняемым.
Зачем нужно компилировать исходные файлы?
Исходный C++ файл — это всего лишь код, но его невозможно запустить как программу или использовать как библиотеку. Поэтому каждый исходный файл требуется скомпилировать в исполняемый файл, динамическую или статическую библиотеки (данные библиотеки будут рассмотрены в следующей статье).
Релокация
Это этап, на котором линкер назначает виртуальные адреса всем разделам и инструкциям. Адреса каждого раздела можно увидеть с помощью команды objdump -h my-binary . Вот вывод для предыдущего примера:
Функция main находится в разделе __text . Его также можно найти с помощью команды objdump -d my-binary , которая отображает инструкцию с адресами:
Функции main назначен адрес 109cfa0 . Функция fmt.Println получила адрес 1096a00 . Как только виртуальные адреса назначены, совершить релокацию вызова fmt.Println становится легко. Линкер просто вычислит адрес fmt.Println из адреса main , смещения и размера инструкции, и мы получим глобальное смещение для вызова инструкции. В предыдущем примере мы получили бы следующую операцию: 1096a00 (fmt.Println) - 109cfa0 (main) - 84 (смещение внутри main) - 4 (размер) = -26109 .
Теперь инструкция знает, что функция fmt.Println расположена по смещению -26109 от текущего адреса памяти, и вызов будет успешным.
Я читаю о библиотеках в C, но я еще не нашел объяснения о том, что такое объектный файл. В чем реальная разница между любым другим скомпилированным файлом и объектным файлом?
Я был бы рад, если бы кто-нибудь объяснил на человеческом языке.
объектный файл является реальным выходом из фазы компиляции. Это в основном машинный код, но есть информация, которая позволяет компоновщику видеть, какие символы в нем, а также символы, необходимые для работы. (Для справки, "символы" в основном имена глобальных объектов, функций и т. д.)
компоновщик берет все эти объектные файлы и объединяет их в один исполняемый файл (предполагая, что он может, т. е. что нет дубликатов или неопределенных символов). Большое компиляторы сделайте это для вас (читайте: они запускают компоновщик самостоятельно), если вы не скажете им "просто скомпилировать", используя параметры командной строки. ( -c является общей опцией" просто компилировать; не связывать".)
объектный файл-это сам скомпилированный файл. Между ними нет никакой разницы.
исполняемый файл формируется путем связывания объектные файлы.
объектный файл содержит инструкции низкого уровня, которые могут быть поняты процессором. Вот почему он также называется машинным кодом.
этот низкоуровневый машинный код является двоичным представлением инструкций, которые вы также можете написать непосредственно используя язык ассемблера, а затем обработайте код языка ассемблера (представленный на английском языке) на машинный язык (представленный на Hex) с помощью ассемблера.
вот типичный поток высокого уровня для этого процесса для кода на языке высокого уровня, таком как C
--> проходит через pre-processor
--> чтобы дать оптимизированный код, все еще в C
--> проходит через компилятор
--> для того чтобы дать агрегат код
--> проходит через ассемблер
--> дать код на машинном языке, который хранится в объектных файлах
--> проходит через Linker
--> чтобы получить исполняемый файл.
этот поток может иметь некоторые варианты, например, большинство компиляторов могут непосредственно генерировать код машинного языка, не проходя через ассемблер. Точно так же они могут сделать предварительную обработку для вас. Тем не менее, приятно разбить избирателей на лучшее понимание.
есть 3 вида объектных файлов.
Состав компилятора g++
Мы не будем вызывать данные компоненты напрямую, так как для того, чтобы работать с C++ кодом, требуются дополнительные библиотеки, позволив все необходимые подгрузки делать основному компоненту компилятора — g++.
Состав компилятора g++
Мы не будем вызывать данные компоненты напрямую, так как для того, чтобы работать с C++ кодом, требуются дополнительные библиотеки, позволив все необходимые подгрузки делать основному компоненту компилятора — g++.
Компиляция
Следующая программа задействует два разных пакета: main и fmt .
При построении этой программы первым отработает компилятор, который скомпилирует каждый пакет отдельно:
В этих промежуточных файлах мы можем увидеть временные адреса инструкций (с помощью команды go tool compile -S -l main.go ):
После того, как компиляция нашей программы будет завершена, мы можем посмотреть сгенерированный файл с помощью команды go tool compile -S -l main.go , которая отображает ассемблерный код.
У нас есть несколько вариантов, как посмотреть на сгенерированную компилятором инструкцию:
Представить результат компиляции в виде ассемблерного кода. Команда: go tool compile -S -l main.go :
Флаг -l используется для предотвращения инлайнинга, чтобы немного упростить нам задачу.
Сгенерированный ассемблерный код показывает, что инструкция для вызова Println расположена со смещением (offset) 88 байт от начала функции main. Это смещение нужно линкеру, чтобы правильно релоцировать вызов функции.
Дизассемблируйте сгенерированный main.o с помощью команды go tool objdump main.o :
Идентификатор R_CALL означает релокацию вызова.
Однако, поскольку функция принадлежит другому пакету, компилятор не знает, где на самом деле находится функция. Это можно подтвердить, проверив сгенерированный файл main.o и перечислив символы с помощью команды go tool nm main.o . Вот результат:
Вы могли заметить, что нужно использовать команду go tool nm вместо нативной команды nm . Это потому что объектный файл (.o), созданный Go, имеет собственный формат.
Символ U расшифровывается как undefined, что означает, что компилятор не знает, где находится этот символ. Этому символу необходима релокация, т. е. нужно найти адрес для успешного вызова Println , и именно здесь на сцену выходит линкер. Прежде чем переходить к линкеру, давайте проанализируем сгенерированный объектный файл main.o , чтобы понять, с какими данными приходится работать линкеру.
1) Препроцессинг
Самая первая стадия компиляции программы.
Получим препроцессированный код в выходной файл driver.ii (прошедшие через стадию препроцессинга C++ файлы имеют расширение .ii), используя флаг -E, который сообщает компилятору, что компилировать (об этом далее) файл не нужно, а только провести его препроцессинг:
Взглянув на тело функции main в новом сгенерированном файле, можно заметить, что макрос RETURN был заменен:
В новом сгенерированном файле также можно увидеть огромное количество новых строк, это различные библиотеки и хэдер iostream.
Читайте также: