This article will provide a complete analysis of the dynamic loading process of kernel modules.
To emphasize, we are organizing the dynamic loading process in this section, which involves compiling into a <span>ko</span> file and using the <span>insmod</span> method to load it, which differs from the <span>built-in</span> process where the kernel automatically loads modules.
First, let’s understand the overall process through a flowchart:

The dynamic loading of the kernel mainly consists of the following steps:
-
Writing Stage:
-
<span>EXPORT_SYMBOL</span>defines the symbol and specifies it to a special section -
<span>module_init/exit</span>renames the custom function entry point to unify the entry function -
Compilation and Linking Stage:
-
<span>EXPORT_SYMBOL</span>compiles and links to the<span>__ksymtab</span>section -
<span>module_init/exit</span>points to<span>init_module/cleanup_module</span> -
All information exists in the
<span>ko</span>file -
Loading Stage:
-
Using
<span>insmod xxx.ko</span>, in user space, data is copied from the disk to memory, and parameters are passed to the kernel through a system call for further processing -
The kernel will perform a series of operations such as allocating memory for the module, finding module sections, relocating, initializing the module, and releasing memory
Next, we will elaborate on the implementation details of each key point in this process. The length of the article may be a bit long, but I believe it will be helpful if you read it patiently.
Before we officially start, I will pose a few questions for everyone to think about. The <span>nm</span> command can display symbol information in binary object files (such as library files and executables). We can see the symbol information of the module example used in the previous section.

Consider these questions:
-
<span>math.c</span>uses symbols exported from<span>add.c/sub.c</span>, why do they appear as<span>U</span>in the symbol table and have no address? -
<span>math.ko</span>uses<span>module_init/exit</span>to define<span>math_init/exit</span>as the entry and exit points of the kernel module, so what are the symbols<span>init_module/cleanup_module</span>in the symbol table? Why do both addresses appear the same?
1. Compilation, Generation, and Linking of Symbols
Since the <span>insmod</span> process involves section and symbol resolution, we will first clarify how the key sections and symbols used in the loading process are generated.
The most important macros in kernel modules are <span>module_init</span>, <span>module_exit</span>, and <span>EXPORT_SYMBOL</span>.
The compilation and linking process of the image file in Kbuild is quite complex, but the core function of these three macros is similar: define and generate symbols, specify the location of sections, and allow the loading process to load and resolve symbols at the specified location.
1.1 EXPORT_SYMBOL
<span>EXPORT_SYMBOL</span> is defined as follows:
#define EXPORT_SYMBOL(sym) _EXPORT_SYMBOL(sym, "")
#define _EXPORT_SYMBOL(sym, license) __EXPORT_SYMBOL(sym, license, "")
#define __EXPORT_SYMBOL(sym, license, ns) \
extern typeof(sym) sym; \
__ADDRESSABLE(sym) \
asm(__stringify(___EXPORT_SYMBOL(sym, license, ns)))
The final expanded assembly code is:
.section ".export_symbol","a"
__export_symbol_atm_charge:
.asciz "" # License information (empty string)
.asciz "" # Namespace (empty string)
.quad atm_charge # Symbol address reference
.previous
At this point, the compiler will generate the <span>.export_symbol</span> section. The linking script will then process it in <span>module.lds</span>, discarding <span>export_symbol</span> and generating three new sections such as <span>__ksymtab</span>.
SECTIONS {
/DISCARD/ : {
*(.discard)
*(.discard.*)
*(.export_symbol)
}
__ksymtab 0 : { *(SORT(___ksymtab+*)) }
__ksymtab_gpl 0 : { *(SORT(___ksymtab_gpl+*)) }
__kcrctab 0 : { *(SORT(___kcrctab+*)) }
__kcrctab_gpl 0 : { *(SORT(___kcrctab_gpl+*)) }
}
I worry that if I elaborate further, everyone will lose patience, so I will summarize here. Those interested can look at the code themselves.
The subsequent process will be handled by <span>modpost</span>. <span>scripts/mod/modpost.c</span> is a module processing tool that will add section information to <span>.mod.c</span>. As shown in the figure below, this information will eventually be linked to <span>math.ko</span> for kernel loading and lookup.

One point we want to emphasize here is that during this process, when the compiler encounters undefined symbols, it will mark <span>add</span> and <span>sub</span> as <span>UND</span> (undefined).
static void handle_symbol(struct module *mod, struct elf_info *info,
const Elf_Sym *sym, const char *symname)
{
switch (sym->st_shndx) {
case SHN_UNDEF:
/* undefined symbol */
if (ELF_ST_BIND(sym->st_info) != STB_GLOBAL &&
ELF_ST_BIND(sym->st_info) != STB_WEAK)
break;
if (ignore_undef_symbol(info, symname))
break;
sym_add_unresolved(symname, mod,
ELF_ST_BIND(sym->st_info) == STB_WEAK);
break;
}
}
<span>modpost.c</span> will use <span>sym_add_unresolved</span> to define these symbols as unresolved references, which also answers our first question posed at the beginning.
Q: <span>math.c</span> uses symbols exported from <span>add.c/sub.c</span>, why do they appear as <span>U</span> in the symbol table and have no address?
A: In the symbol information, U represents an unresolved reference, which will be traversed and searched during the loading process, so the address has not been allocated at this time.
1.2 module_init/exit
<span>module_init/exit</span> defines the entry and exit functions of the kernel module, and its role is also to specify to specific sections. However, it is important to note that the processes for built-in modules and external modules are somewhat inconsistent. As shown in the following code:
#ifndef MODULE
#define module_init(x) __initcall(x);
#else /* MODULE */
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) \
__attribute__((alias(#initfn))); \
___ADDRESSABLE(init_module, __initdata);
#endif
For kernel modules: the entry function is specified to <span>__initcall(6)</span>, and during the startup loading phase, the kernel will load in order. As for why it is 6, it is because the <span>module_init</span> we use is actually at a lower level than <span>device_initcall</span>, and the kernel has higher levels such as <span>early_initcall</span>, <span>subsys_initcall</span>, <span>fs_initcall</span>, etc. We will not elaborate on this here; we will focus on external modules.
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) \
__attribute__((alias(#initfn))); \
___ADDRESSABLE(init_module, __initdata);
The definition of <span>module_init</span> for external modules has two functions:
-
<span>__copy(initfn)</span>: This will copy all information of the module, including section information, to<span>init_module</span>, and the compiler will place the startup entry function in the specified section through<span>__section(".init.text")</span>. -
<span>__attribute__((alias(#initfn))</span>: This gives an alias to<span>init_module</span>, meaning that the kernel recognizes the startup entry function as<span>init_module</span>. For example, when we use<span>module_init(math_init)</span>, it gives an alias to<span>init_module</span>, which is beneficial for the kernel to manage a unified entry function.
Thus, we can answer the second question posed at the beginning.
Q: <span>math.ko</span> uses <span>module_init/exit</span> to define <span>math_init/exit</span> as the entry and exit points of the kernel module, so what are the symbols <span>init_module/cleanup_module</span> in the symbol table? Why do both addresses appear the same?
A: <span>module_init</span> is not the entry function; the kernel has a unified entry point, which is <span>init_module</span>. The <span>module_init</span> just gives an alias, so the addresses of both appear the same. In the above <span>ko</span> section information, it can also be seen that what is stored in the section is <span>init_module</span>.

1.3 Summary
So we see that although the process is complex, the essence of <span>EXPORT_SYMBOL</span> and <span>module_init</span> is not different from creating a variable; they just have an additional step of placing information into a special designated section. This is because this information is important for module initialization, making it easier for the kernel to find it when loading modules later.
All necessary materials for the kernel to load the module have been prepared, and now we will enter the loading process.
2. insmod Process
2.1 User Space
<span>insmod</span> is a user-space tool. When we use <span>insmod xxx.ko</span>, <span>insmod</span> internally uses the <span>open/read</span> interfaces to operate the file system, copying data from the disk to memory, and finally calling <span>sys_init_module</span> to trigger a system call for the kernel to process.
When calling <span>sys_init_module</span>, the first address of <span>xxx.ko</span>, data length, and other data are passed as parameters to the kernel.
insmod (User Space Command)
├── Parameter Parsing and Validation
├── File Operations
│ ├── open() // Open module file
│ ├── read() // Read module data
│ └── close() // Close file descriptor
└── System Call
└── sys_init_module(init_module) // Call kernel module loading system call
2.2 System Call
After triggering the system call, the first processing function in the kernel is <span>init_module</span>:
SYSCALL_DEFINE3(init_module, void __user *, umod, xxxxxxxx)
{
int err;
struct load_info info = { };
err = may_init_module();
if (err)
return err;
err = copy_module_from_user(umod, len, &info);
if (err) {
mod_stat_inc(&failed_kreads);
mod_stat_add_long(len, &invalid_kread_bytes);
return err;
}
return load_module(&info, uargs, 0);
}
<span>may_init_module</span>: Performs preliminary permission checks
<span>copy_module_from_user</span>: Copies the module data passed from user space to kernel space
<span>load_module</span>: The core processing function for dynamic loading
We will focus on analyzing the <span>load_module</span> function
2.3 Preparations Before Loading the Module (load_module)
static int load_module(struct load_info *info, const char __user *uargs,
int flags)
<span>load_module</span> function’s core role is to copy module information from user space <span>struct load_info</span> to kernel space <span>struct module</span>.
<span>struct module</span> is the core structure that describes a loaded kernel module, defined in <span>include/linux/module.h</span>. It contains all information related to the module:
-
Module State (state)
-
Module Name (name)
-
Module Symbol Table (syms)
-
Module Parameters (kp)
-
Module Memory Information (mem)
-
Module Dependencies (source_list, target_list)
Moreover, each loaded module corresponds to an instance of <span>struct module</span> in the kernel.
Below is the complete function call tree of <span>load_module</span>:
load_module()
├── 1. Signature Verification
│ └── module_sig_check()
│
├── 2. ELF File Verification
│ ├── elf_validity_cache_copy()
│ └── early_mod_check()
│
├── 3. Memory Allocation and Layout
│ └── layout_and_allocate()
│ ├── module_frob_arch_sections()
│ ├── module_enforce_rwx_sections()
│ ├── layout_sections()
│ ├── layout_symtab()
│ └── move_module()
│
├── 4. Module Registration
│ ├── add_unformed_module()
│ └── module_augment_kernel_taints()
│
├── 5. Memory Initialization
│ ├── percpu_modalloc()
│ └── module_unload_init()
│
├── 6. Symbol Resolution and Relocation
│ ├── find_module_sections()
│ ├── check_export_symbol_versions()
│ ├── simplify_symbols()
│ └── apply_relocations()
│
├── 7. Module Completion
│ ├── post_relocation()
│ │ ├── sort_extable()
│ │ ├── percpu_modcopy()
│ │ ├── add_kallsyms()
│ │ └── module_finalize()
│ ├── complete_formation()
│ └── prepare_coming_module()
│
├── 8. Parameter Parsing and System Integration
│ ├── parse_args()
│ └── mod_sysfs_setup()
│
└── 9. Module Initialization
└── do_init_module()
├── do_mod_ctors()
├── do_one_initcall()
├── ftrace_free_mem()
├── module_enable_rodata_ro()
└── do_free_init()
The overall process is lengthy, so we will explain a few key functions.
2.3.1 add_unformed_module
This function checks if the module exists; if not, it adds it to a linked list for subsequent traversal and lookup. Therefore, as we mentioned in the previous section, when the kernel module has dependencies, the dependent modules must be loaded first. The module information is registered in the linked list here, allowing dependent modules to find the information through the linked list after loading.
/*
* We try to place it in the list now to make sure it's unique before
* we dedicate too many resources. In particular, temporary percpu
* memory exhaustion.
*/
static int add_unformed_module(struct module *mod)
{
int err;
mod->state = MODULE_STATE_UNFORMED;
mutex_lock(&module_mutex);
err = module_patient_check_exists(mod->name, FAIL_DUP_MOD_LOAD);
if (err)
goto out;
mod_update_bounds(mod);
list_add_rcu(&mod->list, &modules);
mod_tree_insert(mod);
err = 0;
out:
mutex_unlock(&module_mutex);
return err;
}
2.3.2 find_module_sections
We previously mentioned that <span>EXPORT_SYMBOL</span> specifies symbols to the <span>__ksymtab section</span>, and here we will look for the information of these symbol sections.
static const struct symsearch arr[] = {
{ __start___ksymtab, __stop___ksymtab, __start___kcrctab,
NOT_GPL_ONLY, false },
{ __start___ksymtab_gpl, __stop___ksymtab_gpl, __start___kcrctab_gpl,
GPL_ONLY, false },
};
It searches for module sections and stores them in the <span>mod</span> structure for relocation preparation. The <span>find_module_sections</span> function actually searches many sections; below are just some related to <span>__ksymtab</span>.
static int find_module_sections(struct module *mod, struct load_info *info)
{
mod->kp = section_objs(info, "__param",
sizeof(*mod->kp), &mod->num_kp);
mod->syms = section_objs(info, "__ksymtab",
sizeof(*mod->syms), &mod->num_syms);
mod->crcs = section_addr(info, "__kcrctab");
mod->gpl_syms = section_objs(info, "__ksymtab_gpl",
sizeof(*mod->gpl_syms),
&mod->num_gpl_syms);
mod->gpl_crcs = section_addr(info, "__kcrctab_gpl");
.......
}
2.3.3 simplify_symbols & apply_relocations
<span>simplify_symbols</span> is the core processing function for symbol resolution, working with <span>apply_relocations</span> to complete the relocation operation of the kernel module. In simple terms, it resolves symbols and calculates offsets, ultimately allowing our defined variable names and function names to point to the actual memory space for execution. Let’s look at the internal implementation:
<span>simplify_symbols</span> traverses the module’s symbol table and processes symbols based on their types (public symbols, absolute symbols, undefined symbols, defined symbols, etc.), converting relative addresses to absolute addresses and resolving external dependency symbols to ensure all symbols in the module can be correctly referenced and located.
static int simplify_symbols(struct module *mod, const struct load_info *info)
{
Elf_Shdr *symsec = &info->sechdrs[info->index.sym];
Elf_Sym *sym = (void *)symsec->sh_addr;
unsignedlong secbase;
unsignedint i;
int ret = 0;
conststruct kernel_symbol *ksym;
for (i = 1; i < symsec->sh_size / sizeof(Elf_Sym); i++) {
constchar *name = info->strtab + sym[i].st_name;
switch (sym[i].st_shndx) {
case SHN_COMMON:
/* Ignore common symbols */
if (!strncmp(name, "__gnu_lto", 9))
break;
/*
* We compiled with -fno-common. These are not
* supposed to happen.
*/
pr_debug("Common symbol: %s\n", name);
pr_warn("%s: please compile with -fno-common\n",
mod->name);
ret = -ENOEXEC;
break;
case SHN_ABS:
/* Don't need to do anything */
pr_debug("Absolute symbol: 0x%08lx %s\n",
(long)sym[i].st_value, name);
break;
case SHN_LIVEPATCH:
/* Livepatch symbols are resolved by livepatch */
break;
case SHN_UNDEF:
ksym = resolve_symbol_wait(mod, info, name);
/* Ok if resolved. */
if (ksym &&& !IS_ERR(ksym)) {
sym[i].st_value = kernel_symbol_value(ksym);
break;
}
/* Ok if weak or ignored. */
if (!ksym &&&
(ELF_ST_BIND(sym[i].st_info) == STB_WEAK ||
ignore_undef_symbol(info->hdr->e_machine, name)))
break;
ret = PTR_ERR(ksym) ?: -ENOENT;
pr_warn("%s: Unknown symbol %s (err %d)\n",
mod->name, name, ret);
break;
default:
/* Divert to percpu allocation if a percpu var. */
if (sym[i].st_shndx == info->index.pcpu)
secbase = (unsignedlong)mod_percpu(mod);
else
secbase = info->sechdrs[sym[i].st_shndx].sh_addr;
sym[i].st_value += secbase;
break;
}
}
return ret;
}
We previously mentioned that when exporting symbols during compilation, “unresolved references” are generated, which need special handling during relocation. The handling method corresponds to <span>case SHN_UNDEF</span>, which will be processed using <span>resolve_symbol_wait</span>.
resolve_symbol_wait()
├── resolve_symbol()
│ ├── find_symbol()
<span>resolve_symbol_wait</span>‘s core processing is <span>find_symbol</span>, which primarily works as follows:
-
Collect exported symbols in the
<span>section</span> -
Traverse the
<span>mod</span>linked list in the kernel module to check for these symbols -
If matched successfully, link them; if not, report an error and return
This is why, when <span>math.ko</span> depends on <span>add</span> and <span>sub</span>, it is necessary to load <span>add.ko</span> and <span>sub.ko</span> before executing <span>insmod math.ko</span>. After loading <span>add</span> and <span>sub</span>, the corresponding symbols are available in the <span>mod</span> linked list, allowing the loading of <span>math</span> to proceed to the next step.
/*
* Find an exported symbol and return it, along with, (optional) crc and
* (optional) module which owns it. Needs preempt disabled or module_mutex.
*/
bool find_symbol(struct find_symbol_arg *fsa)
{
staticconststruct symsearch arr[] = {
{ __start___ksymtab, __stop___ksymtab, __start___kcrctab,
NOT_GPL_ONLY },
{ __start___ksymtab_gpl, __stop___ksymtab_gpl,
__start___kcrctab_gpl,
GPL_ONLY },
};
struct module *mod;
unsignedint i;
module_assert_mutex_or_preempt();
for (i = 0; i < ARRAY_SIZE(arr); i++)
if (find_exported_symbol_in_section(&arr[i], NULL, fsa))
returntrue;
list_for_each_entry_rcu(mod, &modules, list,
lockdep_is_held(&module_mutex)) {
struct symsearch arr[] = {
{ mod->syms, mod->syms + mod->num_syms, mod->crcs,
NOT_GPL_ONLY },
{ mod->gpl_syms, mod->gpl_syms + mod->num_gpl_syms,
mod->gpl_crcs,
GPL_ONLY },
};
if (mod->state == MODULE_STATE_UNFORMED)
continue;
for (i = 0; i < ARRAY_SIZE(arr); i++)
if (find_exported_symbol_in_section(&arr[i], mod, fsa))
returntrue;
}
pr_debug("Failed to find symbol %s\n", fsa->name);
returnfalse;
}
<span>apply_relocations</span> is the core processing function for relocation. Relocation refers to the process of converting symbol references (such as function names and variable names) in the program to actual memory addresses, allowing the program to correctly access the functions and data it depends on. In simple terms: it transforms “names” into “addresses”.
static int apply_relocations(struct module *mod, const struct load_info *info)
{
unsignedint i;
int err = 0;
/* Now do relocations. */
for (i = 1; i < info->hdr->e_shnum; i++) {
unsignedint infosec = info->sechdrs[i].sh_info;
/* Not a valid relocation section? */
if (infosec >= info->hdr->e_shnum)
continue;
/* Don't bother with non-allocated sections */
if (!(info->sechdrs[infosec].sh_flags & SHF_ALLOC))
continue;
if (info->sechdrs[i].sh_flags & SHF_RELA_LIVEPATCH)
err = klp_apply_section_relocs(mod, info->sechdrs,
info->secstrings,
info->strtab,
info->index.sym, i,
NULL);
elseif (info->sechdrs[i].sh_type == SHT_REL)
err = apply_relocate(info->sechdrs, info->strtab,
info->index.sym, i, mod);
elseif (info->sechdrs[i].sh_type == SHT_RELA)
err = apply_relocate_add(info->sechdrs, info->strtab,
info->index.sym, i, mod);
if (err < 0)
break;
}
return err;
}
This function will find the positions that need to be modified based on the relocation table in the ELF file, then replace the placeholders with the actual memory addresses of the symbols, and assign them to the <span>mod</span> structure, completing the relocation.

2.4 Loading Module do_init_module
<span>do_init_module</span> function is the core function for initializing kernel modules, responsible for completing the final stage of module loading. As noted in the comments, “*This is where the real work happens.*”
However, in reality, this function does not have much to say; it is a relatively standardized initialization function:
-
Allocate space and execute the
<span>module_init</span>function, -
Update the module state to
<span>LIVE</span>, notifying the kernel module and counting -
Asynchronously release the initialization memory
In the <span>init</span> process, the Linux kernel also implements dynamic memory release, further improving memory space utilization. After version 6.x, it uses an asynchronous method to ensure both utilization and efficiency of kernel operation. Let’s look at this process:
The complete version is in <span>kernel/module/main.c</span>, and I will only show the key parts below:
/*
* This is where the real work happens.
*
* Keep it uninlined to provide a reliable breakpoint target, e.g. for the gdb
* helper command 'lx-symbols'.
*/
static noinline int do_init_module(struct module *mod)
{
........
........
freeinit->init_text = mod->mem[MOD_INIT_TEXT].base;
freeinit->init_data = mod->mem[MOD_INIT_DATA].base;
freeinit->init_rodata = mod->mem[MOD_INIT_RODATA].base;
do_mod_ctors(mod);
/* Start the module */
if (mod->init != NULL)
ret = do_one_initcall(mod->init);
if (llist_add(&freeinit->node, &init_free_list))
schedule_work(&init_free_wq);
..........
..........
}
It first checks whether the <span>mod->init</span> pointer is null; <span>do_one_initcall</span> will execute the initialization function defined by <span>module_init</span>.
As we can see, during initialization, the <span>freeinit</span> records the information in the <span>init</span> section, and then the <span>do_free_init</span> function will be executed in the work queue, responsible for:
-
Calling
<span>synchronize_rcu()</span>to ensure no readers are accessing this memory -
Releasing memory for the initialization code section, data section, and read-only data section
-
Cleaning up related memory mappings
Thus, the purpose of designating initialization information to the special section <span>.init.text</span> is to allow the kernel to determine that this information is one-time only, and can be safely released after loading. Additionally, the use of queue scheduling ensures that the success of module loading will not be blocked by memory release operations, while also ensuring the safety of memory release.
3. Summary
In this article, we explained the complete process of loading an external kernel module through <span>insmod</span><span>:</span>
-
Preparation of symbols, entry and exit function symbols, compiling and linking, specifying to special sections
-
<span>insmod</span>user-space tool triggers a system call for kernel processing -
The kernel performs a series of operations such as symbol lookup, resolution, and relocation to complete the loading of the kernel module
-
After loading, the information from the startup phase is released to save memory
Thus, we have completed the content of kernel modules. From understanding to writing to experimentation, we have deeply organized the entire process and analyzed the internal implementation details, truly achieving the learning process of WHAT——>HOW——>WHY. I believe that after this, you will have a deeper understanding of kernel modules.
With the kernel module completed, we will now begin learning Linux device driver development. We will also present the standard device driver development process, provide driver templates, and delve into the internal principles. Stay tuned~
end
One Linux
Follow, reply 【1024】 to receive a wealth of Linux materials
Collection of Wonderful Articles
Recommended Articles
☞【Album】ARM☞【Album】Fan Q&A☞【Album】All Originals☞【Album】linuxIntroduction☞【Album】Computer Networks☞【Album】Linux Drivers☞【Dry Goods】Embedded Driver Engineer Learning Path☞【Dry Goods】All Knowledge Points of Linux Embedded – Mind Map