构建脚本的基础知识
GCC编译过程
以下内容为了便于操作以GCC为例,对于交叉编译工具链也是一样的。
一个C/C++文件要经过预处理(preprocessing)、编译(compilation)、汇编(assembly)和链接(linking)等4步才能变成可执行文件。

通过不同的gcc选项可以控制这些过程:

在日常交流中通常使用“编译”统称这4个步骤,如果不是特指这4个步骤中的某一个,本教程也依惯例使用“编译”这个统称。
gcc使用示例:
gcc hello.c // 输出一个名为a.out的可执行程序,然后可以执行./a.out
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 // 再链接
执行“gcc -o hello hello.c -v”时,可以查看到这些步骤:
cc1 main.c -o /tmp/ccXCx1YG.s
as -o /tmp/ccZfdaDo.o /tmp/ccXCx1YG.s
cc1 sub.c -o /tmp/ccXCx1YG.s
as -o /tmp/ccn8Cjq6.o /tmp/ccXCx1YG.s
collect2 -o test /tmp/ccZfdaDo.o /tmp/ccn8Cjq6.o ....
可以手工执行以下命令体验一下:
gcc -E -o hello.i hello.c
gcc -S -o hello.s hello.i
gcc -c -o hello.o hello.s
gcc -o hello hello.o
预处理
C/C++源文件中,以“#”开头的命令被称为预处理命令,如包含命令“#include”、宏定义命令“#define”、条件编译命令“#if”、“#ifdef”等。预处理就是将要包含(include)的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些东西输出到一个“.i”文件中等待进一步处理。
编译
编译就是把C/C++代码(比如上述的“.i”文件)“翻译”成汇编代码,所用到的工具为cc1(它的名字就是cc1,x86有自己的cc1命令,ARM板也有自己的cc1命令)。
汇编
汇编就是将第二步输出的汇编代码翻译成符合一定格式的机器代码,在Linux系统上一般表现为ELF目标文件(OBJ文件),用到的工具为as。x86有自己的as命令,ARM版也有自己的as命令,也可能是xxxx-as(比如arm-linux-as)。
“反汇编”是指将机器代码转换为汇编代码,这在调试程序时常常用到。
链接
链接就是将上步生成的OBJ文件和系统库的OBJ文件、库文件链接起来,最终生成了可以在特定平台运行的可执行文件,用到的工具为ld或collect2。
编译程序时,加上-v选项就可以看到这几个步骤。比如:
gcc -o hello hello.c -v
可以看到很多输出结果,我们把其中的主要信息摘出来:
cc1 hello.c -o /tmp/cctETob7.s
as -o /tmp/ccvv2KbL.o /tmp/cctETob7.s
collect2 -o hello crt1.o crti.o crtbegin.o /tmp/ccvv2KbL.o crtend.o crtn.o
以上3个命令分别对应于编译步骤中的预处理+编译、汇编和链接,ld被collect2调用来链接程序。预处理和编译被放在了一个命令(cc1)中进行的,可以把它再次拆分为以下两步:
cpp -o hello.i hello.c
cc1 hello.i -o /tmp/cctETob7.s
我们不需要手工去执行cpp、cc1、collect2等命令,我们直接执行gcc并指定不同的参数就可以了。
GCC常用选项
以下内容为例便于操作以GCC为例,对于交叉编译工具链也是一样的。
怎么编译多个文件
① 一起编译、链接:
gcc -o test main.c sub.c
② 分开编译,统一链接:
gcc -c -o main.o main.c
gcc -c -o sub.o sub.c
gcc -o test main.o sub.o
制作、使用动态库
制作、编译动态库:
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 test main.o -lsub -L /libsub.so/所在目录/
使用动态库时,先把libsub.so放到Ubuntu的/lib目录,然后就可以运行test程序。
如果不想把libsub.so放到/lib,也可以放在某个目录比如/a,然后如下执行:
export LD_LIBRARY_PATH=\$LD_LIBRARY_PATH:/a
./test
制作、使用静态库
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不在当前目录下,需要指定它的绝对或相对路径)
运行:不需要把静态库libsub.a放到板子上。
很有用的选项
gcc -E main.c // 查看预处理结果,比如头文件是哪个
gcc -E -dM main.c > 1.txt // 把所有的宏展开,存在1.txt里
gcc -Wp,-MD,abc.dep -c -o main.o main.c // 生成依赖文件abc.dep,后面Makefile会用
echo 'main(){}'| gcc -E -v - // 它会列出头文件目录、库目录(LIBRARY_PATH)
下面的资料来自GCC官方文档及一些中文资料,没必要逐一研读,用到时再翻翻。
常见的头文件问题
编译时找不到头文件。在程序中这样包含头文件:#include <xxx.h>
对于尖括号里的头文件,去哪里找它?
系统目录:就是交叉编译工具链里的某个include目录;
也可以自己指定:编译时用 “ -I dir ”选项指定。
怎么确定“系统目录”?
执行下面命令确定目录:
echo 'main(){}'\| \$CC -E -v -
它会列出头文件目录、库目录(LIBRARY_PATH)。
你需要在头文件目录中确定有没有这个文件,或是自己指定头文件目录。
常见的库文件问题
链接程序时如果有这样的提示:undefined reference to `xxx',它表示xxx函数未定义。
那么解决方法有2:
① 去写出这个函数
② 或是使用库函数,那需要在链接时指定库
怎么指定库?想链接libabc.so,那链接时加上:-labc。
库在哪里?
① 系统目录:就是交叉编译工具链里的某个lib目录
② 也可以自己指定:链接时用 “ -L dir ”选项指定
怎么确定“系统目录”?执行下面命令确定目录:
echo 'main(){}'\| \$CC -E -v -
它会列出头文件目录、库目录(LIBRARY_PATH),你编译出库文件时,可以把它放入系统库目录。
Makefile的使用
Makefile规则
一个简单的Makefile文件包含一系列的“规则”,其样式如下:
目标(target)…: 依赖(prerequiries)…
<tab>命令(command)
目标(target)通常是要生成的文件的名称,可以是可执行文件或OBJ文件,也可以是一个执行的动作名称,诸如`clean’。
依赖是用来产生目标的材料(比如源文件),一个目标经常有几个依赖。
命令是生成目标时执行的动作,一个规则可以含有几个命令,每个命令占一行。
**注意**:每个命令行前面必须是一个Tab字符,即命令行第一个字符是Tab。这是容易出错的地方。
通常,如果一个依赖发生了变化,就需要规则调用命令以更新或创建目标。但是并非所有的目标都有依赖,例如,目标“clean”的作用是清除文件,它没有依赖。
规则一般是用于解释怎样和何时重建目标。make首先调用命令处理依赖,进而才能创建或更新目标。当然,一个规则也可以是用于解释怎样和何时执行一个动作,即打印提示信息。
一个Makefile文件可以包含规则以外的其他文本,但一个简单的Makefile文件仅仅需要包含规则。虽然真正的规则比这里展示的例子复杂,但格式是完全一样的。
对于上面的Makefile,执行“make”命令时,仅当hello.c文件比hello文件新,才会执行命令“arm-linux-gcc –o hello hello.c”生成可执行文件hello;如果还没有hello文件,这个命令也会执行。
运行“make clean”时,由于目标clean没有依赖,它的命令“rm -f hello”将被强制执行。
Makefile变量与赋值
变量的定义语法形式如下:
> immediate = deferred > > immediate ?= deferred > > immediate := immediate > > immediate += deferred or immediate > > define immediate > > deferred > > endef
在GNU make中对变量的赋值有两种方式:延时变量、立即变量。区别在于它们的定义方式和扩展时的方式不同,前者在这个变量使用时才扩展开,意即当真正使用时这个变量的值才确定;后者在定义时它的值就已经确定了。使用`=’,`?=’定义或使用define指令定义的变量是延时变量;使用`:=’定义的变量是立即变量。需要注意的一点是,`?=’仅仅在变量还没有定义的情况下有效,即`?=’被用来定义第一次出现的延时变量。
对于附加操作符`+=’,右边变量如果在前面使用(:=)定义为立即变量则它也是立即变量,否则均为延时变量。
Makefile常用函数
函数调用的格式如下:
$(function arguments)
这里`function’是函数名,`arguments’是该函数的参数。参数和函数名之间是用空格或Tab隔开,如果有多个参数,它们之间用逗号隔开。这些空格和逗号不是参数值的一部分。内核的Makefile中用到大量的函数,现在介绍一些常用的。
字符串替换和分析函数
① $(subst from,to,text)
在文本`text’中使用`to’替换每一处`from’。
比如:$(subst ee,EE,feet on the street)
结果为‘fEEt on the strEEt’。
② $(patsubst pattern,replacement,text)
寻找`text’中符合格式`pattern’的字,用`replacement’替换它们。`pattern’和`replacement’中可以使用通配符。
比如:$(patsubst %.c,%.o,x.c.c bar.c)
结果为:`x.c.o bar.o’。
③ $(strip string)
去掉前导和结尾空格,并将中间的多个空格压缩为单个空格。
比如:$(strip a b c )
结果为`a b c’。
④ $(findstring find,in)
在字符串`in’中搜寻`find’,如果找到,则返回值是`find’,否则返回值为空。
比如:
$(findstring a,a b c)
$(findstring a,b c)
将分别产生值`a’和`’(空字符串)。
⑥ $(filter pattern...,text)
返回在`text’中由空格隔开且匹配格式`pattern...’的字,去除不符合格式`pattern...’的字。
比如:$(filter %.c %.s,foo.c bar.c baz.s ugh.h)
结果为`foo.c bar.c baz.s’。
⑦ $(filter-out pattern...,text)
返回在`text’中由空格隔开且不匹配格式`pattern...’的字,去除符合格式`pattern...’的字。它是函数filter的反函数。
比如:$(filter %.c %.s,foo.c bar.c baz.s ugh.h)
结果为‘ugh.h’。
⑧ $(sort list)
将‘list’中的字按字母顺序排序,并去掉重复的字。输出由单个空格隔开的字的列表。
比如:$(sort foo bar lose)
返回值是‘bar foo lose’。
文件名函数
① $(dir names...)
抽取‘names...’中每一个文件名的路径部分,文件名的路径部分包括从文件名的首字符到最后一个斜杠(含斜杠)之前的一切字符。
比如:$(dir src/foo.c hacks)
结果为‘src/ ./’。
② $(notdir names...)
抽取‘names...’中每一个文件名中除路径部分外一切字符(真正的文件名)。
比如:$(notdir src/foo.c hacks)
结果为‘foo.c hacks’。
③ $(suffix names...)
抽取‘names...’中每一个文件名的后缀。
比如:$(suffix src/foo.c src-1.0/bar.c hacks)
结果为‘.c .c’。
④ $(basename names...)
抽取‘names...’中每一个文件名中除后缀外一切字符。
比如:$(basename src/foo.c src-1.0/bar hacks)
结果为‘src/foo src-1.0/bar hacks’。
⑤ $(addsuffix suffix,names...)
参数‘names...’是一系列的文件名,文件名之间用空格隔开;suffix是一个后缀名。将suffix(后缀)的值附加在每一个独立文件名的后面,完成后将文件名串联起来,它们之间用单个空格隔开。
比如:$(addsuffix .c,foo bar)
结果为‘foo.c bar.c’。
⑥ $(addprefix prefix,names...)
参数‘names’是一系列的文件名,文件名之间用空格隔开;prefix是一个前缀名。将preffix(前缀)的值附加在每一个独立文件名的前面,完成后将文件名串联起来,它们之间用单个空格隔开。
比如:$(addprefix src/,foo bar)
结果为‘src/foo src/bar’。
⑦ $(wildcard pattern)
参数‘pattern’是一个文件名格式,包含有通配符(通配符和shell中的用法一样)。函数wildcard的结果是一列和格式匹配的且真实存在的文件的名称,文件名之间用一个空格隔开。
比如若当前目录下有文件1.c、2.c、1.h、2.h,则:
> c_src := $(wildcard *.c)
结果为‘1.c 2.c’。
其他函数
① $(foreach var,list,text)
前两个参数,‘var’和‘list’将首先扩展,注意最后一个参数‘text’此时不扩展;接着,‘list’扩展所得的每个字,都赋给‘var’变量;然后‘text’引用该变量进行扩展,因此‘text’每次扩展都不相同。
函数的结果是由空格隔开的‘text’ 在‘list’中多次扩展后,得到的新‘list’,就是说:‘text’多次扩展的字串联起来,字与字之间由空格隔开,如此就产生了函数foreach的返回值。
下面是一个简单的例子,将变量‘files’的值设置为 ‘dirs’中的所有目录下的所有文件的列表:
> dirs := a b c d > > files := $(foreach dir,$(dirs),$(wildcard $(dir)/*))
这里‘text’是‘$(wildcard $(dir)/*)’,它的扩展过程如下:
a. 第一个赋给变量dir的值是`a’,扩展结果为‘$(wildcard a/*)’;
b. 第二个赋给变量dir的值是`b’,扩展结果为‘$(wildcard b/*)’;
c. 第三个赋给变量dir的值是`c’,扩展结果为‘$(wildcard c/*)’;
如此继续扩展。
这个例子和下面的例有共同的结果:
> files := $(wildcard a/* b/* c/* d/*)
② $(if condition,then-part[,else-part])
首先把第一个参数‘condition’的前导空格、结尾空格去掉,然后扩展。如果扩展为非空字符串,则条件‘condition’为‘真’;如果扩展为空字符串,则条件‘condition’为‘假’。
如果条件‘condition’为‘真’,那么计算第二个参数‘then-part’的值,并将该值作为整个函数if的值。
如果条件‘condition’为‘假’,并且第三个参数存在,则计算第三个参数‘else-part’的值,并将该值作为整个函数if的值;如果第三个参数不存在,函数if将什么也不计算,返回空值。
**注意:**仅能计算‘then-part’和‘else-part’二者之一,不能同时计算。这样有可能产生副作用(例如函数shell的调用)。
③ $(origin variable)
变量‘variable’是一个查询变量的名称,不是对该变量的引用。所以,不能采用‘$’和圆括号的格式书写该变量,当然,如果需要使用非常量的文件名,可以在文件名中使用变量引用。
函数origin的结果是一个字符串,该字符串变量是这样定义的:
> ‘undefined' :如果变量‘variable’从没有定义; > > ‘default' :变量‘variable’是缺省定义; > > ‘environment' :变量‘variable’作为环境变量定义,选项‘-e’没有打开; > > ‘environment override' :变量‘variable’作为环境变量定义,选项‘-e’已打开; > > ‘file' :变量‘variable’在Makefile中定义; > > ‘command line' :变量‘variable’在命令行中定义; > > ‘override' :变量‘variable’在Makefile中用override指令定义; > > ‘automatic' :变量‘variable’是自动变量
④ $(shell command arguments)
函数shell是make与外部环境的通讯工具。函数shell的执行结果和在控制台上执行‘command arguments’的结果相似。不过如果‘command arguments’的结果含有换行符(和回车符),则在函数shell的返回结果中将把它们处理为单个空格,若返回结果最后是换行符(和回车符)则被去掉。
比如当前目录下有文件1.c、2.c、1.h、2.h,则:
> c_src := $(shell ls *.c)
结果为‘1.c 2.c’。
《Makefile介绍》这小节可以在阅读内核、bootloader、应用程序的Makefile文件时,作为手册来查询。下面以options程序的Makefile作为例子进行演示,Makefile的内容如下:
> File: Makefile > > 01 src := $(shell ls *.c) > > 02 objs := $(patsubst %.c,%.o,$(src)) > > 03 > > 04 test: $(objs) > > 05 gcc -o $@ $^ > > 06 > > 07 %.o:%.c > > 08 gcc -c -o $@ $< > > 09 > > 10 clean: > > 11 rm -f test *.o
上述Makefile中$@、$^、$<称为自动变量。$@表示规则的目标文件名;$^表示所有依赖的名字,名字之间用空格隔开;$<表示第一个依赖的文件名。‘%’是通配符,它和一个字符串中任意个数的字符相匹配。
options目录下所有的文件为main.c,Makefile,sub.c和sub.h,下面一行行地分析:
第1行src变量的值为‘main.c sub.c’。
第2行objs变量的值为‘main.o sub.o’,是src变量经过patsubst函数处理后得到的。
第4行实际上就是:
test : main.o sub.o
目标test的依赖有二:main.o和sub.o。开始时这两个文件还没有生成,在执行生成test的命令之前先将main.o、sub.o作为目标查找到合适的规则,以生成main.o、sub.o。
第7、8行就是用来生成main.o、sub.o的规则:
对于main.o这个规则就是:
> main.o:main.c > > gcc -c -o main.o main.c
对于sub.o这个规则就是:
> sub.o:sub.c > > gcc -c -o sub.o sub.c
这样,test的依赖main.o和sub.o就生成了。
第5行的命令在生成main.o、sub.o后得以执行。
在options目录下第一次执行make命令可以看到如下信息:
> gcc -c -o main.o main.c > > gcc -c -o sub.o sub.c > > gcc -o test main.o sub.o
然后修改sub.c文件,再次执行make命令,可以看到如下信息:
> gcc -c -o sub.o sub.c > > gcc -o test main.o sub.o
可见,只编译了更新过的sub.c文件,对main.c文件不用再次编译,节省了编译的时间。
Kconfig介绍
参考文档:任一个Linux内核的Documentation\kbuild\kconfig-language.rst
对于各类内核,只要支持menuconfig配置界面,都是使用Kconfig。在配置界面中,可以选择、设置选项,这些设置会保存在.config文件里。编译脚本会包含“.config”,根据里面的值决定编译哪些文件、怎么编译文件。“.config”文件也会被转换为头文件,C程序可以从头文件中获得配置信息。
本节使用一个“test_kconfig”工程来讲解,对于openvela,可以执行“m menuconfig”进行操作。
配置界面示例
执行“m menuconfig”命令,可以得到如下配置界面:

问题:
-
这个界面里,各个配置项来自哪里
-
这个界面里,这些配置项是怎么组织的
-
这个界面里,我们的选择、设置,结果保存在哪里
-
这些配置结果,怎么使用
配置结果的保存
在配置界面中操作的结果保存在.config文件中,示例如下:
> # >
> \# Automatically generated file; DO NOT EDIT.
> > # Configuration > > # > > # > > # test menu > > # > > CONFIG_TEST_A=y > > CONFIG_TEST_B=y > > CONFIG_TEST_C=y > > # CONFIG_TEST_D is not set > > # CONFIG_TEST_MENUCONFIG is not set
内核的编译脚本会包含.config文件,它会根据里面的变量选择内核特性。
描述单个配置项config
在menuconfig界面,可以看到这个配置项:

在配置界面,使用方向箭头游走到“TEST E”后,可以:
-
输入Y,选择配置项,在.config中对应“CONFIG_TEST_E=y”
-
输入N,不选择配置项,在.config中对应“# CONFIG_TEST_E is not set”
上图中的配置项怎么实现的?在Kconfig文件中,它对应下列代码:

上面是一个精简的例子,完整的例子可以从Linux中获得,如下:
> config SGI_SNSC
> bool "SGI Altix system controller communication support"
> depends on (IA64_SGI_SN2 || IA64_GENERIC)
> default y
> help
> If you have an SGI Altix and you want to enable system
> controller communication from user space (you want this!),
> say Y. Otherwise, say N.
解释如下:
-
config
表示config option,这是Kconfig的基本entry;其他entry是用来管理config的。
config 表示一个配置选项的开始,紧跟着的 SGI_SNSC 是配置选项的名称。
config 下面几行定义了该配置选项的属性。
属性可以是该配置选项的:类型、输入提示、依赖关系、默认值、帮助信息。 -
bool 表示配置选项的类型,每个 config 菜单项都要有类型定义,变量有5种类型
> bool 布尔类型 > > tristate 三态类型 > > string 字符串 > > hex 十六进制 > > int 整型
"SGI Altix system controller communication support":提示信息
-
depends on:表示依赖关系,只有(IA64SGISN2 || IA64GENERIC)被选中,才可以选择SGISNSC
-
select XXX:表示反向依赖关系,即当前配置选项被选中后,XXX选项就会被选中。
-
default 表示配置选项的默认值,bool 类型的默认值可以是 y/n。
-
help 帮助信息,在menuconfig界面输入H键时,就会提示帮助信息。
实现菜单menu/endmenu
代码如下:
> menu "test menu" > > config TEST_A > > bool "TEST A" > > default y > > config TEST_B > > bool "TEST B" > > default y > > endmenu
界面如下:
语法``解释如下:
-
menu
"xxx"表示一个菜单,菜单名是"xxx" -
menu
和endmenu之间的entry都是"xxx"菜单的选项 -
在上面的例子中子菜单有``2``个选项:
> "TEST A" > > "TEST B"
实现单选choice/endchoice
代码如下:
> choice > > prompt "test choise" > > default TEST_C > > config TEST_C > > bool "TEST C" > > default y > > config TEST_D > > bool "TEST D" > > default y > > endchoice
界面如下:

在上述界面中,对于“``test choise``”,有2个选择:
> TEST C > > TEST D
语法解释如下:
-
choice表示"选择"
-
choice和
endchoice之间的entry是可以选择的项目 -
它们之间,只能有
一个被设置为"y":表示编进内核 -
它们之间,可以设置多个为"m":表示编译为模块
-
比如一个硬件有多个
驱动程序:
同一时间只能有一个驱动能编进内核
但是多个驱动都可以单独编译为模块
menuconfig
menuconfig XXX和config XXX类似,唯一不同的是该选项除了能设置y/m/n外,还可以实现菜单效果(能回车进入该项内部)。
示例代码:
> menuconfig TEST_MENUCONFIG > > bool "test menuconfig" > > default n > > if TEST_MENUCONFIG > > config TEST_E > > bool "TEST E" > > default y > > config TEST_F > > bool "TEST F" > > default y > > endif
界面如下:

menuconfig常用格式有2种:
> menuconfig M
> if M
> config C1
> config C2
> endif
或:
> menuconfig M
> config C1
> depends on M
> config C2
> depends on M
第1项menuconfig M跟config M语法是一样的,不同之处在于menuocnfig M后面可以跟着好几个依赖于M的config C1、config C2等子配置项。
if/endif
在上面的menuconfig中就有if/endif的使用,它的语法如下:
> "if" <expr>
> <if block>
> "endif"
示例如下,只有定义了的TEST_MENUCONFIG项,“``TEST_E``”等才会显示出来:
> if TEST_MENUCONFIG > > config TEST_E > > bool "TEST E" > > default y > > config TEST_F > > bool "TEST F" > > default y > > endif
source
source 语句用于读取另一个文件中的 Kconfig 文件,比如:
> source "``abc``/Kconfig"
comment
comment 语句出现在界面的第一行,用于定义一些提示信息,如:
> comment "abc/ Configurations"
>
> source "abc/Kconfig"
界面如下: