介绍
使用GCC编译的时候,会遇到一些问题。如果我们只编译几个C文件,那么可以通过类似
gcc main.c -o main
这种命令来完成。但如果工程里有很多文件需要编译,继续在终端里输入GCC指令来编译就不现实了。如果我们可以写一个文件,用于描述编译哪些源码、如何编译就好了。每次需要编译工程的时候就使用这个文件。为了实现这个功能,linux上一版会部署一个编译工具:make
,描述如何编译的文件就是Makefile
。Makefile
和脚本文件类似,可以执行系统命令。使用的时候只需要一个make
命令即可编译。
举例
假设我们需要完成一个小工程,计算两个整形数字的乘积并显示到屏幕上。工程中右main.c
、input.c
和mul.c
三个C文件以及input.h
、mul.h
这两个头文件。其中main.c
是主体,input.c
负责从键盘接收输入值,mul.c
计算任意两个数的乘积。代码如下:
main.c
#include <stdio.h>
#include "input.h"
#include "mul.h"
int main(int argc, char* argv[])
{
int a, b, res;
input_int(&a, &b);
res = mul(a,b);
printf("%d * %d = %d\r\n", a, b, res);
}
input.c
#include <stdio.h>
#include "input.h"
void input_int(int* a, int*b)
{
printf("Type 2 numbers:>");
scanf("%d %d",a, b);
printf("\r\n");
}
mul.c
#include "mul.h"
int mul(int a, int b)
{
return (a*b);
}
input.h
#ifndef _INPUT_H
#define _INPUT_H
void input_int(int* a, int* b);
#endif
mul.h
#ifndef _MUL_H
#define _MIL_H
int mul(int a, int b);
#endif
如果不使用Makefile,那么就需要在终端里输入:
gcc main.c mul.c input.c -o main
但是如果这里有几百上千个文件呢?其中一个文件被修改了,需要编译其他的文件吗?所以最好的编译方式是修改了哪个就编译哪个,命令如下:
gcc -c main.c
gcc -c input.c
gcc -c mul.c
gcc main.o input.o mul.o -o main
加了-c之后是只编译不链接,链接是最后一行代码做的事情。修改的文件如果多了,我们需要实现:
- 工程没有编译过,那么编译工程;
- 工程中个别C文件被修改了,那么只编译这些被修改的文件;
- 工程中的头文件被修改了,那么就重新编译引用这个头文件的C文件。
能做这个事情的就是Makefile
。
Makefile
示例
在工程目录里面新建一个文件,起名为Makefile。注意区分大小写,而且名称必须是这个。Makefile的代码如下:
main: main.o input.o mul.o
gcc -o main main.o input.o mul.o
main.o : main.c
gcc -c main.c
input.o : input.c
gcc -c input.c
mul.o : mul.c
gcc -c mul.c
clean:
rm *.o
rm main
需要注意:
代码行首有空的地方是用TAB
,不要用空格
如果使用VI/VIM编辑器,需要修改:/etc/vim/vimrc
在最后加上:
set noexpandtab
这里是因为上述编辑器会使用空格代替TAB
。
使用的时候只需要在终端里输入make就可以了。
语法
Makefile
是由一系列规则组成的,这些规则格式如下:
目标 : 依赖文件集合...
命令1
命令2
...
取上面的一段代码分析一下:
main: main.o input.o mul.o
gcc -o main main.o input.o mul.o
这条规则的目标是main
,依赖文件是main.o input.o mul.o
,执行的命令是gcc -o main main.o input.o mul.o
变量
Makefile是支持变量的,看一下代码:
obj = main.o input.o mul.o
main : $(obj)
gcc -o main $(obj)
这里obj
作为变量名,被赋值为main.o input.o mul.o
。之后在引用变量的时候,格式是$(变量名)
赋值
Makefile里的赋值符有好几种,以下分别说明。
=
使用的时候,变量的值是最后一个赋值的结果。
:=
不使用后面的变量赋值,只使用前面的。
?=
如果变量没有被赋值,那么使用这个赋值。
+=
追加赋值。示例:
obj = main.o input.o
obj += mul.o
此时,obj
的值是 main.o input.o mul.o
模式规则
Makefile
的示例里,每一个C文件都写了一个对应的规则,如果C文件很多,这样做肯定是不行的。遇到这种情况,可以使用Makefile
里的模式规则。模式规则里,至少在规则定义里要包含%
。否则就是一般规则。%
表示任意长度的非空字符串,例如%.c
的意思是所有以.c
结尾的文件。同样的,a.%.c
表示以a.
开头,.c
结尾的所有文件。
使用模式+变量规则修改上述Makefile代码:
obj = main.o input.o mul.o
main : $(obj)
gcc -o main $(obj)
%.o : %.c
命令
clean:
rm *.o
rm main
这里可以看到,通过变量和模式规则,修改了之前的代码。代码中的命令部分需要下面的变量补足。
自动化变量
每次在解析模式规则的时候,都是不同的目标和依赖文件。那么如何通过一行命令来从不同的文件中产生对应的目标?这个就是自动化变量的功能了。自动化变量会把模式中定义的一系列文件自动挨个取出,直至符合模式的文件都取完,自动化变量只能出现在规则的命令中,常用的如下:
自动化变量 | 描述 |
---|---|
$@ | 规则中的目标集合,在规则中,如果有多个目标,则表示匹配模式中定义的目标集合 |
$% | 当目标是函数库的时候表示规则中的目标成员名,如果目标不是函数库,其值为空 |
$< | 依赖文件集合中的第一个文件,如果依赖文件是以模式规则定义(%)的, 那么其是符合模式的一系列文件的集合 |
$? | 所有比目标新的依赖目标集合,以空格分开 |
$^ | 所有依赖文件的集合,使用空格分开,如果依赖文件中有多个重复的文件, 则会删去重复的依赖文件,只保留一份 |
$+ | 与$^类似,但不删除重复的依赖文件 |
$* | 表示目标模式中%及其之前的部分。如果目标是test/a.test.c,目标模式为a.%.c, 那么$*就是test/a.test |
改造过的Makefile
obj = main.o input.o mul.o
main : $(obj)
gcc -o main $(obj)
%.o : %.c
gcc -c $<
clean:
rm *.o
rm main
举个复杂一点的例子
看一下代码:
CROSS_COMPILE ?= arm-linux-gnueabihf-
NAME ?= a
CC := $(CROSS_COMPILE)gcc
LD := $(CROSS_COMPILE)ld
OBJCOPY := $(CROSS_COMPILE)objcopy
OBJDUMP := $(CROSS_COMPILE)objdump
OBJS := start.o main.o
$(NAME).bin : $(OBJS)
$(LD) -Timx6ul.lds -o $(NAME).elf $^
$(OBJCOPY) -O binary -S $(NAME).elf $@
$(OBJDUMP) -D -m arm $(NAME).elf > $(NAME).dis
%.o : %.s
$(CC) -Wall -nostdlib -c -O2 -o $@ $<
%.o : %.S
$(CC) -Wall -nostdlib -c -O2 -o $@ $<
%.o : %.c
$(CC) -Wall -nostdlib -c -O2 -o $@ $<
clean:
rm -rf *.o $(NAME).bin $(NAME).elf $(NAME).dis
分析一下这个Makefile。
CROSS_COMPILE ?= arm-linux-gnueabihf-
NAME ?= a
CC := $(CROSS_COMPILE)gcc
LD := $(CROSS_COMPILE)ld
OBJCOPY := $(CROSS_COMPILE)objcopy
OBJDUMP := $(CROSS_COMPILE)objdump
OBJS := start.o main.o
这些都是变量定义。可以看到,下面几行的变量定义里是使用了上面定义好的变量的。
然后是目标和指令:
$(NAME).bin : $(OBJS)
$(LD) -Timx6ul.lds -o $(NAME).elf $^
$(OBJCOPY) -O binary -S $(NAME).elf $@
$(OBJDUMP) -D -m arm $(NAME).elf > $(NAME).dis
%.o : %.s
$(CC) -Wall -nostdlib -c -O2 -o $@ $<
%.o : %.S
$(CC) -Wall -nostdlib -c -O2 -o $@ $<
%.o : %.c
$(CC) -Wall -nostdlib -c -O2 -o $@ $<
clean:
rm -rf *.o $(NAME).bin $(NAME).elf $(NAME).dis
不使用变量,翻译一下:
a.bin : start.o main.o
arm-linux-gnueabihf-ld -Ta.lds -o a.elf $^
arm-linux-gnueabihf-objcopy -O binary -S a.elf a.bin
arm-linux-gnueabihf-objdump -D -m arm a.elf > a.dis
%.o : %.s
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<
%.o : %.S
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<
%.o : %.c
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<
clean:
rm -rf *.o a.bin a.elf a.dis
这样阅读起来就没什么压力了。注意,模式规则里还是使用了自动化变量,参考常用自动化变量的列表即可。
链接脚本
我们知道,C代码变成可执行文件需要经过预处理、编译、汇编、链接4个步骤,上文例子中的:
$(LD) -Timx6ul.lds -o $(NAME).elf $^
就是在链接代码。其中.lds是链接脚本文件。这里展开说一下。先看一个典型lds
文件的代码:
SECTIONS{
. = 0X10000000
.text : {*(.text)}
. = 0X30000000
.data ALIGN(4) : {*(.data)}
.bss ALIGN(4) : {*(.BSS)}
}
解析一下。
SECTIONS{}
第一行是关键字SECTIONS
,之后是一对大括号,这是必须的,类似C语言里的函数。这个关键字是用于描述输出文件的内存布局。
. = 0X10000000
第二行就是一个特殊符号赋值,就是.
被赋值为0X10000000。这个.
在链接脚本里是定位计数器,默认位置是0。这个代码的意思是把代码链接到以0X10000000为起始的地方。
.text : {*(.text)}
.text
是段名(内存中的区域名),冒号是语法要求,大括号里面才是具体内容。这里可以填上要链接到.text
这个段里的所有文件。*(.text)
中,*
是通配符,写在一起表示将所有输入文件的.text
段都放到.text
中。
. = 0X30000000
这一行更换了定位计数器的值,也就是说,下面数据存放的起始地址是0X30000000
。
.data ALIGN(4) : {*(.data)}
这一行定义了一个名为.data
的段,然后所有文件的.data
段都放到这里面。多出来的ALIGN(4)
表示4字节对齐。
.bss ALIGN(4) : {*(.BSS)}
这一行定义了一个名为.bss
的段,然后所有文件的.bss
数据都放到这里面。同样,ALIGN(4)
表示4字节对齐。这个段是存储那些定义了但是没有初始化的变量。
举个例子
脚本要求:
- 链接起始位置是0X87800000
- start.o要被链接到最开始的地方,因为start.o里面包含第一个要执行的命令。
- 脚本起名为a.lds。
下面是代码:
SECTIONS{
. = 0X87800000
.text :
{
start.o
main.o
*(.text)
}
.rodata ALIGN(4) : {*(.rodata)}
.data ALIGN(4) : {*(.data)}
__bss_start = .;
.bss ALIGN(4) : {*(.bss) *(COMMON)}
__bss_end = .;
}
需要说明的是__bss_start
和__bss_end
。这是用于记录.bss
段的起始地址和结束地址的。对于.bss段,我们需要手动清零。因此在脚本里保存这两个地址,之后在汇编或者C文件里就可以使用了。
链接脚本的使用
在Makefile里添加这样一行:
arm-linux-gnueabihf-ld -Ta.lds -o a.elf $^