Skip to content
Advertisement

Where/how does the kernel loads statically linked modules?

Looking at init/main.c#start_kernel it’s not clear where the statically linked kernel modules are loaded, neither how the kernel gets a list of them.

So, where are the statically linked kernel modules loaded?

Advertisement

Answer

For compiling a module “as a module”, the corresponding Kconfig is set to m. If set to y, all the module code is compiled just like all the other “builtin” kernel source files, and the generated object files are added to all the rest of the “builtin” kernel object files, which are then linked together to create the vmlinux image. Therefore, as the bootloader loads this monolithic image into memory to boot the kernel, all the builtin modules are loaded with it. All that is required is for the functions marked with module_init to be called during initialization.

As to how that happens, the short answer is, unless the module source file is compiled “as a module”, the module_init adds the given function address to an array of function addresses that are then called during initialization.


For the long answer, consider the mkiss module at drivers/net/hamradio/mkiss.c.

Kconfig and Compiler Flags

The Kconfig is found in drivers/net/hamradio/Kconfig:

config MKISS
        tristate "Serial port KISS driver"
        depends on AX25 && TTY
        select CRC16
        help
          ...
          To compile this driver as a module, choose M here: the module
          will be called mkiss.

The selection results in a CONFIG_MKISS=y or CONFIG_MKISS=m line in the .config file. This is then used in the drivers/net/hamradio/Makefile:

obj-$(CONFIG_MKISS)             += mkiss.o

So, if “builtin”, mkiss.o is added to the list of targets obj-y. And if “module”, it is added to obj-m. After some path processing, obj-m becomes real-obj-m in scripts/Makefile.lib, which is then used to determine the compiler flags:

part-of-module = $(if $(filter $(basename $@).o, $(real-obj-m)),y)

modkern_cflags =                                          
        $(if $(part-of-module),                           
                $(KBUILD_CFLAGS_MODULE) $(CFLAGS_MODULE), 
                $(KBUILD_CFLAGS_KERNEL) $(CFLAGS_KERNEL) $(modfile_flags))

c_flags        = -Wp,-MMD,$(depfile) $(NOSTDINC_FLAGS) $(LINUXINCLUDE)     
                 -include $(srctree)/include/linux/compiler_types.h       
                 $(_c_flags) $(modkern_cflags)                           
                 $(basename_flags) $(modname_flags)

The takeaway here is that we get $(KBUILD_CFLAGS_MODULE) for modules, and $(KBUILD_CFLAGS_KERNEL) for builtin. These are defined in the top-level Makefile:

KBUILD_CFLAGS_KERNEL :=
KBUILD_CFLAGS_MODULE  := -DMODULE

Initialization Function for Builtins

This difference in MODULE preprocessor macro definition is utilized in include/linux/module.h to provide alternate definitions to module_init (and module_exit):

#ifndef MODULE
/**
 * module_init() - driver initialization entry point
 * @x: function to be run at kernel boot time or module insertion
 *
 * module_init() will either be called during do_initcalls() (if
 * builtin) or at module insertion time (if a module).  There can only
 * be one per module.
 */
#define module_init(x)  __initcall(x);

/**
 * module_exit() - driver exit entry point
 * @x: function to be run when driver is removed
 *
 * module_exit() will wrap the driver clean-up code
 * with cleanup_module() when used with rmmod when
 * the driver is a module.  If the driver is statically
 * compiled into the kernel, module_exit() has no effect.
 * There can only be one per module.
 */
#define module_exit(x)  __exitcall(x);
#else /* MODULE */
...

Tracking initcall

So, if builtin, module_init becomes __initcall, which is the same as device_initcall, as you can see in include/linux/init.h:

#define __initcall(fn) device_initcall(fn)

If you can follow all the macros:

#define device_initcall(fn)             __define_initcall(fn, 6)
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
#define ___define_initcall(fn, id, __sec)                       
        __unique_initcall(fn, id, __sec, __initcall_id(fn))

#define __unique_initcall(fn, id, __sec, __iid)                 
        ____define_initcall(fn,                                 
                __initcall_stub(fn, __iid, id),                 
                __initcall_name(initcall, __iid, id),           
                __initcall_section(__sec, __iid))

#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
#define ____define_initcall(fn, __stub, __name, __sec)          
        __define_initcall_stub(__stub, fn)                      
        asm(".section   "" __sec "", "a"            n"     
            __stringify(__name) ":                      n"     
            ".long      " __stringify(__stub) " - .     n"     
            ".previous                                  n");   
        static_assert(__same_type(initcall_t, &fn));
#else
#define ____define_initcall(fn, __unused, __name, __sec)        
        static initcall_t __name __used                         
                __attribute__((__section__(__sec))) = fn;
#endif

or if you run the preprocessor, you arrive at something like this:

.section ".initcall6.init", "a"
__initcall__kmod_mkiss__502_979_mkiss_init_driver6:
.long mkiss_init_driver - .
.previous

which is adding a unique (for every distinct invocation of a given function) symbol with an (relative) address to the mkiss_init_driver function, in a section called .initcall6.init. The “6” here represents the initcall order for device. It goes:

0: pure
1: core
2: postcore
3: arch
4: subsys
5: fs
6: device
7: late

When all these object files are linked together, the .initcallX.init sections will be merged by the linker, and each such section will be an array of function pointers to be called in that stage during initialization. The definitions of these arrays can be found in include/asm-generic/vmlinux.lds.h:

#define INIT_DATA_SECTION(initsetup_align)                              
        .init.data : AT(ADDR(.init.data) - LOAD_OFFSET) {               
                INIT_DATA                                               
                INIT_SETUP(initsetup_align)                             
                INIT_CALLS                                              
                CON_INITCALL                                            
                INIT_RAM_FS                                             
        }

#define INIT_CALLS                                                      
                __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)                                     
                __initcall_end = .;

#define INIT_CALLS_LEVEL(level)                                         
                __initcall##level##_start = .;                          
                KEEP(*(.initcall##level##.init))                        
                KEEP(*(.initcall##level##s.init))

Finally, you can see the iteration over these arrays in init/main.c:

static initcall_entry_t *initcall_levels[] __initdata = {
        __initcall0_start,
        __initcall1_start,
        __initcall2_start,
        __initcall3_start,
        __initcall4_start,
        __initcall5_start,
        __initcall6_start,
        __initcall7_start,
        __initcall_end,
};
...
static void __init do_initcalls(void)
{
        ...
        for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) {
                ...
                do_initcall_level(level, command_line);
        }
        ...
}

static void __init do_initcall_level(int level, char *command_line)
{
        ...
        for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
                do_one_initcall(initcall_from_entry(fn));
}

int __init_or_module do_one_initcall(initcall_t fn)
{
        ...
        ret = fn();
        ...
        return ret;
}
User contributions licensed under: CC BY-SA
1 People found this is helpful
Advertisement