makefile

Makefile


1.1背景

在window上我们一般不用管源代码是如何生成可执行程序,从源代码到可执行程序的中间过程,一般有编译器帮你处理。也就是我们熟知的IDE(Integrated Development Envirment)。但是在linux上,这所有的一切都由我们自己处理。我们在linux上写完程序,一般使用gcc 或者 g++ 进行编译,生成 a.out可执行文件。但是有个问题,如果工程本身的源程序就有几百个。并且分布在不同文件夹下面,同时部分文件还依赖外部库。这个时候靠我们手动去gcc编译就显得不切实际。而makefile就是解决这种问题。当然现在流行cmake,其主要原理也是通过一些配置,帮我们生成makefile。这个也就不再我们讨论的范围内了。

1.2 gcc如何生成可执行文件

本来想直接切入主题谈如和写makefile,但是如果我们不懂gcc编译流程的话。后面可能会犯很多低级错误。首先程序编译是分为四步的,预处理、编译、汇编和连接。接下来我们分别说一下以下几步的具体内容。

1. 预处理

替换宏:会将我们编码中的 # 开头的代码。比如 #include<stdio.h>,#define MAX 5等,会把stdio.h 文件的内容替换到 #include<stdio.h> 处,其次会替换代码中所有的宏定义。

  • 删注释:代码中的注释是帮助我们理解,但是没有必要生成到可执行文件中。由此在预处理阶段会将注释删除。

  • 去除#ifdef 不符和要求的内容

      #include<stdio.h>
      #define MAX 5
    
      int main()
      {
      	 int a = MAX;
      	/*为debug模式时 a = 1*/
      	 #ifdef DEBUG
      	 a = 1;
      	 #endif
    
      	 printf("%d\n", a);
      	 return 0;
      }
    

以上是我们 temp.c 的内容。 我们使用 gcc -E temp.c -o temp.i 命令生成temp.i 预处理之后的文件

我们来查看temp.i 文件的内容

	 # 1 "temp.cpp"
	 # 1 "<built-in>"
	 # 1 "<command-line>"
	 # 1 "/usr/include/stdc-predef.h" 1 3 4
	 # 1 "<command-line>" 2
	 # 1 "temp.cpp"
	 # 1 "/usr/include/stdio.h" 1 3 4
	...................省略很多行...........................
	...................省略很多行...........................
	 extern void funlockfile (FILE *__stream) throw ();
	# 943 "/usr/include/stdio.h" 3 4
	}
	# 2 "temp.cpp" 2



	 int main()
	 {
	 		int a = 5;





			 printf("%d\n", a);

			return 0;
	}		

我们的以看出 #include<stdio.h> 被替换了。 int a = MAX; 被替换成 int a = 5。 #ifdef #endf 被去除掉。还有注释也没了。

[注意1:temp.i 的后缀不能更改。.i为某种格式。]

[拓展:预编译会对代码进行[词法分析],词法分析会识别出各个单次,确定单词的类型,将识别出的单词转换成统一词法单元(token)]


2.编译

编译主要解决的是将经过预处理的文件生成汇编代码。(以下几点仅为拓展,不要求理解

  • 词法分析:将单词序列组合成各类语法短语,如“程序”、“语句”和“表达式”等,分析源代码结果上是否正确

  • 语义分析:对结构上正确的源程序进行上下文有关性质的审查

  • 源代码优化

  • 目标代码生成

  • 目标代码优化

      gcc -S temp.i -o test.s
    

好我们接下来执行上述命令,将预处理生成的 temp.i 文件生成汇编代码。

	...................省略很多行...........................
	...................省略很多行...........................
 main:
 .LFB0:
     .cfi_startproc
     pushl   %ebp
     .cfi_def_cfa_offset 8
     .cfi_offset 5, -8
     movl    %esp, %ebp
     .cfi_def_cfa_register 5
     andl    $-16, %esp
     subl    $32, %esp
     movl    $5, 28(%esp)
     movl    28(%esp), %eax
     movl    %eax, 4(%esp)
     movl    $.LC0, (%esp)
     call    printf
     movl    $0, %eax
     leave
     .cfi_restore 5
     .cfi_def_cfa 4, 4
     ret
     .cfi_endproc
 .LFE0:
     .size   main, .-main
     .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.4) 4.8.4"
     .section    .note.GNU-stack,"",@progbits

以上为temp.i 生成的 test.s 汇编代码。由于水平不足,只能看懂一部分。当然也不需要看懂。


3.汇编

我们在第二步之后,将源代码转化为汇编代码。但是汇编代码机器也不能识别。我们要进一步转化,在汇编阶段我们将汇编代码转化为二进制代码

gcc -c test.c -o test.o

这也就是我们熟悉的.o结尾的中间代码。


4.连接

可是可执行的bin文件不一定可以执行,如果你调用了其他程序的函数,就需要将temp.o使用的先关函数正确连接。

	gcc test.o -o test 

[注意:本人之前犯了一个错误,总是把 gcc 中的 -c 参数理解为生成中间文件, 而 -o 理解为链接生成可执行文件。但事实上并非如此, -o 参数只是给生成的文件命名而已。望读者不要犯我出现的错误 ]

2 简单makefile编写

2.1 预备材料

接下来我们创建如下文件及文件夹。以下为其目录结构

	|-- add
	|   |-- add_float.c
	|   |-- add.h
	|   `-- add_int.c
	|-- main.c
	|-- makefile
	|-- sub
	|   |-- sub_float.c
	|   |-- sub.h
	|   `-- sub_int.c

add_float.c

#include "add.h"
/* add_float.c  */
/* 浮点数求和函数*/
float add_float(float a, float b)
{
	return a+b;
}

add_int.c

#include "add.h"
/* add_int.c    */
/* 整数求和函数 */
int add_int(int a, int b)
{
	return a + b;
}

main.c

#include "add/add.h"
#include "sub/sub.h"
#include<stdio.h>

int main(void)
{
	int a =10, b = 12;
	float x = 1.2345, y = 9.8765;

	add_int(a,b);
	
	printf("int a+b IS: %d \n",add_int(a,b));
	printf("float x+y IS: %f \n",add_float(x,y));
	
	return 0;
}

sub_float.c

/* sub_float.c */
/* 浮点数相减 */
float sub_float(float a, float b)
{
 
    return a - b;
    
}

sub.h

/* sub.h */
#ifndef __SUB_H__
#define __SUB_H_

extern float sub_float(float a, float b);
extern int sub_int(int a, int b);

#endif  /* sub.h */

sub_int.c

/* sub_int.c */
/* 整形数相减 */
int sub_int(int a, int b)
{

    return a - b;
    
}   

2.2 makefile基本规则

TARGET... : DEPENDEDS..
	COMMAND
	...
	...
  • TARGET: 规则定义的目标。可以为文件名,也可以为一个动作。其也称之为伪目标

  • DEPENDEDS:执行该规则的依赖条件,即在执行COMMAND之前会检查这些依赖项是否存在

  • COMMAND:规则所执行的命令,即规则的动作。例如编译文件、生成库和进入目录等。

【注意1:所有的COMMAND必须以 一个 Tab 键开头。即COMMAND距离该行开始处为一个 Tab的距离,不能有空格】

【注意2:COMMAND执行前会根据文件的时间戳来判断是否执行该命令。一般来说如果目标文件的时间晚于依赖的时间,则该命令不会执行】

2.3 makefile最基本的写法

  1 target:main.o add_float.o add_int.o sub_int.o sub_float.o
  2     gcc -o target main.o add_float.o add_int.o sub_int.o sub_float.o 
  3 
  4 main.o:main.c
  5     gcc -c main.c -o main.o 
  6 
  7 add_int.o:add/add_int.c
  8     gcc -c add/add_int.c -o add_int.o -Iadd
  9 
 10 add_float.o:add/add_float.c
 11     gcc -c add/add_float.c -o add_float.o -Iadd
 12 
 13 sub_int.o:sub/sub_int.c
 14     gcc -c sub/sub_int.c -o sub_int.o -Isub
 15 
 16 sub_float.o:sub/sub_float.c
 17     gcc -c sub/sub_float.c -o sub_float.o -Isub
 18 
 19 clean:
 20     -rm -rf  target main.o add_float.o add_int.o sub_int.o sub_float.o

以上就是makefile最简单的写法。好我们先对上面的makefile执行流程做一个简要的介绍

首先我们输入make指令时,就相当于执行了一条shell指令,比如我们输入 whereis make 我们就可以找到make的bin文件位置。执行make之后,它会在当前目录下搜索makefile或者Makefile的文件并执行。而我们执行make指令时,并没有加任何参数,由此它会默认执行第一伪目标(即target)。执行COMMAND前会先检查依赖是否存在。首先检查main.o,make会自动搜索后面的规则,于是就到了main.o伪目标(注意依赖找的是伪目标,并非真实文件),执行第五条指令。以此类推,所以依赖完成之后。执行第2条指令。

-rm 有人可能注意到前面多了 “-” 。其含义是,执行此指令,即使出错了,也不要报错

【题外话:本人关于gcc有很多想法并未得到"直接"证实,也就是说以上的过程并非一定正确。gcc】

2.4 makefile 使用变量

假如我们需要增加或者删除一些文件,那makefile基本就得重写。为了提高makefile的重用性,我们使用变量进行重写。

  1 CC = gcc
  2 TARGET = target
  3 OBJS = main.o add/add_float.o add/add_int.o sub/sub_int.o sub/sub_float.o
  4 CFLAGS = -Iadd
  5 CFLAGS += -Isub
  6 
  7 $(TARGET):$(OBJS)
  8     $(CC) -o $(TARGET) $(OBJS) $(CFLAGS)
  9 
 10 $(OBJS):%.o:%.c
 11     $(CC) -c $(CFLAGS) $< -o $@ 
 12 
 13 clean:
 14     -rm -rf  $(OBJS)

当然对linux shell一点都不了解的人可能会看蒙掉。接下来我简述一下基本语法。

  • 定义变量和使用变量:

    • 定义 CC = gcc
    • 使用 $(CC)
    • " = " 为赋值
    • " += " 累加
  • makefile的几个基本变量

    • $@ 规则中的目标
    • $^ 规则中的所有依赖
    • $< 规则中的第一个依赖
  • $(OBJS):%.o:%.c 将OBJS中所有扩展名为.o的文件扩展成为 .c 的文件(记住就行)

【注意:其实makefile还有自动推导规则,但是本人觉得自动推导虽然会使makefile更加简洁,但是会带来其他问题,就是难以理解。所以本文不探究自动推导的规则】

2.5 makefile 使用变量 改进版

按照上述办法使用变量之后可以一定程度解决文件改动问题,但是如果添加文件,还是要进行较大的修改。因此我们使用linux的一些函数来进行改善。具体如下

  1 CC = gcc
  2 CFLAGS = -Iadd -Isub
  3 DIRS = add
  4 DIRS += sub
  5 DIRS += .
  6 OBJSDIR = objs
  7 TARGET = target
  8 CSOURCE = $(foreach dir, $(DIRS), $(wildcard $(dir)/*.c))
  9 OBJS = $(CSOURCE:%.c=%.o)
 10 RM = rm -f
 11 
 12 $(TARGET):$(OBJS)
 13     $(CC) -o $(TARGET) $(OBJS) $(CFLAGS)
 14 
 15 $(OBJS):%.o:%.c
 16     $(CC) $(CFLAGS) -c $< -o $@ 
 17 
 18 test:
 19     @echo "OBJS = $(OBJS)"
 20     @echo "CSOURCE = $(CSOURCE)"
 21 
 22 clean:
 23     -$(RM) $(TARGET)
 24     -$(RM) $(OBJS)

我们可以执行 make test 来查看 OBJS中的变量。 echo 大家都知道。而@echo 中的 "@"含义是不打印此条命令。

  • $(foreach VAR, LIST, TEXT) 将LIST字符串中的一个空格分隔的单词,传给VAR然后执行TEXT指令,最后将执行的结果以字符串返回

  • $(wildcard DIR/*.c) 的含义是搜索DIR目录下所有以.c结尾的文件。

  • 【拓展】 $(patsubst %.c,%.o, add.c) 将add.c替换成add.o

2.5 makefile 嵌套

当工程有很多,需要分别生成不同的文件,这时就需要makefile进行嵌套。

其实语法很简答

add:
	cd add && $(MAKE)

等价于

add:
	$(MAKE) -C add

由此我们在主目录下的 makefile 为

  1 CC = gcc
  2 TARGET = target
  3 
  4 INCLUDE := -Iadd
  5 INCLUDE += -Isub
  6 
  7 CSOURCE += $(wildcard ./*.c)
  8 
  9 COBJS = $(CSOURCE:.c=.o)
 10 
 11 DIR = add
 12 DIR += sub
 13 
 14 GSOURCE = $(foreach dir, $(DIR), $(wildcard $(dir)/*.c))
 15 GOBJS = $(GSOURCE:.c=.o)
 16 
 17 LIBS = -lstdc++
 18 
 19 all: ALLObject $(TARGET)
 20 
 21 $(TARGET):$(COBJS)
 22     $(CC) $^ -o $@ $(LIBS) $(GOBJS) $(INCLUDE)
 23 
 24 ALLObject:
 25     @for dir in $(DIR);do \
 26         echo "$$dir"; \
 27         $(MAKE) -C $$dir; \
 28     done
 29     
 30 $(COBJS):%.o:%.c
 31     $(CC) -c $< -o $@ 
 32 
 33 clean:
 34     @for dir in $(DIR); do \
 35         $(MAKE) -C $$dir clean; \
 36     done
 37     @rm -rf $(COBJS)

add 和 sub 目录下的makefile为

  1 CC = gcc
  2 CSOURCE = $(wildcard ./*.c)
  3 COBJS = $(CSOURCE:.c=.o)
  4 
  5 all:$(COBJS)
  6 
  7 $(COBJS):%.o:%.c
  8     $(CC) -c $< -o $@ $(CFLAGS)
  9 
 10 clean:
 11     @$(RM) $(COBJS)

执行主目录的makefile时,首先会检查 ALLObject 是否存在。显然它会转向执行

@for dir in $(DIR);do \
	 26         echo "$$dir"; \
	 27         $(MAKE) -C $$dir; \
	 28     done

for 循环,简单语法,记住就行。主要是 $(MAKE) -C $$dir 。我们之前说过,它就相当于cd $$dir && $(MAKE) 去执行dir目录下的makefile

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值