Пример написания и использования Makefile
Начнём с таких вот файлов:
prog.c |
fun.c |
|
|
const.c |
outlib.h |
1 int Count=0;
|
|
Их можно собрать в один файл просто с помощью cc *.c -o prog
- Напишем простейший Makefile для этого:
- Помните о табах!
Заодно сделаем цель clean. Пробуем make, make clean.
- Так можно и скриптом было сделать. Обеспечим раздельную компиляцию и компоновку.
- Раздельная компиляция работает:
1 $ make clean 2 rm -f prog a.out *.o 3 $ make prog 4 cc const.c -c -o const.o 5 cc fun.c -c -o fun.o 6 cc prog.c -c -o prog.o 7 cc const.o fun.o prog.o -o prog 8 $ touch fun.c 9 $ make prog 10 cc fun.c -c -o fun.o 11 cc const.o fun.o prog.o -o prog 12 $ touch fun.o 13 $ make prog 14 cc const.o fun.o prog.o -o prog 15
- Кстати, вот альтернативная форма, в которой нет табуляций. Она малочитаема, не будем ей пользоваться:
- (а табуляции и пробелы становятся обычными разделителями)
- И ещё одна. На этот раз полями рецепта определяются не табуляции, а тильды (а табуляции и пробелы становятся обычными разделителями):
- Раздельная компиляция работает:
Хорошо бы задать правило по умолчанию, как делать .o файлы из .c.
Конструкции, начинающиеся с $ — подстановки значения некоторых переменных Make:
$@ означает цель (похожа на цель в тире)
$< означает первую из зависимостей
$^ означает список всех зависимостей
- В Make есть и нормальные переменные, только их подстановка должна заключаться в круглые скобки. Заодно усложним и нашу программу.
prog.c
fun.c
const.c
outlib.h
1 int Count=0;
Makefile
Следуя традиции, мы разделили генераты на совсем ненужные (TRASH) и собственно целевые файлы проекта (GENERATES), которые всё равно стоит удалять, если в репозитории должны остаться только исходники
Традиционно же цель all: в начале файла перечисляет всё, что должно быть собрано (цель в начале считается целью по умолчанию, если make запущен без явно заданной цели)
Теперь сама наша программа участвует в генерации файла README! Поэтому сборка начнётся не с README, а с prog, несмотря на то, что в списке целей all раньше стоит README:
В нашем Makefile есть недочёт: не все зависимости учтены. Например, при изменении outlib.h надо перекомпилировать все файлы, который его включают. Как минимум, fun.c, потому что в нём используется константа VERSION. Но про это ничего не сказано:
Добавим частную зависимость fun.o: outlib.h (без рецепта) в Makefile, и задача решена:
В действительности у make есть огромное количество правил по умолчанию, их можно посмотреть с помощью make -p
Выдержка из make -p:
… CC = cc … COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c … %: %.c # способ, который следует применить (встроенные): $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@ … %.o: %.c # способ, который следует применить (встроенные): $(COMPILE.c) $(OUTPUT_OPTION) $<
Действительная команда, которая приводит к компиляции, получается подстановкой переменных COMPILE.c и OUTPUT_OPTION, которые сами тоже суть результат подстановки…иных переменных но если коротко, то всё смотрится неплохо:
Мы воспользовались не только шаблоном компиляции, но и шаблоном линковки (у цели prog нет рецепта, но make догадался, что .o надо сделать из .c, а prog — из этих .o):
Использование шаблонов довольно гибко управляется: можно подменять флаги сборки на Си (CFLAGS) и других языках, флаги компоновки (LDFLAGS), название компилятора (GCC) и компоновщика, подключаемые библиотеки (LDLIBS) и т. п.
Добавим в Makefile строку CFLAGS = -Wall и пересоберём проект:
Переменные make можно переопеределять из командной строки:
Статическая библиотека собирается с помощью архиватора (т. е. программы, которая складывает много файлов в один) ar. Так что собрать нашу программу можно и с помощью библиотеки:
1 $ nm *.o 2 const.o: 3 0000000000000000 B Count 4 fun.o: 5 U Count 6 U fprintf 7 0000000000000000 T output 8 U printf 9 U stderr 10 0000000000000033 T usage 11 prog.o: 12 U Count 13 0000000000000000 T main 14 U output 15 U usage 16 $ ar -rcs libout.a const.o fun.o 17 $ ls -l libout.a 18 -rw-r--r-- 1 frbrgeorge frbrgeorge 3144 окт 5 15:52 libout.a 19 $ ar -tv libout.a 20 rw-r--r-- 500/500 976 Oct 5 15:52 2020 const.o 21 rw-r--r-- 500/500 1944 Oct 5 15:52 2020 fun.o 22 $ nm libout.a 23 const.o: 24 0000000000000000 B Count 25 fun.o: 26 U Count 27 U fprintf 28 0000000000000000 T output 29 U printf 30 U stderr 31 0000000000000033 T usage 32 $ cc -L. -lout prog.o -o prog 33 /usr/bin/ld.default: prog.o: in function `main': 34 prog.c:(.text+0x14): undefined reference to `Count' 35 /usr/bin/ld.default: prog.c:(.text+0x1a): undefined reference to `Count' 36 /usr/bin/ld.default: prog.c:(.text+0x29): undefined reference to `output' 37 /usr/bin/ld.default: prog.c:(.text+0x51): undefined reference to `output' 38 /usr/bin/ld.default: prog.c:(.text+0x67): undefined reference to `output' 39 /usr/bin/ld.default: prog.c:(.text+0x78): undefined reference to `usage' 40 collect2: error: ld returned 1 exit status 41 $ cc -L. prog.o -lout -o prog 42 frbrgeorge@linuxprac ~/LinuxDev2020/04_Multifile_class $ ./prog qwe ert 43 3: <INIT> 44 4: qwe 45 5: ert 46 6: <DONE> 47 $ nm prog | grep output 48 00000000004011a5 T output 49
.a — это обычный ar-архив, в который сложены объектники, однако nm (анализатор объектников) и ld (компоновщик) могут туда залезать!
Обратите внимание на то, какие символы в каком файле определены (T) или требуются, но не определены (U)
Ключ компоновщика -lБИБЛИОТЕКА заставляет искать файл вида libБИБЛИОТЕКА.a (или .so) в стандартных каталогах с библиотеками. А если каталог нестандартный (например, текущий)), его надо передать с ключом -LПУТЬ.
- Порядок файлов в компоновке имеет значение: неопределённые символы компоновщик накапливает, а затем ищет в библиотеке, и если библиотека идёт в начала, она игнорируется
В файле prog присутствует функция output, которая попала туда вместе с fun.o
Таким образом у нас получилась статическая библиотека, которая целиком компонуется с кодом исходной программы
Динамическая (.so) библиотека имеет совсем другой тип (в некоторых случаях её даже можно запустить), и она подгружется в память в момент запуска программы, а в самой программе не содержится. Собрать её можно, передав компоновщику ключ -shared:
1 $ cc -shared fun.o const.o -o libout.so 2 /usr/bin/ld.default: fun.o: перемещение R_X86_64_PC32 для символ «Count» не может использоваться при создании общий объект; перекомпилируйте с параметром -fPIC 3 /usr/bin/ld.default: final link failed: раздел, непредставимый для вывода 4 collect2: error: ld returned 1 exit status 5 $ make distclean CFLAGS=-fPIC prog.o const.o fun.o 6 rm -f *.o *~ o.* *.a *.so 7 rm -rf prog README 8 cc -fPIC -c -o prog.o prog.c 9 cc -fPIC -c -o const.o const.c 10 cc -fPIC -c -o fun.o fun.c 11 $ cc -shared fun.o const.o -o libout.so 12 $ readelf --dyn-syms libout.so 13 14 Таблица символов «.dynsym» содержит 11 элементов: 15 Чис: Знач Разм Тип Связ Vis Индекс имени 16 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 17 1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 18 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2) 19 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fprintf@GLIBC_2.2.5 (2) 20 4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 21 5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 22 6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2) 23 7: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND stderr@GLIBC_2.2.5 (2) 24 8: 0000000000001115 59 FUNC GLOBAL DEFAULT 12 output 25 9: 000000000000402c 4 OBJECT GLOBAL DEFAULT 23 Count 26 10: 0000000000001150 57 FUNC GLOBAL DEFAULT 12 usage 27 $ cc -L. prog.o -lout -o prog 28 $ ./prog 29 ./prog: error while loading shared libraries: libout.so: cannot open shared object file: No such file or directory 30 $ LD_DEBUG=libs ./prog 31 19737: find library=libout.so [0]; searching 32 19737: search cache=/etc/ld.so.cache 33 19737: search path=/lib64/tls/haswell/x86_64:/lib64/tls/haswell:/lib64/tls/x86_64:/lib64/tls:/lib64/haswell/x86_64:/lib64/haswell:/lib64/x86_64:/lib64:/usr/lib64/tls/haswell/x86_64:/usr/lib64/tls/haswell:/usr/lib64/tls/x86_64:/usr/lib64/tls:/usr/lib64/haswell/x86_64:/usr/lib64/haswell:/usr/lib64/x86_64:/usr/lib64 (system search path) 34 19737: trying file=/lib64/tls/haswell/x86_64/libout.so 35 19737: trying file=/lib64/tls/haswell/libout.so 36 19737: trying file=/lib64/tls/x86_64/libout.so 37 19737: trying file=/lib64/tls/libout.so 38 19737: trying file=/lib64/haswell/x86_64/libout.so 39 19737: trying file=/lib64/haswell/libout.so 40 19737: trying file=/lib64/x86_64/libout.so 41 19737: trying file=/lib64/libout.so 42 19737: trying file=/usr/lib64/tls/haswell/x86_64/libout.so 43 19737: trying file=/usr/lib64/tls/haswell/libout.so 44 19737: trying file=/usr/lib64/tls/x86_64/libout.so 45 19737: trying file=/usr/lib64/tls/libout.so 46 19737: trying file=/usr/lib64/haswell/x86_64/libout.so 47 19737: trying file=/usr/lib64/haswell/libout.so 48 19737: trying file=/usr/lib64/x86_64/libout.so 49 19737: trying file=/usr/lib64/libout.so 50 19737: 51 ./prog: error while loading shared libraries: libout.so: cannot open shared object file: No such file or directory 52 $ LD_LIBRARY_PATH=`pwd` ./prog sdfsdfs sde 53 3: <INIT> 54 4: sdfsdfs 55 5: sde 56 6: <DONE> 57
Разделяемая библиотека получается не из всякого объектника: код должен быть скомпилирован так, чтобы его можно было загрузить в любое место памяти (статический код может доверять абсолютным адресам). Это касается всех .o файлов проекта
Разделяемая библиотека имеет сложную структуру Executable_and_Linkable_Format
При запуске программы система ищет разделяемые библиотеки в стандартных каталогах (подробнее тут: ld.so), и если место нестандартное, надо указывать LD_LIBRARY_PATH
TODO в репозитории есть примеры Makefile для обоих вариантов библиотек