从编译和链接来看kernel驱动注册的过程
http://blog.csdn.net/yili_xie/article/details/5701612
这篇文章通过编译连接来分析驱动注册的顺序。 收藏备用。
我们知道驱动注册的顺序是有要求的,比如说一般先注册MDP,然后才是LCDC,最后才是PANEL。一直以来都在想Kernel是如何控制驱动注册的顺序的,这几天把kernel的编译和链接仔细看了一遍,总算弄明白了,总结一下以备后查~~
在这之前一些基础知识是必须的:
1、ELF格式解析 http://blog.csdn.net/yili_xie/archive/2010/06/23/5689945.aspx
2、LDS文件格式 http://blog.csdn.net/yili_xie/archive/2010/06/24/5692007.aspx
先不管编译,我们先看看kernel的链接过程。在这里我们使用arm-eabi-ld来进行链接,链接过程需要使用到一个链接脚本,这个脚本就是vmlinux.lds了。vmlinux.lds决定了如何将各个输入文件的section放入最后的vmlinux里面,并控制各部分在程序地址空间中的布局,这里的链接都是静态链接(kernel的链接没有动态的概念?)。由此可以看出来vmlinux.lds实际上就是vmlinux内核镜像的一个简略表达方式,它从程序地址空间和文件的角度来描述了kernel image。
首先我们看看vmlinux.lds的生成,vmlinux.lds是由arch/arm/kernel/vmlinux.lds.S生成的,vmlinx.lds.S使用到了很多变量,而这些变量都定义在include/asm-generic/vmlinx.lds.h里面,在top Makefile里面有:
vmlinux-lds := arch/$(SRCARCH)/kernel/vmlinux.lds
而在arch/arm/Makefile里面定义了生成vmlinx.lds的FLAG:
CPPFLAGS_vmlinux.lds = -DTEXT_OFFSET=$(TEXT_OFFSET)
具体如何来生成vmlinux.lds我们不做考虑,我们现在来看看vmlinux.lds的内容,了解一下vmlinx里面各个section的分布:
- OUTPUT_ARCH(arm)
- ENTRY(stext)
- jiffies = jiffies_64;
- SECTIONS
- {
- . = 0x80000000 + 0x00008000;
- .text.head : {
- _stext = .;
- _sinittext = .;
- *(.text.head)
- }
- .init : { /* Init code and data */
- *(.init.text) *(.cpuinit.text) *(.meminit.text)
- _einittext = .;
- __proc_info_begin = .;
- *(.proc.info.init)
- __proc_info_end = .;
- __arch_info_begin = .;
- *(.arch.info.init)
- __arch_info_end = .;
- __tagtable_begin = .;
- *(.taglist.init)
- __tagtable_end = .;
- . = ALIGN(16);
- __setup_start = .;
- *(.init.setup)
- __setup_end = .;
- __early_begin = .;
- *(.early_param.init)
- __early_end = .;
- __initcall_start = .;
- *(.initcallearly.init) __early_initcall_end = .; *(.initcall0.init) *(.initcall0s.init) *(.initcall1.init) *(.initcall1s.init) *(.initcall2.init) *(.initcall2s.init) *(. initcall3.init) *(.initcall3s.init) *(.initcall4.init) *(.initcall4s.init) *(.initcall5.init) *(.initcall5s.init) *(.initcallrootfs.init) *(.initcall6.init) *(.initcall6s. init) *(.initcall7.init) *(.initcall7s.init)
- __initcall_end = .;
- __con_initcall_start = .;
- *(.con_initcall.init)
- __con_initcall_end = .;
- __security_initcall_start = .;
- *(.security_initcall.init)
- __security_initcall_end = .;
- . = ALIGN(32);
- __initramfs_start = .;
- usr/built-in.o(.init.ramfs)
- __initramfs_end = .;
- . = ALIGN(4096);
- __per_cpu_start = .;
- *(.data.percpu)
- *(.data.percpu.shared_aligned)
- __per_cpu_end = .;
- __init_begin = _stext;
- *(.init.data) *(.cpuinit.data) *(.cpuinit.rodata) *(.meminit.data) *(.meminit.rodata) __start___verbose_strings = .; *(__verbose_strings) __stop___verbose_strings = .; . = ALIGN(8); __start___verbose = .; *(__verbose) __stop___verbose = .;
- . = ALIGN(4096);
- __init_end = .;
- }
- /DISCARD/ : { /* Exit code and data */
- *(.exit.text) *(.cpuexit.text) *(.memexit.text)
- *(.exit.data) *(.cpuexit.data) *(.cpuexit.rodata) *(.memexit.data) *(.memexit.rodata)
- *(.exitcall.exit)
- }
- .text : { /* Real text segment */
- _text = .; /* Text and read-only data */
- __exception_text_start = .;
- *(.exception.text)
- __exception_text_end = .;
- . = ALIGN(8); *(.text.hot) *(.text) *(.ref.text) *(.text.init.refok) *(.exit.text.refok) *(.devinit.text) *(.devexit.text) *(.text.unlikely)
- . = ALIGN(8); __sched_text_start = .; *(.sched.text) __sched_text_end = .;
- . = ALIGN(8); __lock_text_start = .; *(.spinlock.text) __lock_text_end = .;
- . = ALIGN(8); __kprobes_text_start = .; *(.kprobes.text) __kprobes_text_end = .;
- *(.fixup)
- *(.gnu.warning)
- *(.rodata)
- *(.rodata.*)
- *(.glue_7)
- *(.glue_7t)
- *(.got) /* Global offset table */
- }
要阅读这个.lds文件必须对.lds的语法有一定了解,我这里只贴出部分,详细的大家可以去看一下自己生成的文件。在这里我们只看.init section部分的内容,我们可以看看init段的一个基本的执行过程(虽然虚拟地址空间不一定反映执行的过程,但我们在这里可以大致的这么理解),先是.init.text / .cpuinit.text /.meminit.text ---> .proc.info.init ---> .arch.info.init ---> .taglist.init ---> .init.setup ---> .early_param.init ---> .initcall……
了解了链接的过程之后,我们来看看driver注册的过程是如何和这个vmlinux.lds联系在一起的,我们首先来跟踪一下module_init这个宏,仔细跟踪一下我们就会发现它最后是被映射到了/include/linux/init.h里面:
module_init <--- #define module_init(x) __initcall(x); <--- #define __initcall(fn) device_initcall(fn) <--- #define device_initcall(fn) __define_initcall("6",fn,6)
最后这个__define_initcall :
#define __define_initcall(level,fn,id) /
static initcall_t __initcall_##fn##id __used /
__attribute__((__section__(".initcall" level ".init"))) = fn
这样我们就可以看到module_init实际上就是声明了一个可以放在.initcall[n][s].init段里面的函数指针,这个n实际上就是优先级,s是sync的意思。那么这些函数实际上是在什么地方被调用的呢,我们来看看start_kernel():
首先setup_arch()会调用msm7x2x_init()初始化device数组platform_add_devices(devices, ARRAY_SIZE(devices)); 然后我们着重看看rest_init();
res_init ---> kernel_init ---> do_basic_setup ---> do_initcalls();
- extern initcall_t __initcall_start[], __initcall_end[], __early_initcall_end[];
- static void __init do_initcalls(void)
- {
- initcall_t *call;
- for (call = __early_initcall_end; call < __initcall_end; call++)
- do_one_initcall(*call);
- /* Make sure there is no pending stuff from the initcall sequence */
- flush_scheduled_work();
- }
哈哈,看到do_initcall里面的for循环是不是很熟悉阿,对的,它就是vmlinux.lds里面定义的几个变量,实际上驱动注册的函数实际上都在这里被统一的调用。看到这里我们基本就可以知道如何修改我们驱动注册的顺序了,下面我们就看看具体如何操作。
从上面的知识我们可以看出所有驱动注册的函数module_init实际上都是注册优先级为6的initcall section,因此最简单的我们可以调整我们的优先级以实现驱动注册的前后,除了已有的几个优先级之外我们甚至能加自己的优先级,比如我们如果加一个7优先级的section,我们首先在init.h里面添加相应的宏:
#define my_initcall(fn) __define_initcall("8",fn,8)
#define my_initcall(fn) __define_initcall("8s",fn,8s)
然后我们在vmlinux.lds.h里面添加相应的部分,在#define INITCALLS宏的末尾添加,顺序是很重要的,如果想让它的优先级为8必须放在末尾:
*(.initcall8.init) /
*(.initcall8s.init)
这样我们就可以用我们自己定义的宏my_initcall了~~不过这样调整驱动注册的优先级以及添加自己的优先级不是推荐的方式,没有确切测试过会对kernel造成什么影响,下面我们就从编译的角度来看看如何改变驱动注册的顺序。kernel通过遍历目录中的kmakefile来将各个.o包含进去进行链接,我没有仔细看过kernel的makefile是以什么样的顺序去遍历的,不过我们可以通过另外一种方式了解kernel遍历的过程,那就是kernel编译生成的system.map文件。system.map实际上是由nm vmlinux生成的,这个命令是比较强大的,默认的system.map只能看到Symbol的虚拟地址,我们可以重新使用nm命令来生成,这样我们还能看到各个symbol在哪个文件里面。除了system.map之外我们还可以用arm-eabi-objdump -S 来查看vmlinux里面的symbol信息,只是生成的东西比较多~
具体system.map的语法可以看看下面的:
符号类型. 小写字母表示局部; 大写字母表示全局(外部).
A
The symbol's value is absolute, and will not be changed by further linking.
B
The symbol is in the uninitialized data section (known as BSS).
C
The symbol is common. Common symbols are uninitialized data. When linking, multiple common symbols may appear with the same name. If the symbol is defined anywhere, the common symbols are treated as undefined references. For more details on common symbols, see the discussion of -warn-common in Linker options.
D
The symbol is in the initialized data section.
G
The symbol is in an initialized data section for small objects. Some object file formats permit more efficient access to small data objects, such as a global int variable as opposed to a large global array.
I
The symbol is an indirect reference to another symbol. This is a GNU extension to the a.out object file format which is rarely used.
N
The symbol is a debugging symbol.
R
The symbol is in a read only data section.
S
The symbol is in an uninitialized data section for small objects.
T
The symbol is in the text (code) section.
U
The symbol is undefined.
V
The symbol is a weak object. When a weak defined symbol is linked with a normal defined symbol, the normal defined symbol is used with no error. When a weak undefined symbol is linked and the symbol is not defined, the value of the weak symbol becomes zero with no error.
W
The symbol is a weak symbol that has not been specifically tagged as a weak object symbol. When a weak defined symbol is linked with a normal defined symbol, the normal defined symbol is used with no error. When a weak undefined symbol is linked and the symbol is not defined, the value of the weak symbol becomes zero with no error.
-
The symbol is a stabs symbol in an a.out object file. In this case, the next values printed are the stabs other field, the stabs desc field, and the stab type. Stabs symbols are used to hold debugging information. For more information, see Stabs.
?
The symbol type is unknown, or object file format specific.
前面说过我们可以通过grep在system.map中出现的函数的顺序,大致可以知道kernel遍历的顺序是:
arch/arm/kernel --> arch/arm/mach-msm --> kernel --> mm --> fs --> ipc --> block --> driver --> sound --> net
虽然看system.map的方法有一点讨巧,但是kernel加载各部分的makefile应该是固定的,所以可以省去大量的时间去仔细分析MAKEFILE。为什么我要强调这个遍历的顺序呢?前面我们说过arm-eabi-ld将各个.o的各个section根据vmlinx.lds放入输出文件的各个section里面,那么LD链接的顺序也就决定了输出文件同一个section里面哪个.o的内容靠前,哪个靠后,也就是说先链接的.o的内容靠前,后链接的在后。举个例子,比如block下面有个makefile包含了一个blockxxx.o文件有一个module_init(xxx),而在driver下面的makefile里面也包含一个driveryyy.o文件里面有一个module_init(yyy),那么在.initcall6.text段里面,block里面相应的内容就在driver中的靠前,换句话说在do-initcalls()里面block的moudle-init会先执行。
根据上面的原理我们就很清楚了,OBJ的顺序决定了section里面的顺序,我们具体的来看看driver里面的MAKEFILE:
- obj-y += gpio/
- obj-$(CONFIG_PCI) += pci/
- obj-$(CONFIG_PARISC) += parisc/
- obj-$(CONFIG_RAPIDIO) += rapidio/
- obj-y += video/
- obj-$(CONFIG_ACPI) += acpi/
- # PnP must come after ACPI since it will eventually need to check if acpi
- # was used and do nothing if so
- obj-$(CONFIG_PNP) += pnp/
- obj-$(CONFIG_ARM_AMBA) += amba/
- obj-$(CONFIG_XEN) += xen/
如果我们想video的驱动注册比rapido早的话,只需要将两部分的内容换一个位置就好了~ 我们再来看看video的各个驱动注册的过程:
- obj-y := msm_fb.o
- obj-$(CONFIG_FB_MSM_LOGO) += logo.o
- obj-$(CONFIG_FB_BACKLIGHT) += msm_fb_bl.o
- # MDP
- obj-y += mdp.o
- ifeq ($(CONFIG_FB_MSM_MDP40),y)
- obj-y += mdp4_util.o
- obj-$(CONFIG_DEBUG_FS) += mdp4_debugfs.o
- else
- obj-y += mdp_hw_init.o
- obj-y += mdp_ppp.o
- ifeq ($(CONFIG_FB_MSM_MDP31),y)
- obj-y += mdp_ppp_v31.o
- else
- obj-y += mdp_ppp_v20.o
- endif
- endif
- obj-y += mdp_dma.o
- obj-y += mdp_dma_s.o
- obj-y += mdp_vsync.o
- obj-y += mdp_dma_lcdc.o
- obj-y += mdp_cursor.o
- obj-y += mdp_dma_tv.o
- # EBI2
- obj-$(CONFIG_FB_MSM_EBI2) += ebi2_lcd.o
- # LCDC
- obj-$(CONFIG_FB_MSM_LCDC) += lcdc.o
同样的道理,如果我们希望LCDC的驱动比MDP早注册的话,只需要改变他们在MAKEFILE里面的顺序就可以了~ 改变顺序以后重新编译你就可以发现在systemap里面驱动注册的函数的顺序被改变了~~
好拉,修改驱动注册的过程大概就是这样了, 如果有什么问题请路过的大虾指正~~ 下面准备仔细分析LINUX的启动过程了~~