App Icon

Stardue128

More

리눅스 커널 소스의 __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 init

45

* section.

46

*/

47

48

/* These are for everybody (although not all archs will actually

49

discard it in modules) */

50

#define __init __section(".init.text") __cold __latent_entropy __noinitretpoline

51

#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 the

313

* 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_idhello라면 이는 컴파일 시 __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.hINIT_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 parameter

615

* parsing is performed in place, and we should allow a component to

616

* 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 command

640

* lines because there could be dashes (separator of init

641

* 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/module

286

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 parameter

168

* @name: a valid C identifier which is the parameter name.

169

* @ops: the set & get operations for this parameter.

170

* @arg: args for @ops

171

* @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_level

134

|| 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(&params[i]))

144

err = params[i].ops->set(val, &params[i]);

145

else

146

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, &param, &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.cparse_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 like

531

* 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 early

214

* 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_nomodesettrue로 설정하고, 이를 드라이버에서 드라이버 초기화 과정 중에 발견하여 드라이버 로드를 중단하는 것입니다.