Makefile 简易脚本

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

365.3.4 Makefile 简易脚本

#include <stdio.h> // 引入 printf() 的声明,供下面输出测试文本。
//#include "hello_pr.h" // 本节末尾回退为单文件示例,因此按视频最终状态屏蔽自定义头文件。

int main() // 定义程序入口;课程源码写成空参数列表,相关含义见正文。
{ // main() 函数体开始。
    //hello_print_func(); // 本节末尾不再链接 hello_pr.c,因此按视频最终状态屏蔽该调用。
    printf("[%s:%d]Hello Makefile \r\n", __FUNCTION__, __LINE__); // 输出当前函数名、源码行号和测试文本。
    return 0; // 向运行环境返回 0,表示程序正常结束。
} // main() 函数体结束。

上面是视频结束时实际使用的 main.c,有效代码和两行被屏蔽的代码均按画面保留,只增加初学者注释。printf() 仍位于原文件第 7 行,所以视频运行结果中的行号是 7;若另行插入或删除物理行,__LINE__ 的结果也会随之改变。

本节先完成并运行了由 main.c、hello_pr.c 和 Inc/hello_pr.h 组成的多文件练习,随后为了第一次讲清 Makefile 规则,又把 main.c 回退为上面的单文件状态。课程配套目录中的 Makefile 已经包含后续课程才加入的 $^、$@、多文件依赖和 clean,因此本篇不把那些内容提前写进本节最终源码。

一、Makefile实践

本节目标是把一条已经能够手工执行的 GCC 命令写进 Makefile,建立“先确认源码可编译,再让 make 管理构建命令”的最小实践流程。

1.1 开机及远程桌面

视频先启动 Ubuntu 虚拟机,再从 Windows 上的 VS Code 连接远端 Ubuntu。画面中的 SSH: 192.168.8.151 表明这里使用的是 VS Code Remote - SSH,不是把 Ubuntu 图形桌面传回来的“远程桌面”。编辑器界面运行在 Windows 上,源码、终端命令、GCC 和 make 实际位于 Ubuntu 中。

课程在此前使用的工程根目录下建立以下练习路径:

Makefile_Test/
└── Makefile1/

目录名中的大小写和下划线都属于实际文件名的一部分。Linux 文件系统通常区分大小写,因此 Makefile1、makefile1 和 MakeFile1 可能是三个不同的名字。视频使用的 IP 地址、用户名和目录位置只代表讲师当时的环境,自己的虚拟机应以实际配置为准。

1.2 编写代码及编译

1.2.1 编写 main.c 文件

视频先在 Makefile1 中创建最简单的 main.c,最初用于验证环境的版本如下:

#include <stdio.h> // 引入标准输入输出接口,获得 printf() 的正确函数声明。

int main() // 定义程序入口;此处按课程原样保留 int main()。
{ // main() 函数体开始。
    printf("Hello Makefile\r\n"); // 输出测试文字,再输出回车符和换行符。
    return 0; // 返回 0,表示正常结束。
} // main() 函数体结束。

课程在终端中手工编译并运行:

gcc main.c -o main
./main
  • gcc 是编译器驱动程序;这条命令会完成预处理、编译、汇编和链接,而不是只完成狭义的“编译”。
  • main.c 是输入源文件。
  • -o main 把主要输出文件命名为 main;这里未使用 -c,所以输出是可执行文件而不是 main.o。
  • ./main 中的 ./ 表示从当前目录执行名为 main 的文件。当前目录通常不在 Shell 的命令搜索路径中,不能只凭文件存在就省略 ./。

程序输出为:

Hello Makefile

字符串末尾的回车符用于把光标移到本行开头,换行符用于换到下一行;Linux 终端通常只需要换行符,这里仍按课堂源码保留二者。

1.2.2 编写 hello_pr.c 及头文件

为了模拟真实项目中的多文件结构,视频把打印功能提取到 hello_pr.c,并创建对应的 hello_pr.h。原笔记中的 hello_PR、hello_print.c 和 hello_print_function() 都是识别错误;视频及配套源码实际使用的是小写文件名 hello_pr.c、hello_pr.h 和函数名 hello_print_func()。

多文件阶段的 main.c 如下:

#include <stdio.h> // 引入标准输入输出声明;多文件阶段的 main.c 自身没有直接调用 printf(),但课程仍保留此行。
#include "hello_pr.h" // 引入自定义函数声明,使编译器能检查 hello_print_func() 调用。

int main() // 定义程序入口。
{ // main() 函数体开始。
    hello_print_func(); // 调用 hello_pr.c 中定义的打印函数。
    // printf("[%s:%d]Hello Makefile \r\n", __FUNCTION__, __LINE__); // 多文件阶段按视频状态屏蔽 main() 内原来的直接打印。
    return 0; // 返回 0,表示程序正常结束。
} // main() 函数体结束。

hello_pr.c 的内容为:

#include "hello_pr.h" // 引入与本实现文件对应的函数声明。
#include <stdio.h> // 引入 printf() 的声明。

void hello_print_func() // 定义不返回值的打印函数;空参数列表的严格含义见下文。
{ // hello_print_func() 函数体开始。

    printf("[%s:%d]Hello Makefile \r\n", __FUNCTION__, __LINE__); // 输出函数名、当前物理行号和测试文本。

    return; // 从 void 函数返回,不携带返回值。
} // hello_print_func() 函数体结束。

头文件随后被移入 Inc/hello_pr.h,内容为:

#ifndef __HELLO_PR_H__ // 若控制宏尚未定义,就进入头文件的有效内容。
#define __HELLO_PR_H__ // 定义控制宏,使同一预处理过程中的后续包含可以跳过内容。


void hello_print_func(); // 声明打印函数;在 C 中的空参数列表表示“参数未指定”,并非严格声明无参数。

#endif // 结束头文件保护条件。

这里有四个需要纠正和补充的点:

  1. void 表示函数不返回值,不是视频口述的“无符号”。函数体中的 return; 可以省略,但这里按课程源码保留。
  2. 在 C 语言中,void hello_print_func(); 是旧式的非原型声明,表示参数信息未指定;明确表示“不接收参数”应写成 void hello_print_func(void);。同理,int main(void) 比 int main() 更明确。本篇只解释差别,不擅自修改课堂源码。
  3. #ifndef、#define、#endif 组成头文件保护。它防止同一个翻译单元的预处理过程中重复展开同一头文件内容,常用于避免结构体、类型等被重复定义;它不是让整个工程“只编译一次头文件”,也不能自动解决任意循环依赖。
  4. 课程使用的 __HELLO_PR_H__ 以双下划线开头,这类标识符保留给实现使用。实际项目的用户头文件宜采用项目化且不以 _ 开头的名字,例如 VIDEO_LEARN_HELLO_PR_H。这里为忠实还原源码,仍保留原宏名。

__LINE__ 是标准预定义宏,展开为当前源文件的行号整数,所以配套格式串用 %d 输出它。__FUNCTION__ 是 GCC 为兼容旧代码提供的函数名标识符;标准 C99 写法是 __func__。本节在 GCC 环境中能正常使用 __FUNCTION__,但编写可移植 C 代码时应优先考虑 __func__。

1.2.3 编译及问题

头文件尚在工程根目录时,视频用下面的命令一次编译两个 .c 文件并完成链接:

gcc main.c hello_pr.c -o main

每个 .c 文件都会先各自成为一个翻译单元,随后生成的目标代码再被链接为同一个 main 可执行文件。头文件不是单独列入这条命令的源文件;它通过各 .c 文件中的 #include 参与预处理。

之后视频新建 Inc 目录并把 hello_pr.h 移进去,目录状态变为:

Makefile1/
├── Inc/
│   └── hello_pr.h
├── hello_pr.c
└── main.c

讲师先用 rm main 删除此前生成的可执行文件,再重复原命令。rm main 只删除这个指定文件,用于确保随后观察到的是一次全新构建;它不等于“清理整个本地目录”。

此时 main.c 和 hello_pr.c 都包含 "hello_pr.h",但头文件已不在这两个源文件所在的目录,因而两次预处理都会报出类似错误:

fatal error: hello_pr.h: 没有那个文件或目录

对 #include "hello_pr.h",GCC 会先从当前源文件所在目录等位置查找;Inc 是工程根目录下的子目录,不会仅因为名字叫 Inc 就被自动递归搜索。视频最终使用大写字母 I 的 -I 选项加入头文件搜索目录:

gcc -I ./Inc main.c hello_pr.c -o main
./main

-I ./Inc 指定的是目录,不是具体的 .h 文件;./Inc 表示当前目录下的 Inc 子目录。构建成功后,视频得到:

[hello_print_func:7]Hello Makefile

这里的 7 是 printf() 在当时 hello_pr.c 中的物理行号,不是函数被调用的次数,也不是运行时动态计算出的代码地址。

1.3 转化为makefile形式

手工 GCC 命令验证成功后,视频在 main.c 同级目录中创建 Makefile。课程说“Linux 规定只能写成首字母大写的 Makefile”并不准确:GNU make 默认依次尝试 GNUmakefile、makefile、Makefile。实际项目常采用醒目的 Makefile;若使用其他文件名,还可以通过 make -f 文件名 显式指定。

为了第一次只讲最简单的规则,视频暂时假设工程中只有 main.c:

  • 恢复 main() 中直接调用 printf();
  • 屏蔽 hello_print_func() 调用;
  • 第一次执行 make 时发现 #include "hello_pr.h" 仍然有效,而配方没有 -I ./Inc,因此查找头文件失败;
  • 随后也屏蔽该 #include,再次执行 make 才成功。

最终 main.c 就是本文顶部的源码。此时 hello_pr.c 和 Inc/hello_pr.h 仍在目录中,但这条最简规则没有把它们列为依赖或编译输入,所以它们不会参与最终可执行文件的生成。

1.4 回顾makefile基础语法

1.4.1 Makefile规则

GNU make 的一条基本规则由目标、依赖和配方组成:

目标: 依赖
	配方

原笔记中的 “pre-request” 是语音识别错误,GNU make 的正式术语是 prerequisite(依赖或先决条件);命令部分在官方手册中称为 recipe(配方)。

  • 目标(target):希望生成或更新的文件,也可以是代表某个动作的名称。
  • 依赖(prerequisite):生成目标所需的文件或其他目标,也是 make 判断目标是否过期的重要依据。
  • 配方(recipe):目标需要更新时交给 Shell 执行的一行或多行命令。

GNU make 通常根据文件是否存在和最后修改时间进行判断:目标不存在,或者某个普通依赖比目标更新时,目标需要重建;若目标不比任何依赖旧,配方通常不会重复执行。make 不是看见一条 GCC 命令就无条件执行,它的核心价值正是依据依赖关系决定“是否需要执行”。

默认情况下,配方行开头必须是真正的 Tab 字符,不能随意换成若干空格。它不是为了视觉缩进,而是 Makefile 语法的一部分。

1.4.1.1 示例

本节最终写出的 Makefile 只有下面一条规则:

main:main.c
	gcc main.c -o main

第二行开头是 Tab。这条规则可拆解为:

位置本例内容含义
目标main希望得到的可执行文件
依赖main.c生成 main 所需的源文件
配方gcc main.c -o main在需要更新目标时执行的 Shell 命令

视频在包含该 Makefile 的目录中执行:

make

因为这里只有一个规则,main 也是默认目标。第一次成功构建时,main 不存在,GNU make 会显示并执行配方:

gcc main.c -o main

随后执行:

./main

视频最终输出为:

[main:7]Hello Makefile

这个结果说明当前运行的是顶部所示的单文件版本:当前函数为 main,打印语句位于第 7 行。若不修改 main.c 就再次执行 make,GNU make 通常会判断 main 已是最新并跳过 GCC 配方;这属于规则的时间戳语义,不是编译器缓存。

视频说执行 make 时“当前命令行和 Makefile 要在同一个目录”,这是本次最简操作的前提,不是绝对限制。GNU make 默认在当前目录查找 makefile,但也可以使用 make -C 目录 先切换工作目录,或用 make -f 路径 指定文件。

本节规则有意保持最小化,因此尚未完成以下工作:

  • 没有让多文件版本的 hello_pr.c 参与构建;
  • 没有加入 Inc/hello_pr.h 及头文件搜索选项;
  • 没有使用 $^、$@ 等自动变量;
  • 没有定义 clean 目标。

这些内容从下一节开始逐步加入,不能反过来当作本节已经完成的最终 Makefile。

参考资料

Prev: Makefile 指令 Next: None