Сборка программы
Сборка (build) – процесс получения программы или библиотеки из исходного кода. Сборку разделяют на 3 крупных этапа: препроцессинг, компиляцию, линковку. Если один из этапов завершился неуспешно, то будет выведено сообщение компилятора или линковщика с ошибками.
Общие сведения
Сборка состоит из нескольких этапов, среди которых выделяют 3 крупных:
Preprocessing – Препроцессирование, препроцессинг. Выполнение директив препроцессора (
#include
,#define
и пр.). Обычно расширение*.i
.clang -E main.c -o main.i # (preprocessing)
Compiling – Компиляция. Перевод в машинный код. Обычно расширение
*.obj
или*.o
(он же объектный файл).clang -c main.c -o main.obj # (preprocessing + compile)
Linking – Линковка (компоновка). Обычно расширение
*.exe/*.out
(исполняемый файл) и*.dll/*.so
(файл динамической библиотеки).lld-link /subsystem:console /defaultlib:libcmt main.obj # (linking)
clang main.c -o main.exe # (all steps)

Сборка из терминала
Пусть имеется файл 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
компиляторы поддерживают большое число других ключей.
Рассмотрим следующий пример:
#include <stdio.h>
#include <stdlib.h>
#define C 1
int bar(int i)
{
return C * 5;
}
int foo(int i)
{
return i * bar(i);
}
int main(void)
{
int n = 20;
int* arr = malloc(sizeof(int) * n);
for (int i = 0; i < C; n; ++i)
arr[i] = foo(i);
free(arr);
return 0;
}
Существует несколько уровней комплексной оптимизации. Каждый следующий уровень обычно включает все техники оптимизации с предыдущего уровня + некоторые новые. Кроме того, могут быть и отдельные наборы для особых случаев (например, оптимизация по размеру исполняемого кода).
Для задания уровня оптимизации используется ключ -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
Пример кода, на котором появляется сообщение с предупреждением:
int main (void)
{
const char * a = "abc";
if (a == "abc")
return 0;
}
Предупреждение -Wstring-compare
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
.
int main(void)
{
print_hello_world();
return 0;
}
#include <stdio.h>
void print_hello_world(void)
{
printf("Hello, world!\n");
}
При попытке скомпилировать файлы будет ошибка компиляции:
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):
void print_hello_world(void); // declaration
int main(void)
{
print_hello_world();
return 0;
}
Объявление говорит о том, что где-то в программе такая функция есть. А когда мы пишем тело функции в фигурных скобках – это определение (definition).
Следуя этому правилу можно писать код, в котором все функции определены ниже их первого вызова, указав до этого первого вызова объявления этих функций:
#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
return_codes.h
Там лежат объявления макросов. В своём коде вы используете именно макросы, а не числа, которые рядом с ним написаны!
#include "return_codes.h"
...
return ERROR_ARGUMENTS_INVALID;
return SUCCESS;
Повторное подключение заголовочных файлов
Следующий пример состоит из 3 файлов.
struct foo
{
int member;
};
#include "grandparent.h"
#include "grandparent.h"
#include "parent.h"
В результате препроцессинга:
clang -E child.c -o child.i
# 1 "child.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 368 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "child.c" 2
1 "./grandparent.h" 1
struct foo
{
int member;
};
3 "child.c" 2
1 "./parent.h" 1
1 "./grandparent.h" 1
struct foo
{
int member;
};
2 "./parent.h" 2
4 "child.c" 2
А теперь скомпилируем это в объектный файл
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
один раз. Для этого используют защиту от повторного включения или include 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
в отдельный исполняемый файл и создадим соответсвующий заголовочный:
#pragma once
#define C 1
int bar(int i);
int foo(int i);
#include "func.h"
int bar(int i)
{
return C * 5;
}
int foo(int i)
{
return i * bar(i);
}
#include <stdio.h>
#include <stdlib.h>
#include "func.h"
int main(void)
{
int n = 20;
int* arr = malloc(sizeof(int) * n);
for (int i = 0; i < C; n; ++i)
arr[i] = foo(i);
free(arr);
return 0;
}
Чтобы задать компилятору include directories нужно использовать ключ -I
.
Сборка без -I
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
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
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
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
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 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