Makefile基础

介绍

使用GCC编译的时候,会遇到一些问题。如果我们只编译几个C文件,那么可以通过类似

gcc main.c -o main

这种命令来完成。但如果工程里有很多文件需要编译,继续在终端里输入GCC指令来编译就不现实了。如果我们可以写一个文件,用于描述编译哪些源码、如何编译就好了。每次需要编译工程的时候就使用这个文件。为了实现这个功能,linux上一版会部署一个编译工具:make,描述如何编译的文件就是MakefileMakefile和脚本文件类似,可以执行系统命令。使用的时候只需要一个make命令即可编译。

举例

假设我们需要完成一个小工程,计算两个整形数字的乘积并显示到屏幕上。工程中右main.cinput.cmul.c三个C文件以及input.hmul.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之后是只编译不链接,链接是最后一行代码做的事情。修改的文件如果多了,我们需要实现:

  1. 工程没有编译过,那么编译工程;
  2. 工程中个别C文件被修改了,那么只编译这些被修改的文件;
  3. 工程中的头文件被修改了,那么就重新编译引用这个头文件的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字节对齐。这个段是存储那些定义了但是没有初始化的变量。

举个例子

脚本要求:

  1. 链接起始位置是0X87800000
  2. start.o要被链接到最开始的地方,因为start.o里面包含第一个要执行的命令。
  3. 脚本起名为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 $^
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值