initcallとは

はじめに

linux kernel1を読んでいると、**_initcall(core_initcallとか__initcallとかが色々なところで定義されている。例えば、sysenterの初期設定(IA32_SYSENTER_(CS|EIP|ESP))をしているarch/i386/kernel/sysenter.cでは、

extern const char vsyscall_int80_start, vsyscall_int80_end;
extern const char vsyscall_sysenter_start, vsyscall_sysenter_end;
static int __init sysenter_setup(void)
{
    void *page = (void *)get_zeroed_page(GFP_ATOMIC);
    __set_fixmap(FIX_VSYSCALL, __pa(page), PAGE_READONLY_EXEC);
    if (!boot_cpu_has(X86_FEATURE_SEP)) {
        memcpy(page,
               &vsyscall_int80_start,
               &vsyscall_int80_end - &vsyscall_int80_start);
        return 0;
    }
    memcpy(page,
           &vsyscall_sysenter_start,
           &vsyscall_sysenter_end - &vsyscall_sysenter_start);
    on_each_cpu(enable_sep_cpu, NULL, 1, 1);
    return 0;
}

__initcall(sysenter_setup);

と定められていて、__initcallが起点になっているようだ。他にも、 https://gist.github.com/knknkn1162/0a766148463c1ccb972ae2999cca1bfd のような場所で定義されている。

__initcall

// include/linux/init.h
typedef int (*initcall_t)(void);

#define __define_initcall(level,fn) static initcall_t __initcall_##fn __attribute_used__ __attribute__((__section__(".initcall" level ".init"))) = fn

#define device_initcall(fn)        __define_initcall("6",fn)
#define __initcall(fn) device_initcall(fn)

とあるので、__initcall(sysenter_setup);は以下のような形になる:

static initcall_t __initcall_sysenter_setup __attribute_used__ __attribute__((__section__(".initcall6.init"))) = sysenter_setup

つまり、関数ポインタが.initcall6.initのsectionに格納されているようだ。

__attribute_used__は以下の通り:

// include/linux/compiler.h
#if __GNUC__ > 3
# include <linux/compiler-gcc+.h>    /* catch-all for GCC 4, 5, etc. */
#elif __GNUC__ == 3
# include <linux/compiler-gcc3.h>
#elif __GNUC__ == 2
# include <linux/compiler-gcc2.h>
#else
# error Sorry, your compiler is too old/not recognized.
#endif
// include/linux/compiler-gcc+.h
#define __attribute_used__ __attribute__((__used__)) // tells the compiler that this function is used

.initcall6.initarch/i386/kernel/vmlinux.lds.Sを見ると、uninitialized dataの部分に2、以下のように定められている:

  _edata= .
  // skip
  .init.data : { *(.init.data) }
  . = ALIGN(16);
  __setup_start = .;
  .init.setup : { *(.init.setup) }
  __setup_end = .;
  __initcall_start = .;
  .initcall.init : {
    *(.initcall1.init) 
    *(.initcall2.init) 
    *(.initcall3.init) 
    *(.initcall4.init) 
    *(.initcall5.init) 
    *(.initcall6.init) 
    *(.initcall7.init)
  }
  __initcall_end = .;
// skip

定義されている場所はわかったがこれはどこで使われているのか?

__initcall_start ~ __initcall_end

これは、init() -> do_basic_setup -> do_initcalls関数で以下のように使われている(ちなみに、initはstart_kernel -> rest_init -> kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);で定義されている):

static void __init do_initcalls(void)
{
    initcall_t *call; // typedef int (*initcall_t)(void);
    int count = preempt_count();

    for (call = __initcall_start; call < __initcall_end; call++) {
        char *msg;
                // skip

        (*call)();
                // skip
          }
       // skip
}

call関数は勿論、callbackなので、__initcall_start__initcall_end間にある関数ポインタ達が順番に呼ばれる。initcall${number}.initのようになっていたのは、呼ばれる優先順位の意味だった。


  1. versionはいつもと同じく、2.6.11。 http://cstmize.hatenablog.jp/entry/2019/04/14/linux_kernel%E3%81%A7%E3%81%AEFPU%2C_MMX%2C_SSE%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6 のreferenceも参照のこと

  2. Linux Kernelにおけるkernel dataの位置関係は、Understanding the Linux kernel Figure 2-13. にある。