0%

GCC和Makefile学习

1.GCC编译器的使用

image-20230910100317585

  • 预处理:预处理阶段会根据指令对源代码进行替换、宏展开等操作。在这个阶段,编译器会根据#include指令查找头文件,并将头文件的内容插入到main.c文件中。预处理后的文件通常以.i或.ii为扩展名。
  • 编译:编译阶段将预处理后的文件翻译成汇编语言文件(通常使用. s扩展名)。编译器将C代码转换为汇编代码,将高级语言代码转换成底层机器语言的表示形式,同时进行语法检查和优化
  • 汇编:汇编阶段将汇编语言代码转换成机器码指令。汇编器将每条汇编指令转换为可执行的机器码,并生成目标文件(通常使用.o扩展名)。目标文件包含机器码指令和一些附加信息,如符号表等。
  • 链接:链接阶段将目标文件与其他库文件进行组装,生成最终的可执行程序。在这个阶段,链接器会解析代码中的符号引用,并将其与库函数的定义进行关联。如果找不到某个符号的定义,链接将会失败,并报出”undefined reference“的错误。

1.1 常用编译选项

常用选项 描述
-E 预处理
-S 编译
-c 进行预处理、编译、汇编,但是不链接
-o 指定输出文件
-I 指定头文件目录
-L 指定链接时库文件目录
-l 指定链接哪一个库文件
-v 输出详细编译信息(比如说库文件查找路径)
  • 举例

    1
    2
    3
    4
    5
    6
    gcc -E -o hello.i hello.c    // 预处理
    gcc -S -o hello.s hello.i -v // 编译,输出编译信息
    gcc -o hello hello.c // 输出名为 hello 的可执行程序,然后可以执行./hello
    gcc -o hello hello.c -static // 静态链接
    gcc -c -o hello.o hello.c // 先编译(不链接)
    gcc -o hello hello.o // 再链接

1.2 编译多个文件

  • 一起编译、链接:

    1
    gcc -o test main.c sub.c
  • 分开编译,统一链接:

    1
    2
    3
    gcc -c -o main.o main.c 
    gcc -c -o sub.o sub.c
    gcc -o test main.o sub.o

1.3 动态库和静态库

  • 类 unix 系统,静态库为 .a(archive), 动态库为 .so(shared object)。
  • windows 系统静态库为 .lib, 动态库为 .dll
  • 静态库被使用目标代码最终和可执行文件在一起,而动态库与它相反,它的目标代码在运行时或者加载时链接。
  • 浅谈静态库和动态库

1.3.1 制作使用静态库

1
2
3
4
gcc -c -o main.o main.c 
gcc -c -o sub.o sub.c
ar crs libsub.a sub.o sub2.o sub3.o(可以使用多个.o 生成静态库)
gcc -o test main.o libsub.a (如果.a不在当前目录下,需要使用-L参数指定它的绝对或相对路径)

1.3.2 制作使用动态库

1
2
3
4
5
6
gcc -c -o main.o main.c 
gcc -c -o sub.o sub.c
gcc -shared -o libsub.so sub.o sub2.o sub3.o(可以使用多个.o 生成动态库)
gcc -o test1 main.o libsub.so //在当前目录下寻找动态库libsub.so
gcc -o test main.o -lsub -L 指定库文件目录 //-lsub:在工具链指定目录中寻找libsub.so文件
// 因此需要使用-L指定该动态库文件的目录

1.4 程序运行的基础知识

  • 头文件:<xx.h>
    • 在编译工具链指定include目录寻找
    • -I dir:指定头文件目录
  • 链接:-lxxx
    • 在编译工具链指定的lib目录寻找
    • -L dir:指定库文件所在目录
  • 运行:寻找动态库.so文件
    • 在运行程序的系统的指定/lib目录寻找
    • 修改环境变量LD_LIBRARY_PATH添加新路径,例如export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/new_lib

1.4.1 查找编译工具链的指定目录

1
echo 'main(){}'| arm-buildroot-linux-gnueabihf-gcc -E -v -

echo在标准输入中打印字符串”main(){}”,gcc命令最后的-表示从标准输入中获取输入数据,即gcc对于代码main(){}进行预处理,并输出详细编译信息。在这些编译信息中,会列出头文件目录(#include <…>)和库文件目录(LIBRARY_PATH)

1.4.2 运行时库的系统路径

  • 在开发板上/lib或者/usr/lib目录。

  • 或者在bash中添加新的库路径,见1.4

1.4.3 如何交叉编译开源软件

如果开源软件的目录中有configure文件,则使用交叉编译工具链arm-buildroot-linux-gnueabihf编译的万能命令如下:

1
2
3
./configure --host=arm-buildroot-linux-gnueabihf --prefix=$PWD/tmp 
make
make install
  • make执行makefile中编译命令,make install将生成的软件安装到系统对应目录中,例如/usr/local/bin用于可执行文件,/usr/local/lib用于库文件等。
  • 在上述命令中即为安装到tmp目录下的bin,lib,include目录中,然后在手动将对应文件放到工具链和板子上的对应目录中。
  • prefix=/usr/local:通常make会将生成的软件安装在这个目录中,为了不污染系统库,所以通过--prefix=$PWD/tmp 指定安装目录

2. Makefile

  • Makefile的核心规则:
1
2
目标 : 依赖1 依赖2
[TAB]命令

目标文件不存在或者依赖文件比目标文件新的时候,就会执行命令。

一般来说,在Makefile中将编译代码文件分为预处理/编译/汇编和链接两步,当某个代码文件重新修改时,Makefile会根据修改时间仅对修改文件进行步骤一,然后重新链接,不必重新编译所有代码文件。

  • make命令基本语法:
1
make [目标]
  • 假设待编译的实例文件如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//a.c
#include <stdio.h>
int main()
{
func_b();
func_c();
}
//b.c
#include <stdio.h>
void func_b()
{
printf("This is func B!\n");
}
//c.c
#include <stdio.h>
void func_c()
{
printf("This is func C!\n");
}

2.2 Makefile编写

2.2.1 第一版

1
2
3
4
5
6
7
8
test : a.o b.o c.o
gcc -o test a.o b.o c.o
a.o : a.c
gcc -c -o a.o a.c
b.o : b.c
gcc -c -o b.o b.c
c.o : c.c
gcc -c -o c.o c.c

2.2.2 使用通配符

1
2
3
4
5
6
7
test : a.o b.o c.o
gcc -o test $^
%.o : %.c
gcc -c -o $@ $<
clean :
rm *.o
.PHONY : clean
  • %.o : %.c:匹配模式规则
    • %通配符用于匹配模式规则中的字符序列
    • *通配符用于匹配文件名
  • $@:表示目标
  • $<:表示第一个依赖文件
  • $^:表示所有依赖文件
  • .PHONY:声明clean为假想目标,防止目录下存在clean文件而导致命令不执行

2.2.3 变量

1
2
3
4
A := xxx   //即时变量,在定义时即确定
B = xxx //延时变量,在使用时才确定
C ?= xxx //延时变量,如果是第一次定义才起效,如果前面该变量已经定义,则忽略此句
C += xxx //附加变量,是即时还是延时变量取决于前面的定义
  • 举例

    1
    2
    3
    4
    5
    6
    7
    A := ${C}
    B = ${C}
    all:
    @echo ${A}
    @echo ${B}
    C ?= abc
    C += 123

    A是即时变量,在定义时C还未定义,所有A的值为空;B是延时变量,在使用时C已经定义,B的值为abc 123

2.2.4 常用函数

1
2
3
4
5
6
7
$(foreach var, list, text)       # 遍历list的元素var,对于var生成text的格式

$(filter pattern..., text) # 在text中取出符合patten格式的值
$(filter-out pattern..., txt) # 在text中取出不符合patten格式的值

$(wildcard pattern) # pattern定义了文件名的格式,wildcard取出其中存在的文件
$(patsubst pattern, replacement, $(var)) #对变量var中符合pattern格式的值,替换为repl格式
  • 举例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    A = a b c
    B = $(foreach f, $(A), $(f).o)

    C = a b c d/
    D = $(filter %/, $(C))
    E = $(filter-out %/, $(C))

    file1 = $(wildcard *.c) # 从当前目录下取出符合格式的文件名
    file_name = a.c b.c d.c e.c abc
    file2 = $(wildcard $(file_name)) # 判断file_name中的文件在当前目录下是否存在
    dep_files = $(patsubst %.c, %.d, $(file_name))
    all:
    @echo B = $(B)
    @echo D = $(D)
    @echo E = $(E)
    @echo file1 = $(file1)
    @echo file2 = $(file2)
    @echo dep_files = $(dep_files)

    输出:

    1
    2
    3
    4
    5
    6
    B = a.o b.o c.o
    D = d/
    E = a b c
    file1 = a.c c.c b.c
    file2 = a.c b.c
    dep_files = a.d b.d d.d e.d abc

2.2.5 判断语句

1
2
3
4
5
ifneq (condition1, condition2)
# 代码块
else
# 代码块
endif

2.2.6 考虑头文件

假设c.c文件包含了头文件c.h,并引用了其中的宏定义:

1
2
3
4
5
6
7
8
9
//c.h
#define C 'C'
//c.c
#include <stdio.h>
#include "c.h"
void func_c()
{
printf("This is func %c!\n", C);
}

由于Makefile仅在目标文件不存在或者依赖文件更新时才执行目录,所以如果在编译完成之后,再修改.h文件,则使用原来的Makefile不会更新所引用的宏,因此需要在Makefile中考虑头文件

使用gcc命令生成头文件依赖
1
2
3
gcc -M c.c                          // 打印出头文件依赖
gcc -M -MF c.d c.c // 把依赖写入文件c.d
gcc -c -o c.o c.c -MD -MF c.d // 编译c.c文件,同时把依赖写入文件c.d
修改Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
objs = a.o b.o c.o

dep_files := $(patsubst %, .%.d, $(objs)) # 修改文件名的格式
dep_files := $(wildcard $(dep_files)) # 找出当前目录所包含的文件
CFLAGS = -Werror -Iinclude

test: $(objs)
gcc -o test $^

ifneq ($(dep_files), ) # 判断dep_files是否为空
include $(dep_files) # 不空说明不是首次编译,则包含头文件依赖
endif

%.o: %.c
gcc $(CFLAGS) -c -o $@ $< -MD -MF .$@.d

clean:
rm *.o test
dep_clean:
rm $(dep_files)
.PHONY : clean dep_clean
  • 使用该Makefile,在首次编译之后,修改c.h文件,再一次执行Makefile:

    1
    2
    gcc -c -o c.o c.c -MD -MF .c.o.d
    gcc -o test a.o b.o c.o

    包含头文件依赖,发现头文件c.h发生改变,所以重新编译c.c文件,最后重新链接生成可执行文件

  • CFLAGS = -Werror -Iinclude:为gcc增加编译选项

    • -Werror:将警告视为错误
    • -Iinclude:在当前目录的include目录下搜索头文件

2.3 通用Makefile模板

主目录下Makefile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 指定编译工具链的前缀
CROSS_COMPILE =
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm

STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump

export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP

# 指定所有源文件的编译选项
CFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/include
# 指定所有.o文件的链接选项
LDFLAGS :=

export CFLAGS LDFLAGS

TOPDIR := $(shell pwd)
export TOPDIR
# 指定可执行文件的名称
TARGET := test

# 指定需要编译的源文件和目录
obj-y += main.o
obj-y += sub.o
obj-y += a/


all : start_recursive_build $(TARGET)
@echo $(TARGET) has been built!

start_recursive_build:
make -C ./ -f $(TOPDIR)/Makefile.build

$(TARGET) : start_recursive_build
$(CC) -o $(TARGET) built-in.o $(LDFLAGS)

clean:
rm -f $(shell find -name "*.o")
rm -f $(TARGET)

distclean:
rm -f $(shell find -name "*.o")
rm -f $(shell find -name "*.d")
rm -f $(TARGET)

主目录下Makefile.build:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
PHONY := __build
__build:

obj-y :=
subdir-y :=
EXTRA_CFLAGS :=

include Makefile

# obj-y := a.o b.o c/ d/
# $(filter %/, $(obj-y)) : c/ d/
# __subdir-y : c d
# subdir-y : c d
__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y += $(__subdir-y)

# c/built-in.o d/built-in.o
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)

# a.o b.o
cur_objs := $(filter-out %/, $(obj-y))
dep_files := $(foreach f,$(cur_objs),.$(f).d)
dep_files := $(wildcard $(dep_files))

ifneq ($(dep_files),)
include $(dep_files)
endif

PHONY += $(subdir-y)

__build : $(subdir-y) built-in.o

# 对于子目录使用最顶层的Makefile.build
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build

built-in.o : $(subdir-y) $(cur_objs)
$(LD) -r -o $@ $(cur_objs) $(subdir_objs)

dep_file = .$@.d

%.o : %.c
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<

.PHONY : $(PHONY)

子目录下Makefile示例:

1
2
3
4
5
6
7
# 指定子目录中文件的编译选项
EXTRA_CFLAGS := -D DEBUG
# 为具体文件指定额外的编译选项
CFLAGS_sub3.o := -D DEBUG_SUB3

obj-y += sub2.o
obj-y += sub3.o