[리눅스][커널] 커널 커맨드 라인 파싱 확인 @parse_one() 함수 분석
부트로더는 커널을 RAM에 로딩하고 실행을 시키는 역할 뿐 아니라 커널에게 어떤 아규먼트를 전달할 수 있습니다. 이를 커맨드 라인이라고 하며 proc 파일 시스템에서 커맨드 라인을 출력할 수 있는 인터페이스를 지원합니다.
커널 커맨드 라인은 왜 쓸까?
리눅스 커널을 지원하는 부트로더의 예로 uboot, LK(Little Kernel) EDK를 들 수 있습니다.
혹은 SoC(퀄컴, 인텔, 엔비디아)에서 구현한 자체 부트로더를 써서 커널을 램에 로딩할 수 있습니다.
이렇게 리눅스 커널을 RAM에 로딩하는 부트로더 종류는 언급한 이유는 어떤 부트로더를 적용해도 반드시 커널 커맨드 라인을 지원해야 한다는 사실을 말하고 싶어서입니다.
그러면 커널 커맨드 라인을 쓰는 이유는 무엇일까요?
다시 반복하지만 커널 커맨드 라인은 부트로더에서 커널을 부팅시킬 때 전달하는 아규먼트라고 보면 됩니다. 우리는 함수를 호출할 때 인자를 전달하듯 커널 커맨드 라인과 함께 커널을 로딩하는 것입니다.
커널은 부팅할 때 커널 커맨드 라인에 설정된 값을 읽어서 세부 디바이스나 커널 설정을 합니다. 따라서 커널에 특별한 다른 설정을 한 후 부팅을 시키려면 커널 커맨드 라인 값을 변경하면 됩니다.
커널 커맨드 라인을 어떻게 확인할까?
커널 커맨드 라인은 어떻게 확인할 수 있을까요? 다음 명령어를 입력합시다.
cat /proc/cmdline rcupdate.rcu_expedited=1 rcu_nocbs=0-7 cgroup.memory=nokmem,nosocket firmware_class.path=/vendor/firmware_mnt/image buildvariant=userdebug ... init=/init
대부분 커널 로그에서 커널 커맨드 라인을 출력하지만 proc 파일 시스템 /proc/cmdline 노드를 통해 커맨드 라인을 확인할 수 있습니다.
커맨드 라인 형식은 무엇일까?
부트로더에서 커널에 커맨드 라인을 전달할 때 반드시 다음 규칙을 따라야 합니다.
"커맨드"="값" "커맨드"="값" "커맨드"="값"
"커맨드"="값" "커맨드"="값" 사이는 반드시 한 칸 공백을 줘야 커널에서 커맨드 라인을 읽을 수 있습니다.
부트 로더에서 커맨드 라인을 추가할 때 주의할 필요가 있습니다.
커널 어느 코드에서 커맨드 라인을 파싱할까?
부트 로더에서 커널 커맨드 라인을 커널에게 전달하면 어느 코드에서 커맨드 라인을 파싱할까요?
이 의문을 풀기 위해서는 parse_one() 함수를 분석할 필요가 있습니다.
https://elixir.bootlin.com/linux/v4.14.70/source/kernel/params.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); param_check_unsafe(¶ms[i]); err = params[i].ops->set(val, ¶ms[i]); kernel_param_unlock(params[i].mod); return err; } } ... }
parse_one() 함수로 무려 9개 인자를 전달하는데 중요한 인자는 다음과 같습니다.
char *param: "커맨드" 입니다. char *val: "커맨드 설정 값"입니다.
이해를 돕기 위해 다음 전체 커맨드 라인이 있다고 가정해보겠습니다.
cat /proc/cmdline rcupdate.rcu_expedited=1 rcu_nocbs=0-7 cgroup.memory=nokmem,nosocket firmware_class.path=/vendor/firmware_mnt/image buildvariant=userdebug
전체 커널 커맨드 라인을 읽은 다음 한 칸 씩 구분된 커맨드 라인을 parse_one() 함수 char *param와 char *val 인자로 전달하는 것입니다.
rcupdate.rcu_expedited=1 : parse_one() - char *param: "rcupdate.rcu_expedited" - char *val: "1" rcu_nocbs=0-7 : parse_one() - char *param: "rcu_nocbs" - char *val: "0-7"
디버깅 코드 입력으로 동작 확인
이번에는 커널에 디버깅 코드를 입력한 다음에 커맨드 라인을 직접 읽어 보겠습니다.
입력할 패치 코드는 다음과 같습니다.
diff --git a/kernel/params.c b/kernel/params.c index cc9108c..7fd76d4 100644 --- a/kernel/params.c +++ b/kernel/params.c @@ -117,6 +117,8 @@ static void param_check_unsafe(const struct kernel_param *kp) } } +static int debug_routine = 1; + static int parse_one(char *param, char *val, const char *doing, @@ -131,6 +133,9 @@ static int parse_one(char *param, const char *doing, @@ -131,6 +133,9 @@ static int parse_one(char *param, unsigned int i; int err; + if ( debug_routine ) { + printk("[+]param:%s val: %s, doing:%s \n", param, val, doing); + } /* Find parameter */ for (i = 0; i < num_params; i++) { if (parameq(param, params[i].name)) { @@ -138,13 +143,21 @@ static int parse_one(char *param, || 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)) + printk("param %s params %s, index: %d\n", param, params[i].name, i); + if (!val + && !(params[i].ops->flags & KERNEL_PARAM_OPS_FL_NOARG)) return -EINVAL; - pr_debug("handling %s with %p\n", param, + printk("handling %s with %p\n", param, params[i].ops->set); kernel_param_lock(params[i].mod); param_check_unsafe(¶ms[i]); + + if ( debug_routine ) { + void *setup_handler = (void*)params[i].ops->set; + printk("[+] setup_handler: %pS caller:(%pS) \n", + setup_handler, (void *)__builtin_return_address(0)); + } + err = params[i].ops->set(val, ¶ms[i]); kernel_param_unlock(params[i].mod); return err; @@ -152,11 +165,16 @@ static int parse_one(char *param, } if (handle_unknown) { - pr_debug("doing %s: %s='%s'\n", doing, param, val); + printk("doing %s: %s='%s'\n", doing, param, val); + if ( debug_routine ) { + void *setup_handler = (void*)handle_unknown; + printk("[+] setup_handler: %pS caller:(%pS) \n", + setup_handler, (void *)__builtin_return_address(0)); + } return handle_unknown(param, val, doing, arg); } - pr_debug("Unknown argument '%s'\n", param); + printk("Unknown argument '%s'\n", param); return -ENOENT; }
위와 같이 코드를 입력한 후 커널 이미지에 반영해서 시스템을 부팅시킵니다. 이후 커널 로그를 받으면 다음과 같은 메시지를 확인할 수 있습니다.
[ 9.449092 / 01-01 00:00:11.429][6] [+]param:firmware_class.path val: /vendor/firmware_mnt/image, doing:postcore .. [ 10.804994 / 01-01 00:00:10.789][6] [+]param:init val: /init, doing:core [ 10.812043 / 01-01 00:00:10.799][6] doing core: init='/init' [ 10.817883 / 01-01 00:00:10.799][6] [+] setup_handler: repair_env_string+0x0/0x94 caller:(kernel_init_freeable+0x188/0x238) ...
다음은 커맨드 라인 스트링입니다.
cat /proc/cmdline rcupdate.rcu_expedited=1 rcu_nocbs=0-7 cgroup.memory=nokmem,nosocket firmware_class.path=/vendor/firmware_mnt/image buildvariant=userdebug ... init=/init
결론
이 방식으로 부트 로더에서 커널로 전달하는 커널 커맨드 라인을 확인해 볼 수 있습니다.
(개인블로그)
http://rousalome.egloos.com
댓글 달기