Краткая справка по C: сборка

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

Получение бинарника (.exe, .elf, .hex, .dll, .so, .a) в любом компиляторе С/С++ почти всегда состоит из трех действ —
препроцессинга, компиляции и линковки.

Препроцессинг — это (как можно догадаться из название) раскрытие деректив препроцессора (#define,#include, #ifdef итд) — на этом этапе происходит включение хедеров, подстановка макросов, вырезание ненужных участков кода (если есть директивы #ifdef #ifndef)

Компиляция — преобразование исходных текстов в промежуточный (объектный) код — в нем содержатся откомпилированные функции и классы, но связи между ними никакой нет, нет адресации. Каждый c/cpp файл компилируется отдельно и соответственно, про остальные файлы компилятор на этом этапе ничего не знает. Это увеличивает скорость — если файл не изменялся, то перекомпилировать его уже не нужно (например, поменяли один, а файлов в проекте очень и очень много, посмотрите например ядро Linux — выигрыш в скорости будет колоссальным)

Линковка — создание на основе объектных файлов исполнимых. Линковщик компонует функции в один файл, настраивает адресацию, короче связывает куски получившиеся на прошлом этапе в одно целое.

Что когда происходит и почему не собирается?

В препроцессигне может быть несколько проблем
Во-первых #define действует только в том файле, где определён. т.е. если вы определили

#define F_CPU 16000000UL

в main.cpp, то скажем в файле uart.c никаких упоминаний про этот define нет и если он нужен — будет ошибка.
Решается тремя способами:

  1. Определением в каждом файле — неправильный (захотели поменять частоту, приходится ковыряться во всех исходниках).
  2. Созданием и подключением хидера с #define в каждый файл где используется — правильный.
  3. Определением глобальных макросов/определения (ключ компиляции -D для gcc)

Последний — тоже правильный, но используется немного в другом контексте. Например, для быстрой конфигурации программы (./configure думаю знаком многим *никсоидам, помимо всего именно эти ключи он и задает в Makefile).

Многие среды разработки поддерживают несколько конфигураций сборки (targets), по-умолчанию обычно их две debug и release. При сборе debug-версии установлен глобальный define DEBUG, в release — не установлен. т.е. код
Код:

#ifdef DEBUG
// блабла
#endif

будет выполнятся только при сборке отладочной версии (DEBUG), при сборке релиза, отладочный код помеченный этими директивами компилироваться вообще не будет.

Макросы
Макросы это код, который «выполняет» препроцессор. Определяются той же директивой #define, но имеют два параметра — название и тело.

#define _BV(x) (1<<x)

определив этот макрос, в тексте программы можно использовать как обычную функцию:

PORTD = _BV(1);
PORTC = _BV(i);

— разница лишь в том, что фактически вызова функции не будет, после обработки препроцессором этот текст примет вид:

PORTD = (1<<1);
PORTC = (1<<i);

По скольку макросы раскрываются ещё до компиляции — от этого возможны бонусы и проблемы: константы определённые в #define будут подставляться как числа (не будет обращения к памяти).

Если под #define стоит выражение — оно тоже посчитается (если возможно) на этапе компиляции и в программе будет уже не выражением, а числовой константой. Проблемы — в синтаксисе. Надо учитывать, что дифайн подставляется в исходник «как есть»
например

#define SUM(a,b) a+b

в выражение 2*SUM(x,y) подставится как 2*x+y и даст совсем не тот результат 2*(x+y) на который хочется рассчитывать — поэтому макросы заключают в скобки:

#define SUM(a,b) (a+b)

— Так правильно.

Ну и конечно же подключение заголовков:
Директива #include говорит препроцессору, что нужно найти такой-то файл и вставить его текст (прямо вместо этой директивы). Проблемы начинаются, когда файл почему-то не находится. А не находится он может (банальное отсутствие файла на диске рассматривать не будем) по одной причине: препроцессор не знает где его искать.
Тут надо обратить внимание на то, как написано название файла:

#include <file.h>

— в «угловых» скобках — будет выполнен поиск по стандартным системных папкам (прилагающимся к компилятору)

#include "file.h"

— в двойных кавычках — будет выполнен поиск по текущей папке проекта и потом по системным папкам

Компиляция. По скольку компилятор совершенно ничего не знает о функциях определённых в других файлах — ему надо о них рассказать. Делается это тоже по средствам уже упомянутых заголовочных файлов (*.h) . В них находятся только определения функций и классов, т.е. название, количество и тип параметров, возвращаемое значение — для компилятора это все что нужно знать.

А ещё, компилятор читает текст программы сверху вниз и в один проход, поэтому если функция объявлена ниже, чем используется — случится ошибка. Объявление в заголовочном файле и подключение его в начало текста помогает (как я уже говорил — компилятору плевать на внутренности вызываемых функций, главное знать как эту функцию вызвать). Отсюда рождаются пары uart.h uart.c: в первом находятся описания функций, во втором реализации.

Для того, чтобы один и тот же заголовочный файл не включился несколько раз его защищают директивами препроцессора:

 
#ifndef BLAHBLAH_H
#define BLAHBLAH_H 1
// код файла
#endif

т.е. если не определён символ BLAHBLAH_H — он определяется и включается код файла, если уже определён — весь файл пропускается.

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

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

  • A: библиотеки не надо компилировать в месте с программой
  • B: библиотеки надо связывать (линковать) вместе с исходниками собираемой программы

Линковать можно статично — когда код библиотеки встраивается в приложение. И динамично, библиотека лежит сама-по себе, но получившийся в результате сборки файл использует её функции.

P.S.
Этого должно хватить для понимания работы GCC (и прочих С компиляторов), материал не претендует на полноту изложения, а так же сильно упрощён, так что не следует использовать как истину в последней инстанции. А вот в качестве вводного курса может и сгодится.

Добавить комментарий