您的位置:首页 > 运维架构 > Linux

Linux内核学习之编译篇

2015-09-27 12:56 567 查看

Linux内核学习之编译篇

Linux内核发展至今,文件数已经超过5万,代码量相当巨大。这一方面是内核功能不断增强补充的原因,另一方面当然是Linux的兼容性考虑,导致整个工程非常浩荡,而且很多文件名都一样,处于不同目录而已,这样一来,读者在学习阅读时就容易困惑,到底那个文件才是我需要的呢?这就涉及到本篇要谈到的问题。当然本文不只是告诉你怎么找文件,更主要是谈谈编译方面。

了解Makefile

Kconfig概述

Linux Makefile

了解Makefile

关于Makefile的语法,基本规则很简单,就是告诉make程序你的工程结构,你所需要编译的目标,以及目标又依赖于什么文件生成。最简单的makfile只要有这几个基本要素就够了。比如Linux 根目录下的Makefile中的一个规则如下:

config: scripts_basic outputmakefile FORCE
$(Q)$(MAKE) $(build)=scripts/kconfig $@


含义:config是编译目标,依赖于3个文件(或伪目标):scripts_basic outputmakefile FORCE,而接下来的一行以TAB键开始(必须如此),就是针对这个目标所要执行的命令,这个命令是Linux环境下可执行的程序或shell脚本。

更加具体的语法,我强烈推荐阅读陈皓的文章:跟我一起写Makefile

Kconfig概述

这又是什么东西?其实我很想直接就开始分析Linux Makefile的内容,但是Linux这个系统的配置比较复杂,又绕不开这个。本节主要简要介绍其作用,而具体的语法不打算详述。可以参考linux工程目录下的文档:Documentation\kbuild\kconfig-language.txt,英文偏弱的,就去度娘搜一些中文材料看看。不过本人建议结合配置界面和Kconfig对照就很清楚了。

我所用的版本为4.1.6,具体的版本可以用任意ftp软件连接ftp.kernel.org获取,或者直接登录kernel网页版

OK,我们随便打开一个Linux子目录的Makefile来大概了解一下,比如mm目录下的Makefile部分内容:

obj-$(CONFIG_SWAP) += page_io.o swap_state.o swapfile.o
obj-$(CONFIG_FRONTSWAP)    += frontswap.o
obj-$(CONFIG_ZSWAP)    += zswap.o
obj-$(CONFIG_HAS_DMA)  += dmapool.o
obj-$(CONFIG_HUGETLBFS)    += hugetlb.o


以上你会看到有很多
CONFIG_
开头的宏,这个哪里来的?别急,打开同一个目录下的Kconfig文件,截取部分如下:

config FRONTSWAP
bool "Enable frontswap to cache swap pages if tmem is present"
depends on SWAP
default n
help
Frontswap is so named because it can be thought of as the opposite
of a "backing" store for a swap device.  The data is stored into
"transcendent memory", memory that is not directly accessible or
addressable by the kernel and is of unknown and possibly
time-varying size.  When space in transcendent memory is available,
a significant swap I/O reduction may be achieved.  When none is
available, all frontswap calls are reduced to a single pointer-
compare-against-NULL resulting in a negligible performance hit
and swap data is stored as normal on the matching swap device.

If unsure, say Y to enable frontswap.


Now,你看到了”config FRONTSWAP“,这个配置项最终会构成”CONFIG_FRONTSWAP“,其值可以为n或y。这样就和前面提到的obj-$(CONFIG_FRONTSWAP)匹配上了,y表示obj-y指定的文件会最终编译进内核,n表示obj-n指定的文件不参与编译。看到这里,我希望你明白Kconfig里面的配置项和最终的宏的关系,也就是会自动添加上”CONFIG_“。

接下来,我们就应该要知道怎么来配置这些宏了,最一般的方式,我们通过在根目录下面执行如下命令来手动配置:

weimh1@weimh1-ubuntu:~/work/sourcecode/linux-4.1.6$ make menuconfig


这个编译目标(menuconfig)对应于根Makefile中的这条规则:

%config: scripts_basic outputmakefile FORCE
$(Q)$(MAKE) $(build)=scripts/kconfig $@


上面编译命令最终又会进入scripts/kconfig 中的Makefile执行这个目标:

menuconfig: $(obj)/mconf
$< $(silent) $(Kconfig)


之后,运行的结果如下图所示,请和前面的config FRONTSWAP 内容核对,就会大概明白其中一些含义:

首页面:



mm页面FRONTSWAP配置:



只是,因为kernel配置项繁多,要了解全部配置项基本是不可能,也不必要的。这种方式只是给你提供了一个入口,然后针对性修改相关项。通常,你可以基于某一个默认配置项文件进行配置,然后再通过这个menuconfig修改特定项。不同的架构有不同的默认文件,比如x86平台,可以在
arch/x86/configs
找到相关文件:i386_defconfig。通过执行
make i386_defconfig
即可基于这个文件生成.config文件,在此基础上可以再运行
make menuconfig
来进行个别的调整。更多的细节请参考:Documentation\kbuild\kconfig.txt。 总之,无论怎么配置,最终都是为了生成.config文件,这个文件的内容如下形式:



这些宏最终将影响Makefile中参与编译的文件。正如你前面看到的obj-xxx类型的部分。

Linux Makefile

有了前面2个的基础,现在再来看Linux Makefile就相对明朗了些。本节开始,我们将开始真正进入内核的编译。当然,前面的config也是Makefile的一部分。Makefile的阅读,一般直接从头开始。

内核完整的版本号,在开头即写明:

VERSION = 4
PATCHLEVEL = 1
SUBLEVEL = 6
EXTRAVERSION =
NAME = Series 4800


忽略掉注释部分,继续往下看:

MAKEFLAGS += -rR --include-dir=$(CURDIR)


MAKEFLAGS是make内置的环境变量,这些参数的含义,可以通过
man make
获得详细解释,以上”-rR“表示禁用内置的隐含规则和变量定义,”–include-dir” 指明嵌套脚本的搜索路径。

以下表示可以通过命令行参数
make V=1
来输出完整的命令,便于跟踪。

ifeq ("$(origin V)", "command line")
KBUILD_VERBOSE = $(V)
endif
ifndef KBUILD_VERBOSE
KBUILD_VERBOSE = 0
endif

ifeq ($(KBUILD_VERBOSE),1)
quiet =
Q =
else
quiet=quiet_
Q = @
endif


接下来一部分,给你提供了
make O=out
的方式将编译输出的目标文件放在一个单独的目录,比如这里指定的out目录,这样可以更加清晰区分开源文件和目标文件。当然,你也可以不这么做。比如,如果不指定
O
参数,
.config
文件就在当前根目录生成,如果指定
O=out
,那么.config文件会放在
out/.config


ifeq ("$(origin O)", "command line")
KBUILD_OUTPUT := $(O)
endif
...

ifneq ($(KBUILD_OUTPUT),)
# Invoke a second make in the output directory, passing relevant variables
# check that the output directory actually exists
saved-output := $(KBUILD_OUTPUT)
KBUILD_OUTPUT := $(shell mkdir -p $(KBUILD_OUTPUT) && cd $(KBUILD_OUTPUT) \
&& /bin/pwd)
$(if $(KBUILD_OUTPUT),, \
$(error failed to create output directory "$(saved-output)"))

PHONY += $(MAKECMDGOALS) sub-make

$(filter-out _all sub-make $(CURDIR)/Makefile, $(MAKECMDGOALS)) _all: sub-make
@:

sub-make: FORCE
$(Q)$(MAKE) -C $(KBUILD_OUTPUT) KBUILD_SRC=$(CURDIR) \
-f $(CURDIR)/Makefile $(filter-out _all sub-make,$(MAKECMDGOALS))

# Leave processing to above invocation of make
skip-makefile := 1
endif # ifneq ($(KBUILD_OUTPUT),)


从上可以看出,当指定了
O=out
时,将会进入out目录执行:

$(Q)$(MAKE) -C $(KBUILD_OUTPUT) KBUILD_SRC=$(CURDIR) \

-f $(CURDIR)/Makefile $(filter-out _all sub-make,$(MAKECMDGOALS))


继续往下看:

ifeq ("$(origin M)", "command line")
KBUILD_EXTMOD := $(M)
endif

# If building an external module we do not care about the all: rule
# but instead _all depend on modules
PHONY += all
ifeq ($(KBUILD_EXTMOD),)
_all: all
else
_all: modules
endif
...
...
...
all: vmlinux
...
...
...
vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORCE
ifdef CONFIG_HEADERS_CHECK
$(Q)$(MAKE) -f $(srctree)/Makefile headers_check
endif
ifdef CONFIG_SAMPLES
$(Q)$(MAKE) $(build)=samples
endif
ifdef CONFIG_BUILD_DOCSRC
$(Q)$(MAKE) $(build)=Documentation
endif
ifdef CONFIG_GDB_SCRIPTS
$(Q)ln -fsn `cd $(srctree) && /bin/pwd`/scripts/gdb/vmlinux-gdb.py
endif
+$(call if_changed,link-vmlinux)


这部分允许你单独编译某个模块,用
make M=dir
的形式。而且可以看到,如果不是编译模块,那么默认的目标是all,否则是modules。all又依赖与vmlinux,vmlinux又依赖于vmlinux-deps等,以此一层层完成编译,最终生成vmlinux映像。

下面几个宏也需要了解一下:

ARCH        ?= $(SUBARCH)
CROSS_COMPILE   ?= $(CONFIG_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
...


其中,
ARCH
CROSS_COMPILE
可以作为命令行参数传入,尤其是在编译嵌入式的Linux系统时,需要明确这2个宏。我这里仅仅举个例子,因为这个具体内容并不影响我们理解Makefile内容,不再赘述:

make ARCH=arm64 CROSS_COMPILE=/home/weimh1/work/k5_dev/prebuilts/gcc/linux-x86/aarch64/cit-aarch64-linux-android-4.9/bin/aarch64-linux-android-


下面2个宏也需要关注,看懂了这个,你就能够明白在C代码中
#include xxxx
该怎么写相对路径:

USERINCLUDE    := \
-I$(srctree)/arch/$(hdr-arch)/include/uapi \
-Iarch/$(hdr-arch)/include/generated/uapi \
-I$(srctree)/include/uapi \
-Iinclude/generated/uapi \
-include $(srctree)/include/linux/kconfig.h

# Use LINUXINCLUDE when you must reference the include/ directory.
# Needed to be compatible with the O= option
LINUXINCLUDE    := \
-I$(srctree)/arch/$(hdr-arch)/include \
-Iarch/$(hdr-arch)/include/generated/uapi \
-Iarch/$(hdr-arch)/include/generated \
$(if $(KBUILD_SRC), -I$(srctree)/include) \
-Iinclude \
$(USERINCLUDE)


接下来,将面对大量的目标和依赖关系,这些内容梳理清楚了,整个工程结构也就大概清楚了。

写到这里,发现如果要写得很详细,篇幅会很长,所以只能点到为止,权当入门参考。

几个重要的文件

接下来我们先转去介绍几个相关文件。

[b]Kbuild.include[/b]

根Makefile中有下面这行:

include scripts/Kbuild.include


打开这个文件,里面有几个定义需要了解。

首先是
build
:

###
# Shorthand for $(Q)$(MAKE) -f scripts/Makefile.build obj=
# Usage:
# $(Q)$(MAKE) $(build)=dir
build := -f $(srctree)/scripts/Makefile.build obj


它用在哪里?举个例子:

比如前面谈配置的时候提到的:

%config: scripts_basic outputmakefile FORCE
$(Q)$(MAKE) $(build)=scripts/kconfig $@


把上面的build定义,展开后就很清晰了:

%config: scripts_basic outputmakefile FORCE
$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.build obj=scripts/kconfig $@


可见,实际上build的作用是执行scripts/Makefile.build, obj指向具体的目录。于是,这里引出了第二个需要关注的文件
Makefile.build
,我们留到后面再简述。

继续看Kbuild.include的另外几个定义

# echo command.
# Short version is used, if $(quiet) equals `quiet_', otherwise full one.
echo-cmd = $(if $($(quiet)cmd_$(1)),\
echo '  $(call escsq,$($(quiet)cmd_$(1)))$(echo-why)';)

# printing commands
cmd = @$(echo-cmd) $(cmd_$(1))
...
# Execute command if command has changed or prerequisite(s) are updated.
#
if_changed = $(if $(strip $(any-prereq) $(arg-check)),                       \
@set -e;                                                             \
$(echo-cmd) $(cmd_$(1));                                             \
printf '%s\n' 'cmd_$@ := $(make-cmd)' > $(dot-target).cmd)


请记住上面2个变量,很多地方用上,比如:

cmd_link_o_target = $(if $(strip $(obj-y)),\
$(LD) $(ld_flags) -r -o $@ $(filter $(obj-y), $^) \
$(cmd_secanalysis),\
rm -f $@; $(AR) rcs$(KBUILD_ARFLAGS) $@)

$(builtin-target): $(obj-y) FORCE
$(call if_changed,link_o_target)


展开后实际上最终执行了
cmd_link_o_target
。其他不再赘述,自己对照原文去理解。

[b]Makefile.build[/b]

前文已经提到这个文件,这个文件主要定义了一些通用的模式匹配规则,比如:

# Built-in and composite module parts
$(obj)/%.o: $(src)/%.c $(recordmcount_source) FORCE
$(call cmd,force_checksrc)
$(call if_changed_rule,cc_o_c)


上面这个规则定义了所有默认的c文件如何编译成.o目标文件。执行的函数为
$(call if_changed_rule,cc_o_c),而
if_changed_rule
请在前文描述的Kbuild.include中查找。


其他规则类似,请自行阅读。

写到这里,如果你已经学会联系着这几个文件来阅读Makefile,我想你就已经入门了。至于要看到多深入,那纯粹是个人喜好问题,对于大多数人而言,其实大概了解也就够了。

OK,我们又回到根目录的总Makefile继续。剩下的部分,我们挑选几个来看看,不再从头到尾一个个描述。为了方便,我将几个重要的列在一块,中间省略的用
...
表示。

init-y      := init/
drivers-y   := drivers/ sound/ firmware/
net-y       := net/
libs-y      := lib/
core-y      := usr/
...
core-y      += kernel/ mm/ fs/ ipc/ security/ crypto/ block/

vmlinux-dirs    := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \
$(core-y) $(core-m) $(drivers-y) $(drivers-m) \
$(net-y) $(net-m) $(libs-y) $(libs-m) )
...

init-y      := $(patsubst %/, %/built-in.o, $(init-y))
core-y      := $(patsubst %/, %/built-in.o, $(core-y))
drivers-y   := $(patsubst %/, %/built-in.o, $(drivers-y))
net-y       := $(patsubst %/, %/built-in.o, $(net-y))
libs-y1     := $(patsubst %/, %/lib.a, $(libs-y))
libs-y2     := $(patsubst %/, %/built-in.o, $(libs-y))
libs-y      := $(libs-y1) $(libs-y2)

# Externally visible symbols (used by link-vmlinux.sh)
export KBUILD_VMLINUX_INIT := $(head-y) $(init-y)
export KBUILD_VMLINUX_MAIN := $(core-y) $(libs-y) $(drivers-y) $(net-y)
export KBUILD_LDS          := arch/$(SRCARCH)/kernel/vmlinux.lds
export LDFLAGS_vmlinux
# used by scripts/pacmage/Makefile
export KBUILD_ALLDIRS := $(sort $(filter-out arch/%,$(vmlinux-alldirs)) arch Documentation include samples scripts tools virt)

vmlinux-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN)

# Final link of vmlinux
cmd_link-vmlinux = $(CONFIG_SHELL) $< $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux)
quiet_cmd_link-vmlinux = LINK    $@

# Include targets which we want to
# execute if the rest of the kernel build went well.
vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORCE
ifdef CONFIG_HEADERS_CHECK
$(Q)$(MAKE) -f $(srctree)/Makefile headers_check
endif
ifdef CONFIG_SAMPLES
$(Q)$(MAKE) $(build)=samples
endif
ifdef CONFIG_BUILD_DOCSRC
$(Q)$(MAKE) $(build)=Documentation
endif
ifdef CONFIG_GDB_SCRIPTS
$(Q)ln -fsn `cd $(srctree) && /bin/pwd`/scripts/gdb/vmlinux-gdb.py
endif
+$(call if_changed,link-vmlinux)


以上内容主要描述了
vmlinux
是如何生成的,它依赖于
vmlinux-deps
,而
vmlinux-deps
又依赖于
$(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN)


这三个变量又是什么?

1.
KBUILD_LDS
:定义的是各个目标在最终映像文件中的布局;

2.
KBUILD_VMLINUX_INIT
: 除了init-y指定的内容,还有一个此文中没有出现过的
$(head-y)
,这个文件和具体的架构相关,所以它在另外一个文件中定义,请看下面文件:

include arch/$(SRCARCH)/Makefile


如果你的
SRCARCH
是x86,那么请打开文件
arch/x86/Makefile
,你可以找到下面内容:

head-y := arch/x86/kernel/head_$(BITS).o
head-y += arch/x86/kernel/head$(BITS).o
head-y += arch/x86/kernel/head.o


其中,
BITS
的值可以是32或者64位,取决你的机器实际配置。

head-y和init-y就构成了linux的启动和初始化代码,并被链接到最后的vmlinux中。

3.
KBUILD_VMLINUX_MAIN
: (core−y)(core-y) (libs-y) (drivers−y)(drivers-y) (net-y)

这个定义的则是整个内核的核心文件。编译的时候会依次进入这些目录,编译出对应的
built-in.o
并最终链接到vmlinux中。

其他的诸多内容,最终都是为上面服务所引出的一系列规则。已经不再影响大局,不再赘述。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: