Сборка программы (компиляция)

Сборка (build) – процесс получения программы или библиотеки из исходного кода. Сборку разделяют на 3 крупных этапа: препроцессинг, компиляцию, линковку. Если один из этапов завершился неуспешно, то будет выведено сообщение компилятора или линковщика с ошибками.

Общие сведения

Сборка состоит из нескольких этапов, среди которых выделяют 3 крупных:

  1. Preprocessing – Препроцессирование, препроцессинг. Выполнение директив препроцессора (#include, #define и пр.). Обычно расширение *.i.

    > clang -E main.c -o main.i      # (only run the preprocessor)
  2. Compiling – Компиляция. Перевод в машинный код. Обычно расширение *.obj или *.o (он же объектный файл).

    > clang -c main.c -o main.obj    # (only run preprocess and compile steps)
  3. Linking – Линковка (компоновка). Обычно расширение *.exe/*.out (исполняемый файл) и *.dll/*.so (файл динамической библиотеки).

    > clang main.c -o main.exe       # (run all steps)

Сборка из терминала

Пусть имеется файл main.c, содержащий следующий код:

main.c
#include <stdio.h>

int main (int argc, char *argv[])
{
    printf("%i\n", argc);
    return 0;
}

Компиляция из терминала с использованием компилятора Clang:

> clang main.c

На выходе будет создан файл a.exe или a.out в зависимости от системы (Windows или Linux соответственно).

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

Зададим компилятору имя результирующего файла как main.exe:

> clang main.c -o main.exe

Ключи компиляции

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

Рассмотрим следующий пример (всё в файле main.c):

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

Для задания уровня оптимизации используется ключ -O*: -O0, -O1 (или -O), -O2, -Ofast и т.д. Набор таких ключей для каждого компилятора свой.

Зададим компилятору ключ оптимизации:

> clang main.c -O2 -o main.exe

Рассмотрим ряд других ключей для компилятора clang и llvm-based intel:

КлючЗадаётПримерПо умолчанию

-o

имя результирующего файла

-o main.exe

a.exe или a.out для Windows или Linux соответственно

-O*

уровень оптимизации

-O0, -O1 (или -O), -O2, -Ofast ...

-O0

-I

include directory

-I include

-I .

-L

library directory

-L lib

-L .

-l

library file

-l lab2.lib

-std=

стандарт языка

-std=c17

зависит от версии компилятора

-D

preprocessor definitions

-D_CRT_SECURE_NO_WARNINGS

-v

вывод подробного лога сборки

-v

-m

битность целевой платформы

-m32, -m64

-m64

-W

enable the specified warning

-Wall

зависит от версии компилятора

-Wno

disable the specified warning

-Wno-endif-labels

зависит от версии компилятора

debug VS release

Debug – код не оптимизируется (-O0), работает медленно, но хорошо работает отладчик.

Release – код оптимизируется (-O2 или -O3), работает быстро, но под отладчиком не видно значения переменных, не работают точки остановок отладки и прочее.

В Debug конфигурации неинициализированные локальные переменные нередко инициализируются специальным, так называемым, "мусором" (0xBAADF00D, 0xCCCCCCCC и другие) для упрощения отладки.

В Release же ничего не инициализируется и неинициализированные переменные содержат то, что оказалось в памяти (часто 0, но это не гарантируется).

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

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

Общие предупреждения

КлючНазначение

-Werror

делает все предупреждения ошибками

-Werror=[error-type]

делает только указанное предупреждение ошибкой

-Wall

"агрегатор" базовых предупреждений (не всех)

-Wextra

дополнительные предупреждения, которых нет в `-Wall`

-Wpedantic

проверяет соответствие кода стандарту ISO C++, сообщает об использовании запрещённых расширений, о наличии лишних точек с запятой, нехватке переноса строки в конце файла и прочих полезных штуках.

-Weverything

использование всех предупреждений

КлючНазначение

-Warray-bounds

выход за пределы массивов

-Warray-bounds-pointer-arithmetic

выход за границы массивов при использовании арифметике указателей

-Wreturn-type

не все ветви исполнения кода возвращают значение

-Wvla

использование VLA (VLA + VMT)

-Wunused-variable

объявлена неиспользуемая переменная

-Wunused

набор предупреждений `-Wunused-argument, -Wunused-but-set-variable, -Wunused-function, -Wunused-label, -Wunused-lambda-capture, -Wunused-local-typedef, -Wunused-private-field, -Wunused-property-ivar, -Wunused-value, -Wunused-variable.`

-Wshift-op-parentheses

warning: operator ‘A’ has lower precedence than ‘B’; ‘B’ will be evaluated first

Пример кода, на котором появляется сообщение с предупреждением:

main.c
int main (void)
{
    const char * a = "abc";
    if (a == "abc")
        return 0;
}

Предупреждение -Wstring-compare

clang main.c
> clang main.c

main.c: In function 'int main()':
main.c:4:11: warning: comparison with string literal results in unspecified behavior [-Wstring-compare]
    4 |     if (a == "abc")
      | 

Компиляция с игнорированием этого исключения (лог пустой, сборка прошла успешно):

> clang -Wno-string-compare main.c

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

Имена элементов программы, таких как переменные, функции, классы и т. д., должны быть объявлены перед их использованием. Например, вы не можете просто написать x = 42, не объявив x.

Рассмотрим пример кода, представленного в двух разных файлах: main.c и print_hello_world.c.

main.c
int main(void)
{
    print_hello_world();
    return 0;
}
print_hello_world.c
#include <stdio.h>

void print_hello_world(void) 
{
    printf("Hello, world!\n");
}

При попытке скомпилировать файлы будет ошибка компиляции:

clang main.c print_hello_world.c
> clang main.c print_hello_world.c

main.c:3:5: error: call to undeclared function 'print_hello_world'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
    print_hello_world();
    ^
1 error generated.

Для того, чтобы вызвать функцию, надо что-то про неё знать – а именно объявление функции (declaration):

main.c
void print_hello_world(void); // declaration

int main(void)
{
    print_hello_world();
    return 0;
}

Объявление говорит о том, что где-то в программе такая функция есть. А когда мы пишем тело функции в фигурных скобках – это определение (definition).

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

main.c
#include <stdio.h>

void print_hello_world(void); // declaration (объявление)

int main(void)
{
    print_hello_world();
    return 0;
}

void print_hello_world(void) // definition (определение)
{
    printf("Hello, world!\n");
}

Подключение заголовочных файлов #include

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

Заголовочные файлы "подключаются" в файлы с исходным кодом при помощи директивы препроцессора #include. Используются #include <...> или #include "..." в зависимости от того, где компилятору следует искать этот файл – в своих include directories или относительно текущей директории.

#include <stdlib.h>
#include <math.h>

Подключение СИСТЕМНЫХ и СТАНДАРТНЫХ (поставляемых вместе с компилятором) заголовочных файлов – указанный заголовочный будет искаться в папках с заголовчными компилятора, системных папках и в тех папках, который указаны при компиляции через ключ -I.

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

#include "myheader.h"
#include "include_folder/myheader.h"

Подключение остальных (ВАШИХ) заголовочных. Имя указаывается относительно текущей директории.

Таким образом:

  • системные/стандартные заголовочные файлы подключаем через #include <...>

  • заголовочные файлы из библиотек/сторонних проектов подключаем через #include <...> + добавляем пути до этих файлов в параметры компиляции

  • свои заголовочные подключаем через #include "..."

рекомендуется указывать пути с использованием /, а не обратных косых черт.

return_codes.h

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

#include "return_codes.h"

...
    return ERROR_ARGUMENTS_INVALID;
    return SUCCESS;

Повторное подключени заголовочных файлов

Следующий пример состоит из 3 файлов.

В результате препроцессинга:

> clang -E child.c -o child.i

А теперь скомпилируем это в объектный файл

clang -c child.c -o child.obj
> clang -c child.c -o child.obj

In file included from child.c:3:
In file included from ./parent.h:1:
./grandparent.h:1:8: error: redefinition of 'foo'
    1 | struct foo
      |        ^
child.c:2:10: note: './grandparent.h' included multiple times, additional include site here
    2 | #include "grandparent.h"
      |          ^
./parent.h:1:10: note: './grandparent.h' included multiple times, additional include site here
    1 | #include "grandparent.h"
      |          ^
./grandparent.h:1:8: note: unguarded header; consider using #ifdef guards or #pragma once
    1 | struct foo
      |        ^
1 error generated.

(」°ロ° )

Ошибка компиляции redefinition of 'smth'

Для исправления такой ошибки необходимо сделать так, чтобы код из файла grandparent.h попал в итоговый child.c один раз. Для этого используют защиту от повторного включения или inlcude guard.

Include guard (#define)

Old-style: всё содержимое заголовочного "обернуть" в

#ifndef GRANDPARENT_H
#define GRANDPARENT_H

//... contents of grandparent.h

#endif /* !GRANDPARENT_H */

New-style: в начале заголовочного указать

#pragma once

//... contents of grandparent.h

Последнее является нестандартной, но широкораспространённой директивой препроцессора.

Не следует делать!

Оставлять названия макросов, которые вам сгенерировала IDE на основе названия проекта

#ifndef UNTITLED12_LAB00011
#define UNTITLED12_LAB00011

//... contents of file

#endif

или что-то не относящееся к самому заданию

#ifndef OCHEN_HOCHU_STO_BALLOV
#define OCHEN_HOCHU_STO_BALLOV

//... contents of file

#endif

Подключать файлы с исходным кодом (*.c/*.cpp) через #include – зло (вас будут карать баллами)

୧((#Φ益Φ#))୨

Include directories

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

Вынесем определение (definition) функций foo и bar в отдельный исполняемый файл и создадим соответсвующий заголовочный:

Чтобы задать компилятору include directories нужно использовать ключ -I.

Сборка без -I

clang main.c func/func.c -o main.exe
> clang main.c func/func.c -o main.exe

main.c:3:10: fatal error: 'func.h' file not found
#include "func.h"
         ^~~~~~~~
1 error generated.

Сборка с -I

clang main.c func/func.c -I func -o main.exe
> clang main.c func/func.c -I func -o main.exe

Library directories и library files

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

Для подключения сторонних библиотек помимо указания директорий, где следует искать заголовочные файлы (чаще всего это директории include или inc) может потребоваться указание директорий библиотек (lib). Для этого используются ключи -L и -l.

Рассмотрим пример кода, использующий фреймворк OpenCL. Для компиляции такого кода помимо необходимого заголовочного файла, расположение которого укажем через ключ -I, необходимо указать, где располагается файл статической библиотеки opencl.lib и сам этот файл.

Структура проекта

project [dir]
├── opencl_root [dir]
    ├── bin [dir]
        ├── opencl.dll [dynamic library file | Windows]
        ├── opencl.so [dynamic library file | Linux]
        ├── opencl.dylib [dynamic library file | MacOS]
    ├── include [dir]
        ├── CL [dir]
            ├── cl.h [header file]
    ├── lib
        ├── opencl.lib [static library file | Windows]
        ├── lopencl.a [static library file | Linux / MacOS]
├── opencl_test.c [source file]

Сборка без указания директории заголовочных и библиотек (без -I и -L и -l)

clang opencl_test.c -o ocl.exe
> clang opencl_test.c -o ocl.exe

opencl_test.c:7:10: fatal error: 'CL/cl.h' file not found
#include <CL/cl.h>
         ^~~~~~~~~
1 error generated.

Видно, что в первую очередь не хватает заголовочного файла. Исправим ситуацию, указан компилятору include directory opencl_root/include.

Сборка без указания директории библиотек (с -I и без -L и -l)

clang opencl_test.c -I"opencl_root/include" -o ocl.exe
> clang opencl_test.c -I"opencl_root/include" -o ocl.exe

opencl_test-d454e7.o : error LNK2019: unresolved external symbol clGetPlatformIDs referenced in function main
opencl_test-d454e7.o : error LNK2019: unresolved external symbol clGetPlatformInfo referenced in function main       
opencl_test-d454e7.o : error LNK2019: unresolved external symbol clGetDeviceIDs referenced in function main
opencl_test-d454e7.o : error LNK2019: unresolved external symbol clGetDeviceInfo referenced in function main
ocl.exe : fatal error LNK1120: 4 unresolved externals
clang: error: linker command failed with exit code 1120 (use -v to see invocation)

Видно, что все текущие ошибки – ошибки линковки. Линкеру необходимо явно указать library directory и сами файлы библиотек.

Сборка без указания имени библиотеки (с -I и -L и без -l)

clang -L"opencl_root/lib" -I"opencl_root/include" -O2 -o ocl.exe opencl_test.c
> clang -L"opencl_root/lib" -I"opencl_root/include" -O2 -o ocl.exe opencl_test.c

opencl_test-f81bbf.o : error LNK2019: unresolved external symbol clGetPlatformIDs referenced in function main
opencl_test-f81bbf.o : error LNK2019: unresolved external symbol clGetPlatformInfo referenced in function main
opencl_test-f81bbf.o : error LNK2019: unresolved external symbol clGetDeviceIDs referenced in function main
opencl_test-f81bbf.o : error LNK2019: unresolved external symbol clGetDeviceInfo referenced in function main
ocl.exe : fatal error LNK1120: 4 unresolved externals
clang: error: linker command failed with exit code 1120 (use -v to see invocation)

Несмотря на указанную директорию всё ещё видим ошибки линковки. Укажем явно файл библиотеки, который лежит в указанной library directory. Файл библиотеки указывается без расширения, а также ключ -l с названием файла библиотеки должен идти последним параметром.

Сборка с -I и -L и -l

clang -L"opencl_root/lib" -I"opencl_root/include" -O2 -o ocl.exe opencl_test.c -lopencl
> clang -L"opencl_root/lib" -I"opencl_root/include" -O2 -o ocl.exe opencl_test.c -lopencl 

clang version 15.0.7
Target: x86_64-pc-windows-msvc
Thread model: posix
InstalledDir: C:\Program Files\LLVM\bin
 "...LLVM\\bin\\clang.exe" -cc1 -triple x86_64-pc-windows-msvc19.34.31937 -emit-obj -mincremental-linker-compatible ... -x c opencl_test.c
clang -cc1 version 15.0.7 based upon LLVM 15.0.7 default target x86_64-pc-windows-msvc
#include "..." search starts here:
#include <...> search starts here:
 C:\SDK\OCL_SDK_Light\include
 C:\Program Files\LLVM\lib\clang\15.0.7\include
 C:\Program Files\Microsoft Visual Studio 2022\VC\Tools\MSVC\14.34.31933\include
 C:\Program Files\Microsoft Visual Studio 2022\VC\Tools\MSVC\14.34.31933\atlmfc\include
 C:\Program Files (x86)\Windows Kits\10\Include\10.0.20348.0\ucrt
 C:\Program Files (x86)\Windows Kits\10\Include\10.0.20348.0\shared
 C:\Program Files (x86)\Windows Kits\10\Include\10.0.20348.0\um
 C:\Program Files (x86)\Windows Kits\10\Include\10.0.20348.0\winrt
 C:\Program Files (x86)\Windows Kits\10\Include\10.0.20348.0\cppwinrt
End of search list.
 "..VC\\Tools\\MSVC\\14.34.31933\\bin\\Hostx64\\x64\\link.exe" -out:ocl.exe -defaultlib:oldnames ... opencl.lib

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

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

При попытке запустить полученный ocl.exe будет ошибка. Чтобы программа отработала необходимо поместить рядом с исполняемым файлом файлы динамически подключаемых библиотек (.dll для Windows; os для UNIX систем; dylib для MacOS). Такие файлы чаще всего располагаются в директории bin библиотеки.

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

Last updated