Dynamic calling method and system for non-exported functions of kernel
By defining the kprobe structure and function pointer types, the kernel virtual address of unexported kernel functions is dynamically obtained and converted into a callable function pointer, which solves the problem of calling unexported functions under different kernel versions and realizes dynamic adaptation and compatibility of kernel functions.
Patent Information
- Authority / Receiving Office
- CN · China
- Patent Type
- Patents(China)
- Current Assignee / Owner
- CHINA UNICOM DIGITAL TECNOLOGY CO LTD
- Filing Date
- 2026-04-16
- Publication Date
- 2026-07-07
AI Technical Summary
In different versions of the Linux kernel, some kernel functions are not exported, causing the cusmartcache module to be unable to be called normally. The kernel needs to be modified to adapt, which cannot be compatible with the kernel versions of external vendors in a timely manner, resulting in the inability to meet the module's calling requirements.
By defining the kprobe structure, the kernel virtual address of unexported kernel functions is obtained and converted into a callable function pointer, enabling dynamic calling of unexported kernel functions, avoiding hard-coding kernel function addresses, and supporting compatibility with different kernel versions.
It enables dynamic calling of unexported kernel functions, enhances package compatibility, avoids kernel symbol table mismatch and module loading failure issues, and supports batch calling of multiple unexported kernel functions.
Smart Images

Figure CN122044701B_ABST
Abstract
Description
Technical Field
[0001] This application belongs to the field of operating systems, and in particular relates to a method and system for dynamically calling functions not exported by the kernel. Background Technology
[0002] Cloud providers offer numerous IaaS-related data centers and servers, using a wide variety of Linux operating system versions. Especially under the trend of domestic substitution, various vendors have developed their own operating systems, corresponding to four major Linux kernel versions, from v3 to v6. Each major version has many minor versions, and the code generally changes significantly between these kernel versions.
[0003] Taking the storage acceleration kernel module cusmartcache as an example, the cusmartcache module calls exported functions related to block I / O operations in the Linux kernel. In lower versions of the Linux kernel, such as V3 or V4, these functions are exported via EXPORT_SYMBOL, allowing them to be called by the cusmartcache module. However, in V5 or V6 kernel versions, the I / O subsystem code has changed drastically. During the adaptation process, the cusmartcache module discovered that critical functions such as blk_mq_submit_bio and blk_queue_enter were not exported and could not be called externally. Since the operating system uses a higher version of the V5 or V6 kernel, the operating system development team needs to modify the kernel to export the blk_mq_submit_bio and blk_queue_enter functions required by cusmartcache to solve the cusmartcache module adaptation problem, enabling the cusmartcache module to call them successfully. However, when the kernel is developed by an external vendor, it cannot be compatible in a timely manner, resulting in the inability to meet the module's calling requirements in a timely manner, thus exhibiting many shortcomings. Summary of the Invention
[0004] This application provides a method and system for dynamically calling unexported kernel functions to solve the problem of calling unexported functions under different kernel versions.
[0005] This application provides a method for dynamically calling functions not exported by the kernel, including:
[0006] For targets that do not export kernel functions, define and configure a kprobe structure containing the symbol names of the target functions;
[0007] Register the configured kprobe structure and obtain the kernel virtual address of the target kernel function that has not been exported;
[0008] Define a function pointer type that matches the prototype of a native function that does not export a kernel function from the target;
[0009] Based on the obtained kernel virtual address, it is converted into a callable function pointer of the corresponding type;
[0010] The function pointer obtained through the conversion is used to execute a call to a kernel function that is not exported from the target.
[0011] In an optional embodiment, the step of defining and configuring a kprobe structure containing the symbolic name of the target function, for which no kernel function is exported, includes:
[0012] For each target, a controlled function description structure is defined corresponding to the kernel function definition that is not exported.
[0013] The description structure records the symbol name, dependencies, and state information of the objective function; based on the description structure, a kprobe structure is allocated and initialized for the objective function.
[0014] Bind the kprobe structure to the corresponding description structure to complete the configuration of the kprobe structure.
[0015] In an optional embodiment, the registration configuration of the kprobe structure, obtaining the kernel virtual address of the target kernel function that has not been exported, includes:
[0016] A directed acyclic graph is constructed based on the dependencies of the kernel functions not exported by each target, and a function registration order that conforms to the dependency rules is generated by a topological sorting algorithm.
[0017] Register the kprobe structure corresponding to each objective function in the generated registration order;
[0018] After the kernel completes symbol address resolution, it extracts the kernel virtual address of the target unexported kernel function from the kprobe structure and writes the extracted address into the corresponding descriptor structure.
[0019] In an optional embodiment, the step of sequentially registering the kprobe structures corresponding to each objective function according to the generated registration order includes:
[0020] Disable local interrupts and kernel preemption, enter the atomic critical section, and execute the registration operation of the kprobe structure in the registration order within the atomic critical section;
[0021] For each registered kprobe structure, perform address validity pre-check. If any kprobe structure fails to register or pre-check, unregister the successfully registered kprobe structures in reverse order of registration and release the corresponding resources.
[0022] If all kprobe structures are registered and pre-verified, submit the registration results and exit the atomic critical region.
[0023] In an optional embodiment, defining a function pointer type that matches the native function prototype of the target function that does not export a kernel function, and converting it to a callable function pointer of the corresponding type based on the obtained kernel virtual address, includes:
[0024] Match the native input parameter count, input parameter type, and return value type of the target non-exported kernel function, define the corresponding function pointer type, and perform multi-level validity checks on the obtained kernel virtual address;
[0025] After verification, the kernel virtual address is forcibly converted to a function pointer of the corresponding type, and an independent function pointer retrieval interface is encapsulated for each target function.
[0026] In an optional embodiment, performing multi-level validity checks on the obtained kernel virtual address includes:
[0027] Verify whether the kernel virtual address falls within the address range of the kernel code segment, and verify whether the memory page containing the kernel virtual address has kernel executable permissions;
[0028] Identify the current CPU architecture type and match the corresponding instruction feature rules;
[0029] Based on the matching rules, the address is deemed valid after all verification items pass the function's entry point instruction characteristics.
[0030] In an optional embodiment, the step of executing a call to a target unexported kernel function via the function pointer obtained through conversion includes:
[0031] Read the callable function pointer corresponding to the target function from the global read-only cache pool, and check the stop flag corresponding to the target function;
[0032] When marked as being in normal operating condition, the atom increments the active call count of the target function;
[0033] After re-verifying the stop flag, the target function is called via the function pointer. After the call is completed, the active call count of the target function is atomically decremented.
[0034] In an optional embodiment, the global read-only cache pool is constructed in the following manner:
[0035] After obtaining the kernel virtual addresses of all target functions that have not exported kernel functions, the description structure, kernel virtual address, and callable function pointer of each target function are filled into the global read-only cache pool.
[0036] Perform a memory barrier operation to ensure that the contents of the cache pool are visible to all CPU cores; during runtime, only read-only access operations are performed on the global read-only cache pool.
[0037] In an optional embodiment, the following steps are performed when the kernel module is unloaded:
[0038] Atomically sets the stop flag for all target functions that have not exported kernel functions, waits for the active call count of all target functions to return to zero; unregisters all registered kprobe structures;
[0039] Release the memory resources corresponding to the kprobe structure, and clear the global read-only cache pool and the description structure information of all target functions.
[0040] The second aspect of this application provides a dynamic calling system for kernel functions not exported, including a kprobe management module, a function pointer encapsulation module, an address validity verification module, a function call control module, and a resource lifecycle management module;
[0041] The kprobe management module is used to define and configure a kprobe structure for the target unexported kernel functions, perform registration and deregistration operations of the kprobe structure, and obtain the kernel virtual address of the target unexported kernel functions;
[0042] The function pointer encapsulation module is used to define function pointer types that match the native prototypes of unexported kernel functions of the target, and to convert kernel virtual addresses into callable function pointers of the corresponding types.
[0043] The address validity verification module is used to perform multi-level validity verification on the obtained kernel virtual address;
[0044] The function call management module is used to maintain a global read-only cache pool, manage the status flags and activity counts of the function call process, and execute the call to the target function through a callable function pointer;
[0045] The resource lifecycle management module is used for resource synchronization and release operations when the kernel module is uninstalled.
[0046] As can be seen from the above technical solutions, the method and system for dynamically calling unexported kernel functions provided in this application, through the kprobes mechanism, combined with the function pointer type definition and the mechanism of forcibly casting function addresses to function pointers, realizes the dynamic acquisition of function addresses and forced function calls for unexported kernel functions. After the program is adapted and modified by this application, it not only supports the calling of unexported kernel functions, but also no longer needs to hardcode the address of the called kernel function, supports the dynamic acquisition of kernel function addresses, avoids the change of kernel address caused by kernel version differences, enhances the compatibility of unexported kernel function packages, eliminates the need to recompile packages for each kernel minor version, and avoids the problem of kernel symbol table mismatch and kernel module loading failure. Attached Figure Description
[0047] To more clearly illustrate the technical solutions in the embodiments of this application or the prior art, the drawings used in the description of the embodiments or the prior art will be briefly introduced below. Obviously, the drawings described below are some embodiments of this application. For those skilled in the art, other drawings can be obtained based on these drawings without creative effort.
[0048] Figure 1 This paper illustrates a flowchart of a method for dynamically calling unexported kernel functions in an embodiment of this application.
[0049] Figure 2 A schematic diagram of a system architecture for dynamic invocation of kernel-unexported functions is shown in an embodiment of this application. Detailed Implementation
[0050] It should be noted that the embodiments of this application use the Linux kernel as an example for illustration, but it is understood that the core concept of this application is also applicable to other kernels. This application does not depend on any specific kernel. Therefore, if other kernels adopt the technical concept of this application, they should be within the protection scope of this application.
[0051] This application provides a method for dynamically calling functions not exported by the kernel, such as... Figure 1 As shown, it includes:
[0052] S1: Define and configure a kprobe structure containing the symbol name of the target function for targets that do not export kernel functions;
[0053] S2: Register the configured kprobe structure and obtain the kernel virtual address of the target kernel function that has not been exported;
[0054] S3: Defines a function pointer type that matches the prototype of a native function that does not export a kernel function from the target;
[0055] S4: Based on the obtained kernel virtual address, convert it into a callable function pointer of the corresponding type;
[0056] S5: Using the function pointer obtained through conversion, execute the call to the target's unexported kernel function.
[0057] The method and system for dynamically calling unexported kernel functions provided in this application, through the kprobes mechanism, combined with function pointer type definition and the mechanism of forcibly casting function addresses to function pointers, enables the dynamic acquisition of function addresses and forced function calls for unexported kernel functions. After the program is adapted and modified according to this application, it not only supports the calling of unexported kernel functions, but also eliminates the need to hardcode the address of the called kernel function, supports the dynamic acquisition of kernel function addresses, avoids address changes caused by kernel version differences, enhances the compatibility of unexported kernel function packages, eliminates the need to recompile packages for each minor kernel version, and avoids problems such as kernel symbol table mismatch and kernel module loading failures.
[0058] In this embodiment of the application, the method for dynamically calling unexported kernel functions disclosed in this application is exemplary applicable to all versions of the Linux kernel from V3 to V6, and of course, it can also be applied to multiple different versions of non-Linux kernels.
[0059] It should be noted that the unexported kernel functions mentioned in this application refer to kernel global functions in the Linux kernel source code that are not explicitly exported using the EXPORT_SYMBOL and EXPORT_SYMBOL_GPL macros. The Linux kernel symbol system is divided into two categories: exported symbols and non-exported symbols. Only symbols explicitly declared through export macros can be directly called by loadable kernel modules (LKMs) through external symbol references. Non-exported functions, on the other hand, only exist in the kernel's full symbol table and are not exposed to external kernel modules.
[0060] The kernel virtual address mentioned in this application refers to the entry address of the target unexported function in the kernel virtual address space. The CPU can directly jump to the instruction execution entry point of the corresponding function through this address to complete the function call.
[0061] The kprobe structure described in this application is a data structure defined by the Linux kernel kprobe subsystem for dynamic probing of kernel functions. With the help of this structure, the symbol name of the target kernel function can be bound through the symbol_name field. After successful registration, the kernel will automatically fill the virtual address of the target function into the addr field, without the need to manually traverse the kernel symbol table.
[0062] The function pointer type described in this application refers to a C language function pointer type that completely matches the prototype of the native function that does not export the kernel function. It includes the number of input parameters, the type of input parameters, and the return type that are completely consistent with the original function. The function pointer type can provide the compiler and CPU with a standard basis for function calls, ensuring that the calling process fully conforms to the calling convention of the CPU architecture, and avoiding kernel crashes caused by stack frame layout abnormalities and register allocation errors. This application will not elaborate on this.
[0063] In this embodiment, for targets that do not export kernel functions, a kprobe structure containing the target function's symbol name is defined and configured. This establishes a binding relationship with the kernel symbol table for subsequent address retrieval. It should be noted that the core design purpose of the Linux kernel kprobe subsystem is kernel debugging and performance tracing. Its native usage logic involves registering a kprobe structure and inserting probe hook functions at the target function's execution entry / exit point to intercept and trace the kernel function's execution flow. This application innovatively strips away the kprobe's probe interception function, utilizing only its automatic resolution capability from kernel symbol names to virtual addresses. It avoids any interception, modification, or hook insertion of the target function's execution flow, thus preventing interference with the normal kernel execution flow and mitigating the kernel pollution risk caused by long-term resident kprobe hooks. This is one of the core differences between this application and existing kprobe-related technologies.
[0064] For example, taking the kernel function `blk_queue_enter` as an example, a `kprobe` structure is defined to track the kernel function `blk_queue_enter`. At this point, it is not yet registered; only the parameter name to be probed is defined.
[0065] static struct kprobe kp_blk_queue_enter={
[0066] .symbol name="blk queue enter"
[0067] };
[0068] After registering the kprobe, kp_blk_queue_enter.addr will be filled with the actual kernel address of blk_queue_enter. This is a crucial step in dynamically obtaining the kernel function address. This function is called during module initialization.
[0069] void register_kernel_symbol(void){
[0070] register_kprobe(&kp_blk_queue_enter);
[0071] }
[0072] Next, a function pointer type `blk_queue_enter_func` is defined, which will be used later to cast the address of `blk_queue_enter` to a callable function pointer:
[0073] typedef int blk_queue_enter_func(struct request_queue q, blk mg_reg_flags_t flags);
[0074] Declare a function that returns a function pointer to blk_queue_enter:
[0075] blk_queue_enter_func blk_queue_enter_func_get(void);
[0076] The function returns a function pointer to blk_queue_enter for internal use within the module.
[0077] blk_queue_enter_func blk_queue_enter_func_get(void){
[0078] return (blk_queue_enter_func )kp_blk_queue_enter.addr;
[0079] }
[0080] The original implementation (due to kernel modifications that exported the blk_mq_submit_bio function, it can be called directly as shown below)
[0081] ret = blk_mq_submit_bio(bio);
[0082] The same effect can be achieved by changing the implementation as follows.
[0083] func = blk_mq_submit_bio_func_get();
[0084] ret = func(bio);
[0085] In summary, this invention, through the use of the kprobes mechanism, combined with function pointer type definition and the mechanism of forcibly casting function addresses to function pointers, enables dynamic acquisition of function addresses and forced function calls for non-exported Linux kernel functions. After being adapted and modified according to this invention, the program not only supports calls to non-exported Linux kernel functions but also eliminates the need for hard-coding the addresses of the called kernel functions, supporting dynamic acquisition of kernel function addresses and avoiding address changes caused by kernel version differences. This enhances the compatibility of the cusmartcache package. Compared to existing technologies that require recompiling the package for each minor kernel version, this application avoids the problem of kernel symbol table mismatch and kernel module loading failures.
[0086] The above technical solution is applicable to the dynamic calling of a single unexported kernel function. In the preferred embodiment of this application, multiple unexported kernel functions can be dynamically called. It should be understood that multiple unexported kernel functions generally have function dependencies. The following is a detailed description of the preferred method of dynamically calling multiple unexported kernel functions in this application.
[0087] In a preferred embodiment, the step of defining and configuring a kprobe structure containing the symbolic name of the target function, for the target function that has not been exported, includes:
[0088] For each target, a controlled function description structure is defined corresponding to the kernel function definition that is not exported.
[0089] The description structure records the symbol name, dependencies, and state information of the objective function; based on the description structure, a kprobe structure is allocated and initialized for the objective function.
[0090] Bind the kprobe structure to the corresponding description structure to complete the configuration of the kprobe structure.
[0091] It should be noted that the controlled function description structure described in this application is a custom data structure used to bind the symbol name, kprobe instance, dependency relationship, function pointer, running status, synchronization mechanism and other life-cycle information of the target function in an integrated manner, so as to realize the control of each target unexported function and avoid the management chaos and race state risk caused by scattered variable definitions.
[0092] Those skilled in the art will understand that there are clear business dependencies between functions in different subsystems of the Linux kernel. Taking the block I / O subsystem as an example, the normal execution of the blk_mq_submit_bio function depends on the completion of the pre-call of the blk_queue_enter function. In the network subsystem, the execution of the tcp_sendmsg function depends on the state synchronization of the sock_queue_rcv_skb function. If the dependencies between functions are not managed, and the address acquisition and initialization of the dependent functions are completed first, it will lead to the failure of business logic initialization, or even cause a kernel null pointer exception. Therefore, this application uses a controlled function description structure to explicitly define the dependencies of each target function, which will not be elaborated here.
[0093] Furthermore, in the preferred embodiment of this application, the complete definition of the controlled function description structure must match the compilation environment of all versions of the Linux kernel, and be compatible with mainstream CPU architectures such as x86_64, arm64, and riscv64. Specifically, the definition includes: firstly, importing the native header file dependencies of the Linux kernel, including...<linux / kernel.h> ,<linux / kprobes.h> ,<linux / atomic.h> ,<linux / spinlock.h> ,<linux / completion.h> , <linux / sched.The kernel uses common header files such as `<h>` to ensure compatibility of the structure definitions. Then, a macro is defined to specify the maximum number of unexported functions supported. This macro can be flexibly adjusted according to business needs; in this embodiment, it is set to `MAX_UNEXPORT_FUNCS 16`, which can cover the unexported function call requirements of most self-developed kernel modules. Next, a controlled function description structure `struct unexport_func_desc` is defined. Inside this structure, a `const char symbol_name` field is first defined. This field is a constant character pointer used to store the symbol name of the target unexported function. The symbol name must be completely consistent with the function name in the kernel source code and the symbol name in the kernel symbol table to ensure that `kprobe` can complete symbol address resolution. Next, a `struct kprobe kp` field is defined. This field is a pointer to a `kprobe` structure used to point to the `kprobe` instance specific to the target function, achieving a one-to-one binding between the description structure and the `kprobe` structure. Finally, the `struct unexport_func_desc deps` field and an `int` field are defined. The `dep_count` field is defined, where `deps` is a double pointer pointing to an array of pointers to other controlled function description structures that the current function depends on. `dep_count` represents the number of dependent functions, used to explicitly define business dependencies between functions. Next, the `int in_degree` field is defined, used for in-degree counting during topology sorting, a core parameter of the Kahn topology sorting algorithm. Then, the `void func_ptr` field is defined, an untyped pointer used to cache the kernel virtual addresses of unexported functions of the target. After initialization, it only performs read-only access and is the core address carrier for business layer function calls. Next, the `atomic_t stop_flag` field is defined, an atomic stop flag; 0 represents normal operation, allowing new function calls, and 1 represents module unloading, rejecting new function calls, ensuring concurrency safety under multi-CPU cores through atomic operations. Then, the `atomic_t active_call_cnt` field is defined, an atomic active call count used to record the number of threads currently executing the target function, a core synchronization parameter to prevent dangling pointer crashes during module unloading. Finally, the `struct completion` field is defined. The `exit_completion` field is a pointer to a completion structure. It's used for synchronous waiting during the module unloading process, ensuring that all existing function calls are completed before releasing related resources.
[0094] In this embodiment, after defining the controlled function description structure, it is necessary to define the corresponding description structure instance and dependency relationship for each target unexported function required by the business scenario. This embodiment takes the two unexported functions blk_queue_enter and blk_mq_submit_bio that the storage acceleration kernel module cusmartcache core depends on as examples to illustrate the complete instantiation. First, declare the description structure instances of the two target functions as staticstruct unexport_func_desc g_blk_queue_enter_desc and static struct unexport_func_desc g_blk_mq_submit_bio_desc, where g_blk_queue_enter_desc is the dependent function and g_blk_mq_submit_bio_desc is the dependent function. Then, define the dependency list of blk_mq_submit_bio, staticstruct unexport_func_desc submit_bio_deps[] = The array `{&g_blk_queue_enter_desc}` explicitly states that the `blk_mq_submit_bio` function depends only on the `blk_queue_enter` function. Then, it initializes two descriptor structure instances. For the `g_blk_queue_enter_desc` instance, its `symbol_name` field is assigned the value "blk_queue_enter", exactly matching the function name in the kernel symbol table. The `deps` field is set to NULL, and the `dep_count` field is set to 0, indicating that the function has no prerequisites. The `in_degree` field is initialized to 0, the `func_ptr` field is initialized to NULL, and the `stop_flag` field is initialized using `ATOMIC_INIT(0...)`. The macro is initialized to 0, the active_call_cnt field is also initialized to 0, and the exit_completion field is initialized to NULL. For the g_blk_mq_submit_bio_desc instance, its symbol_name field is assigned the value "blk_mq_submit_bio", the deps field is assigned the submit_bio_deps dependency array, the dep_count field is assigned the value 1 through the ARRAY_SIZE(submit_bio_deps) macro, which means that the function has 1 dependency, the in_degree field is also initialized to 1, and the initialization rules of the other fields are consistent with those of the g_blk_queue_enter_desc instance.
[0095] Furthermore, to achieve unified management of all target unexported functions, this embodiment defines a global controlled function array `static struct unexport_func_desc g_unexport_funcs[]`, storing pointers to the description structure instances of all target functions in this array. In this embodiment, the array contains two elements: `&g_blk_queue_enter_desc` and `&g_blk_mq_submit_bio_desc`. A global variable `static int g_unexport_func_count` is also defined, and its value is assigned the total number of globally controlled functions using the macro `ARRAY_SIZE(g_unexport_funcs)`. Subsequent topology sorting, batch registration, resource cleanup, and other operations are all based on this global array and the total number variable, eliminating the need to repeatedly write logic for individual functions. When a business requires the addition of a call to an unexported function, only the corresponding description structure instance needs to be added and placed in the global controlled function array; no modification to the core logic code is required.
[0096] In this embodiment of the application, after the instantiation and dependency definition of the controlled function description structure are completed, it is necessary to allocate and initialize the kprobe structure for the target function based on the description structure, bind the kprobe structure to the corresponding description structure, and complete the pre-initialization configuration of the kprobe structure.
[0097] It should be noted that the memory allocation and initialization of the kprobe structure in this step are completed in the non-critical section and are not performed in the subsequent atomic critical section. In the Linux kernel, the memory allocation function kzalloc can only use the GFP_ATOMIC allocation flag in the atomic context. However, memory allocation with the GFP_ATOMIC flag may fail, especially in scenarios where system memory is scarce. At the same time, the execution time of the atomic critical section needs to be shortened as much as possible to avoid kernel scheduling exceptions caused by excessive time spent disabling interrupts and kernel preemption. Therefore, this application completes the memory allocation and initialization of the kprobe structure before the critical section and only performs the registration operation in the critical section, which ensures the success rate of memory allocation while shortening the execution time of the critical section.
[0098] Next, this application completes the allocation and configuration of the kprobe structure through a pre-initialization function named unexport_func_kprobe_preinit. The input parameter is a pointer to the target function's description structure, and the return value is an int type, where 0 represents success and a negative number represents the kernel standard error code. The specific execution flow of the function is as follows: First, memory for the kprobe structure is allocated using the kzalloc function, with the allocation flag GFP_KERNEL. This flag allows the kernel to block and wait during memory allocation, ensuring a high success rate. The allocated size is sizeof(struct If memory allocation fails, the function prints an error log and returns an ENOMIM out-of-memory error code. If memory allocation is successful, the allocated kprobe structure pointer is assigned to the kp field of the description structure, completing the binding between the description structure and the kprobe structure. Then, the kprobe structure undergoes core configuration: the symbol_name field of the kprobe structure is assigned a constant character pointer to the symbol_name in the description structure, establishing the binding relationship between kprobe and the target function's symbol name. Next, the private data field of the kprobe structure is assigned a pointer to the current description structure. This private data field is a custom data field reserved by the kprobe subsystem and will not be modified by the kernel. In subsequent verification processes, the corresponding description structure instance is obtained through the kprobe structure, eliminating the need to write separate verification logic for each function. Finally, the function prints a log indicating successful pre-initialization and returns 0 to indicate successful execution.
[0099] Those skilled in the art will understand that this step only configures the symbol_name and private fields of the kprobe structure, without setting hook functions such as pre_handler and post_handler. The core reason for this is that this application only utilizes the symbol address resolution capability of kprobe, without needing to intercept the execution of the target function, thus avoiding interference of hook functions with the normal kernel execution process.
[0100] In this embodiment, after completing the pre-initialization of the kprobe structure for all target functions, the entire process of defining and configuring the kprobe structure is completed. Next, the process proceeds to register the configured kprobe structure and obtain the kernel virtual address of the target unexported kernel function. When the Linux kernel kprobe subsystem executes the register_kprobe function to register the kprobe structure, it calls the kprobe_addr function inside the kernel. Regardless of whether the function is exported through the EXPORT_SYMBOL macro, as long as it exists in the kernel's full symbol table, address resolution can be completed, and the resolved function entry address is filled into the addr field of the kprobe structure.
[0101] It should be noted that this application fills in the actual kernel addresses of unexported functions after successful registration via the kprobe structure. As is known to those skilled in the art, the data registered by the kprobe structure has read-only permissions. Utilizing the native read-only feature of kp.addr, no additional lock is needed to protect the function pointer. In this way, the process of traversing the actual kernel address is separated from the function call process. The traversal process occurs during the kprobe structure registration phase, and during the call phase, it is not necessary to traverse the actual kernel addresses of unexported functions throughout the entire kernel. That is, all complex dependency management, batch registration, and data filling work are completed in the single-threaded, non-concurrent initialization phase; in the multi-threaded, high-concurrency runtime phase, only read-only data is accessed without locks.
[0102] Furthermore, in this embodiment of the application, the registration process of the kprobe structure can adopt a transactional batch registration mechanism. For example, a directed acyclic graph can be constructed based on the dependency relationship of each target's unexported kernel functions. A function registration order that conforms to the dependency rules can be generated by a topological sorting algorithm. The kprobe structures corresponding to each target function are registered in sequence according to the generated registration order. After the kernel completes symbol address resolution, the kernel virtual address of the target's unexported kernel function is extracted from the kprobe structure, and the extracted address is written into the corresponding description structure.
[0103] Those skilled in the art will understand that a Directed Acyclic Graph (DAG) is a directed graph structure without circular dependencies, capable of expressing dependencies between functions. The Kahn topological sorting algorithm can generate an execution sequence that conforms to dependency rules based on the in-degree information of the DAG, ensuring that the dependent function is registered first, followed by the dependent function, thus avoiding initialization failures due to incorrect registration order. This application will not elaborate further on this point. This application does not provide extensive examples of DAGs, and it is understood that other methods can also be used to express dependencies between functions. The above-mentioned representation of dependencies is existing well-known technology, and this embodiment will not elaborate further.
[0104] Next, this application generates the registration order using a topological sorting function named `unexport_func_topological_sort`. The function takes an array of pointers to sorted function descriptions (`struct unexport_func_desc sorted_order`) as input, outputs the sorting result, and returns an integer, where 0 represents success and a negative number represents an error code. The specific execution flow and principle of the function are as follows:
[0105] First, define the loop variable and the sorting index variable, and also define a queue array for the Kahn algorithm. The maximum length of the queue is MAX_UNEXPORT_FUNCS. Define the head and tail pointers of the queue. Then, reset the in-degree count of all objective functions, and reassign the in_degree field of each function to its dep_count dependency count. Simultaneously, traverse all functions, and add functions with an in-degree of 0, i.e., functions with no dependencies, to the tail of the queue. These functions are the starting nodes of the topological sort because they can perform their own registration operation without waiting for other functions to complete registration. Then, enter the core loop of the Kahn algorithm. When the head pointer of the queue is less than the tail pointer, retrieve the description of the current function at the head of the queue. For example, add the sorted function to the sorted output array, incrementing the sort index. Then, iterate through all globally controlled functions, checking if each function's dependency list contains the current function. If it does, decrement the function's in-degree count by 1. When the function's in-degree count reaches 0, it means all its dependent functions have been sorted, and it is added to the tail of the queue, waiting for subsequent sorting. After the core loop finishes executing, check if the number of sorted functions matches the total number of globally controlled functions. If they don't match, it means there is a circular dependency between functions, and a valid topological sort sequence cannot be generated. The function prints a circular dependency error log and returns the EINVAL invalid parameter error code. If they match, it prints a log indicating successful topological sorting and returns 0 to indicate successful execution.
[0106] It should be noted that the registration order generated by topological sorting in this step follows the dependency relationships between functions. For example, the blk_queue_enter function has no dependency and an in-degree of 0, so it is ranked first in the registration sequence. The blk_mq_submit_bio function depends on blk_queue_enter and has an in-degree of 1. Only after blk_queue_enter completes the sorting will its in-degree decrease to 0, and it will be ranked second in the registration sequence. This conforms to the business dependency rules, ensuring that the dependent function completes address acquisition and initialization first, and avoiding initialization anomalies in the business logic.
[0107] Those skilled in the art will understand that this topology sorting mechanism is scalable. When multiple unexported functions with complex dependencies are added to the business, the algorithm can automatically generate a valid registration order simply by correctly defining the dependencies in the description structure. There is no need to manually adjust the registration logic, which greatly reduces the maintenance cost of the code. It should be understood that the topology sorting mechanism is a currently common method for maintaining functions with complex dependencies. The core of this application is not the creation of a topology sorting mechanism, and it will not be described in detail here.
[0108] In this embodiment, after generating a valid registration order, the core step of transactional batch registration is entered. The kprobe structures corresponding to each target function are registered sequentially according to the generated registration order. In this step, the registration operation is preferably performed in an atomic critical section where local interrupts and kernel preemption are disabled. Address validity pre-verification is performed on each registered kprobe structure. If any kprobe structure fails to register or pre-verify, the successfully registered kprobe structures are deregistered in reverse order and the corresponding resources are released. If all kprobe structures are registered and pre-verification passes, the registration result is submitted and the atomic critical section is exited.
[0109] It should be noted that the use of single-critical-section atomic operations in this step ensures the successful batch export of a batch of unexported functions. Simultaneously, the Linux kernel's `register_kprobe` and `unregister_kprobe` functions internally hold a global mutex `kprobe_mutex`, guaranteeing that only one thread can execute the kprobe registration and deregistration operations at any given time. This application disables local interrupts and kernel preemption, encapsulating the registration, verification, and rollback operations of all target functions within the same uninterruptible critical section. This ensures that either all function kprobes are successfully registered, or if any function fails, all registered kprobes are completely deregistered, preventing intermediate states of partial success and partial failure, and avoiding residual kprobe instances triggering the kernel's `TAINT_PROBE` pollution flag.
[0110] Furthermore, before performing batch registration, this application first defines a globally unified address validity verification hook function named `unexport_func_addr_check_handler`. This function conforms to the function prototype requirements of the `pre_handler` hook function of the kprobe subsystem. Its input parameters are the pointer to the current kprobe instance `struct kprobe p` and the register context pointer `struct pt_regsregs`. The return value is fixed at 0, and it does not intercept the execution of the original function. This function can verify that the address resolved by kprobe is a valid and callable kernel function entry point. In this way, the kernel will only trigger the `pre_handler` hook function when the target function is executed if the `addr` field of the `kprobe` structure is filled with the correct kernel function entry address. If the address is invalid, the hook function will never be triggered. Therefore, the validity of the address can be verified in one go by the execution status of this hook function. At the same time, this hook function has no business logic inside and only returns 0, which will not have any performance impact on the execution of the target function, nor will it intercept the execution flow of the original function.
[0111] Next, this application completes the entire registration process through a transactional batch registration function named `unexport_func_batch_transaction_register`. This function has no input parameters and returns an integer, where 0 represents success and a negative number represents an error code. Simultaneously, a globally sorted array of function registration sequences, `static struct unexport_func_desc g_sorted_funcs[MAX_UNEXPORT_FUNCS]`, is defined to store the topologically sorted function sequence. A global read-only cache pool, `static struct unexport_func_desc g_ro_func_pool[MAX_UNEXPORT_FUNCS]`, and `static int g_ro_pool_count = 0`, are defined for subsequent lock-free access by the business layer. The specific execution flow of the function is as follows:
[0112] The first step is to perform a topological sort by calling the `unexport_func_topological_sort` function and storing the sorting results in the global array `g_sorted_funcs`. If the topological sort fails, the corresponding error code is returned directly.
[0113] The second step is to pre-initialize all kprobe instances. Iterate through all globally controlled functions and call the unexport_func_kprobe_preinit function in turn to complete the allocation and initialization of the kprobe structure for each function. If the pre-initialization of any function fails, jump to the preinit_fail label, roll back all allocated kprobe memory, and return an error code.
[0114] The third step is to enter the atomic critical section, disable local interrupts and save the interrupt status flags through the local_irq_save(irq_flags) function, and disable kernel preemption through the preempt_disable() function to ensure that the entire batch operation cannot be interrupted, thus achieving transactional atomicity guarantee.
[0115] The fourth step involves batch registering kprobe structures in the topologically sorted order. Iterating through each sorted function, the `pre_handler` hook function is first set to `unexport_func_addr_check_handler` for the current function's kprobe structure. Then, the `register_kprobe` function is called to register the current kprobe instance. If registration fails, an error log is printed, and the process jumps to the `rollback` label to perform a transaction rollback. If registration succeeds, the success count is incremented, and the hook function's execution is triggered once to verify the address's validity. After verification, the `pre_handler` field of the kprobe structure is set to null, and the hook function is removed to avoid any impact on the subsequent execution of the target function. Finally, the kernel virtual address stored in the `addr` field of the kprobe structure is assigned to the `func_ptr` field of the descriptor structure to complete address caching, and a registration success log is printed.
[0116] Fifth, after all functions are successfully registered, the transaction is committed, the sorted array of function descriptions is copied completely to the global read-only cache pool g_ro_func_pool, the number of global functions is assigned to g_ro_pool_count, and then the smp_wmb() function is called to write the memory barrier function to ensure that the contents of the cache pool are immediately visible to all CPU cores, avoiding access exceptions caused by multi-core cache inconsistency. Then the preempt_enable() function is called to enable kernel preemption, the local_irq_restore(irq_flags) function is called to restore the local interrupt state, the atomic critical section is exited, and the log of all successful registrations is printed. Returning 0 indicates that the execution was successful.
[0117] Step 6, Transaction rollback process: When any function registration fails, the successfully registered functions are traversed in reverse order of registration. The unregister_kprobe function is called in turn to unregister the corresponding kprobe instance, the kfree function is called to release the memory of the kprobe structure, the kp and func_ptr fields in the description structure are set to null, the rollback is completed, the atomic critical section is exited, the preinit_fail label is jumped to rollback all pre-allocated kprobe memory, and an error code is returned.
[0118] Step 7: Pre-initialization failure rollback process. Loop through all functions, release the allocated kprobe structure memory, set the relevant fields to null, and return the error code.
[0119] It should be noted that this embodiment removes the `pre_handler` hook function of `kprobe` immediately after registration. Address validity is only checked once during registration, and no hooks are inserted during the execution of the target function. Therefore, it does not cause any performance loss to the normal kernel execution flow, nor does it trigger kernel debugging pollution marking. Existing kprobe-based hook interception technology requires interception during function execution. This application cleverly utilizes the hook function to perform validity checks during the registration phase. Of course, in other embodiments, if address validity checks are not performed, this application may not set any hook function, and the address validity checks in this application can also be implemented through other known methods, which will not be elaborated here. It should be understood that this embodiment inserts a temporary hook function during the registration phase, and the hook function call and interception will not be triggered during the invocation phase. Furthermore, the transactional rollback mechanism used in this step ensures that even if any error occurs during registration, no kprobe instance will remain, avoiding kernel resource leakage and pollution marking risks, and significantly improving the operational stability of the kernel module.
[0120] Those skilled in the art will understand that the function calling process in C language strictly follows the calling convention of the corresponding CPU architecture. In the x86_64 architecture, it follows the System V AMD64 ABI calling convention, and in the arm64 architecture, it follows the AAPCS64 calling convention. The CPU needs to know the number of input parameters, the types of input parameters, and the return value type of the function in order to correctly complete register allocation, stack frame layout, parameter passing, and return value reception. If the function pointer type does not match the original function prototype, the compiler will generate an incorrect calling instruction, resulting in abnormal stack frame layout. This can lead to incorrect function execution results or even cause the entire kernel to crash.
[0121] In this embodiment, after registering the kprobe structure and obtaining the kernel virtual address, the process proceeds to defining a function pointer type that matches the native function prototype of the target unexported kernel function, and converting it into a callable function pointer of the corresponding type based on the obtained kernel virtual address. These two steps enable safe calls to unexported function types, which can solve the problem of type-safe conversion from kernel function address to executable function call, ensuring that the function call process fully conforms to the calling convention of the CPU architecture, and avoiding serious problems such as stack frame corruption, register corruption, and kernel crash caused by type mismatch. These details will not be elaborated here.
[0122] In this embodiment of the application, a function pointer type matching the prototype of the native function that does not export kernel functions is defined. The specific implementation is as follows:
[0123] Match the native input parameter count, input parameter type, and return value type of the target non-exported kernel function, define the corresponding function pointer type, and perform multi-level validity checks on the obtained kernel virtual address;
[0124] After verification, the kernel virtual address is forcibly converted to a function pointer of the corresponding type, and an independent function pointer retrieval interface is encapsulated for each target function.
[0125] It should be noted that the native function prototype of the target unexported function must be completely consistent with the function definition of the corresponding version in the Linux kernel source code, including the return type, the type and order of each input parameter. Taking the functions blk_queue_enter and blk_mq_submit_bio as examples, the native prototypes in the kernel source code are matched to complete the definition of the function pointer type. For the blk_queue_enter function, its native function prototype in the kernel source code is int blk_queue_enter(struct request_queue q, blk_mq_req_flags_t flags), with a return value of type int and two input parameters. The first input parameter is a block device request queue pointer of type struct request_queue, and the second input parameter is the request flags of type blk_mq_req_flags_t. Therefore, the corresponding function pointer type is defined as typedef int(blk_queue_enter_func_t)(struct request_queue q, blk_mq_req_flags_t). The function pointer type is defined as `blk_queue_enter_func_t`, where `blk_queue_enter_func_t` is a custom function pointer type name. The `typedef` keyword is used to define the function pointer type, indicating that the type is a pointer type. The content within the parentheses is exactly the same as the input parameter list of the original function, and the `int` before the parentheses is exactly the same as the return value type of the original function. For the `blk_mq_submit_bio` function, its native function prototype in the kernel source code is `blk_status_t blk_mq_submit_bio(struct bio bio)`, which returns a block I / O status type `blk_status_t` and takes a pointer to a block I / O request structure of type `struct bio`. Therefore, the corresponding function pointer type is defined as `typedef blk_status_t (blk_mq_submit_bio_func_t)(struct biobio)`, which perfectly matches the original function prototype.
[0126] In this embodiment of the application, after defining the function pointer type, it is necessary to perform multi-level legality verification on the obtained kernel virtual address. After the verification is passed, the type safe conversion is then completed. The multi-level legality verification mentioned in this step includes verifying whether the kernel virtual address falls within the address range of the kernel code segment, verifying whether the memory page where the kernel virtual address is located has kernel executable permissions, identifying the current CPU architecture type, matching the corresponding instruction feature rules, and verifying the instruction features of the function entry point based on the matching rules. After all verification items pass, the address is determined to be legal.
[0127] It should be noted that successful kprobe registration only means that the symbol name exists in the kernel symbol table, and does not mean that the resolved address is necessarily a legal function entry point that can be called safely. For example, kernel hot patching, kernel function inline optimization, kernel address space randomization KASLR and other mechanisms may cause the resolved address to be abnormal. If an illegal address is called directly, it will directly cause the kernel Oops crash. Therefore, this application uses multi-level legality verification to filter illegal addresses layer by layer, which further improves the security of function calls.
[0128] Next, this application performs address validity verification using a general multi-level fuzzy check function named `unexport_func_addr_fuzzy_check`. The input parameter is `unsigned long addr`, representing the address of the kernel function to be verified. The return value is an integer, where 1 represents a valid address and 0 represents an invalid address. The specific execution flow of the function is as follows:
[0129] The first level of verification is the kernel code segment range verification, which determines whether the address to be verified falls within the legal range of the kernel code segment. During linking, the Linux kernel automatically generates two global symbols, _text and _etext, which represent the start and end addresses of the kernel code segment, respectively. All kernel function instructions are stored in this address range. If the address to be verified is less than (unsigned long)_text or greater than (unsigned long)_etext, it means that the address is not within the kernel code segment and is an illegal address, so 0 is returned directly.
[0130] The second level of verification is the executable permission verification, which calls the Linux kernel's native kernel_text_address function to determine whether the address is a valid kernel text address. This function verifies whether the memory page where the address is located has executable permissions, and excludes non-executable memory addresses dynamically allocated by the kernel. If the function returns false, it means that the memory page where the address is located does not have executable permissions and is an illegal address, so it returns 0.
[0131] The third level of verification involves fuzzy matching of function entry instruction features. This verification further verifies whether the address is a valid function entry point. First, the `copy_from_kernel_nofault` function safely reads the first 8 bytes of instruction data from the address entry point into a buffer. This function can safely read data from the kernel address space; even if the address is invalid, it will not trigger a kernel page fault and will only return a non-zero error code. If the read fails, it will directly return 0. Then, based on the CPU architecture of the current kernel compilation, it matches the corresponding instruction feature rules and performs fuzzy matching. For the x86_64 architecture, valid kernel function entry points typically have three types of features: the first is the `endbr64` instruction, corresponding to hexadecimal codes 0xf3 0x0f 0x1e 0xfa, which is the standard function entry instruction for Intel CET control flow enforcement technology; the second is... The `%rbp` stack frame initialization instruction, corresponding to hexadecimal code 0x55, is a standard stack frame initialization instruction for traditional x86 function entry points. The third type is the `jmp` short or long jump instruction, corresponding to hexadecimal code 0xe9 or 0xeb, which is a commonly used function entry jump instruction for kernel hot patching. Matching any one of these three characteristics determines the instruction's validity. For the arm64 architecture, valid kernel function entry points also possess these three characteristics: the first is the `stp x29, x30` push instruction, corresponding to hexadecimal code 0xfd7bbfa9, a standard function stack frame initialization instruction for arm64; the second is the `mov x29, sp` stack top assignment instruction, corresponding to hexadecimal code 0xfd030091, a standard stack frame initialization instruction; and the third is the `b` unconditional jump instruction, corresponding to the high 6 bits being 0x14, a commonly used jump instruction for kernel hot patching. Matching any one of these characteristics determines the instruction's validity. If the instruction characteristics match successfully, it returns 1 indicating a valid address; otherwise, it returns 0 indicating an invalid address.
[0132] It should be noted that the instruction feature verification in this step uses fuzzy matching rules instead of fixed instruction sequence verification. The instruction sequence of function entry points will differ under different kernel versions, different compilation optimization options, and different CPU architectures. In addition, kernel hot patches will modify the instructions of function entry points. Using fuzzy matching rules can adapt to all legal function entry scenarios and will not cause false judgments. At the same time, it can filter out illegal addresses that are not function entry points, further improving the accuracy and compatibility of verification.
[0133] Those skilled in the art will understand that this multi-level verification mechanism is progressive, and only after passing the previous level of verification will it proceed to the next level of verification. This can ensure the comprehensiveness of verification while minimizing the performance overhead of verification. In addition, the function is defined as a static inline function, and the compiler will expand it to the call location during compilation, without incurring additional overhead from function calls.
[0134] In this embodiment, after defining the address validity verification function, an independent function pointer retrieval interface is encapsulated for each target function to achieve type-safe conversion for business layer calls. In this embodiment, for the blk_queue_enter function, the encapsulated function pointer retrieval interface is blk_queue_enter_func_get. The function return value is the corresponding function pointer type blk_queue_enter_func_t, with no input parameters. The specific execution flow of the function is as follows: First, the smp_rmb() memory barrier function is called to ensure that the latest read-only cache pool content is read, avoiding read exceptions caused by multi-core cache inconsistency; then, the corresponding function description structure instance is retrieved from the read-only cache pool, and the kernel virtual address stored in its func_ptr field is obtained and converted to unsigned long type; then, the unexport_func_addr_fuzzy_check function is called to perform multi-level validity verification on the address. If the verification fails, NULL is returned; if the verification passes, the typeless func_ptr address is forcibly converted to a function pointer of type blk_queue_enter_func_t and returned to the caller. For the blk_mq_submit_bio function, the encapsulated function pointer retrieval interface is blk_mq_submit_bio_func_get. Its execution flow is completely consistent with the above interface, only the function pointer type and subscript macro are different, ensuring code consistency and maintainability.
[0135] In this embodiment of the application, after completing the type-safe conversion and acquisition interface encapsulation of the function pointer, the step of entering the function pointer obtained by conversion and executing the call to the target unexported kernel function can ensure concurrency safety and avoid wild pointer crashes, while realizing high-performance, lock-free calls to unexported kernel functions.
[0136] It's important to note that in multi-CPU, multi-core concurrent scenarios within the Linux kernel, there are two main race conditions during function calls. The first risk occurs when a business thread is executing a function call during module unloading. If the kernel module releases resources related to the function pointer at this time, the function pointer will become a dangling pointer, triggering a kernel crash. The second risk is that in multi-core concurrent scenarios, simultaneous reading and writing of shared function pointer data can lead to cache inconsistencies, data tearing, and call anomalies. Existing technologies typically use synchronization mechanisms such as spinlocks and mutexes to address race conditions, but locking operations incur significant performance overhead, especially in high-frequency storage I / O scenarios. Lock contention can cause a substantial drop in I / O performance, failing to meet business requirements. This application eliminates race conditions by registering writes once during initialization and then enabling lock-free read-only access during runtime. This achieves zero-overhead lock-free access, balancing security and high performance.
[0137] In this embodiment, the global read-only cache pool is constructed in the following way: after obtaining the kernel virtual addresses of all target functions that have not exported kernel functions, the description structure, kernel virtual address, and callable function pointer of each target function are filled into the global read-only cache pool, and a memory barrier operation is performed to ensure that the contents of the cache pool are visible to all CPU cores. During runtime, only read-only access operations are performed on the global read-only cache pool.
[0138] Those skilled in the art will understand that in multi-threaded concurrent scenarios, the sole cause of race conditions is the simultaneous reading and writing of shared data. This application eliminates read-write race conditions by strictly separating the initialization phase from the runtime phase. During the module loading process in the initialization phase, a single thread completes the filling and writing of the cache pool without concurrency. In the subsequent runtime phase, the business layer only performs read-only access to the cache pool and does not perform any write operations, thus eliminating read-write race conditions. Therefore, data consistency in multi-core concurrent scenarios can be guaranteed without any spin locks or mutex locks, achieving zero-overhead lock-free access.
[0139] In this embodiment of the application, the function pointer obtained through conversion is used to execute a call to a target function that has not been exported from the kernel. The specific implementation process is as follows:
[0140] Read the callable function pointer corresponding to the target function from the global read-only cache pool, and check the stop flag corresponding to the target function;
[0141] When marked as being in normal operating condition, the atom increments the active call count of the target function;
[0142] After re-verifying the stop flag, the target function is called via the function pointer. After the call is completed, the active call count of the target function is atomically reduced. By checking the stop flag and managing the atomic active count, the wild pointer race problem in the module unloading process is solved, ensuring the safety of the call process.
[0143] Next, this application defines a general pre-call security check function and a post-call cleanup function to achieve standardized security control of the call process. The pre-call security check function is named unexport_func_call_enter, the input parameter is the index of the target function in the read-only cache pool, and the return value is of type int, where 0 represents that the call is allowed and a negative number represents an error code. The specific execution flow of the function is as follows:
[0144] First, the `smp_rmb()` function is called to read the memory barrier, ensuring the latest status data is read. Then, an index out-of-bounds check is performed; if the index exceeds the valid range, the `EINVAL` invalid parameter error code is returned. Next, the corresponding function description structure instance is retrieved from the read-only cache pool, and the atomic stop flag is read. If the stop flag is 1, it means the module is being unloaded, and new calls are rejected, returning the `ESHUTDOWN` device shutdown error code. If the stop flag is 0, it represents normal operation; the active call count is incremented using the `atomic_inc()` atomic operation, and the memory barrier is called again to ensure the count increment is completed before the function call. A second stop flag check is then performed to close the extreme race condition window, avoiding extreme race conditions where the stop flag is set at the moment the atomic count is incremented. If the second check finds the stop flag is already set, the active call count is rolled back using the `atomic_dec()` atomic operation, and the memory barrier is called. If the count returns to zero after the rollback, and the module is waiting to be unloaded, the `complete()` function wakes up the unloading waiting thread, and then the `-ESHUTDOWN` error code is returned. If the second check passes, 0 is returned, indicating that the call is allowed.
[0145] The post-export cleanup function is named `unexport_func_call_exit`. Its input parameter is the index of the target function in the read-only cache pool. It has no return value. The specific execution flow of the function is as follows:
[0146] First, the `smp_wmb()` function is called to write the memory barrier, ensuring that the count is reduced only after the function execution is complete, thus avoiding premature count reduction caused by compiler instruction reordering. Then, the active call count is reduced using the `atomic_dec()` atomic operation, and the memory barrier is written again to ensure that the count update is visible to all CPU cores. Next, it is checked whether the active call count has reached zero and whether the module's unloading completion count has been bound. If the conditions are met, the `complete()` function is used to wake up the unloading waiting thread and complete the synchronization operation.
[0147] Those skilled in the art will understand that this pre- and post-security control mechanism ensures the accuracy of counting under multiple CPU cores through atomic operations and closes the race window through stop marker checks. Even in extreme scenarios where module unloading and function calls occur simultaneously, there will be no problem with dangling pointer calls. At the same time, the entire control process only uses atomic operations and memory barriers, without any locking operations, resulting in low performance overhead and applicability to high-frequency call scenarios.
[0148] In this embodiment, based on the aforementioned security control mechanism, the business layer encapsulates the calls to unexported functions. This embodiment takes the IO submission and queue entry encapsulation of the storage acceleration module as an example for a complete explanation:
[0149] First, for the `blk_mq_submit_bio` function, we encapsulate the business layer call function `cusmartcache_submit_bio`. The input parameter is `struct bio bio`, representing the block I / O request to be submitted. The return value is of type `blk_status_t`, indicating the I / O execution status. The specific execution flow of the function is as follows: First, we define the corresponding function pointer variable `submit_func`, the return value variable `ret`, and the error code variable `err`. Then, we call the `unexport_func_call_enter` function, passing in the index macro of the target function, to perform a pre-call safety check. If the check fails, it directly returns `BLK_STS_IOERR`. IO error status; if the check passes, call the `blk_mq_submit_bio_func_get` function to obtain a type-safe function pointer. If the function pointer is NULL, set the return value to `BLK_STS_IOERR` and jump to the `out` label to perform post-processing cleanup operations. If the function pointer is valid, execute the call to the target function through the function pointer, pass the `bio` parameter, and obtain the return value. Finally, jump to the `out` label, call the `unexport_func_call_exit` function to perform post-call cleanup operations, and return the function execution result. For the `blk_queue_enter` function, a business layer call function `cusmartcache_queue_enter` is encapsulated. The input parameters are `struct request_queue q` (a pointer to the block device request queue) and `blk_mq_req_flags_t flags` (request flags). The return value is of type `int`. Its execution flow is completely consistent with the above-mentioned encapsulated function, with only the function pointer type, subscript macro, and input parameters and return value being different. This decouples the business layer call from the underlying address acquisition and security control. The business layer code does not need to pay attention to the underlying address acquisition and security mechanisms. The calling method is completely consistent with directly calling the kernel exported function, which greatly reduces the cost of modifying the business code.
[0150] In this embodiment of the application, when the kernel module is uninstalled, a complete resource synchronization and release process needs to be executed. The specific steps are as follows:
[0151] Atom sets the stop flag for all target functions that have not exported kernel functions, waits for the active call count of all target functions to return to zero, and unregisters all registered kprobe structures;
[0152] Release the memory resources corresponding to the kprobe structure, and clear the global read-only cache pool and the description structure information of all target functions.
[0153] This ensures a safe order of first stopping new calls, then waiting for existing calls to complete, and finally releasing resources, avoiding dangling pointer crashes during module unloading.
[0154] It should be noted that when a Linux kernel module is unloaded, the kernel directly releases the entire memory space of the module. If business threads are still executing calls to unexported functions at this time, function pointers, callback functions, etc., will become dangling pointers, triggering a kernel Oops crash. This application combines atomic stop markers with a completion synchronization mechanism to ensure that the entry point for new calls is closed before any resources are released, while waiting for all existing function calls to complete, thus eliminating the dangling pointer problem.
[0155] For example, this application completes resource release during module unloading through a batch cleanup function named `unexport_func_batch_cleanup`. This function has no input parameters and no return value. The specific execution flow of the function is as follows: First, atomically set the stop flag for all functions. It iterates through all function description structure instances in the read-only cache pool, sets the `stop_flag` to 1 using the `atomic_set()` function, closes the entry point for new calls, and then calls the `smp_wmb()` function to write the memory barrier, ensuring that the stop flag is immediately visible to all CPU cores. Simultaneously, it binds the unloading synchronization completion amount to each function description structure. Second, check if any functions are already in a zero-activity state. If so, mark them as complete and wake up waiting threads. Third, wait for all existing calls to complete, using `wait_for_completion_timeout`. The `t` function waits for the completion of the task, setting a 5-second timeout to prevent module unloading from stalling due to extreme conditions. Even if the timeout occurs, the subsequent resource release process will continue to ensure the module can be unloaded normally. The fourth step involves unregistering all `kprobe` instances, iterating through all function description structures, and calling `unregister_kprobe` to unregister the corresponding `kprobe` instance if the `kp` field is not empty. The `kfree` function is then called to release the memory of the `kprobe` structure, setting the `kp` and `func_ptr` fields to null, and unbinding the unload completion task. The fifth step clears the global read-only cache pool by zeroing the cache pool array using the `memset` function, setting `g_ro_pool_count` to 0, calling a write memory barrier to ensure the cache pool clearing is visible to all CPU cores, and resetting the status fields of all function description structures, thus completing the release of all resources.
[0156] Those skilled in the art will understand that this unloading process achieves synchronization between the unloading thread and the business call thread through a completion quantity synchronization mechanism. The resource release operation will only be performed after all existing function calls have been executed, thus avoiding the wild pointer crash problem. At the same time, a timeout protection mechanism is set to prevent the unloading from getting stuck in extreme scenarios, ensuring the security and stability of kernel module unloading.
[0157] In this embodiment, the complete method flow described above implements overall loading and unloading through module entry and exit functions. In the module initialization function `init cusmartcache_module_init`, the `unexport_func_batch_transaction_register` function is first called to complete the batch registration and address acquisition of unexported functions. If registration fails, an error log is printed and an error code is returned. If registration succeeds, other initialization logic of the storage acceleration module is executed, and 0 is returned to indicate that the module is loaded successfully. In the module unloading function `exit cusmartcache_module_exit`, other cleanup logic of the storage acceleration module is first executed, and then the `unexport_func_batch_cleanup` function is called to complete the resource cleanup of unexported functions, thus completing the module unloading.
[0158] This application also discloses a dynamic calling system for kernel-unexported functions, which corresponds one-to-one with the above-described methods, such as... Figure 2 As shown, it includes:
[0159] Structure configuration module 1 defines and configures a kprobe structure containing the symbol names of the target functions for targets that do not export kernel functions;
[0160] Structure registration module 2 registers the configured kprobe structure and obtains the kernel virtual address of the target kernel function that has not been exported.
[0161] Pointer definition module 3 defines function pointer types that match the native function prototypes of targets that do not export kernel functions;
[0162] Pointer conversion module 4 converts the obtained kernel virtual address into a callable function pointer of the corresponding type;
[0163] Function call module 5 executes calls to unexported kernel functions of the target by using the converted function pointer.
[0164] It is understood that the kernel-unexported function dynamic call system provided in this application has the same technical effect as the aforementioned method embodiments, and will not be elaborated here.
[0165] It should be noted that although the technical solution of this application is described using the Linux storage acceleration kernel module as an example, its scope of application is not limited to the storage field and the Linux kernel. Any scenario that requires calling non-exported kernel functions in the Linux kernel module or other system kernel modules, including network protocol stack optimization, operating system security hardening, virtualization performance optimization, hardware device driver development, etc., can directly apply the technical solution of this application. Those skilled in the art can make scenario-specific adaptation modifications, parameter adjustments, and structural replacements based on the core technical solution of this application without creative effort, and all such modifications are within the protection scope of this application.
Claims
1. A method for dynamically calling functions not exported by the kernel, characterized in that, include: For targets that do not export kernel functions, define and configure a kprobe structure containing the symbol names of the target functions; The target function symbol name is the symbol name of the function whose kernel function is not exported by the target; Register the configured kprobe structure and obtain the kernel virtual address of the target kernel function that has not been exported; Define a function pointer type that matches the prototype of a native function that does not export a kernel function from the target; Based on the obtained kernel virtual address, it is converted into a callable function pointer of the corresponding type; The function pointer obtained through the conversion is used to execute a call to a kernel function that is not exported from the target. For targets that do not export kernel functions, the definition and configuration of the kprobe structure containing the symbol name of the target function includes: For each target, a controlled function description structure is defined corresponding to the kernel function definition that is not exported. The description structure records the symbol name, dependencies, and state information of the objective function; based on the description structure, a kprobe structure is allocated and initialized for the objective function. Bind the kprobe structure to the corresponding description structure to complete the configuration of the kprobe structure; The kprobe structure, once registered and configured, obtains the kernel virtual address of the target kernel function that has not been exported, including: A directed acyclic graph is constructed based on the dependencies of the kernel functions not exported by each target, and a function registration order that conforms to the dependency rules is generated by a topological sorting algorithm. Register the kprobe structure corresponding to each objective function in the generated registration order; After the kernel completes symbol address resolution, it extracts the kernel virtual address of the target unexported kernel function from the kprobe structure and writes the extracted address into the corresponding description structure. The step of registering the kprobe structure corresponding to each objective function in the generated registration order includes: Disable local interrupts and kernel preemption, enter the atomic critical section, and execute the registration operation of the kprobe structure in the registration order within the atomic critical section; For each registered kprobe structure, perform address validity pre-check. If any kprobe structure fails to register or pre-check, unregister the successfully registered kprobe structures in reverse order of registration and release the corresponding resources. If all kprobe structures are registered and pre-verified, submit the registration results and exit the atomic critical region.
2. The method according to claim 1, characterized in that, The definition matches the function pointer type of the target's native function prototype that does not export kernel functions, and converts it to a callable function pointer of the corresponding type based on the obtained kernel virtual address, including: Match the native input parameter count, input parameter type, and return value type of the target non-exported kernel function, define the corresponding function pointer type, and perform multi-level validity checks on the obtained kernel virtual address; After verification, the kernel virtual address is forcibly converted to a function pointer of the corresponding type, and an independent function pointer retrieval interface is encapsulated for each target function.
3. The method according to claim 2, characterized in that, The multi-level validity verification of the obtained kernel virtual address includes: Verify whether the kernel virtual address falls within the address range of the kernel code segment, and verify whether the memory page containing the kernel virtual address has kernel executable permissions; Identify the current CPU architecture type and match the corresponding instruction feature rules; Based on the matching rules, the address is deemed valid after all verification items pass the function's entry point instruction characteristics.
4. The method according to claim 1, characterized in that, The execution of calls to unexported kernel functions by the function pointer obtained through conversion includes: Read the callable function pointer corresponding to the target function from the global read-only cache pool, and check the stop flag corresponding to the target function; When marked as being in normal operating condition, the atom increments the active call count of the target function; After re-verifying the stop flag, the target function is called via the function pointer. After the call is completed, the active call count of the target function is atomically decremented.
5. The method according to claim 4, characterized in that, The global read-only cache pool is constructed in the following way: After obtaining the kernel virtual addresses of all target functions that have not exported kernel functions, the description structure, kernel virtual address, and callable function pointer of each target function are filled into the global read-only cache pool. Perform a memory barrier operation to ensure that the contents of the cache pool are visible to all CPU cores; during runtime, only read-only access operations are performed on the global read-only cache pool.
6. The method according to claim 4, characterized in that, When a kernel module is uninstalled, the following steps are performed: Atomically sets the stop flag for all target functions that have not exported kernel functions, waits for the active call count of all target functions to return to zero; unregisters all registered kprobe structures; Release the memory resources corresponding to the kprobe structure, and clear the global read-only cache pool and the description structure information of all target functions.
7. A system for dynamically calling functions not exported by the kernel, characterized in that, include: The structure configuration module defines and configures a kprobe structure containing the symbol names of the target functions for targets that do not export kernel functions. The structure registration module registers the configured kprobe structure and obtains the kernel virtual address of the target kernel function that has not been exported. The pointer definition module defines function pointer types that match the native function prototypes of targets that do not export kernel functions; The pointer translation module converts the obtained kernel virtual address into a callable function pointer of the corresponding type. The function call module, through the converted function pointer, executes calls to target functions that are not exported from the kernel. The kernel-unported function dynamic call system is used to perform the method as described in any one of claims 1-6.