리눅스 커널 소스의 __setup() 매크로
First Published at
8/25/2023 12:42:38 AM
Last Edited at
8/29/2023 4:21:00 AM
리눅스 커널 소스에서 볼 수 있는 `__setup()` 매크로를 알아봅니다.
리눅스 커널 소스에서 볼 수 있는
__setup()
매크로를 알아보겠습니다.#
부팅 시 커널이 체크할 파라미터를 정의하는 __setup()
매크로
__setup()
매크로는 아래와 같이 정의되어 있습니다.C
include/linux/init.h@328
#define __setup(str, fn) \__setup_param(str, fn, fn, 0)
여기서
str
은 identifier 역할, fn
은 콜백 함수 역할을 합니다.__setup()
매크로는 GRUB 부트로더가 커널에 넘기는 파라미터 중 obs_kernel_param
을 정의하도록 합니다. 그럼 커널은 부팅 시 이 __setup()
매크로가 정의한 파라미터가 실제로 정의되었는지를 판단하고, 실제로 그렇다면 __setup()
매크로에 함께 넘긴 콜백 함수를 수행하도록 합니다.예로 들 파일은 간단한
drivers/gpu/drm/drm_nomodeset.c
입니다. 커널 버전은 6.1 기준으로 하겠습니다.C
drivers/gpu/drm/drm_nomodeset.c
1
// SPDX-License-Identifier: GPL-2.0···
14
static int __init disable_modeset(char *str)15
{16
drm_nomodeset = true;17
18
pr_warn("Booted with the nomodeset parameter. Only the system framebuffer will be available\n");19
20
return 1;21
}22
23
/* Disable kernel modesetting */24
__setup("nomodeset", disable_modeset);
이 파일에서는 맨 마지막 라인에서
__setup()
매크로를 사용하고 있습니다.__setup()
매크로의 정의를 살펴봅시다.C
include/linux/init.h
···
43
* Don't forget to initialize data not at file scope, i.e. within a function,44
* as gcc otherwise puts the data into the bss section and not into the init45
* section.46
*/47
48
/* These are for everybody (although not all archs will actually49
discard it in modules) */50
#define __init __section(".init.text") __cold __latent_entropy __noinitretpoline51
#define __initdata __section(".init.data")52
#define __initconst __section(".init.rodata")
···
310
* Only for really core code. See moduleparam.h for the normal way.311
*312
* Force the alignment so the compiler doesn't space elements of the313
* obs_kernel_param "array" too far apart in .init.setup.314
*/315
#define __setup_param(str, unique_id, fn, early) \316
static const char __setup_str_##unique_id[] __initconst \317
__aligned(1) = str; \318
static struct obs_kernel_param __setup_##unique_id \319
__used __section(".init.setup") \320
__aligned(__alignof__(struct obs_kernel_param)) \321
= { __setup_str_##unique_id, fn, early }322
323
/*324
* NOTE: __setup functions return values:325
* @fn returns 1 (or non-zero) if the option argument is "handled"326
* and returns 0 if the option argument is "not handled".327
*/328
#define __setup(str, fn) \329
__setup_param(str, fn, fn, 0)
__setup()
매크로의 정의에서 이 매크로는 내부적으로 __setup_param()
매크로를 호출하고 있습니다. __setup_param()
매크로 내부를 살펴봅니다.init.h
의 316 라인에 있는 __initconst
매크로는 include/linux/init.h
에 나와있듯이 .init.rodata
섹션에 해당 데이터를 저장하도록 설정합니다. 여기서 rodata
란 초기화 된 전역 변수를 말합니다. 일반적으로 초기화되지 않은 전역 변수는 bss
영역에 저장되는데, 43~45 라인에 나와있듯이 __section
매크로를 사용할 때에는 변수가 초기화가 되어있는지를 꼭 확인해야 합니다. __section()
매크로에 대한 자세한 정보는 해당 라인과 GCC 문서(함수, 변수)를 참고하시기 바랍니다.C
include/linux/init.h
···
329
__setup_param("nomodeset", disable_modeset, disable_modeset, 0)
앞선
drivers/gpu/drm/drm_nomodeset.c
의 24라인에서 __setup("nomodeset", disable_modeset);
으로 __setup()
매크로를 호출한 것을 상기해봅니다. 그렇다면 include/linux/init.h
에서 이 매크로는 __setup_param("nomodeset", disable_modeset, disable_modeset, 0)
을 호출하고, 이는 컴파일 시 아래와 같이 치환됩니다.C
static const char __setup_str_nomodeset[] __initconst __aligned(1) = "nomodeset";static struct obs_kernel_param __setup_disable_modeset __used __section("init.setup") __aligned(_alignof__(struct obs_kernel_param)) = {__setup_str_disable_modeset, disable_nomodeset, 0}
“
C에서
##
연산자는 매크로 내부에서 문자열을 붙이는 역할을 합니다. 예를 들어 __setup_str_##unique_id[]
에서 unique_id
가 hello
라면 이는 컴파일 시 __setup_str_hello[]
로 치환됩니다.여기서 구조체
obs_kernel_param
의 정의는 이렇습니다.C
include/linux/init.h
···
303
struct obs_kernel_param {304
const char *str;305
int (*setup_func)(char *);306
int early;307
};···
순서대로
str
은 커널 매개변수의 이름, *setup_func
는 어떤 동작을 할지 정의하는 콜백 함수, early
는 이 파라미터가 early parameter인지를 결정하는 변수입니다. early
의 값이 1
이면 early parameter이고, 0
이면 아닙니다. 여기서는 __setup_param(str, fn, fn, 0)
으로 호출했으므로 early의 값은 0
이 됩니다.C
include/asm-generic/vmlinux.lds.h
···
940
#define INIT_SETUP(initsetup_align) \941
. = ALIGN(initsetup_align); \942
__setup_start = .; \943
KEEP(*(.init.setup)) \944
__setup_end = .;···
C
init/main.c
···
199
extern const struct obs_kernel_param __setup_start[], __setup_end[];···
또
include/linux/init.h
의 318라인에서 __section(".init.setup")
을 사용했는데, include/asm-generic/vmlinx.lds.h
의 INIT_SETUP()
매크로에 의해 __setup()
매크로로 정의되는 __setup_##unique_id
변수들은 __setup_start
와 __setup_end
사이에 위치하게 됩니다. __setup_start
와 __setup_end
는 각각 .init.setup
섹션의 시작, 끝을 나타냅니다.“
__aligned
는 GCC 키워드로서 __aligned(n)
는 최소 n
의 배수 위치에 변수가 위치하도록 강제로 정렬하도록 합니다.#
실제 부팅 과정 살펴보기
C
init/main.c
···
612
/*613
* We need to store the untouched command line for future reference.614
* We also need to store the touched command line since the parameter615
* parsing is performed in place, and we should allow a component to616
* store reference of name/value for future reference.617
*/618
static void __init setup_command_line(char *command_line)619
{
···
633
static_command_line = memblock_alloc(len, SMP_CACHE_BYTES);634
if (!static_command_line)635
panic("%s: Failed to allocate %zu bytes\n", __func__, len);636
637
if (xlen) {638
/*639
* We have to put extra_command_line before boot command640
* lines because there could be dashes (separator of init641
* command line) in the command lines.642
*/643
strcpy(saved_command_line, extra_command_line);644
strcpy(static_command_line, extra_command_line);645
}646
strcpy(saved_command_line + xlen, boot_command_line);647
strcpy(static_command_line + xlen, command_line);
···
670
}
···
936
asmlinkage __visible void __init __no_sanitize_address start_kernel(void)937
{
···
974
after_dashes = parse_args("Booting kernel",975
static_command_line, __start___param,976
__stop___param - __start___param,977
-1, -1, NULL, &unknown_bootoption);
이제 부팅 과정에서 어떻게
nomodeset
옵션이 적용되는지를 살펴보겠습니다.init/main.c
에는 리눅스 커널이 부팅할 때 가장 처음 실행되는 코드들이 모여있습니다. 부팅 과정의 일부로 중간에 936라인의 start_kernel()
함수도 실행되는데, 이를 살펴보겠습니다.#
parse_args()
함수
C
include/linux/moduleparmam.h
···
385
/* Called on module insert or kernel boot */386
extern char *parse_args(const char *name,387
char *args,388
const struct kernel_param *params,389
unsigned num,390
s16 level_min,391
s16 level_max,392
void *arg,393
int (*unknown)(char *param, char *val,394
const char *doing, void *arg));···
#
const char *name
으로서 "Booting kernel"
name
파라미터는 콘솔에 출력하는 정보입니다.#
const char *args
로서 static_command_line
start_kernel()
함수 내부에서는 부팅 옵션(parameter)을 처리하는 parse_args()
함수도 호출합니다. 여기서 static_command_line
변수는 static char*
타입으로서 setup_command_line()
함수에서 값이 채워집니다. 이때 이 변수에 공간을 할당해주는 memblock_alloc()
함수(633라인)의 구현은 아키텍처 종속적입니다.#
const struct kernel_param *params
로서 __start___param
이 절에서는
__start___param
및 __stop___param
도 알아보겠습니다. 두 변수는 아래와 같이 정의되어 있습니다.C
include/linux/moduleparam.h
···
83
extern const struct kernel_param __start___param[], __stop___param[];···
C
include/asm-generic/vmlinux.lds.h
···
552
/* Built-in module parameters. */ \553
__param : AT(ADDR(__param) - LOAD_OFFSET) { \554
__start___param = .; \555
KEEP(*(__param)) \556
__stop___param = .; \557
}···
그리고
include/asm-generic/vmlinux.lds.h
의 552라인에 의해 두 변수는 built-in 모듈 파라미터의 시작점과 끝점을 나타내며, 위치는 __param
섹션입니다.C
include/linux/moduleparam.h
···
285
/* This is the fundamental function for registering boot/module286
parameters. */287
#define __module_param_call(prefix, name, ops, arg, perm, level, flags) \288
/* Default value instead of permissions? */ \289
static const char __param_str_##name[] = prefix #name; \290
static struct kernel_param __moduleparam_const __param_##name \291
__used __section("__param") \292
__aligned(__alignof__(struct kernel_param)) \293
= { __param_str_##name, THIS_MODULE, ops, \294
VERIFY_OCTAL_PERMISSIONS(perm), level, flags, { arg } }···
이렇게
include/linux/moduleparam.h
의 285라인에서 built-in 모듈 파라미터를 __param
섹션에 저장하는 모습을 볼 수 있습니다. 추가로, __aligned(__alignof__(struct kernel_param))
으로 구조체 kernel_param
의 크기에 맞게 정렬하였습니다. 단, __module_param_call()
매크로가 일반 코드에서 직접 호출되지는 않는데, 이는 아래에서 살펴보겠습니다.#
unsigned num
으로서 __stop___param - __start___param
따라서
__stop___param - __start___param
은 built-in 모듈 파라미터의 개수가 됩니다.#
s16 level_min
, s16 level_max
로서 -1
, -1
C
include/linux/moduleparam.h
···
166
/**167
* module_param_cb - general callback for a module/cmdline parameter168
* @name: a valid C identifier which is the parameter name.169
* @ops: the set & get operations for this parameter.170
* @arg: args for @ops171
* @perm: visibility in sysfs.172
*173
* The ops can have NULL set or get functions.174
*/175
#define module_param_cb(name, ops, arg, perm) \176
__module_param_call(MODULE_PARAM_PREFIX, name, ops, arg, perm, -1, 0)···
한편
__module_param_call()
매크로를 호출하는 module_param_cb()
매크로가 __module_param_call()
매크로의 level 파라미터에 -1
을 지정하고 있으며, module_param_cb()
매크로는 module_param_named()
매크로에 의해 호출되고, 이 module_param_named()
매크로는 호출하는 module_param()
매크로가 호출합니다. built-in 모듈은 module_param()
매크로로 초기화를 하니, level이 항상 -1
이 되는 셈입니다.parse_args()
함수에서 level_min
, level_max
가 둘 다 -1
로 설정되었는데 이것이 어떤 영향을 미치는지는 아래에서 알아봅니다.#
void *arg
로서 NULL
이번 사례에서는 이
arg
파라미터가 하는 일이 없습니다. 자세한 사항은 아래에서 살펴봅니다.#
int (*unknown)(char *param, char *val, const char *doing, void *arg)
로서 &unknown_bootoption
Unknown Boot Option을 핸들링하는 함수의 포인터입니다. 여기서는
unknown_bootoption
함수의 포인터를 넘겨주었는데, 자세한 사항은 아래에서 알아봅니다.#
parse_args()
함수의 메커니즘
C
linux/kernel/params.c
···
116
static int parse_one(char *param,117
char *val,118
const char *doing,119
const struct kernel_param *params,120
unsigned num_params,121
s16 min_level,122
s16 max_level,123
void *arg,124
int (*handle_unknown)(char *param, char *val,125
const char *doing, void *arg))126
{127
unsigned int i;128
int err;129
130
/* Find parameter */131
for (i = 0; i < num_params; i++) {132
if (parameq(param, params[i].name)) {133
if (params[i].level < min_level134
|| params[i].level > max_level)135
return 0;136
/* No one handled NULL, so do it here. */137
if (!val &&138
!(params[i].ops->flags & KERNEL_PARAM_OPS_FL_NOARG))139
return -EINVAL;140
pr_debug("handling %s with %p\n", param,141
params[i].ops->set);142
kernel_param_lock(params[i].mod);143
if (param_check_unsafe(¶ms[i]))144
err = params[i].ops->set(val, ¶ms[i]);145
else146
err = -EPERM;147
kernel_param_unlock(params[i].mod);148
return err;149
}150
}151
152
if (handle_unknown) {153
pr_debug("doing %s: %s='%s'\n", doing, param, val);154
return handle_unknown(param, val, doing, arg);155
}156
157
pr_debug("Unknown argument '%s'\n", param);158
return -ENOENT;159
}160
161
/* Args looks like "foo=bar,bar2 baz=fuz wiz". */162
char *parse_args(const char *doing,163
char *args,164
const struct kernel_param *params,165
unsigned num,166
s16 min_level,167
s16 max_level,168
void *arg,169
int (*unknown)(char *param, char *val,170
const char *doing, void *arg))171
{172
char *param, *val, *err = NULL;173
174
/* Chew leading spaces */175
args = skip_spaces(args);176
177
if (*args)178
pr_debug("doing %s, parsing ARGS: '%s'\n", doing, args);179
180
while (*args) {181
int ret;182
int irq_was_disabled;183
184
args = next_arg(args, ¶m, &val);185
/* Stop at -- */186
if (!val && strcmp(param, "--") == 0)187
return err ?: args;188
irq_was_disabled = irqs_disabled();189
ret = parse_one(param, val, doing, params, num,190
min_level, max_level, arg, unknown);191
if (irq_was_disabled && !irqs_disabled())192
pr_warn("%s: option '%s' enabled irq's!\n",193
doing, param);
···
215
}···
linux/kernel/params.c
의 parse_args()
함수 내부 184라인에서 next_arg()
함수를 호출하여 다음 파라미터를 가져옵니다. 그 다음 189라인에서 parse_one()
함수를 호출하여 파라미터를 해석합니다.parse_one()
함수에서는 131라인에서 모듈 파라미터를 순회합니다. 우리가 주목하는 __setup()
매크로로 설정한 obs_kernel_param
은 대상이 아니며, 이는 152라인에서 핸들링합니다.C
init/main.c
···
974
after_dashes = parse_args("Booting kernel",975
static_command_line, __start___param,976
__stop___param - __start___param,977
-1, -1, NULL, &unknown_bootoption);···
parse_args()
함수를 호출할 때 unknown_bootoption()
함수의 포인터를 넘겨줬는데, 이를 쭉 따라가보면 parse_one()
함수의 154라인에서 이를 실행하게 됩니다.C
init/main.c
···
529
/*530
* Unknown boot options get handed to init, unless they look like531
* unused parameters (modprobe will find them in /proc/cmdline).532
*/533
static int __init unknown_bootoption(char *param, char *val,534
const char *unused, void *arg)535
{536
size_t len = strlen(param);537
538
repair_env_string(param, val);539
540
/* Handle obsolete-style parameters */541
if (obsolete_checksetup(param))542
return 0;···
이
unknown_bootoption()
함수 내부에서는 obsolete_checksetup()
함수를 실행합니다.C
init/main.c
···
201
static bool __init obsolete_checksetup(char *line)202
{203
const struct obs_kernel_param *p;204
bool had_early_param = false;205
206
p = __setup_start;207
do {208
int n = strlen(p->str);209
if (parameqn(line, p->str, n)) {210
if (p->early) {211
/* Already done in parse_early_param?212
* (Needs exact match on param part).213
* Keep iterating, as we can have early214
* params and __setups of same names 8( */215
if (line[n] == '\0' || line[n] == '=')216
had_early_param = true;217
} else if (!p->setup_func) {218
pr_warn("Parameter %s is obsolete, ignored\n",219
p->str);220
return true;221
} else if (p->setup_func(line + n))222
return true;223
}224
p++;225
} while (p < __setup_end);226
227
return had_early_param;228
}···
obsolete_checksetup()
함수에서는 __setup_start
~__setup_end
를 순회하며 p->setup_func(line + n))
으로, __setup()
매크로에서 넘겨주었던 함수를 실행합니다.C
include/linux/init.h
···
303
struct obs_kernel_param {304
const char *str;305
int (*setup_func)(char *);306
int early;307
};···
C
include/linux/init.h
···
315
#define __setup_param(str, unique_id, fn, early) \316
static const char __setup_str_##unique_id[] __initconst \317
__aligned(1) = str; \318
static struct obs_kernel_param __setup_##unique_id \319
__used __section(".init.setup") \320
__aligned(__alignof__(struct obs_kernel_param)) \321
= { __setup_str_##unique_id, fn, early }···
구조체
obs_kernel_param
과 매크로 __setup_param()
의 정의를 다시 떠올려보면 이해가 쉽습니다.이번 글에서
__setup()
매크로에서 넘겨주었던 함수는 disable_modeset()
이 되겠네요. nomodeset
이 부팅 시 파라미터로 주어지면, disable_modeset()
함수를 실행하여 static 변수인 drm_nomodeset
을 true
로 설정하고, 이를 드라이버에서 드라이버 초기화 과정 중에 발견하여 드라이버 로드를 중단하는 것입니다.