--- tags: 2022 linux kernel --- # Linux 核心如何處理傳遞到核心模組的參數 contributed by < [`Risheng1128`](https://github.com/Risheng1128) > 參考 [Passing Command Line Arguments to a Module](https://sysprog21.github.io/lkmpg/#passing-command-line-arguments-to-a-module) ,發現 kernel 是使用巨集 `module_param` 傳遞參數,接著可以在檔案 `main.c` 發現該巨集的使用,可以得知 `khpptd` 可以讓使用者自己設定 `port` 及 `backlog` ```c static ushort port = DEFAULT_PORT; module_param(port, ushort, S_IRUGO); static ushort backlog = DEFAULT_BACKLOG; module_param(backlog, ushort, S_IRUGO); ``` 接著研究 `module_param` 的實作,可以在 [linux/include/linux/moduleparam.h](https://github.com/torvalds/linux/blob/master/include/linux/moduleparam.h) 找到數個定義,將相關定義表示在下方 ```c #define module_param(name, type, perm) \ module_param_named(name, name, type, perm) #define module_param_named(name, value, type, perm) \ param_check_##type(name, &(value)); \ module_param_cb(name, &param_ops_##type, &value, perm); \ __MODULE_PARM_TYPE(name, #type) ``` 可以分成 `param_check_##type`, `module_param_cb` 及 `__MODULE_PARM_TYPE` 做討論 ## `param_check_##type` 由於 `khttpd` 的變數是使用 `ushort` 的型態,因此巨集會被展開成 `param_check_ushort` ,以下為相關巨集 ```c #define param_check_ushort(name, p) __param_check(name, p, unsigned short) /* All the helper functions */ /* The macros to do compile-time type checking stolen from Jakub Jelinek, who IIRC came up with this idea for the 2.4 module init code. */ #define __param_check(name, p, type) \ static inline type __always_unused *__check_##name(void) { return(p); } ``` 從註解很明顯可以知道 `param_check_##type` 的目的是要在編譯時期就判斷變數 `p` 是否真的是 `type` 型態,方法是藉由回傳 `p` 判斷函式是否回傳相同型態 ## `module_param_cb` ```c /** * module_param_cb - general callback for a module/cmdline parameter * @name: a valid C identifier which is the parameter name. * @ops: the set & get operations for this parameter. * @arg: args for @ops * @perm: visibility in sysfs. * * The ops can have NULL set or get functions. */ #define module_param_cb(name, ops, arg, perm) \ __module_param_call(MODULE_PARAM_PREFIX, name, ops, arg, perm, -1, 0) /* This is the fundamental function for registering boot/module parameters. */ #define __module_param_call(prefix, name, ops, arg, perm, level, flags) \ /* Default value instead of permissions? */ \ static const char __param_str_##name[] = prefix #name; \ static struct kernel_param __moduleparam_const __param_##name \ __used __section("__param") \ __aligned(__alignof__(struct kernel_param)) \ = { __param_str_##name, THIS_MODULE, ops, \ VERIFY_OCTAL_PERMISSIONS(perm), level, flags, { arg } } ``` `__module_param_call` 建立一個型態為 `kernel_param` 且名稱為 `__param_##name` 的結構,並告訴編譯器以下資訊 - 將資料放在 `__param` 區 - 對齊 `__alignof__(struct kernel_param)` 的大小 接著查看結構 `kernel_param` 的宣告 ```c struct kernel_param { const char *name; struct module *mod; const struct kernel_param_ops *ops; const u16 perm; s8 level; u8 flags; union { void *arg; const struct kparam_string *str; const struct kparam_array *arr; }; }; ``` 由以上資訊我們可以得到最後 `__module_param_call` 建立的結構,以變數 `port` 作為範例,如以下所示 ```c struct kernel_param _param_name { const char *name = "port"; struct module *mod = THIS_MODULE; const struct kernel_param_ops *ops = &param_ops_ushort; const u16 perm = VERIFY_OCTAL_PERMISSIONS(S_IRUGG); s8 level = -1; u8 flags = 0; void *arg = &port; }; ``` 最後使用命令 `readelf -r khttpd.ko` 查看 `__param` 的區域,的確有 `port` 和 `backlog` 的資料 ```shell Relocation section '.rela__param' at offset 0xc1a48 contains 8 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000000 000700000001 R_X86_64_64 0000000000000000 .rodata + 1248 000000000008 005600000001 R_X86_64_64 0000000000000000 __this_module + 0 000000000010 005400000001 R_X86_64_64 0000000000000000 param_ops_ushort + 0 000000000020 000e00000001 R_X86_64_64 0000000000000000 .data + 4 000000000028 000700000001 R_X86_64_64 0000000000000000 .rodata + 1250 000000000030 005600000001 R_X86_64_64 0000000000000000 __this_module + 0 000000000038 005400000001 R_X86_64_64 0000000000000000 param_ops_ushort + 0 000000000048 000e00000001 R_X86_64_64 0000000000000000 .data + 6 ``` ## `__MODULE_PARM_TYPE` ```c #define __MODULE_PARM_TYPE(name, _type) \ __MODULE_INFO(parmtype, name##type, #name ":" _type) #define __MODULE_INFO(tag, name, info) \ static const char __UNIQUE_ID(name)[] \ __used __section(".modinfo") __aligned(1) \ = __MODULE_INFO_PREFIX __stringify(tag) "=" info #define MODULE_PARAM_PREFIX KBUILD_MODNAME "." #define KBUILD_MODNAME /* empty */ ``` 參考 [Linux 核心模組掛載機制](https://hackmd.io/@sysprog/linux-kernel-module) 可以很清楚的知道 `__UNIQUE_ID` 的功能 - `__UNIQUE_ID` 會根據參數產生一個不重複的名字,其中使用到的技術是利用巨集中的 `##` 來將兩個參數合併成一個新的字串 - 透過 `__attribute__` 關鍵字告訴編譯器,這段訊息 - 要被放在 `.modinfo` 區 (`__section(".modinfo")`) - 不會被程式使用到,所以不要產生警告訊息 (`__used`) - 最小的對齊格式是 1 bit (`__aligned(1)`) - 巨集 `__stringify` 的目的是為了把參數轉換成字串形式 - 巨集 `MODULE_PARAM_PREFIX` 由巨集 `KBUILD_MODNAME` 和 `"."` 組合而成,簡單來說就只是個字串 最後以變數 `port` 為例,會產生以下巨集 ```c #define __MODULE_INFO(tag, name, info) \ static const char __UNIQUE_ID(name)[] \ __used __section(".modinfo") __aligned(1) \ = ".parmtype=port:ushort." ``` 接著使用命令 `objdump -s khttpd.ko` 查看 `.modinfo` 的區域 ```diff ... Contents of section .modinfo: ... 0070 00706172 6d747970 653d6261 636b6c6f .parmtype=backlo + 0080 673a7573 686f7274 00706172 6d747970 g:ushort.parmtyp + 0090 653d706f 72743a75 73686f72 74007372 e=port:ushort.sr ... ``` 繼續根據 [Linux 核心模組掛載機制](https://hackmd.io/@sysprog/linux-kernel-module) 使用 strace 追蹤 `insmod fibdrv.ko` ```shell= $ sudo strace insmod khttpd.ko port=1999 execve("/usr/sbin/insmod", ["insmod", "khttpd.ko", "port=1999"], 0x7fff08f9ff70 /* 25 vars */) = 0 brk(NULL) = 0x5607976a6000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 ... mmap(NULL, 1366608, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa7c761b000 finit_module(3, "port=1999", 0) = -1 EEXIST (File exists) write(2, "insmod: ERROR: could not insert "..., 62insmod: ERROR: could not insert module khttpd.ko: File exists ) = 62 munmap(0x7fa7c761b000, 1366608) = 0 close(3) = 0 exit_group(1) = ? +++ exited with 1 +++ ``` 查看位於第 8 行 `finit_module` 的實作,參考 [kernel/module.c](https://github.com/torvalds/linux/blob/master/kernel/module.c) 及 [finit_module(2) - Linux man page](https://linux.die.net/man/2/finit_module) ```c int finit_module(int fd, const char *param_values, int flags); ``` 對應 strace 的結果 - `fd = 3` - `param_values = "port=1999"` - `flag = 0` ```c SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags) { ... return load_module(&info, uargs, flags); } ``` 函式 `finit_module` 呼叫函式 `load_module` ,接著繼續分析 ```c static int load_module(struct load_info *info, const char __user *uargs, int flags) { /* Now copy in args */ mod->args = strndup_user(uargs, ~0UL >> 1); if (IS_ERR(mod->args)) { err = PTR_ERR(mod->args); goto free_arch_cleanup; } } ``` 從上述程式碼可以看到從命令列的輸入參數已經被複製到 `mod->arg` ,且 `mod` 的型態為 `struct mod` ,參考 [include/linux/module.h](https://github.com/torvalds/linux/blob/master/include/linux/module.h) ```c struct module { ... /* The command line arguments (may be mangled). People like keeping pointers to this stuff */ char *args; ... } ``` 找到了 `args` 的宣告,從註解可以知道 `args` 的目的就是儲存 command line 的設定參數 回到 `load_module` ,發現了函式 `parse_args` ,從註解可以知道是要將 command line 的字串拆解 ```c /* Module is ready to execute: parsing args may do that. */ after_dashes = parse_args(mod->name, mod->args, mod->kp, mod->num_kp, -32768, 32767, mod, unknown_module_param_cb); ``` 進到函式 `parse_args` ,參考 [kernel/params.c](https://github.com/torvalds/linux/blob/master/kernel/params.c) ```c /* Args looks like "foo=bar,bar2 baz=fuz wiz". */ char *parse_args(const char *doing, char *args, const struct kernel_param *params, unsigned num, s16 min_level, s16 max_level, void *arg, int (*unknown)(char *param, char *val, const char *doing, void *arg)) { char *param, *val, *err = NULL; /* Chew leading spaces */ args = skip_spaces(args); if (*args) pr_debug("doing %s, parsing ARGS: '%s'\n", doing, args); while (*args) { int ret; int irq_was_disabled; args = next_arg(args, &param, &val); /* Stop at -- */ if (!val && strcmp(param, "--") == 0) return err ?: args; irq_was_disabled = irqs_disabled(); ret = parse_one(param, val, doing, params, num, min_level, max_level, arg, unknown); if (irq_was_disabled && !irqs_disabled()) pr_warn("%s: option '%s' enabled irq's!\n", doing, param); switch (ret) { case 0: continue; case -ENOENT: pr_err("%s: Unknown parameter `%s'\n", doing, param); break; case -ENOSPC: pr_err("%s: `%s' too large for parameter `%s'\n", doing, val ?: "", param); break; default: pr_err("%s: `%s' invalid for parameter `%s'\n", doing, val ?: "", param); break; } err = ERR_PTR(ret); } return err; } ``` 函式 `parse_args` 做了以下幾件事 1. 使用函式 `skip_spaces` 將字串的第一個字元如果為空白字元,將空白字元全部移除 2. 使用函式 `next_arg` 找到下一個 argument ,參考 [lib/cmdline.c](https://github.com/torvalds/linux/blob/a79cdfba68a13b731004f0aafe1155a83830d472/lib/cmdline.c) 3. 使用函式 `parse_one` 將試著將 argument 加進 module 裡,該函式位於 [kernel/params.c](https://github.com/torvalds/linux/blob/master/kernel/params.c) 最後討論函式 `parse_one` ```c= static int parse_one(char *param, char *val, const char *doing, const struct kernel_param *params, unsigned num_params, s16 min_level, s16 max_level, void *arg, int (*handle_unknown)(char *param, char *val, const char *doing, void *arg)) { unsigned int i; int err; /* Find parameter */ for (i = 0; i < num_params; i++) { if (parameq(param, params[i].name)) { if (params[i].level < min_level || params[i].level > max_level) return 0; /* No one handled NULL, so do it here. */ if (!val && !(params[i].ops->flags & KERNEL_PARAM_OPS_FL_NOARG)) return -EINVAL; pr_debug("handling %s with %p\n", param, params[i].ops->set); kernel_param_lock(params[i].mod); if (param_check_unsafe(&params[i])) err = params[i].ops->set(val, &params[i]); else err = -EPERM; kernel_param_unlock(params[i].mod); return err; } } if (handle_unknown) { pr_debug("doing %s: %s='%s'\n", doing, param, val); return handle_unknown(param, val, doing, arg); } pr_debug("Unknown argument '%s'\n", param); return -ENOENT; } ``` 注意第 17 行的部份, linux 核心逐步尋找符合的參數,並在第 29 行呼叫函式指標 `params[i].ops->set(val, &params[i])` ,將輸入的資料複製到模組的資料裡,以下為其結構宣告 ```c struct kernel_param_ops { /* How the ops should behave */ unsigned int flags; /* Returns 0, or -errno. arg is in kp->arg. */ int (*set)(const char *val, const struct kernel_param *kp); /* Returns length written or -errno. Buffer is 4k (ie. be short!) */ int (*get)(char *buffer, const struct kernel_param *kp); /* Optional function to free kp->arg when module unloaded. */ void (*free)(void *arg); }; ```