linux的initcall机制应用层用链接脚本实现
initcall机制的由来
我们都知道,linux对驱动程序提供静态编译进内核和动态加载两种方式,当我们试图将一个驱动程序编译进内核时,开发者通常提供一个xxx_init()函数接口以启动这个驱动程序同时提供某些服务。
那么,根据常识来说,这个xxx_init()函数肯定是要在系统启动的某个时候被调用,才能启动这个驱动程序。
最简单直观地做法就是:开发者试图添加一个驱动程序时,在内核启动init程序的某个地方直接添加调用自己驱动程序的xxx_init()函数,在内核启动时自然会调用到这个程序。
但是,回头一想,这种做法在单人开发的小系统中或许可以,但是在linux中,如果驱动程序是这么个添加法,那就是一场灾难,这个道理我想不用我多说。
不难想到另一种方式,就是集中提供一个地方,如果你要添加你的驱动程序,你就将你的初始化函数在这个地方进行添加,在内核启动的时候统一扫描这个地方,再执行这一部分的所有被添加的驱动程序。
那到底怎么添加呢?直接在C文件中作一个列表,在里面添加初始化函数?我想随着驱动程序数量的增加,这个列表会让人头昏眼花。
当然,对于linus大神而言,这些都不是事,linux的做法是:
底层实现上,在内核镜像文件中,自定义一个段,这个段里面专门用来存放这些初始化函数的地址,内核启动时,只需要在这个段地址处取出函数指针,一个个执行即可。
对上层而言,linux内核提供xxx_init(init_func)宏定义接口,驱动开发者只需要将驱动程序的init_func使用xxx_init()来修饰,这个函数就被自动添加到了上述的段中,开发者完全不需要关心实现细节。
对于各种各样的驱动而言,可能存在一定的依赖关系,需要遵循先后顺序来进行初始化,考虑到这个,linux也对这一部分做了分级处理。
initcall的源码
在平台对应的init.h文件中,可以找到xxx_initcall的定义:/*Only for built-in code, not modules.*/ #define early_initcall(fn) __define_initcall(fn, early) #define pure_initcall(fn) __define_initcall(fn, 0) #define core_initcall(fn) __define_initcall(fn, 1) #define core_initcall_sync(fn) __define_initcall(fn, 1s) #define postcore_initcall(fn) __define_initcall(fn, 2) #define postcore_initcall_sync(fn) __define_initcall(fn, 2s) #define arch_initcall(fn) __define_initcall(fn, 3) #define arch_initcall_sync(fn) __define_initcall(fn, 3s) #define subsys_initcall(fn) __define_initcall(fn, 4) #define subsys_initcall_sync(fn) __define_initcall(fn, 4s) #define fs_initcall(fn) __define_initcall(fn, 5) #define fs_initcall_sync(fn) __define_initcall(fn, 5s) #define rootfs_initcall(fn) __define_initcall(fn, rootfs) #define device_initcall(fn) __define_initcall(fn, 6) #define device_initcall_sync(fn) __define_initcall(fn, 6s) #define late_initcall(fn) __define_initcall(fn, 7) #define late_initcall_sync(fn) __define_initcall(fn, 7s)
xxx_init_call(fn)的原型其实是__define_initcall(fn, n),n是一个数字或者是数字+s,这个数字代表这个fn执行的优先级,数字越小,优先级越高,带s的fn优先级低于不带s的fn优先级。
继续跟踪代码,看看__define_initcall(fn,n):#define __define_initcall(fn, id) static initcall_t __initcall_##fn##id __used __attribute__((__section__(".initcall" #id ".init"))) = fn;
值得注意的是,attribute()是gnu C中的扩展语法,它可以用来实现很多灵活的定义行为,这里不细究。
attribute((section(".initcall" #id ".init")))表示编译时将目标符号放置在括号指定的段中。
而#在宏定义中的作用是将目标字符串化,##在宏定义中的作用是符号连接,将多个符号连接成一个符号,并不将其字符串化。
__used是一个宏定义,
#define __used __attribute__((__used__))
使用前提是在编译器编译过程中,如果定义的符号没有被引用,编译器就会对其进行优化,不保留这个符号,而__attribute__((used))的作用是告诉编译器这个静态符号在编译的时候即使没有使用到也要保留这个符号。
为了更方便地理解,我们拿举个例子来说明,开发者声明了这样一个函数:pure_initcall(test_init);
所以pure_initcall(test_init)的解读就是:
首先宏展开成:__define_initcall(test_init, 0)
然后接着展开:static initcall_t __initcall_test_init0 = test_init;这就是一个简单的变量定义。
同时声明__initcall_test_init0这个变量即使没被引用也保留符号,且将其放置在内核镜像的.initcall0.init段处。
需要注意的是,根据官方注释可以看到early_initcall(fn)只针对内置的核心代码,不能描述模块。
xxx_initcall修饰函数的调用
既然我们知道了xxx_initcall是怎么定义而且目标函数的放置位置,那么使用xxx_initcall()修饰的函数是怎么被调用的呢?
我们就从内核C函数起始部分也就是start_kernel开始往下挖,这里的调用顺序为:start_kernel -> rest_init(); -> kernel_thread(kernel_init, NULL, CLONE_FS); -> kernel_init() -> kernel_init_freeable(); -> do_basic_setup(); -> do_initcalls();
这个do_initcalls()就是我们需要寻找的函数了,在这个函数中执行所有使用xxx_initcall()声明的函数,接下来我们再来看看它是怎么执行的:static initcall_t *initcall_levels[] __initdata = { __initcall0_start, __initcall1_start, __initcall2_start, __initcall3_start, __initcall4_start, __initcall5_start, __initcall6_start, __initcall7_start, __initcall_end, }; int __init_or_module do_one_initcall(initcall_t fn) { ... if (initcall_debug) ret = do_one_initcall_debug(fn); else ret = fn(); ... return ret; } static void __init do_initcall_level(int level) { initcall_t *fn; ... for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++) do_one_initcall(*fn); } static void __init do_initcalls(void) { int level; for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) do_initcall_level(level); }
在上述代码中,定义了一个静态的initcall_levels数组,这是一个指针数组,数组的每个元素都是一个指针.
do_initcalls()循环调用do_initcall_level(level),level就是initcall的优先级数字,由for循环的终止条件ARRAY_SIZE(initcall_levels) - 1可知,总共会调用do_initcall_level(0)~do_initcall_level(7),一共七次。
而do_initcall_level(level)中则会遍历initcall_levels[level]中的每个函数指针,initcall_levels[level]实际上是对应的__initcall##level##_start指针变量,然后依次取出__initcall##level##_start指向地址存储的每个函数指针,并调用do_one_initcall(*fn),实际上就是执行当前函数。
可以猜到的是,这个__initcall##level##start所存储的函数指针就是开发者用xxx_initcall()宏添加的函数,对应".initcall##level##.init"段。
do_one_initcall(*fn)的执行:判断initcall_debug的值,如果为真,则调用do_one_initcall_debug(fn);如果为假,则直接调用fn。事实上,调用do_one_initcall_debug(fn)只是在调用fn的基础上添加一些额外的打印信息,可以直接看成是调用fn。
那么,在initcall源码部分有提到,在开发者添加xxx_initcall(fn)时,事实上是将fn放置到了".initcall##level##.init"的段中,但是在do_initcall()的源码部分,却是从initcall_levelslevel取出,initcall_levels[level]是怎么关联到".initcall##level##.init"段的呢?
答案在vmlinux.lds.h中:#define INIT_CALLS_LEVEL(level) VMLINUX_SYMBOL(__initcall##level##_start) = .; KEEP(*(.initcall##level##.init)) KEEP(*(.initcall##level##s.init)) #define INIT_CALLS VMLINUX_SYMBOL(__initcall_start) = .; KEEP(*(.initcallearly.init)) INIT_CALLS_LEVEL(0) INIT_CALLS_LEVEL(1) INIT_CALLS_LEVEL(2) INIT_CALLS_LEVEL(3) INIT_CALLS_LEVEL(4) INIT_CALLS_LEVEL(5) INIT_CALLS_LEVEL(rootfs) INIT_CALLS_LEVEL(6) INIT_CALLS_LEVEL(7) VMLINUX_SYMBOL(__initcall_end) = .;
在这里首先定义了__initcall_start,将其关联到".initcallearly.init"段。
然后对每个level定义了INIT_CALLS_LEVEL(level),将INIT_CALLS_LEVEL(level)展开之后的结果是定义__initcall##level##_start,并将
__initcall##level##_start关联到".initcall##level##.init"段和".initcall##level##s.init"段。
到这里,__initcall##level##_start和".initcall##level##.init"段的对应就比较清晰了,所以,从initcall_levels[level]部分一个个取出函数指针并执行函数就是执行xxx_init_call()定义的函数。
总结
便于理解,我们需要一个示例来梳理整个流程,假设我是一个驱动开发者,开发一个名为beagle的驱动,在系统启动时需要调用beagle_init()函数来启动启动服务。
我需要先将其添加到系统中:
core_initcall(beagle_init)
core_initcall(beagle_init)宏展开为__define_initcall(beagle_init, 1),所以beagle_init()这个函数被放置在".initcall1.init"段处。
在内核启动时,系统会调用到do_initcall()函数。 根据指针数组initcall_levels[1]找到__initcall1_start指针,在vmlinux.lds.h可以查到:__initcall1_start对应".initcall1.init"段的起始地址,依次取出段中的每个函数指针,并执行函数。
添加的服务就实现了启动。
可能有些C语言基础不太好的朋友不太理解do_initcall_level()函数中依次取出地址并执行的函数执行逻辑:
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++) do_one_initcall(*fn);
fn为函数指针,fn++相当于函数指针+1,相当于:内存地址+sizeof(fn),sizeof(fn)根据平台不同而不同,一般来说,32位机上是4字节,64位机则是8字节(关于指针在操作系统中的大小可以参考另一篇博客:不同平台下指针大小 )。
而initcall_levels[level]指向当前".initcall##level##s.init"段,initcall_levels[level+1]指向".initcall##(level+1)##s.init"段,两个段之间的内存就是存放所有添加的函数指针。
也就是从".initcall##level##s.init"段开始,每次取一个函数出来执行,并累加指针,直到取完。
写个测试用例吧,应用层:
c_init.c/* * @Description: * @Version: 2.0 * @Autor: lsh * @Date: 2022-02-25 19:48:58 * @LastEditors: lsh * @LastEditTime: 2022-02-25 20:00:04 */ #include #include #include typedef void (*init_call)(void); /* * These two variables are defined in link script. */ extern init_call _init_start; extern init_call _init_end; #define _init __attribute__((unused, section(".myinit"))) #define DECLARE_INIT(func) init_call _fn_##func _init = func static void C_init(void) { printf("C_init "); } DECLARE_INIT(C_init);
ldscript.lds/* Script for -z combreloc -z separate-code: combine and sort reloc sections with separate code segment */ /* Copyright (C) 2014-2018 Free Software Foundation, Inc. Copying and distribution of this script, with or without modification, are permitted in any medium without royalty provided the copyright notice and this notice are preserved. */ OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64", "elf64-x86-64") OUTPUT_ARCH(i386:x86-64) ENTRY(_start) SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib"); SECTIONS { /* Read-only sections, merged into text segment: */ PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS; .interp : { *(.interp) } .note.gnu.build-id : { *(.note.gnu.build-id) } .hash : { *(.hash) } .gnu.hash : { *(.gnu.hash) } .dynsym : { *(.dynsym) } .dynstr : { *(.dynstr) } .gnu.version : { *(.gnu.version) } .gnu.version_d : { *(.gnu.version_d) } .gnu.version_r : { *(.gnu.version_r) } .rela.dyn : { *(.rela.init) *(.rela.text .rela.text.* .rela.gnu.linkonce.t.*) *(.rela.fini) *(.rela.rodata .rela.rodata.* .rela.gnu.linkonce.r.*) *(.rela.data .rela.data.* .rela.gnu.linkonce.d.*) *(.rela.tdata .rela.tdata.* .rela.gnu.linkonce.td.*) *(.rela.tbss .rela.tbss.* .rela.gnu.linkonce.tb.*) *(.rela.ctors) *(.rela.dtors) *(.rela.got) *(.rela.bss .rela.bss.* .rela.gnu.linkonce.b.*) *(.rela.ldata .rela.ldata.* .rela.gnu.linkonce.l.*) *(.rela.lbss .rela.lbss.* .rela.gnu.linkonce.lb.*) *(.rela.lrodata .rela.lrodata.* .rela.gnu.linkonce.lr.*) *(.rela.ifunc) } .rela.plt : { *(.rela.plt) PROVIDE_HIDDEN (__rela_iplt_start = .); *(.rela.iplt) PROVIDE_HIDDEN (__rela_iplt_end = .); } . = ALIGN(CONSTANT (MAXPAGESIZE)); .init : { KEEP (*(SORT_NONE(.init))) } .plt : { *(.plt) *(.iplt) } .plt.got : { *(.plt.got) } .plt.sec : { *(.plt.sec) } .text : { *(.text.unlikely .text.*_unlikely .text.unlikely.*) *(.text.exit .text.exit.*) *(.text.startup .text.startup.*) *(.text.hot .text.hot.*) *(.text .stub .text.* .gnu.linkonce.t.*) /* .gnu.warning sections are handled specially by elf32.em. */ *(.gnu.warning) } .fini : { KEEP (*(SORT_NONE(.fini))) } PROVIDE (__etext = .); PROVIDE (_etext = .); PROVIDE (etext = .); . = ALIGN(CONSTANT (MAXPAGESIZE)); /* Adjust the address for the rodata segment. We want to adjust up to the same address within the page on the next page up. */ . = SEGMENT_START("rodata-segment", ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1))); .rodata : { *(.rodata .rodata.* .gnu.linkonce.r.*) } .rodata1 : { *(.rodata1) } .eh_frame_hdr : { *(.eh_frame_hdr) *(.eh_frame_entry .eh_frame_entry.*) } .eh_frame : ONLY_IF_RO { KEEP (*(.eh_frame)) *(.eh_frame.*) } .gcc_except_table : ONLY_IF_RO { *(.gcc_except_table .gcc_except_table.*) } .gnu_extab : ONLY_IF_RO { *(.gnu_extab*) } /* These sections are generated by the Sun/Oracle C++ compiler. */ .exception_ranges : ONLY_IF_RO { *(.exception_ranges .exception_ranges*) } /* Adjust the address for the data segment. We want to adjust up to the same address within the page on the next page up. */ . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE)); /* Exception handling */ .eh_frame : ONLY_IF_RW { KEEP (*(.eh_frame)) *(.eh_frame.*) } .gnu_extab : ONLY_IF_RW { *(.gnu_extab) } .gcc_except_table : ONLY_IF_RW { *(.gcc_except_table .gcc_except_table.*) } .exception_ranges : ONLY_IF_RW { *(.exception_ranges .exception_ranges*) } /* Thread Local Storage sections */ .tdata : { PROVIDE_HIDDEN (__tdata_start = .); *(.tdata .tdata.* .gnu.linkonce.td.*) } .tbss : { *(.tbss .tbss.* .gnu.linkonce.tb.*) *(.tcommon) } .preinit_array : { PROVIDE_HIDDEN (__preinit_array_start = .); KEEP (*(.preinit_array)) PROVIDE_HIDDEN (__preinit_array_end = .); } .init_array : { PROVIDE_HIDDEN (__init_array_start = .); KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*))) KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors)) PROVIDE_HIDDEN (__init_array_end = .); } .fini_array : { PROVIDE_HIDDEN (__fini_array_start = .); KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*))) KEEP (*(.fini_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .dtors)) PROVIDE_HIDDEN (__fini_array_end = .); } .ctors : { /* gcc uses crtbegin.o to find the start of the constructors, so we make sure it is first. Because this is a wildcard, it doesn"t matter if the user does not actually link against crtbegin.o; the linker won"t look for a file to match a wildcard. The wildcard also means that it doesn"t matter which directory crtbegin.o is in. */ KEEP (*crtbegin.o(.ctors)) KEEP (*crtbegin?.o(.ctors)) /* We don"t want to include the .ctor section from the crtend.o file until after the sorted ctors. The .ctor section from the crtend file contains the end of ctors marker and it must be last */ KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors)) KEEP (*(SORT(.ctors.*))) KEEP (*(.ctors)) } .dtors : { KEEP (*crtbegin.o(.dtors)) KEEP (*crtbegin?.o(.dtors)) KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .dtors)) KEEP (*(SORT(.dtors.*))) KEEP (*(.dtors)) } .jcr : { KEEP (*(.jcr)) } .data.rel.ro : { *(.data.rel.ro.local* .gnu.linkonce.d.rel.ro.local.*) *(.data.rel.ro .data.rel.ro.* .gnu.linkonce.d.rel.ro.*) } .dynamic : { *(.dynamic) } .got : { *(.got) *(.igot) } . = DATA_SEGMENT_RELRO_END (SIZEOF (.got.plt) >= 24 ? 24 : 0, .); .got.plt : { *(.got.plt) *(.igot.plt) } .data : { *(.data .data.* .gnu.linkonce.d.*) SORT(CONSTRUCTORS) } .data1 : { *(.data1) } _edata = .; PROVIDE (edata = .); . = .; _init_start = .; .myinit : { *(.myinit) } _init_end = .; __bss_start = .; .bss : { *(.dynbss) *(.bss .bss.* .gnu.linkonce.b.*) *(COMMON) /* Align here to ensure that the .bss section occupies space up to _end. Align after .bss to ensure correct alignment even if the .bss section disappears because there are no input sections. FIXME: Why do we need it? When there is no .bss section, we don"t pad the .data section. */ . = ALIGN(. != 0 ? 64 / 8 : 1); } .lbss : { *(.dynlbss) *(.lbss .lbss.* .gnu.linkonce.lb.*) *(LARGE_COMMON) } . = ALIGN(64 / 8); . = SEGMENT_START("ldata-segment", .); .lrodata ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)) : { *(.lrodata .lrodata.* .gnu.linkonce.lr.*) } .ldata ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)) : { *(.ldata .ldata.* .gnu.linkonce.l.*) . = ALIGN(. != 0 ? 64 / 8 : 1); } . = ALIGN(64 / 8); _end = .; PROVIDE (end = .); . = DATA_SEGMENT_END (.); /* Stabs debugging sections. */ .stab 0 : { *(.stab) } .stabstr 0 : { *(.stabstr) } .stab.excl 0 : { *(.stab.excl) } .stab.exclstr 0 : { *(.stab.exclstr) } .stab.index 0 : { *(.stab.index) } .stab.indexstr 0 : { *(.stab.indexstr) } .comment 0 : { *(.comment) } /* DWARF debug sections. Symbols in the DWARF debugging sections are relative to the beginning of the section so we begin them at 0. */ /* DWARF 1 */ .debug 0 : { *(.debug) } .line 0 : { *(.line) } /* GNU DWARF 1 extensions */ .debug_srcinfo 0 : { *(.debug_srcinfo) } .debug_sfnames 0 : { *(.debug_sfnames) } /* DWARF 1.1 and DWARF 2 */ .debug_aranges 0 : { *(.debug_aranges) } .debug_pubnames 0 : { *(.debug_pubnames) } /* DWARF 2 */ .debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) } .debug_abbrev 0 : { *(.debug_abbrev) } .debug_line 0 : { *(.debug_line .debug_line.* .debug_line_end ) } .debug_frame 0 : { *(.debug_frame) } .debug_str 0 : { *(.debug_str) } .debug_loc 0 : { *(.debug_loc) } .debug_macinfo 0 : { *(.debug_macinfo) } /* SGI/MIPS DWARF 2 extensions */ .debug_weaknames 0 : { *(.debug_weaknames) } .debug_funcnames 0 : { *(.debug_funcnames) } .debug_typenames 0 : { *(.debug_typenames) } .debug_varnames 0 : { *(.debug_varnames) } /* DWARF 3 */ .debug_pubtypes 0 : { *(.debug_pubtypes) } .debug_ranges 0 : { *(.debug_ranges) } /* DWARF Extension. */ .debug_macro 0 : { *(.debug_macro) } .debug_addr 0 : { *(.debug_addr) } .gnu.attributes 0 : { KEEP (*(.gnu.attributes)) } /DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) } }
main.c/* * @Description:内核模块原理 * @Version: 2.0 * @Autor: lsh * @Date: 2022-02-25 17:47:46 * @LastEditors: lsh * @LastEditTime: 2022-02-25 19:54:20 */ #include #include #include typedef void (*init_call)(void); /* * These two variables are defined in link script. */ extern init_call _init_start; extern init_call _init_end; #define _init __attribute__((unused, section(".myinit"))) #define DECLARE_INIT(func) init_call _fn_##func _init = func static void A_init(void) { write(1, "A_init ", sizeof("A_init ")); } DECLARE_INIT(A_init); static void B_init(void) { printf("B_init "); } DECLARE_INIT(B_init); // static void C_init(void) // { // printf("C_init "); // } // DECLARE_INIT(C_init); /* * DECLARE_INIT like below: * static init_call _fn_A_init __attribute__((unused, section(".myinit"))) = A_init; * static init_call _fn_C_init __attribute__((unused, section(".myinit"))) = C_init; * static init_call _fn_B_init __attribute__((unused, section(".myinit"))) = B_init; */ void do_initcalls(void) { init_call *init_ptr = &_init_start; for (; init_ptr < &_init_end; init_ptr++) { printf("init address: %p ", init_ptr); (*init_ptr)(); } } int main(void) { do_initcalls(); return 0; }
编译教程
执行:gcc -c section.c -o section.o 编译应用源码。
执行:readelf -S section.o 查看段信息. 可以看到,段[6]是我们自定义的数据段.
执行:gcc -T ldscript.lds section.o -o section 链接成可执行的bin文件
执行:readelf -S section 查看bin文件的段分布情况 在我链接成的可执行bin中,在[25]段中存在我们自定义的段。
运行后可以看到打印为调用了所有3个函数。
————————————————
版权声明:本文为CSDN博主「若灮」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u011164819/article/details/123281172