首页 > 系统相关 >Linux基础知识(12)- GCC 简单使用(二)| Makefile 的高级用法

Linux基础知识(12)- GCC 简单使用(二)| Makefile 的高级用法

时间:2022-12-09 14:00:11浏览次数:58  
标签:GCC 12 -- gcc arm func Linux main hello


在 “Linux基础知识(11)- GCC 简单使用(一)| GCC 安装配置和 Makefile 的基本用法” 里我们演示了 GCC 安装配置和 Makefile 的基本用法,本文将继续演示 Makefile 的高级用法。


1. 伪目标

    前文我们在 Makefile 的基本用法的演示中,Makefile 编写的目标,在 make 看来其实都是目标文件,例如 make 在执行的时候由于在目录找不到 target_a 文件,所以每次 make target_a 的时候,它都会去执行target_a 的命令,期待执行后能得到名为 target_a 的同名文件。
    
    如果目录下真的有 target_a、target_b、target_c 的文件,即假如目标文件和依赖文件都存在且是最新的,那么 make target_a 就不会被正常执行了,这会引起误会。

    为了避免这种情况,Makefile 使用 “.PHONY” 前缀来区分目标代号和目标文件,并且这种目标代号被称为 “伪目标”,phony 单词翻译过来本身就是假的意思。

    也就是说,只要我们不期待生成目标文件,就应该把它定义成伪目标,前文的演示代码修改如下。

        示例1:

            # 使用 .PHONY 表示 target_a 是个伪目标
            .PHONY: target_a

            # target_a 是第一个目标,是最终目标,即 make 的默认目标
            # 执行 ls 命令列出当前目录下的内容,target_a 依赖于 target_b 和 target_c
            target_a: target_b target_c
                ls

            .PHONY: target_b

            # 执行 touch 命令创建 test.txt 文件,target_b 无依赖
            target_b:
                touch test.txt

            .PHONY: target_c

            # 执行 pwd 命令显示当前路径,target_c 无依赖
            target_c:
                pwd

            .PHONY: target_d

            # 执行 rm 命令删除 test.txt 文件,target_d 无依赖
            target_d:
                rm -f test.txt


        示例2:

            # 默认目标,hello_main 依赖于 hello_main.c 和 hello_func.c
            hello_main: hello_main.c hello_func.c
                gcc -o hello_main hello_main.c hello_func.c -I .

            # clean目标,删除编译生成的中间文件
            .PHONY:clean
            clean:
                rm -f *.o hello_main


    GNU 组织发布的软件工程代码的 Makefile,除了 clean 伪目标,常见的还有 all、install、print、tar 等,虽然并没有固定的要求伪目标必须用这些名字,但可以参考这些习惯来编写自己的 Makefile。

    如果以上代码中不写 “.PHONY:clean” 语句,并且在目录下创建一个名为 clean 的文件,那么当执行 “make clean” 时,clean 的命令并不会被执行。


2. 默认规则

    上文示例2 的 Makefile 里的 hello_main 依赖于 hello_main.c 和 hello_func.c,hello_main 目标文件本质上并不是依赖 hello_main.c 和 hello_func.c 文件,而是依赖于 hello_main.o 和 hello_func.o,把这两个文件链接起来就能得到我们最终想要的 hello_main 目标文件。
    
    另外,由于 make 有一条默认规则,当找不到 *.o 文件时,会查找目录下的同名 xxx.c 文件进行编译。根据这样的规则,我们可把示例2 的 Makefile 做如下修改:

        # 默认目标,hello_main 依赖于 hello_main.o 和 hello_func.o
        hello_main: hello_main.o hello_func.o
            gcc -o hello_main hello_main.o hello_func.o

        # 根据 make 的默认规则,下面两行可以不写
        #hello_main.o: hello_main.c
        #   gcc -c hello_main.c

        # 根据 make 的默认规则,下面两行可以不写
        #hello_func.o: hello_func.c
        #   gcc -c hello_func.c

        # clean目标,删除编译生成的中间文件
        .PHONY:clean
        clean:
            rm -f *.o hello_main


    使用修改后的 Makefile 编译结果如下:
        
        $ make

            cc    -c -o hello_main.o hello_main.c
            cc    -c -o hello_func.o hello_func.c
            gcc -o hello_main hello_main.o hello_func.o


    从 make 的输出可看到,它先执行了两条额外的 “cc” 编译命令,这是由 make 默认规则执行的,它们把 C 代码编译生成了同名的 .o 文件,然后 make 根据 Makefile 的命令链接这两个文件得到最终目标文件 hello_main。


3. 使用变量

    使用 C 自动编译成 *.o 的默认规则有个缺陷,由于没有显式地表示 *.o 依赖于 .h 头文件,假如我们修改了头文件的内容,那么 *.o 并不会更新,这是不可接受的。并且默认规则使用固定的 “cc” 进行编译,假如我们想使用ARM-GCC 进行交叉编译,那么系统默认的 “cc” 会导致编译错误。

    要解决这些问题并且让 Makefile 变得更加通用,需要引入变量和分支进行处理。

    1) 基本语法

        在 Makefile 中的变量,有点像 C 语言的宏定义,在引用变量的地方使用变量值进行替换。变量的命名可以包含字符、数字、下划线,区分大小写,定义变量的方式有以下四种:

定义方式 描述
=  延时赋值,该变量只有在调用的时候,才会被赋值
:=  直接赋值,与延时赋值相反,使用直接赋值的话,变量的值定义时就已经确定了。
?= 若变量的值为空,则进行赋值,通常用于设置默认值。
+= 追加赋值,可以往变量后面增加新的内容。


        变量使用的语法如下:

            $(变量名)

        示例:

            VAR_A = FILE_A
            VAR_B = $(VAR_A)
            VAR_C := $(VAR_A)
            VAR_A += FILE_B
            VAR_D ?= FILE_D
            .PHONY:check
            check:
                @echo "VAR_A: "$(VAR_A)
                @echo "VAR_B: "$(VAR_B)
                @echo "VAR_C: "$(VAR_C)
                @echo "VAR_D: "$(VAR_D)


        运行:

            $ make

                VAR_A: FILE_A FILE_B
                VAR_B: FILE_A FILE_B
                VAR_C: FILE_A
                VAR_D: FILE_D


       注:执行完 make 命令后,只有 VAR_C 是FILE_A。这是因为 VAR_B 采用的延时赋值,只有当调用时,才会进行赋值。当调用 VAR_B 时,VAR_A 的值已经被修改为 FILE_A FILE_B,因此 VAR_B 的变量值也就等于 FILE_A FILE_B。

            要注意,变量的值后面的不能带有空格。

    2) 改造默认规则

        接下来使用变量对前面 hello_main 的 Makefile 进行大改造,代码修改如下:

            # 定义变量
            CC = gcc
            CFLAGS = -I.
            DEPS = hello_func.h

            # 目标文件
            hello_main: hello_main.o hello_func.o
                $(CC) -o hello_main hello_main.o hello_func.o

            # *.o 文件的生成规则
            %.o: %.c $(DEPS)
                $(CC) -c -o $@ $< $(CFLAGS)

            # 伪目标
            .PHONY: clean
            clean:
                rm -f *.o hello_main


        代码说明:

            第 1 ~ 4 行:分别定义了 CC、CFLAGS、DEPS 变量,变量的值就是等号右侧的内容,定义好的变量可通过 “$(变量名)” 的形式引用,如后面的 “$(CC)”、“$( CFLAGS)”、“$(DEPS)” 等价于定义时赋予的变量值 “gcc”、“-I.” 和 “hello_func.h”;

            第 8 行:使用 $(CC) 替代了 gcc,这样编写的 Makefile 非常容易更换不同的编译器,如要进行交叉编译,只要把开头的编译器名字修改掉即可;

            第 11 行:“%” 是一个通配符,功能类似 “*”,如 “%.o” 表示所有以 ”.o” 结尾的文件。所以 “%.o: %.c”在本例子中等价于 “hello_main.o: hello_main.c”、“hello_func.o: hello_func.c”,即等价于 *.o 文件依赖于 *.c 文件的默认规则。不过这行代码后面的 “$(DEPS)” 表示它除了依赖 *.c 文件,还依赖于变量 “$(DEPS)” 表示的头文件,所以当头文件修改的话,*.o 文件也会被重新编译;

            第 12 行:这行代码出现了特殊的变量 “$@”、“$<”,可理解为 Makefile 文件保留的关键字,是系统保留的自动化变量,“$@” 代表了目标文件,“$<” 代表了第一个依赖文件。即 “$@” 表示 “%.o”、“$<” 表示 “%.c”,例如:

                # 当 "%" 匹配的字符为 "hello_func" 的话
                $(CC) -c -o $@ $< $(CFLAGS)

                # 等价于
                gcc -c -o hello_func.o func_func.c -I .


        以上示例代码说明 Makefile 可以利用变量及自动化变量,来重写 *.o 文件的默认生成规则,以及增加头文件的依赖。

    3) 改造链接规则

        与 *.o 文件的默认规则类似,我们也可以使用变量来修改生成最终目标文件的链接规则,代码修改如下:

            # 定义变量
            CC = gcc
            CFLAGS = -I.
            DEPS = hello_func.h
            TARGET = hello_main
            OBJS = hello_main.o hello_func.o

            # 目标文件
            $(TARGET): $(OBJS)
                $(CC) -o $@ $^ $(CFLAGS)

            # *.o 文件的生成规则
            %.o: %.c $(DEPS)
                $(CC) -c -o $@ $< $(CFLAGS)

            # 伪目标
            .PHONY: clean
            clean:
                rm -f *.o hello_main


        代码说明:

            第 5 行:定义了 TARGET 变量,它的值为目标文件名 hello_main;

            第 6 行:定义了 OBJS 变量,它的值为依赖的各个 *.o 文件,如 hello_main.o、hello_func.o 文件;

            第 9 行:使用 TARGET 和 OBJS 变量替换原来固定的内容;

            第 10 行:使用自动化变量 “$@” 表示目标文件 “$(TARGET)”,使用自动化变量 “$^” 表示所有的依赖文件即 “$(OBJS)”;

        以上示例代码中的 Makefile 把编译及链接的过程都通过变量表示出来了,非常通用。使用这样的 Makefile 可以针对不同的工程直接修改变量的内容就可以使用;

    4) 其它自动化变量

        Makefile 中还有其它自动化变量,见下表。

符号    描述
$@ 匹配目标文件
$% 与 $@ 类似,但 $% 仅匹配库类型的目标文件
$< 依赖中的第一个目标文件
$^ 所有的依赖目标,如果依赖中有重复的,只保留一份
$+ 所有的依赖目标,即使依赖中有重复的也原样保留
$? 所有比目标要新的依赖目标


4. 使用分支

    为方便直接切换 GCC 编译器,我们还可以使用条件分支增加切换编译器的功能。在 Makefile 中的条件分支语法如下:

        ifeq (arg1, arg2)
        分支1
        else
        分支2
        endif

    分支会比较括号内的参数 arg1 和 arg2 的值是否相 同,如果相同,则为真,执行 “分支1” 的内容,否则执行 “分支2” 的内容,参数 arg1 和 arg2 可以是变量或者是常量。

    常用的条件语句,见下表。

语句 描述
ifeq 判断一个变量是否等于某个值
ifneq 判断一个变量是否不等于某个值
ifdef 判断一个变量是否有值
ifndef 判断一个变量是否空值
else 表示条件表达式为假的情况
endif 表示一个条件语句的结束,任何一个条件表达式都应该以 endif 结束

 

    使用分支切换 GCC 编译器的 Makefile,代码修改如下:

        # 定义变量
        CC = gcc
        CFLAGS = -I.
        DEPS = hello_func.h
        TARGET = hello_main
        OBJS = hello_main.o hello_func.o

        # ARCH 默认为 x86,使用 gcc 编译器,否则使用 arm 编译器
        ARCH ?= x86

        # 根据 ARCH 变量来选择编译器
        ifeq ($(ARCH),x86)
        CC = gcc
        else
        CC = arm-linux-gnueabihf-gcc
        endif

        # 目标文件
        $(TARGET): $(OBJS)
            $(CC) -o $@ $^ $(CFLAGS)

        # *.o 文件的生成规则
        %.o: %.c $(DEPS)
            $(CC) -c -o $@ $< $(CFLAGS)

        # 伪目标
        .PHONY: clean
        clean:
            rm -f *.o hello_main


    Makefile 主要是增加了 ARCH 变量用于选择目标平台,第 9 行代码中使用 “?=” 给 ARCH 赋予默认值 x86,然后在代码 12 ~ 16 行增加了根据 ARCH 变量值的内容对 CC 变量赋予不同的编译器名。

    在执行 make 命令的时候,通过给 ARCH 赋予不同的变量值切换不同的编译器平台:

        # 清除编译输出,确保不受之前的编译输出影响
        $ make clean

        # 使用 ARM 平台
        $ make ARCH=arm

        # 清除编译输出
        $ make clean

        # 默认是 x86 平台
        $ make


5. 使用函数

    在更复杂的工程中,头文件、源文件可能会放在二级目录,编译生成的 *.o 或可执行文件也放到专门的编译输出目录方便整理。上文的示例做一些调整,把 *.h 头文件放在 includes 目录下,*.c 文件放在 sources 目录下,不同平台的编译输出分别存放在 build_x86 和 build_arm 中,目录结构如下:

        |
        |- includes
        |   |- hello_func.h
        |
        |- sources
        |   |- hello_func.c
        |   |- hello_main.c
        |
        |- Makefile


    实现这些复杂的操作通常需要使用 Makefile 的函数,修改 Makefile 代码如下:

        # 定义变量
        # ARCH 默认为 x86,使用 gcc 编译器,否则使用 arm 编译器
        ARCH ?= x86
        TARGET = hello_main

        # 存放中间文件的路径
        BUILD_DIR = build_$(ARCH)
        # 存放源文件的文件夹
        SRC_DIR = sources
        # 存放头文件的文件夹
        INC_DIR = includes .

        # 源文件
        SRCS = $(wildcard $(SRC_DIR)/*.c)
        # 目标文件(*.o)
        OBJS = $(patsubst %.c, $(BUILD_DIR)/%.o, $(notdir $(SRCS)))
        # 头文件
        DEPS = $(wildcard $(INC_DIR)/*.h)

        # 指定头文件的路径
        CFLAGS = $(patsubst %, -I%, $(INC_DIR))

        # 根据输入的 ARCH 变量来选择编译器
        ifeq ($(ARCH), x86)
        CC = gcc
        else
        CC = arm-linux-gnueabihf-gcc
        endif

        # 目标文件
        $(BUILD_DIR)/$(TARGET): $(OBJS)
            $(CC) -o $@ $^ $(CFLAGS)

        # *.o 文件的生成规则
        $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c $(DEPS)
            # 创建一个编译目录,用于存放过程文件
            # 命令前带 “@”,表示不在终端上输出
            @mkdir -p $(BUILD_DIR)
            $(CC) -c -o $@ $< $(CFLAGS)

        # 伪目标
        .PHONY: clean cleanall
        # 按架构删除
        clean:
            rm -rf $(BUILD_DIR)

        # 全部删除
        cleanall:
           rm -rf build_x86 build_arm


    代码说明:

        第 7 ~ 11 行:定义了变量 BULID_DIR、SRC_DIR、INC_DIR 分别赋值为工程的编译输出路径 build_$(ARCH)、源文件路径 sources 以及头文件路径 includes 和当前目录 “.”。其中编译输出路径包含了架构 $(ARCH) 的内容,ARCH=x86 时编译输出路径为 build _x86,ARCH=arm 时编译输出路径为 build_arm,方便区分不同的编译输出;

        第 14 行:定义了变量 SRCS 用于存储所有需要编译的源文件,它的值为 wildcard 函数的输出,该函数的输出为 “sources/hello_func.c sources/hello_main.c sources/test.c”;

        第 16 行:定义了 OBJS 变量用于存储所有要生成的的 *.o 文件,它的值为 patsubst 函数的输出,该函数是把所有 *.c 文件名替换为同名的 *.o 文件,并添加 build 目录,即函数的输出为 “build/hello_func.o build/hello_main.o build/test.o”;

        第 18 行:与 SRCS 变量类似,定义一个 DEPS 变量存储所有依赖的头文件,它的值为 wildcard 函数的输出,该函数的输出为 “includes/hello_func.h”;

        第 21 行:定义了 CFLAGS 变量,用于存储包含的头文件路径,它的值为 patsubst 函数的输出,该函数是把 includes 目录添加到 “-I” 后面,函数的输出为 “-Iincludes”;

        第 31 行:相对于之前的 Makefile,我们在 $(TARGET) 前增加了 $(BUILD_DIR) 路径,使得最终的可执行程序放在 build 目录下;

        第 35 行:与上面类似,给 *.o 目标文件添加 $(BUILD_DIR) 路径;

        第 38 行:在执行编译前先创建 build 目录,以存放后面的 *.o 文件,命令前的 “@” 表示执行该命令时不在终端上输出;

        第 45 行:rm 删除命令也被修改成直接删除编译目录 $(BUILD_DIR);

        第 48 ~ 49 行:增加了删除所有架构编译目录的伪目标 cleanall;

    本示例中用到的函数,见下表:

函数名称 描述
notdir 判断文件路径不是目录,格式: $(notdir 文件路径)
wildcard 用于获取文件列表,并使用空格分隔开。格式:$(wildcard 匹配规则)
patsubst 模式字符串替换,格式:$(patsubst 匹配规则, 替换规则, 输入的字符串)


        调用函数的方法跟变量的使用类似,以 “$()” 或 “${}” 符号包含函数名和参数,语法如下:

            $(函数名 参数)

        或

            ${函数名 参数}

    运行:

        # 默认是 x86 平台
        $ make

        # 使用 ARM 平台
        $ make ARCH=arm


    查看目录结构:

        |- build_arm
        |   |- hello_func.o
        |   |- hello_main.o
        |   |- hello_main
        |
        |- build_x86
        |   |- hello_func.o
        |   |- hello_main.o
        |   |- hello_main
        |
        |- includes
        |   |- hello_func.h
        |
        |- sources
        |   |- hello_func.c
        |   |- hello_main.c
        |
        |- Makefile


    注: 可以使用 tree 命令查看目录结构,若提示找不到命令,运行 sudo apt install tree 命令 (CentOS: sudo yum install tree)安装 tree。

        如果没有安装 arm-linux-gnueabihf-gcc 交叉编译工具链,运行 make ARCH=arm 后会出现如下错误:

            arm-linux-gnueabihf-gcc -c -o build_arm/hello_func.o sources/hello_func.c  -Iincludes  -I.
            make: arm-linux-gnueabihf-gcc: Command not found
            make: *** [build_arm/hello_func.o] Error 127


        安装 arm-linux-gnueabihf-gcc 交叉编译工具链,本文在 Ubuntu 20.04 下安装:

            $ sudo apt install gcc-arm-linux-gnueabihf

            # 查看版本
            $ arm-linux-gnueabihf-gcc -v

                Using built-in specs.
                COLLECT_GCC=arm-linux-gnueabihf-gcc
                COLLECT_LTO_WRAPPER=/usr/lib/gcc-cross/arm-linux-gnueabihf/9/lto-wrapper
                Target: arm-linux-gnueabihf
                Configured with: ../src/configure -v --with-pkgversion='Ubuntu 9.4.0-1ubuntu1~20.04.1' --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-libitm --disable-libquadmath --disable-libquadmath-support --enable-plugin --enable-default-pie --with-system-zlib --without-target-system-zlib --enable-libpth-m2 --enable-multiarch --enable-multilib --disable-sjlj-exceptions --with-arch=armv7-a --with-fpu=vfpv3-d16 --with-float=hard --with-mode=thumb --disable-werror --enable-multilib --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=arm-linux-gnueabihf --program-prefix=arm-linux-gnueabihf- --includedir=/usr/arm-linux-gnueabihf/include
                Thread model: posix
                gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)


            # 使用 ARM 平台
            $ make ARCH=arm

                arm-linux-gnueabihf-gcc -c -o build_arm/hello_func.o sources/hello_func.c  -Iincludes  -I.
                arm-linux-gnueabihf-gcc -c -o build_arm/hello_main.o sources/hello_main.c  -Iincludes  -I.
                arm-linux-gnueabihf-gcc -o build_arm/hello_main build_arm/hello_func.o build_arm/hello_main.o  -Iincludes  -I.

 




标签:GCC,12,--,gcc,arm,func,Linux,main,hello
From: https://www.cnblogs.com/tkuang/p/16968744.html

相关文章