3-gcc-Makefile-安卓的编译

gcc

编译:把自己写的代码转成计算机能识别的代码,因为计算机只识别二进制的东西。例如把.c文件转为二进制bin文件。 百度百科:编译:就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。

GNU compiler collection, 是linux下最主要的编译工具,可以通过不同前端模块来支持各种语言,如c,c++,java。g++是gcc中的一个工具,专门用来编译c++语言。

c++预处理阶段主要完成的工作:处理以#开头的预编译命令:

  1. #define(宏定义):对程序中所有出现的宏名,都用宏定义中的字符串去代换
  2. #include(文件包含):把指定头文件插入该命令行位置取代该命令行,从而把指定的文件和当前的源程序文件连成一个源文件。
  3. #ifdef(条件编译):一般情况下,源程序中所有的行都参加编译,但有时希望当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。

连接器阶段非常复杂,简要叙述以下主要任务:

  1. 函数重载:c++编译器为实现函数重载普遍使用的是名字改编的方式为每一个函数生成一个独一无二的名字,链接的时候就能找到正确的重载版本;
  2. inline函数:如果函数体不太大,对此函数的所有调用都以函数本体去替代,注意inline只是对编译器的一个建议申请,不是强制命令。
  3. 模板处理:函数定义(包括函数模板,类模板的成员函数),变量定义(包括函数模板的静态数据变量等)
  4. 虚函数

c和c++都是单遍编译,即从头到尾扫描一遍源码,一边解析源码,一边即刻生成目标代码,这么做是因为:编译器无法在内存里完整地表示单个源文件的抽象语法树,更不可能把整个程序放入内存。

gcc/g++在编译时,一共4步(如果不加参数的话):1.预处理preprocessing,生成 .i的文件;2.将预处理后的文件编译compilation为汇编语言,生成 .s文件[编译器egcs];3.汇编assembly变为目标代码(机器代码)生成 .o的文件[汇编器as];4.连接linking库文件和其他目标代码,生成可执行程序[链接器ld]。

参数:

gcc -E   : 只做预处理,预处理后停止编译,把hello.c变成hello.i。该操作不生成一个文件,但可以手动保存至某文件:gcc -E hello.c > hello.i 。

gcc -S   : 只预处理、编译,输出 .s 的汇编语言文件,把hello.c/i变成hello.s。gcc -S hello.c(生成的.s文件名字必定是hello.s,可以用 -o改名)。也可以gcc -E hello.c > hello.i后gcc -S hello.i ,也会生成hello.s 。

gcc -c  : (compile),只预处理、编译、汇编,生成 .o文件(obj文件, object file),不链接。eg: gcc -c hello.c(生成的 .o文件的名字必定是hello.o,可以用 -o改名),也可以在gcc -S hello.c生成hello.s后gcc -c hello.s 。试了也可以gcc -c hello.i ,可见,gcc可以对从.c源文件到当前状态的所有类型文件都gcc 。

gcc     : 链接 link,(将.o文件连接成可执行文件),gcc hello.o ,也可以:gcc hello.c/i/s/o  。

gcc -o   : 在生成可执行文件时,指定输出文件名:gcc hello.o -o <filename>(-o位置不能乱动,指定的输出文件名貌似必须跟在 -o 后面)。gcc -E/-S/-c 都可以加 -o 。不加-o输出文件名默认a.out。eg: gcc hello.exe -o hello.c , gcc *.c -o jxw,之后会生成文件jxw,执行命令 ./jxw 即可运行。

gcc -v   : 显示编译过程中很多的信息。

gcc -Wall : 生成所有警告信息。比如某个定义后的变量没有使用。

gcc -w   : 不生成警告信息

gcc -g   : 在编译时,产生调试信息。

~ 遇到要include其他分文件编写的情况:头文件不用编译,编译时不用写头文件,写出所有.c文件即可;但 .h 必须在编译器能够找到的地方,比如和main.c在同一文件夹。如:main函数在jxw1.c里,jxw2.h和jxw2.c是一对分文件编写,jxw1.c里#include "jxw2.h":gcc jxw1.c jxw2.c -o jxw,之后会生成文件jxw,执行命令 ./jxw 即可运行。方法2:gcc *.c,之后生成a.out文件,如果先前有a.out,会覆盖。(如果在本地Visual C++ 6.0编写这三个文件,可能会:编译成功,运行报错,原因大概是编译时没有把jxw2.c编进去;如果gcc jxw1 .c -o jxw 会报错。)


 

Kconfig 语法

重要 eg : 完整的提交见:http://10.8.9.113/c/MTKD9000/ap_vendor/+/152256 ,该例子简单易看懂。

drivers/odmm 下有6个文件:Kconfig, Makefile, hardadc-diag.c, hardware_info.c, mcu_version.c, mmc_use_protect.c 

drivers/odmm/Makefile 里面恰好对应4个.c文件:
obj-$(CONFIG_HWADC_DIAG)    += hardadc-diag.o
obj-$(CONFIG_MMC_USE_PROTECT_***_CUST)     += mmc_use_protect.o
obj-$(CONFIG_HARDWARE_INFO)     += hardware_info.o
obj-$(CONFIG_GET_MCU_VERSION)     += mcu_version.o

在 kernel-5.10/arch/arm64/configs/mgk_64_k510_defconfig 里加:
CONFIG_HWADC_DIAG=m

drivers/Makefile 里要加:
obj-y    += odmm/
# 这个表示include了odmm目录下的Makefile。

drivers/Kconfig 里要加:
source "drivers/odmm/Kconfig"

drivers/odmm/Kconfig 里面是:
config HWADC_DIAG
    tristate "Enable Hwadc Detect Support"
    help
        The driver for Hwadc detect to electricity
config MMC_USE_PROTECT_***_CUST
    bool "MMC_USE_PROTECT_***_CUST Feature"
    default y
    help
      support for mmc use record
等等。

安卓的编译

编译安卓系统命令:3步:1.source build/envsetup.sh, 2.lunch <一个特定的类型>, 3.make
在linux下:ap目录下:
(1)source build/envsetup.sh     //加载命令和分支;作用:将envsetup.sh里的所有命令加载到系统环境变量里(也叫“初始化编译环境”)
(2)lunch lahaina-userdebug    //选择平台(分支)以及平台相关的编译选项,AP12 是 lunch qssi-userdebug
(3)make aboot -j8     // 执行编译

在 ap/build/make/core/main.mk或device/qcom/common/generate_extra_images.mk中:

main.mk 里面有: .PHONY: bootimage。(在上面的文件中,可以搜索在out目录输出的目标文件:$(PRODUCT_OUT)/boot.img)

.PHONY开头,后面就是可以单独编译的image

main.mk中的模块:有很多 .PHONY: ***image

make aboot :编译bootloader目录。会生成:AP/AP_11/out/target/product/lahaina/abl.elf。

make bootimage  :编译kernel, 会生成AP/AP_11/out/target/product/lahaina/boot.img;

make dtboimage  :会生成AP/AP_11/out/target/product/lahaina/dtbo.img。
会调用: ./build/core/main.mk , .PHONY: bootimage , 之后会生成boot.img,这个镜像文件中包含Linux Kernel,Ram disk,生成的boot.img只能通过fastboot进行烧写,这在只修改了Linux内核的时候有用。

Little Kernel bootloader的mk文件是:/bootable/bootloader/lk/AndroidBoot.mk , linux kernel的mk文件是/kernel/AndroidKernel.mk。

make clean 可以完全成功:Entire build directory removed.

$ grep -rn ".PHONY: aboot"
device/qcom/common/generate_extra_images.mk:216:.PHONY: aboot
vendor/qcom/opensource/core-utils/build/generate_extra_images.mk:230:.PHONY: aboot

(老王说dtbo编译脚本在这里: LINUX/android/vendor/qcom/proprietary/devicetree-4.19/qcom )

build/envsetup.sh :提供了几个有用的shell命令,使用source build/envsetup.sh可以引入到shell环境中。vendor/和device/目录下还有vendorsetup.sh脚本,envsetup.sh还同时会把他们引入。
envsetup.sh中的函数包括:
(1)lunch :配置lunch。
(2)m :make from top; 从树顶make;编译整个安卓系统;
(3)mm   :make from current directory; 构建(build)当前目录中的所有模块;说法2:编译当前目录下的模块,当前目录下需要有Android.mk,否则就往上找最近的Android.mk文件。注:本地模块的Makefile文件就是我们在安卓里随处可见的Android.mk。安卓进行编译时会通过下面的函数来遍历所有子目录中的Android.mk,一旦找到就不会再往子目录继续寻找(所以你的模块定义的顶层Android.mk必须包含自己定义的子目录中的Android.mk)。
(4)mmm :make the supplied directory; 构建(build)所提供的目录中所有模块(可以编译单个源码模块);编译指定路径下的模块,指定路径下需要有Android.mk。必须全编过才能用。必须source、lunch后才能用。
(5)add_lunch_combo:添加lunch项目。

envsetup.sh主要做了下面几个事情:1.定义一些 lunch, m, mm, mmm, provision 等函数。2.确认当前shell环境,建立shell命令。3.从device/vendor/product 等目录遍历搜索 vendorsetup.sh,并source进来。4.将下面这些bash文件导入到当前环境中:system/core/adb/adb.bash、system/core/fastboot/fastboot.bash、tools/asuite/asuite.sh。

lunch: 有以下几种,userdebug和tests不能交给最终用户。

eng: 工程机

user: 最终用户机

userdebug: 调试测试机

tests: 测试机

安卓:

单编 external / toybox 目录(实测成功)

单编前要 source , lunch。

单编指令:
$ cd  AP_11/external/toybox/
$ mm

生成文件的位置:out/target/product/<target>/system/bin/toybox ,这里面 grep 可以找到修改后的内容。

编译后的 push 位置:  system/bin/toybox 

mtk :

单编:

把全编指令(python vendor/mediatek/proprietary/scripts/releasetools/split_build_helper.py ***)中的   --run 去掉,会输出 编译每个image的cmd以及最后merge的cmd

单编这个路径:ap_vendor/vendor/mediatek/proprietary/custom/mt6983/hal/pq

在ap_vendor目录下 :

source build/envsetup.sh && export OUT_DIR=out_hal && lunch hal_mgvi_spm_64_armv82-userdebug
mmm vendor/mediatek/proprietary/custom/mt6983/hal/pq 2>&1 | tee pq.log

生成的libpq_cust_base.so 路径:out_hal/target/product/mgvi_spm_64_armv82/vendor/lib64/mt6983/libpq_cust_base.so
out_hal/target/product/mgvi_spm_64_armv82/vendor/lib/mt6983/libpq_cust_base.so

刷机生效,将上述 .so push 到设备中:
adb remount
adb push libpq_cust_base.so vendor/lib64
adb push libpq_cust_base.so vendor/lib

注意64位与32位的区别,都要push
adb reboot 即可生效

单编 vendor\mediatek\proprietary\hardware\libc2 :

和上面那个mmm一样先 source、lunch。
mmm vendor/mediatek/proprietary/hardware/libc2
会生成 out_hal\target\product\mgvi_spm_64_armv82\vendor\lib或lib64/libcodec2_mtk_vdec.so 。主机中在 vendor/lib或lib64下,分别换到lib和lib64下。
然后 adb shell sync,再 adb reboot 。

单编 vendor/mediatek/proprietary/hardware/pq/v2.0/lib/mmlpq  :

和上面那个mmm一样先 source、lunch。然后:
mmm vendor/mediatek/proprietary/hardware/pq/v2.0/lib/mmlpq/  2>&1 | tee mmlpq.log
// 这个单编前不需要全编
// 会生成 out_hal\target\product\mgvi_spm_64_armv82\vendor\lib或lib64/mt6983/libpqpconfig.so ,分别push到 vendor/lib或lib64/mt6983 下。

mtk: 修改 hardware 中的代码,如何不全编生效(实测成功):

编译这个目录: ap_vendor/vendor/mediatek/proprietary/hardware/libhwcomposer

使用 ninja 来快速编译,但是需要先 full build 一次或 mm / mmm 一次,生成 out/combined**.ninja 文件。如果修改了 Android.bp 或 Android.mk 文件,需要使用 make 或 mm / mmm 编译一次,以重新生成 **.ninja 文件。

修改 ap_vendor/vendor/mediatek/proprietary/hardware/libhwcomposer/2.0.0/bliter_async.cpp ,加了一句 BLOGE(**);    ,
在 ap_vendor 执行:./prebuilts/build-tools/linux-x86/bin/ninja -f out_hal/combined-hal_mgvi_spm_64_armv82.ninja hwcomposer.mtk_common(CSDN上有问题,直接复制可能执行失败)
(该命令编译了这个目录: ap_vendor/vendor/mediatek/proprietary/hardware/libhwcomposer )
会生成:  out_hal/target/product/mgvi_spm_64_armv82/obj/SHARED_LIBRARIES/hwcomposer.mtk_common_intermediates/hwcomposer.mtk_common.so(看时间戳是新生成的),push 到机器的 vendor/lib64/hw/ 里(要 remount)。

make, Makefile

执行make时,会查找当前的Makefile文件或者makefile文件并且执行。在安卓顶级源码目录下有个Makefile,它只有三行:

### DO NOT EDIT THIS FILE ###
include build/make/core/main.mk
### DO NOT EDIT THIS FILE ###

所以,真正执行的是 build/make/core/main.mk 。

make命令:

make可以批量处理C语言文件,在.c文件很多的情况下很方便。一旦Makefile文件编写好后在linux命令行中执行一条make命令即可自动编译整个工程。make默认在工作目录中寻找名为GNUmakefile、makefile、Makefile的文件作为makefile输入文件。最好不用GNUmakefile,这是GNU的make识别的,最好用Makefile,因为第一个字母大写很醒目。

make命令格式:

make  [ options ] [ targets ]

[ options ]

-f  <file>  :-f 可以指定以上名字以外的文件作为makefile输入文件,因为makefile可以重命名为其他名字,但建议还是用:Makefile。-f也可以改为--file。

-v: 显示make工具的版本信息

-C dir:读取makefile之前改变工作路径至dir目录; 说法二:跳转到指定的目录,读取那里的Makefile。

-n:(同 --just-print ) 只打印要执行的命令,但不执行命令。

-s:执行但不打印执行的命令。make参数-s或--slient则是全面禁止命令的显示。这样echo就不用加@了。

-w: 在处理makefile之前和之后显示工作路径

-i :有时,命令的出错并不表示就是错误的,加上 -i ,表示不管命令出不出错都认为是成功的,可以忽略命令的出错。也可以在Makefile命令前加一个 - 。

-k :(同 --keep-going )如果某规则中 的命令出错了,那么就终止该规则的执行,但继续执行其它规则。

-j <N>  : 设置并发数,即同时允许N个任务;又叫使用到的CPU核数,在配置好的电脑上特别有用,一般选8。

[ targets ]:

若使用make命令时没有指定目标,则make工具默认会实现makefile文件内的第一个目标,然后退出

指定了make工具要实现的目标(伪目标),目标可以是一个或多个(多个目标间用空格隔开)。

Makefile里有一些变量放在Kconfig里面的。查查二者的关系。

Makefile文件语法: 赋值

=            :最基本的赋值。eg: x = A, y = $(x), x = B, 然后echo $(y)会输出B,即取决于最后一次赋值。

:=           :和上面=的区别是,eg: x = A, y := $(x), x = B, 然后echo $(y)会输出A,即只用前面定义好的值。但是这只在赋值给其他变量时生效:如果:x = a, x := b, echo ${x} 还是输出b。

?=           :eg: x ?= b, 这代码意思是 如果变量x前面没有被赋值则此变量就是b,如果前面已经赋值了就用前面的值。

+=           :Makefile里的变量都是字符串,+=为追加字符串:jxw = aaa , jxw += bbb ,此时echo ${jxw}变成了“aaa bbb”,中间有个空格。

(上面这些“符号”的左右最好加上空格)

obj-y := 编译进内核的文件(夹)列表(可以同时弄2个:obj-y += asm330lhh.o asm330lhh_reg.o)

obj-m := 编译成外部可加载模块的列表(怎么记:m就是module)

lib-y := 和 lib-m := 编译成库文件

always := 总是需要被编译的模块

targets := 编译目标

subdir-y := 和 subdir-m := 表示需要递归进入的子目录

ifeq, else, endif,

eg:

ifeq ($(CC), gcc)

      echo gcc 

else

      echo cc 

endif

ifeq: 表示条件语句的开始,并指定一个条件表达式,表达式包含两个参数,以逗号分隔,表达式以圆括号括起来,表示判断表达式与某个值是否相等。

else: 条件表达式为假的情况;

endif: 任何一个条件表达式都以endif结束。

ifneq: 和ifeq相反,判断后面括号里两个值是否不同。

ifdef: 例子:ifdef <variable-name>; 如果变量variable-name的值非空,则表达式为真,否则,表达式为假。variable-name也可以是一个函数的返回值。注:ifdef只是测试一个变量是否有值,不会把变量扩展到当前位置。

ifndef: ifnotdefine和ifdef相反。

(ifeq这些类似于预处理,在某个target下面作为command时,ifeq这些需要顶格写,否则make报错,ifeq后面的命令需要tab)

变量:

先定义变量,再 $( ) / ${}来使用(索性和shell里面一样用${})。makefile里的变量是个字符串,可以理解为C语言/C++中的宏(如果变量objects = *.o ,则不是说*.o会展开,而是objects的值就是*.o),当Makefile被执行时,其中的变量都会被原模原样地展开到使用的位置上。与C/CPP不同的是:可以在makefile中改变其值,变量可以用在 目标、依赖文件、命令、makefile的其他部分中。

变量名字可以包括:字符、数字、下划线,可以是数字开头,但不能有 : # = 或空字符(空格、回车等)。变量是大小写敏感的,传统的makefile变量名都是大写的。

使用变量时,要在变量名前面加$,最好用()或{}把变量名包起来。如果要使用$字符,用 $$ 或 \$$。

有时遇到一行内给某个变量赋了两个值,用空格隔开。如:jxw = mediatek/spm8673p1_64 mediatek/spm8673p2_64,则@echo ${jxw}后,输出:mediatek/spm8673p1_64 mediatek/spm8673p2_64 ,但编译时某些代码会把jxw处理为编译两个东西。

自动化变量:

$0 :当前脚本文件名。

Sn (n>=1) :传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是 $1,第二个参数是 $2。

$# :传递给脚本或函数的参数个数。

$* :传递给脚本或函数的所有参数。

$@ :是上一句“目标:依赖”中的目标文件

$^ :是上一句“目标:依赖”中的全部依赖文件

$< :是上一句“目标:依赖”中的第一个依赖文件

$(@D) : 指向$@的目录名

$(@F) : 指向$@的文件名

$(<D) : 指向 $< 的目录名

$(<F) : 指向 $< 的文件名

(更多自动变量见官网 https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html)

默认的宏定义 / 内置变量

CC : cc(可以自己改为gcc)。可以直接用,如echo ${CC}。

CPP   : cc -E。 C预编译器的名称,默认值为 $(CC) -E,即echo ${CPP} 输出cc -E。

CXX : g++。++C++ 编译器的名称,默认值为 g++。

FC : f77。FORTRAN编译器的名称,默认值为 f77。

RM : rm -f

(更多内置变量见官网 https://www.gnu.org/software/make/manual/html_node/Implicit-Variables.html ,上面这些都可以在该网页查到)(默认变量没有OBJ)

下面实测输出都是空

hide : $(hide)相当于@,AP11/build/make/core/config.mk里有hide := @  。

如果 Makefile 中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。定义 这种命令序列的语法以 define 开始,以 endef 结束,如:

define jxw

pwd

ls

endef

mytool:tool1.c

        ${jxw}

语法:

目标target*** : 依赖prerequisites***

命令command 1

命令command 2

格式也可以写成:target : prerequisites ; command

1.目标即要生成的文件,可以是object file,也可以是可执行文件。命令要执行必须满足以下两个条件之一:1.没有“目标”这个文件,2.“依赖”文件比“目标”文件新。

2.依赖:生成目标所需的文件或目标。

3.命令:命令前必须是tab,不能是4个空格,要在linux上vim时能变色;即前面有tab才可能是“命令”;这里命令可以是任何shell命令。每个命令只在当前那一行生效,如果你要让上一条命令的结果应用在下一条命令,要用分号分隔这两条命令,如cd ..; pwd 和cd .. 换行 pwd。

一个makefile中有多个规则,但最终形成一个makefile文件,且只有一个终极目标,默认情况下Makefile的第一个目标为终极目标。

Makefile中的“命令”会被打印出来,可以在命令前加 "@" 隐藏打印。

Makefile里echo也是打印字符串。格式:echo <字符串>。在echo前面加@隐藏打印。echo后面加不加""都一样。打印字符串的另一种方法:$(warning <字符串>),或 $(info <字符串>) 。

Makefile里在行末加“ \ ”也表示下一行接在这行后面,同C语言一样。

Makefile里 # 表示注释,只有行注释,和C语言的//一样。如果要使用#,用反斜杠转义,/#。

eg1:

edit : main.o main.h add.o

gcc main.o main.h add.o  -o edit

add.o : add.c add.h

gcc -c add.c

格式:1.目标文件:如edit add.o 都是目标文件,就是这个规则要生成的东西,但edit是最终要生成的,第一条规则就是最终目标,其他目标都是为了生成edit服务的。2.依赖文件:目标文件是依赖这个文件的,如:main.o main.h add.o add.c ......  3. 执行make命令,当文件名是makefile时执行make命令,makefile是make读入的唯一配置文件,系统默认调用它。之后会生成一个可执行文件,文件名就是上面的命名为edit的文件,执行./edit即可。

以此为例讲make是怎么工作的:输入make命令,则:1.make在当前目录下找名字叫Makefile或makefile的文件,2.如果找到,它会找文件中第一个目标文件target,在上例中会找到edit并把这作为最终的目标文件,3.如果edit不存在,或edit所依赖的后面的.o文件的修改时间比edit这个文件新,那么就执行后面所定义的命令来生成edit(如果这个例子已经被编译过,然后又改了add.c,则在下一次make的时候add.c比add.o要新,add.o要被重新编译,这样add.o的时间又比edit新,edit又要重新链接了),4.如果edit所依赖的.o文件不存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到,则根据对应的依赖规则生成.o文件。

以上就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最后编译出第一个目标文件,在寻找的过程中,如果出错,如最后被依赖的文件找不到,make就会退出并报错。make只管文件的依赖性,而对定义的命令的错误或编译不成功则不管。

eg2: (在进行eg2之前需要先编写本文件中接着eg2之后的那5个文件,该例子参考https://zhuanlan.zhihu.com/p/558438717)

mytool : main.o tool1.o tool2.o #最终目标文件

gcc main.o tool1.o tool2.o -o mytool #这里 .o文件写了两遍,有时.o文件很多,所以要简化

main.o : main.c #中间目标文件

gcc main.c -c -Wall -g -o main.o

# -Wall可以看到所有的警告;-g可以调试;-c只执行到汇编,不链接。其实就是gcc编译。这里-c必须写。

tool1.o : tool1.c

gcc tool1.c -c -Wall -g -o tool1.o

tool2.o : tool2.c

gcc tool2.c -c -Wall -g -o tool2.o

.PHONY: clean

#这一句可以不加,但最好加上。.PHONY 表示clean是个伪目标文件。不生成clean这个文件。伪目标并不是一个文件,只是一个标签,由于伪目标不是文件,所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显示地指明这个目标才能让其生效。当然,伪目标的取名不能和文件名重名。如果没有clean这个伪目标,则make clean报错,其他伪目标如all都是需要自己定义的。

clean:    #这个是伪目标文件。make clean则执行下面命令。

-rm *.o mytool -rf #意思是 f强制 r递归 删除 .o后缀的文件和mytool。前面的减号可以不加,但最好加上,这减号意思是:也许某些文件出现问题,但是不要管,继续后面的事。

#保存退出,直接执行make,会生成可执行文件mytool,最后./mytool即可。(如果之前那个文件名不是makefile,而是xxx,则需要这样: make -f xxx)(如果没有进行防头文件重复包含设置会出错;如果已经make后的情况下没有删除.o文件也会出错,不会覆盖)。也可以make clean ; make tool1.o ; make mytool; make -f xxx clean(如果自己命名makefile)这样输入命令。   注:上面那个clean不是一个文件,它很像C语言中的lable,它没有被第一个目标文件直接或间接关联,它的冒号后面什么也没有,所以make不会自动去找文件的依赖,就不会自动执行其后定义的命令,要执行其后的命令,就要在make命令后面指出这个label的名字,就像: make clean 。

最终目标后面的.o依赖文件一般可以命名为objects, OBJECTS, objs, OBJS, obj, OBJ,就是obj文件,但这不是默认宏定义。

1.

CC = gcc #这行注释掉也make不报错

OBJS = main.o tool1.o tool2.o #删掉这行会报错,OBJS不是默认宏定义。

CFLAGS = -c #不是预定义的宏定义,删掉报错

mytool:$(OBJS)

$(CC) $(OBJS) -o mytool

main.o:main.c

$(CC) main.c $(CFLAGS) -o main.o

tool1.o:tool1.c

$(CC) tool1.c $(CFLAGS) -o tool1.o

tool2.o:tool2.c

$(CC) tool2.c $(CFLAGS) -o tool2.o

.PHONY: clean

clean:   

rm *.o mytool -rf

2.

OBJS = main.o tool1.o tool2.o

CFLAGS = -c -Wall -g

mytool:$(OBJS)

$(CC) $^ -o mytool

main.o:main.c

$(CC) $^ $(CFLAGS) -o $@

tool1.o:tool1.c

$(CC) $^ $(CFLAGS) -o $@

tool2.o:tool2.c

$(CC) $^ $(CFLAGS) -o $@

clean:   

$(RM) *.o mytool -r

3.

# %.o:%.c 百分号相当于一个通配符,%.c表示所有以.c结尾的文件。类似于Linux里的* 。

OBJS = main.o tool1.o tool2.o

CFLAGS = -c -Wall -g

mytool:$(OBJS)

$(CC) $^ -o mytool

%.o:%.c

$(CC) $^ $(CFLAGS) -o $@

clean:   

$(RM) *.o mytool -r

4.  make自动推导:只要make看到一个.o文件,就会自动把其对应的.c文件加在依赖文件中,如make找到一个files.o,那么files.c就是files.o的依赖文件,并且gcc -c files.c也会被推导出来。eg:

files.o : files.c    

gcc -c files.c

可以写成:

files.o :   

下面不加命令

没有makefile的项目如何创建运行: 创建5个文件:touch main.c tool1.c tool1.h tool2.c tool2.h

//tool1.h, 声明函数

#ifndef TOOL1_H__

#define TOOL1_H__

void mytool1(void);

#endif

//tool1.c

#include <stdio.h>

#include"tool1.h"

void mytool1(void){

    printf("tool1 print\n");

}

//tool2.h

#ifndef TOOL2_H__

#define TOOL2_H__

void mytool2(void);

#endif

//tool2.c

#include<stdio.h>

#include"tool2.h"

void mytool2(void){

    printf("tool2 print\n");

}

//main.c

#include<stdio.h>

#include"tool1.h"

#include"tool2.h"

int main(){

mytool1();

mytool2();

return 0;

}

编译运行:gcc *.c  (注意到没有编译.h),之后生成a.out文件,如果先前有a.out,会覆盖。

引用其他Makefile: 用include可以包含别的Makefile,和C语言很像,被包含的文件会原封不动地放到当前文件的包含位置。格式:

include <filename>

filename可以包含路径和通配符(如*.c)。在include之前可以有一些空字符,但绝不能是tab键。include和<filename>可以用一个或多个空格隔开。

eg: 现在有这样几个Makefile: a.mk, b.mk, c.mk, 还有一个foo.make,还有一个变量$(bar),该变量包含了e.mk, f.mk,那么,

include  foo.make  *.mk  $(bar)

等价于:

include  foo.make  a.mk b.mk c.mk e.mk f.mk

make命令开始时,先寻找include所指出的其他Makefile,并把其内容安置在当前位置,和C语言的#include类似。如果文件都没有指定绝对路径或相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么还会在下面几个目录下找:1.如果make执行时,有-I或--include-dir参数,那么make就会在这个参数所指定的目录下去寻找;2.如果目录<prefix>/include(一般是/usr/local/bin 或/usr/include)存在的话,make也会去找。

如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以在include前加一个减号-。如:-include <filename> 。其表示,无论include过程中出现什么错误,都不要报错继续执行。和其它版本make兼容的相关命令是sinclude,其作用和这一个是一样的。

make的工作方式:

GNU的make工作时的执行步骤如下:(其他的make也类似)

  1. 读入所有makefile
  2. 读入被include的其他makefile
  3. 初始化文件中的变量
  4. 推导隐晦规则,并分析所有规则
  5. 为所有的目标文件创建依赖关系链
  6. 根据依赖关系,决定哪些目标要重新生成。
  7. 执行生成命令

其中,1-5步为第一个阶段,6-7为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但make并不会完全马上展开,make使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。

通配符

make支持三种通配符:* , ? , [...] 。

*.c表示全部以.c为后缀的文件,如果文件名中有通配符如*,那么可以用转义字符/,/*表示真实的*字符。

通配符也可以出现在 依赖、命令 中。

~/test也表示在当前用户的$HOME目录下的test目录。

VPATH : 如果不指明这个变量,则make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,make就会在当前目录找不到的情况下,到所指定的目录中去寻找文件了。eg:

VPATH = src:../headers

上面的的定义指定了两个目录,src和../headers,make会按照这个顺序进行搜索。目录由冒号:分隔。(当然,当前目录永远是最高优先搜索的地方)

另一个设置文件搜索路径的方法是使用make的vpath关键字(注意,它是全小写的)。(详见:https://blog.csdn.net/whitefish520/article/details/103968609:搜“文件搜索”)

命令执行

当依赖目标新于目标时,也就是当规则的目标需要被更新时,make会一条一条的执行其后的命令。需要注意的是,如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令,你希望第二条命令得在cd之后的基础上运行,那么你就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。如:

示例一:

exec:

        cd /home/hchen

        pwd

示例二:

exec:

        cd /home/hchen; pwd

当我们执行make exec时,第一个例子中的cd没有作用,pwd会打印出当前的Makefile目录,而第二个例子中,cd就起作用了,pwd会打印出/home/hchen

嵌套执行make

在一些大的工程中,我们会把我们不同模块或是不同功能的源文件放在不同的目录中,我们可以在每个目录中都书写一个该目录的Makefile,这有利于让我们的Makefile变得更加地简洁,而不至于把所有的东西全部写在一个Makefile中,这样会很难维护我们的Makefile,这个技术对于我们模块编译和分段编译有着非常大的好处。例如,我们有一个子目录叫subdir,这个目录下有个Makefile文件指明了这个目录下文件的编译规则,那么我们总控的Makefile可以这样书写:

subsystem:

        cd subdir && $(MAKE)

其等价于:

subsystem:

$(MAKE) -C subdir

定义$(MAKE)宏变量相当于make,可能用起来方便点。上面两个例子都是先进入subdir目录,然后执行make命令。

我们把这个Makefile叫做总控Makefile,总控Makefile的变量可以传递到下级的Makefile中(如果你显示的声明),但是不会覆盖下层的Makefile中所定义的变量,除非指定了e参数。

如果你要传递变量到下级Makefile中,那么你可以使用这样的声明:

 export <variable ...>

如果你不想让某些变量传递到下级Makefile中,那么你可以这样声明:

unexport <variable ...>

内核树:putty上那个服务器已经自带Linux内核树,足以用来编译驱动程序。

自带的内核树位于:/lib/modules/<系统内核版本>/build

其中,系统内核版本可以用uname -r 来查看

eg3:

随便进入一个文件夹,将驱动代码放入这个目录,比如helloworld.c。然后创建一个Makefile,内容如下:

obj-m += helloworld.o

all:

make -C /lib/modules/4.15.0-29-generic/build M=$(shell pwd) modules

(!注:make前面必须是Tab缩进

上面也可写成:make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules)

代码解释:

1. obj-m += <.o文件>将指定文件(必须以 .o 结尾)设为编译时以模块形式编译。obj-m表示以模块形式编译生成 .o文件,(make那行才是真正的编译命令);

+=表示在obj-m原有内容的基础上再添加文件xxx.o,假如原来 obj = a.o,如后面有obj += b.o,则此时obj = a.o b.o

     = 是最基本的赋值

    := 是覆盖之前的值

    ?= 是如果没有被赋值过就赋予等号后面的值

    += 是添加等号后面的值

“=”make会将整个makefile展开后,再决定变量的值。也就是说,变量的值将会是整个makefile中最后被指定的值。eg:

            x = foo

            y = $(x) bar

            x = xyz

      在上例中,y的值将会是 xyz bar ,而不是 foo bar

“:=”表示变量的值决定于它在makefile中的位置,而不是整个makefile展开后的最终值。eg:

            x := foo

            y := $(x) bar

            x := xyz

      在上例中,y的值将会是 foo bar

2.make -C <目录> :跳转到指定的目录,读取那里的Makefile

3.M=<目录> :跳转到指定的目录,执行先前读取的Makefile

4.在源代码和Makefile所在目录下运行make,将在相同目录下生成模块helloworld.ko

设备树dts的编译

#dtbo-$(CONFIG_ARCH_***)

dtbo-$

dtb-$(CONFIG_ARCH_***) += xxx.dtb   : 例如xxx的开发板,只要设置CONFIG_ARCH_xxx = y, 所有用到这颗SOC的DTS都会编译成DTB。如果后续还用到了这颗SOC设计的开发板,只要新建一个DTS文件,并将对应名称的DTB文件名加到dtb-$(CONFIG_ARCH_***)中,在编译设备树时就会将DTS编译成二进制的DTB文件。

eg: 某Makefile文件里:

ifeq ( $(BSP_BUILD_FAMILY), XXX )

dtbo-$(CONFIG_ARCH_SPRD) += \

sp9863a-3h10-overlay.dtbo \

sp9863a-1c10-overlay.dtbo \

sp9863a-1h10-overlay.dtbo

sp9863a-3h10-overlay.dtbo-base := sp9863a.dtb

sp9863a-1c10-overlay.dtbo-base := sp9863a.dtb

sp9863a-1h10-overlay.dtbo-base := sp9863a.dtb

endif

该例是多dtbo结构,编译时同一个BSP_BUILD_FAMILY下的多个DTBO都会打包到dtbo.img中,其中sp9863a-3h10-overlay.dtbo-base、sp9863a-1c10-overlay.dtbo-base、sp9863a-1h10-overlay.dtbo-base 指定的sp9863a.dtb都会编译打包到dtb.img中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值