Пример написания и использования Makefile

Репозиторий с примером

Начнём с таких вот файлов:

prog.c

fun.c

   1 #include "outlib.h"
   2 int main(int argc, char *argv[]) {
   3     Count = argc;
   4     output("<INIT>");
   5     output(argc>1 ? argv[1] : "<NOPE>");
   6     output("<DONE>");
   7     return 0;
   8 }

   1 #include <stdio.h>
   2 #include "outlib.h"
   3 void output(char *str) {
   4         printf("%d: %s\012", Count++, str);
   5 }

const.c

outlib.h

   1 int Count=0;

   1 void output(char *);
   2 extern int Count;
  1. Их можно собрать в один файл просто с помощью cc *.c -o prog

  2. Напишем простейший Makefile для этого:
       1 prog:   const.c fun.c prog.c
       2         cc const.c fun.c prog.c -o prog
       3 
       4 clean:
       5         rm -f prog a.out 
    
    • Помните о табах!
    • Заодно сделаем цель clean. Пробуем make, make clean.

  3. Так можно и скриптом было сделать. Обеспечим раздельную компиляцию и компоновку.
       1 prog:   const.o fun.o prog.o
       2         cc const.o fun.o prog.o -o prog
       3 
       4 fun.o:  fun.c 
       5         cc fun.c -c -o fun.o
       6 
       7 prog.o: prog.c 
       8         cc prog.c -c -o prog.o
       9 
      10 const.o:        const.c 
      11         cc const.c -c -o const.o
      12 
      13 clean:
      14         rm -f prog a.out *.o
      15  
    
    • Раздельная компиляция работает:
         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 
      
    • Кстати, вот альтернативная форма, в которой нет табуляций. Она малочитаема, не будем ей пользоваться:
         1 prog:   const.o fun.o prog.o ; cc const.o fun.o prog.o -o prog
         2 
         3 fun.o:  fun.c  ; cc fun.c -c -o fun.o
         4 
         5 prog.o: prog.c  ; cc prog.c -c -o prog.o
         6 
         7 const.o:        const.c  ; cc const.c -c -o const.o
         8 
         9 clean: ; rm -f prog a.out *. 
      
      • (а табуляции и пробелы становятся обычными разделителями)
    • И ещё одна. На этот раз полями рецепта определяются не табуляции, а тильды (а табуляции и пробелы становятся обычными разделителями):
         1 .RECIPEPREFIX = ~
         2 prog:   const.o fun.o prog.o
         3 ~       cc const.o fun.o prog.o -o prog
         4 
         5 fun.o:  fun.c
         6 ~       cc fun.c -c -o fun.o
         7 
         8 prog.o: prog.c
         9 ~       cc prog.c -c -o prog.o
        10 
        11 const.o:        const.c
        12 ~       cc const.c -c -o const.o
        13 
        14 clean:
        15 ~       rm -f prog a.out *.o
      
  4. Хорошо бы задать правило по умолчанию, как делать .o файлы из .c.

       1 %.o:    %.c
       2         cc $< -c -o $@
       3 
       4 prog:   const.o fun.o prog.o
       5         cc $^ -o $@
       6 
       7 clean:
       8         rm -f prog a.out *.o
    
    • Конструкции, начинающиеся с $ — подстановки значения некоторых переменных Make:

      • $@ означает цель (похожа на цель в тире)

      • $< означает первую из зависимостей

      • $^ означает список всех зависимостей

  5. В Make есть и нормальные переменные, только их подстановка должна заключаться в круглые скобки. Заодно усложним и нашу программу.

    prog.c

    fun.c

       1 #include <stdio.h>
       2 #include "outlib.h"
       3 int main(int argc, char *argv[]) {
       4   int i;
       5         if((Count = argc)>1) {
       6                 output("<INIT>");
       7              for(i=1; i<argc; i++)
       8                  output(argv[i]);
       9               output("<DONE>");
      10      }
      11      else
      12           usage(argv[0]);
      13        return 0;
      14 }
    

       1 #include <stdio.h>
       2 #include "outlib.h"
       3 void output(char *str) {
       4         printf("%d: %s\012", Count++, str);
       5 }
       6 
       7 void usage(char *prog) {
       8     fprintf(stderr, "%s v%.2f: Print all arguments\012\t"\
       9                 "Usage: %s arg1 [arg2 […]]\012", prog, VERSION, prog);
      10 }
    

    const.c

    outlib.h

       1 int Count=0;
    

       1 void output(char *);
       2 void usage(char *);
       3 extern int Count;
       4 #define VERSION 0.0
       5 
    

    Makefile

    •    1 GENERATES = prog README
         2 TRASH = *.o *~ o.*
         3 
         4 %.o:    %.c
         5        cc $< -c -o $@
         6 
         7 all:    README prog
         8 
         9 prog:   const.o fun.o prog.o
        10  cc $^ -o $@
        11 
        12 README: prog
        13    ./$< 2> $@
        14 
        15 clean:
        16   rm -f $(TRASH)
        17 
        18 distclean:      clean
        19        rm -rf $(GENERATES)
      
    • Следуя традиции, мы разделили генераты на совсем ненужные (TRASH) и собственно целевые файлы проекта (GENERATES), которые всё равно стоит удалять, если в репозитории должны остаться только исходники

    • Традиционно же цель all: в начале файла перечисляет всё, что должно быть собрано (цель в начале считается целью по умолчанию, если make запущен без явно заданной цели)

    • Теперь сама наша программа участвует в генерации файла README! Поэтому сборка начнётся не с README, а с prog, несмотря на то, что в списке целей all раньше стоит README:

         1 $ make
         2 cc const.c -c -o const.o
         3 cc fun.c -c -o fun.o
         4 cc prog.c -c -o prog.o
         5 cc const.o fun.o prog.o -o prog
         6 ./prog 2> README
         7 
      
  6. В нашем Makefile есть недочёт: не все зависимости учтены. Например, при изменении outlib.h надо перекомпилировать все файлы, который его включают. Как минимум, fun.c, потому что в нём используется константа VERSION. Но про это ничего не сказано:

       1 $ make
       2 
       3 $ vim outlib.h 
       4 …hack-hack-hack…
       5 $ make          
       6 make: Цель «all» не требует выполнения команд.
       7 $ # :((  
       8 
    
    • Добавим частную зависимость fun.o:  outlib.h (без рецепта) в Makefile, и задача решена:

         1 $ touch outlib.h
         2 $ make          
         3 cc fun.c -c -o fun.o
         4 cc const.o fun.o prog.o -o prog
         5 ./prog 2> README
         6 
      
  7. В действительности у 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, которые сами тоже суть результат подстановки…иных переменных но если коротко, то всё смотрится неплохо:

         1 GENERATES = prog README
         2 TRASH = *.o *~ o.*
         3 
         4 all:    README prog
         5 
         6 prog:   const.o fun.o prog.o
         7 
         8 README: prog
         9         ./$< 2> $@
        10 
        11 fun.o:  outlib.h
        12 
        13 clean:
        14         rm -f $(TRASH)
        15 
        16 distclean:      clean
        17         rm -rf $(GENERATES)
        18   
      
    • Мы воспользовались не только шаблоном компиляции, но и шаблоном линковки (у цели prog нет рецепта, но make догадался, что .o надо сделать из .c, а prog — из этих .o):

         1 $ make distclean all 
         2 rm -f *.o *~ o.*
         3 rm -rf prog README
         4 cc    -c -o prog.o prog.c
         5 cc    -c -o const.o const.c
         6 cc    -c -o fun.o fun.c
         7 cc   prog.o const.o fun.o   -o prog
         8 ./prog 2> README
         9 
      
  8. Использование шаблонов довольно гибко управляется: можно подменять флаги сборки на Си (CFLAGS) и других языках, флаги компоновки (LDFLAGS), название компилятора (GCC) и компоновщика, подключаемые библиотеки (LDLIBS) и т. п.

    • Добавим в Makefile строку CFLAGS = -Wall и пересоберём проект:

         1 $ make distclean all
         2 rm -f *.o *~ o.*
         3 rm -rf prog README
         4 cc -Wall   -c -o prog.o prog.c
         5 cc -Wall   -c -o const.o const.c
         6 cc -Wall   -c -o fun.o fun.c
         7 cc   prog.o const.o fun.o   -o prog
         8 ./prog 2> README
         9 
      
    • Переменные make можно переопеределять из командной строки:

       1 $ touch fun.c
       2 $ make CFLAGS=-Werror LDLIBS=-lm
       3 cc -Werror   -c -o fun.o fun.c
       4 cc   prog.o const.o fun.o  -lm -o prog
       5 ./prog 2> README
       6 
    
  9. Статическая библиотека собирается с помощью архиватора (т. е. программы, которая складывает много файлов в один) 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

    • Таким образом у нас получилась статическая библиотека, которая целиком компонуется с кодом исходной программы

  10. Динамическая (.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 для обоих вариантов библиотек

FrBrGeorge/MakefileExample (последним исправлял пользователь FrBrGeorge 2024-09-28 21:22:13)