在 Linux 中使用 Make 实用程序和 Makefile [指南]在 Linux 中使用 Make 实用程序和 Makefile [指南]在 Linux 中使用 Make 实用程序和 Makefile [指南]在 Linux 中使用 Make 实用程序和 Makefile [指南]
  • 业务
  • 目标
  • 支持
  • 登录
找到的结果: {phrase} (显示: {results_count} 共: {results_count_total})
显示: {results_count} 共: {results_count_total}

加载更多搜索结果...

搜索范围
模糊匹配
搜索标题
搜索内容

在 Linux 中使用 Make 实用程序和 Makefile [指南]

发表 admin at 2025年2月28日
类别
  • 未分类
标签

通过示例 C 项目了解 makefile 的基础知识以及如何使用 make 实用程序在 Linux 中构建应用程序。

这是在 Linux 中使用 make 命令的完整初学者指南。

你将学到:

  • make命令的目的

  • make命令的安装

  • 创建并使用示例 C 项目的 makefile

make 实用程序是什么?

make 实用程序是程序员最方便的实用程序之一。其主要目的是编译一个中型到大型的软件项目。 make 实用程序非常有用且用途广泛,甚至 Linux 内核也使用它!

要了解 make 实用程序的用处,必须首先了解为什么需要它。

随着您的软件变得越来越广泛,您开始越来越依赖外部依赖项(即库)。你的代码开始分成多个文件,天知道每个文件里有什么。编译每个文件并将它们合理地链接在一起以生成必要的二进制文件变得很复杂。

“但我可以为此创建一个 Bash 脚本!”

为什么是的,你可以!给你更多的力量!但随着项目的增长,您必须处理增量重建。一般而言,您将如何处理它,以便即使文件数量增加时逻辑也保持正确?

这一切都由 make 实用程序处理。因此,让我们不要重新发明轮子,而是看看如何安装和充分利用 make 实用程序。

安装 make 实用程序

make 实用程序已在几乎所有 Linux 发行版的第一方存储库中提供。

要在 Debian、Ubuntu 及其衍生版本上安装 make,请使用 apt 包管理器,如下所示:

sudo apt install make

要在 Fedora 和基于 RHEL 的 Linux 发行版上安装 make,请使用 dnf 包管理器,如下所示:

sudo dnf install make

要在 Arch Linux 及其衍生版本上安装 make,请使用 pacman 包管理器,如下所示:

sudo pacman -Sy make

现在 make 实用程序已安装,您可以继续通过示例来理解它。

创建基本的 makefile

make 实用程序根据项目代码存储库顶级目录中的 makefile 中指定的指令编译代码。

下面是我的项目的目录结构:

$ tree make-tutorial

make-tutorial
└── src
    ├── calculator.c
    ├── greeter.c
    ├── main.c
    └── userheader.h

1 directory, 4 files

以下是 main.c 源文件的内容:

#include <stdio.h>

#include "userheader.h"

int main()
{
    greeter_func();

    printf("\nAdding 5 and 10 together gives us '%d'.\n", add(5, 10));
    printf("Subtracting 10 from 32 results in '%d'.\n", sub(10, 32));
    printf("If 43 is  multiplied with 2, we get '%d'.\n", mul(43, 2));
    printf("The result of dividing any even number like 78 with 2 is a whole number like '%f'.\n", div(78, 2));

    return 0;
}

接下来是 greeter.c 源文件的内容:

#include <stdio.h>

#include "userheader.h"

void greeter_func()
{
    printf("Hello, user! I hope you are ready for today's basic Mathematics class!\n");
}

以下是 calculator.c 源文件的内容:

#include <stdio.h>

#include "userheader.h"

int add(int a, int b)
{
    return (a + b);
}

int sub(int a, int b)
{
    if (a > b)
        return (a - b);
    else if (a < b)
        return (b - a);
    else return 0;
}

int mul(int a, int b)
{
    return (a * b);
}

double div(int a, int b)
{

    if (a > b)
        return ((double)a / (double)b);
    else if (a < b)
        return ((double)b / (double)a);
    else
        return 0;
}

最后,下面是 userheader.h 头文件的内容:

#ifndef USERHEADER_DOT_H
#define USERHEADER_DOT_H

void greeter_func();

int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
double div(int a, int b);

#endif /* USERHEADER_DOT_H */

makefile 的基础知识

在创建基本的 makefile 之前,让我们先看一下 makefile 的语法。 Makefile 的基本构建块由一个或多个“规则”和“变量”组成。

makefile 中的规则

让我们首先看一下makefile 中的规则。 makefile 的规则具有以下语法:

target : prerequisites
    recipe
    ...
  • target 是将由 make 生成的文件的名称。这些通常是稍后用于将所有内容链接在一起的目标文件。

  • 先决条件是生成目标所必需的文件。您通常在此处指定 .c、.o 和 .h 文件。

  • 最后,配方是生成目标所需的一个或多个步骤。

makefile 中的宏/变量

在 C 和 C++ 中,一个基本的语言特性是变量。它们允许我们存储我们可能想要在很多地方使用的值。这有助于我们在需要时使用相同的变量名称。另一个好处是,如果我们需要更改值,我们只需要进行一项更改。

类似地,makefile 可以包含变量。它们有时被称为宏。在 Makefile 中声明变量的语法如下:

variable = value

变量及其保存的值由等号 (=) 分隔。多个值之间用空格分隔。

一般来说,变量用于存储编译所需的各种项目。假设您想要启用运行时缓冲区溢出检测并为可执行文件启用完整的 ASLR;这可以通过将所有编译器标志存储在一个变量(例如 CFLAGS)中来实现。

下面是执行此操作的演示:

CFLAGS = -D_FORTIFY_SOURCE=2 -fpie -Wl,-pie

我们创建了一个名为 CFLAGS(编译器标志)的变量,并在此处添加了所有编译器标志。

要使用我们的变量,我们可以将其括在以美元符号开头的括号中,如下所示:

gcc $(CFLAGS) -c main.c

我们的 makefile 中的上述行将添加所有指定的编译器标志并根据需要编译 main.c 文件。

自动变量

make 实用程序有一些自动变量,可以帮助进一步简化重复操作。这些变量通常在规则配方中使用。

一些自动变量如下:

$@

目标规则的名称。通常用于指定输出文件名。

$<

第一个先决条件的名称。

$?

比目标新的所有先决条件的名称。即最近一次代码编译后修改过的文件

$^

所有先决条件的名称,它们之间有空格。

您可以在 GNU Make 的官方文档中找到自动变量的完整列表。

隐式变量

与上面介绍的自动变量一样,make 也有一些具有固定用途的变量。由于我之前使用 CFLAGS 宏/变量来存储编译器标志,因此还有其他变量具有假定用途。

这不能被认为是“保留关键字”,而更像是命名变量的“普遍共识”。

这些常规变量如下:

VPATH

使实用程序等效于 Bash 的 PATH 变量。路径由冒号 (:) 分隔。默认情况下这是空的。

AS

这是汇编器。默认值是 as 汇编器。

CC

编译C文件的程序。默认为cc。 (通常,cc 指向gcc。)

CXX

用于编译 C++ 文件的程序。默认是 g++ 编译器。

CPP

运行 C 预处理器的程序。默认设置为$ (CC) -E。

LEX

将词法语法转换为源代码的程序。默认值为lex。 (您应该将其更改为flex。)

LINT

对源代码进行 lint 检查的程序。默认值为 lint。

RM

删除文件的命令。默认值为rm -f。 (请强烈注意这一点!)

ASFLAGS

这包含汇编器的所有标志。

CFLAGS

它包含 C 编译器 (cc) 的所有标志。

CXXFLAGS

它包含 C++ 编译器 (g++) 的所有标志。

CPPFLAGS

它包含 C 预处理器的所有标志。

.PHONY

指定与文件名不同的目标。一个例子是“make clean”目标;其中 clean 是 .PHONY 的值

makefile 中的注释

makefile 中的注释与 shell 脚本中的注释类似。它们以井号/井号 (#) 开头,并且该行的内容(在井号/井号之后)被 make 实用程序视为注释,并且是被忽略。

下面是一个演示这一点的示例:

CFLAGS = -D_FORTIFY_SOURCE=2 -fpie -Wl,-pie
# The '-D_FORTIFY_SOURCE=2' flag enables run-time buffer overflow detection
# The flags '-fpie -Wl,-pie' are for enabling complete address space layout randomization

makefile 的初始草稿

现在我已经描述了 makefile 元素的基本语法以及我的简单项目的依赖关系树,现在让我们编写一个非常简单的 Makefile 来编译我们的代码并将所有内容链接在一起。

让我们从设置编译所需的 CFLAGS、CC 和 VPATH 变量开始。 (这不是完整的 makefile。我们将逐步构建它。)

CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src

完成后,让我们定义构建规则。我将为每个 .c 文件创建 3 条规则。我的可执行二进制文件将被称为 make_tutorial 但你的可以是任何你想要的!

CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src


make_tutorial : main.o calculator.o greeter.o
        $(CC) $(CFLAGS) $? -o $@

main.o : main.c
        $(CC) $(CFLAGS) -c $? -o $@

calculator.o : calculator.c
        $(CC) $(CFLAGS) -c $? -o $@

greeter.o : greeter.c
        $(CC) $(CFLAGS) -c $? -o $@

如您所见,我将所有 .c 文件编译为目标文件 (.o) 并在最后将它们链接在一起。

当我们运行 make 命令时,它将从第一条规则 (make_tutorial) 开始。该规则是创建同名的最终可执行二进制文件。每个 .c 文件都有 3 个必备目标文件。

make_tutorial 规则之后的每个连续规则都从同名的源文件创建一个目标文件。我能理解这种感觉有多么复杂。因此,让我们分解每个自动变量和隐式变量并理解它们的含义。

  • $ (CC):调用 GNU C 编译器 (gcc)。

  • $ (CFLAGS):一个隐式变量,用于传递我们的编译器标志,例如 -Wall 等。

  • $?:比目标更新的所有必备文件的名称。在 main.o 的规则中,$? 将扩展为 main.c IF main.在生成 main.o 后,c 已被修改。

  • $@:这是目标名称。我用它来省略输入规则名称两次。在 main.o 的规则中,$@ 扩展为 main.o。

最后,选项-c和-o是gcc用于编译/汇编源文件的选项不链接并分别指定输出文件名。您可以通过在终端中运行 man 1 gcc 命令来检查这一点。

现在让我们尝试运行这个 makefile,并希望它能够在第一次尝试时起作用!

$ make
gcc -Wall -Wextra -c src/main.c -o main.o
gcc -Wall -Wextra -c src/calculator.c -o calculator.o
gcc -Wall -Wextra -c src/greeter.c -o greeter.o
gcc -Wall -Wextra main.o calculator.o greeter.o -o make_tutorial

如果仔细观察,编译的每个步骤都包含我们在 CFLAGS 隐式变量中指定的所有标志。我们还可以看到源文件是自动从“src”目录获取的。这是自动发生的,因为我们在 VPATH 隐式变量中指定了“src”。

让我们尝试运行 make_tutorial 二进制文件并验证一切是否按预期运行。

$ ./make_tutorial
Hello, user! I hope you are ready for today's basic Mathematics class!

Adding 5 and 10 together gives us '15'.
Subtracting 10 from 32 results in '22'.
If 43 is  multiplied with 2, we get '86'.
The result of dividing any even number like 78 with 2 is a whole number like '39.000000'.

通过 GIPHY

改进 makefile

“有什么需要改进的地方?”
让我们运行 ls 命令,您可以亲自看到;)

$ ls --group-directories-first -1
src
calculator.o
greeter.o
main.o
Makefile
make_tutorial

您看到构建工件(目标文件)吗?是的,他们会让事情变得更糟。让我们使用我们的构建目录来减少这种混乱。

下面是修改后的makefile:

CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src:build


make_tutorial : main.o calculator.o greeter.o
        $(CC) $(CFLAGS) $? -o $@

build/main.o : main.c
        mkdir build
        $(CC) $(CFLAGS) -c $? -o $@

build/calculator.o : calculator.c
        $(CC) $(CFLAGS) -c $? -o $@

build/greeter.o : greeter.c
        $(CC) $(CFLAGS) -c $? -o $@

在这里,我做了一个简单的更改:在生成目标文件的每个规则之前添加了 build/ 字符串。这会将每个目标文件放入“build”目录中。我还将“build”添加到了 VPATH 变量中。

如果你仔细观察,我们的第一个编译目标是 make_tutorial。但它不会是迂腐的第一目标。配方运行的第一个目标是 main.o (或者更确切地说 build/main.o)。因此,我在 main.o 目标中添加了“mkdir build”命令作为配方。

如果我不创建“build”目录,我会收到以下错误:

$ make
gcc -Wall -Wextra -c src/main.c -o build/main.o
Assembler messages:
Fatal error: can't create build/main.o: No such file or directory
make: *** [Makefile:12: build/main.o] Error 1

现在我们已经修改了 makefile,让我们删除当前的构建工件以及编译的二进制文件并重新运行 make 实用程序。

$ rm -v *.o make_tutorial
removed 'calculator.o'
removed 'greeter.o'
removed 'main.o'
removed 'make_tutorial'

$ make
mkdir build
gcc -Wall -Wextra -c src/main.c -o build/main.o
gcc -Wall -Wextra -c src/calculator.c -o build/calculator.o
gcc -Wall -Wextra -c src/greeter.c -o build/greeter.o
gcc -Wall -Wextra build/main.o build/calculator.o build/greeter.o -o make_tutorial

这编译完美!如果仔细观察,我们已经在 VPATH 变量中指定了“build”目录,使得 make 实用程序可以在“build”目录中搜索我们的目标文件。

我们的源文件和头文件是从“src”目录中自动找到的,构建工件(目标文件)保存在“build”目录中并从“build”目录链接,正如我们预期的那样。

添加 .PHONY 目标

我们可以将这一改进更进一步。让我们添加“make clean”和“make run”目标。

下面是我们最终的 makefile:

CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src:build


build/bin/make_tutorial : main.o calculator.o greeter.o
        mkdir build/bin
        $(CC) $(CFLAGS) $? -o $@

build/main.o : main.c
        mkdir build
        $(CC) $(CFLAGS) -c $? -o $@

build/calculator.o : calculator.c
        $(CC) $(CFLAGS) -c $? -o $@

build/greeter.o : greeter.c
        $(CC) $(CFLAGS) -c $? -o $@


.PHONY = clean
clean :
        rm -rvf build


.PHONY = run
run: make_tutorial
        ./build/bin/make_tutorial

有关构建目标的所有内容都是相同的,除了我指定要将 make_tutorial 二进制可执行文件放置在 build/bin/ 目录中的更改之外。

然后,我将 .PHONY 变量设置为 clean,以指定 clean 不是 make 实用程序需要担心的文件。这是……假的。在 clean 目标下,我指定了必须删除的内容才能“清理所有内容”。

我对 run 目标执行相同的操作。如果您是 Rust 开发人员,您会喜欢这种模式。与 cargo run 命令一样,我使用 make run 命令来运行已编译的二进制文件。

为了让我们运行 make_tutorial 二进制文件,它必须存在。因此,我将其添加到 run 目标的先决条件中。

我们先运行make clean,然后直接运行make run!

$ make clean
rm -rvf build
removed 'build/greeter.o'
removed 'build/main.o'
removed 'build/calculator.o'
removed 'build/bin/make_tutorial'
removed directory 'build/bin'
removed directory 'build'

$ make run
mkdir build
gcc -Wall -Wextra -c src/main.c -o build/main.o
gcc -Wall -Wextra -c src/calculator.c -o build/calculator.o
gcc -Wall -Wextra -c src/greeter.c -o build/greeter.o
mkdir build/bin
gcc -Wall -Wextra build/main.o build/calculator.o build/greeter.o -o build/bin/make_tutorial
./build/bin/make_tutorial
Hello, user! I hope you are ready for today's basic Mathematics class!

Adding 5 and 10 together gives us '15'.
Subtracting 10 from 32 results in '22'.
If 43 is  multiplied with 2, we get '86'.
The result of dividing any even number like 78 with 2 is a whole number like '39.000000'.

正如您在此处看到的,我们没有首先运行 make 命令来编译我们的项目。运行 make run 后,编译就完成了。让我们了解一下它是如何发生的。

运行 make run 命令时,make 实用程序首先查看 run 目标。 run 目标的先决条件是我们编译的二进制文件。因此我们的 make_tutorial 二进制文件首先被编译。

make_tutorial 有其自己的先决条件,这些先决条件位于 build/ 目录中。一旦这些目标文件被编译,我们的 make_tutorial 二进制文件就会被编译;最后,Make 实用程序返回到 run 目标,并执行二进制文件 ./build/bin/make_tutorial。

如此优雅哇

结论

本文介绍了 makefile 的基础知识,makefile 是 make 实用程序所依赖的文件,用于简化软件存储库的编译。这是通过从基本的 Makefile 开始并随着我们的需求的增长而构建它来完成的。

©2015-2025 Norria support@alaica.com