읽는 지도
Zephyr에서 Kconfig는 옵션 파일이 아니라 constraint system이다. 내가 CONFIG_FOO=y를 썼는데 결과가 n이면, 누가 덮었는지보다 어떤 dependency가 만족되지 않았는지를 봐야 한다.
이 글은 Zephyr 3.7.0 LTS3 기준이다. Zephyr 최신 stable은 별도로 움직이지만, LTS 독해의 장점은 product branch에서 바뀌지 않을 기반을 길게 추적할 수 있다는 점이다. 아래 그림은 Kconfig를 단일 함수가 아니라 빌드 산출물과 런타임 object가 만나는 경로로 나눈 것이다.
왼쪽에서 오른쪽으로 갈수록 빌드 시점의 선언이 런타임 상태로 바뀐다. 문C식으로 읽을 때는 이 그림을 먼저 놓고, 아래 원본 코드 조각이 어느 칸을 건드리는지 표시하면서 따라간다.
원본 코드 좌표
Zephyr 3.7.0 LTS3에서 먼저 열 파일
Kconfig
scripts/kconfig
boards/*/*_defconfig
prj.conf
설명: 이 글은 한 파일을 위에서 아래로 베끼는 방식이 아니라, Kconfig를 이해하는 데 필요한 kernel object, macro, generated file, runtime state를 같은 흐름 위에 놓고 읽는다. Zephyr는 빌드 단계에서 많은 결정을 끝내기 때문에 원본 C 파일과 함께 Kconfig, devicetree, linker section, generated header를 같이 봐야 한다.
원본 코드 / 사용 패턴 / 설명
아래 코드는 Kconfig를 읽을 때 실제 원본에서 찾게 되는 macro와 API의 최소 단위다. 전체 파일을 통째로 붙이면 읽기보다 스크롤이 많아지므로, 각 조각마다 "이 줄이 어떤 상태를 바꾸는가"를 붙였다.
1. symbol 선언
config MY_DRIVER
bool "My driver"
depends on GPIO
select PINCTRL
default y if BOARD_MY_EVB
depends on
이 조건이 false면 사용자가 y를 요청해도 최종 값은 켜질 수 없다.
select
다른 symbol을 강제로 켤 수 있다. dependency를 우회할 수 있어 조심해서 써야 한다.
default
사용자가 명시하지 않았을 때의 제안이다. 최종 결정은 dependency와 override를 통과해야 한다.
읽는 방법: 이 조각은 단독 기능 설명이 아니라 앞의 그림에서 어느 칸의 상태를 바꾸는지 보면서 읽어야 한다. Zephyr 코드의 많은 macro는 C 함수 호출처럼 보여도 실제로는 section 배치, symbol 생성, compile-time assertion, generated constant 참조를 동시에 수행한다.
2. application 설정
CONFIG_GPIO=y
CONFIG_LOG=y
CONFIG_MY_DRIVER=y
CONFIG_MAIN_STACK_SIZE=2048
CONFIG_GPIO
driver dependency를 만족시키는 기본 subsystem enable이다.
CONFIG_LOG
debug visibility를 얻지만 RAM/flash와 timing 비용이 생긴다.
STACK_SIZE
runtime failure를 설정값으로 피하는 예다. 하지만 분석 없이 키우면 footprint가 불어난다.
읽는 방법: 이 조각은 단독 기능 설명이 아니라 앞의 그림에서 어느 칸의 상태를 바꾸는지 보면서 읽어야 한다. Zephyr 코드의 많은 macro는 C 함수 호출처럼 보여도 실제로는 section 배치, symbol 생성, compile-time assertion, generated constant 참조를 동시에 수행한다.
3. 코드에서 config 조건 사용
#if IS_ENABLED(CONFIG_MY_DRIVER)
my_driver_submit();
#endif
BUILD_ASSERT(CONFIG_MAIN_STACK_SIZE >= 1024);
IS_ENABLED
preprocessor와 C expression 사이에서 config를 안전하게 다루는 관용구다.
BUILD_ASSERT
설정 조합이 잘못되었을 때 runtime까지 가지 않게 한다.
조건부 코드
Kconfig가 꺼지면 object code 자체가 사라질 수 있다. 디버깅 시 symbol 존재 여부부터 봐야 한다.
읽는 방법: 이 조각은 단독 기능 설명이 아니라 앞의 그림에서 어느 칸의 상태를 바꾸는지 보면서 읽어야 한다. Zephyr 코드의 많은 macro는 C 함수 호출처럼 보여도 실제로는 section 배치, symbol 생성, compile-time assertion, generated constant 참조를 동시에 수행한다.
소스 트리 추적 순서
Kconfig를 길게 읽을 때는 파일 하나를 열고 끝내면 안 된다. Zephyr는 macro, generated header, linker section, Kconfig symbol, devicetree node가 한 덩어리로 동작한다. 아래 순서는 실제로 코드를 따라갈 때의 탐색 루트다.
1단계. public API에서 시작한다
먼저 application이나 driver가 실제로 include하는 header를 본다. 이 단계에서는 함수 body를 찾으려 하지 말고, type과 macro가 어떤 storage와 호출 계약을 요구하는지 확인한다. 많은 Zephyr API는 함수처럼 보여도 static inline, macro, conditional compilation으로 갈라진다.
Kconfig
scripts/kconfig
boards/*/*_defconfig
설명: public header는 사용자에게 보이는 계약이다. 여기서 timeout 인자가 있는지, ISR에서 허용되는지, object를 caller가 제공해야 하는지, return value가 어떤 error domain을 쓰는지 먼저 표시한다.
2단계. macro expansion을 눈으로 풀어 본다
Zephyr의 많은 object는 선언 macro 하나로 끝나지 않는다. 예를 들어 device, thread stack, queue, init entry는 macro가 storage object, metadata, section attribute, alignment를 같이 만든다. 따라서 macro 이름만 보고 "등록된다"라고 넘어가면 실제 binary layout을 놓친다.
/* 읽을 때 머릿속에서 이렇게 풀어 본다 */
DECLARATION_MACRO(name, arg0, arg1)
-> static storage
-> metadata structure
-> linker section placement
-> generated symbol reference
-> runtime iterator consumes it
설명: 이 풀이는 실제 C 전처리 결과와 1:1로 같다는 뜻이 아니라, 독해 관점에서 확인할 항목을 나열한 것이다. 필요하면 build/zephyr/include/generated와 map file에서 symbol 이름을 확인한다.
3단계. runtime body로 내려간다
그 다음에야 kernel/, subsys/, drivers/ 아래의 구현 파일로 내려간다. 구현 파일에서는 happy path보다 state check, wait queue, timeout, lock, reference ownership, error return을 먼저 본다. 작은 RTOS에서는 실패 처리 하나가 전체 firmware의 deadline과 recovery 정책을 결정한다.
| 볼 것 | 질문 | 놓치면 생기는 증상 |
|---|---|---|
| lock | 어느 context에서 잡고 얼마나 오래 보유하는가 | 간헐적 latency spike |
| wait queue | 누가 block되고 누가 wake-up시키는가 | 영원히 깨어나지 않는 thread |
| timeout | 시간 만료가 정상 경로인가 실패 경로인가 | 재현 어려운 -EAGAIN/-ETIMEDOUT |
| return value | caller가 모든 실패를 구분해서 처리하는가 | 초기화 실패 후 조용한 no-op |
4단계. build artifact로 되돌아온다
마지막에는 반드시 build directory로 돌아온다. 원본 파일을 읽은 뒤 실제 image에 어떤 symbol이 들어갔는지 확인하지 않으면, 분석은 소스 트리의 가능성만 본 것이다. 제품 firmware는 가능성의 집합이 아니라 특정 configuration 하나의 결과물이다.
build/zephyr/.config
build/zephyr/zephyr.dts
build/zephyr/include/generated/
build/zephyr/zephyr.map
build/zephyr/zephyr.elf
설명: 이 다섯 개가 실제 검증 대상이다. 특히 Kconfig처럼 subsystem boundary가 많은 주제는 원본 C보다 generated output이 더 직접적인 진실을 보여줄 때가 많다.
깊게 읽기
논문 읽듯이 볼 핵심 질문: Zephyr에서 Kconfig는 옵션 파일이 아니라 constraint system이다. 내가 CONFIG_FOO=y를 썼는데 결과가 n이면, 누가 덮었는지보다 어떤 dependency가 만족되지 않았는지를 봐야 한다.
1. 빌드 시점과 런타임 시점을 분리한다
Kconfig를 잘 읽으려면 menuconfig 화면보다 최종 .config와 Kconfig dependency path를 본다. select 남용, hidden symbol, board defconfig, module Kconfig가 한데 섞이면 문제는 C 코드가 아니라 configuration algebra가 된다.
Zephyr에서 "코드가 있다"와 "런타임에 탐색된다"는 같은 뜻이 아니다. devicetree macro, Kconfig symbol, linker section, generated header가 C 코드보다 먼저 세계를 좁혀 둔다. 따라서 분석할 때는 west build 결과물의 zephyr.dts, .config, include/generated, map file을 같이 열어야 한다.
2. object lifetime을 먼저 표시한다
작은 RTOS에서는 동적 할당보다 정적 object가 많다. 이 말은 lifetime이 단순하다는 장점도 있지만, 잘못된 초기화 순서나 readiness check 누락이 시스템 전체 lifetime 동안 유지된다는 뜻이기도 하다. Kconfig를 읽을 때는 object가 ROM 성격인지, RAM runtime state인지, per-thread인지, per-device인지, ISR에서 만지는지를 먼저 적는다.
3. context를 함수 이름보다 우선한다
같은 API라도 ISR에서 부르는지, cooperative thread에서 부르는지, preemptible thread에서 부르는지, system workqueue에서 부르는지에 따라 의미가 달라진다. sleep 가능 여부, timeout 의미, lock 사용 가능성, priority inversion 위험이 context에서 나온다. 그래서 Zephyr 글은 항상 "이 코드는 어느 context에서 실행되는가"를 반복해서 확인해야 한다.
4. 실패 경로를 정상 경로만큼 읽는다
embedded bring-up에서 중요한 failure는 대개 조용하다. device가 ready가 아니거나, Kconfig dependency가 꺼졌거나, devicetree status가 disabled이거나, memory slab이 고갈되어도 시스템은 그냥 다른 방식으로 멈춘 것처럼 보일 수 있다. 원본 코드에서 return value와 timeout, null pointer, assert 조건을 정상 흐름과 나란히 읽어야 한다.
불변조건 체크리스트
| 항목 | 질문 | 확인 방법 |
|---|---|---|
| 빌드 산출물 | DTS, Kconfig, linker section이 기대한 symbol을 만들었는가 | generated files와 map file을 확인 |
| 초기화 순서 | 이 object를 쓰는 쪽보다 provider가 먼저 ready가 되는가 | init level과 priority 비교 |
| 실행 context | sleep/timeout/lock이 허용되는 context인가 | ISR, workqueue, thread 구분 |
| lifetime | object storage가 caller보다 오래 살아 있는가 | static object와 stack object 구분 |
| 관찰 지점 | 실패했을 때 어떤 log/assert/return value로 보이는가 | CONFIG_ASSERT, LOG, tracing 사용 |
이 표는 Kconfig 전용으로 끝나는 표가 아니다. Zephyr의 대부분 subsystem에서 같은 방식으로 쓸 수 있다. 특히 driver bring-up이나 board porting에서는 "코드가 맞는데 안 된다"는 말의 상당수가 이 표의 첫 세 줄에서 걸린다.
상태 머신으로 다시 그리기
코드가 길어질수록 함수 호출 순서만으로는 구조가 잘 안 보인다. Kconfig는 아래처럼 상태 머신으로 바꿔 그리면 어디서 block되고, 어디서 publish되고, 어디서 실패가 소비되는지 더 잘 보인다.
실제 코드는 이보다 복잡하지만, 분석할 때는 declared/linked/initialized/ready/failed 다섯 칸을 먼저 채워 넣는다. 어느 칸이 비어 있는지 찾으면 bring-up 실패 원인이 빨리 좁혀진다.
declared에서 linked로
이 전이는 C 함수 호출이 아니라 build system과 linker가 만든다. macro가 section attribute를 붙이고, linker script가 section을 모으고, map file에 순서가 남는다. 이 단계에서 빠진 object는 런타임에서 절대 찾을 수 없다.
initialized에서 ready로
초기화 함수가 호출되었다고 ready가 되는 것은 아니다. return value, dependency readiness, clock/reset/pinctrl, memory allocation 결과가 모두 맞아야 consumer가 사용할 수 있다. 그래서 device_is_ready(), return code, assert를 습관적으로 본다.
ready에서 failed로 되돌아가는 경우
RTOS에서는 ready 이후에도 runtime failure가 많다. queue full, timeout, workqueue backlog, device suspend, network down, heap exhaustion 같은 사건은 initialized 상태를 지우지 않지만 operation 단위 실패를 만든다. 이 차이를 구분해야 "초기화 문제"와 "운영 중 압력 문제"를 나눌 수 있다.
실패 매트릭스
아래 표는 Kconfig에서 자주 보이는 증상을 원인 후보로 나눈 것이다. 이 표를 넣는 이유는 길게 설명한 내용을 실제 디버깅 순서로 압축하기 위해서다.
| 증상 | 가장 먼저 볼 곳 | 원인 후보 | 확인 명령/코드 |
|---|---|---|---|
| 빌드는 성공, 동작 없음 | .config, zephyr.dts, map file | disabled node, unmet dependency, section 미포함 | grep으로 symbol/node 확인 |
| 초기화 중 멈춤 | init level과 priority | provider보다 consumer가 먼저 실행 | init 로그와 map 순서 비교 |
| 가끔 timeout | wait queue와 thread priority | priority inversion, workqueue backlog, ISR storm | timestamp log 추가 |
| 메모리 부족 | stack/slab/heap sizing | burst 미반영, fragmentation, logging 증가 | stack watermark와 allocation 실패 로그 |
| 보드별로만 실패 | board DTS와 defconfig | pinctrl, clock, chosen, alias 차이 | merged DTS diff |
| 최적화 후 실패 | lifetime과 volatile/atomic 경계 | stack object 비동기 사용, ordering 가정 | assert와 address lifetime 추적 |
중요한 습관은 증상을 곧바로 코드 bug로 단정하지 않는 것이다. Zephyr에서는 build-time 선택이 runtime 현상처럼 나타나는 경우가 많다. 특히 Kconfig 문제는 source, generated artifact, runtime log 세 가지를 같이 봐야 재현성이 생긴다.
자주 하는 오해와 안티패턴
Kconfig를 처음 읽을 때 많이 하는 실수는 Linux에서 익숙한 감각을 그대로 가져오는 것이다. Zephyr도 POSIX-like API와 driver model을 제공하지만, build-time composition과 작은 footprint라는 전제가 훨씬 강하다.
안티패턴 1. source tree만 보고 최종 image를 안 본다
소스에 함수가 있다는 말은 firmware에 들어갔다는 뜻이 아니다. Kconfig가 끄면 compile unit 자체가 빠질 수 있고, devicetree status가 disabled이면 driver instance가 생성되지 않을 수 있다. 따라서 Kconfig 관련 bug를 볼 때는 source tree보다 build output을 먼저 확인하는 습관이 필요하다.
안티패턴 2. timeout을 디버깅 회피 수단으로 쓴다
K_MSEC(100) 같은 값을 넣고 문제가 사라졌다고 판단하면 안 된다. timeout은 설계 값이어야 한다. producer/consumer 속도, interrupt rate, workqueue backlog, scheduler priority를 계산하지 않고 넣은 timeout은 field에서 더 큰 latency spike로 돌아온다.
안티패턴 3. system workqueue를 개인 thread처럼 쓴다
workqueue는 편하지만 공유 자원이다. 오래 걸리는 handler, blocking I/O, 큰 parsing 작업을 system workqueue에 넣으면 다른 subsystem의 deferred work까지 지연된다. Kconfig가 workqueue를 거친다면 handler 실행 시간과 queue depth를 따로 적어야 한다.
안티패턴 4. stack size를 감으로 키운다
stack overflow가 의심될 때 값을 두 배로 키우는 것은 임시 처치다. call depth, logging 사용, printf formatting, crypto/network path, userspace guard 비용을 따로 봐야 한다. Zephyr에서는 stack watermark와 thread analyzer를 통해 근거 있는 값을 잡는 편이 맞다.
안티패턴 5. ready check를 생략한다
device pointer가 compile-time에 존재한다고 해서 runtime usable 상태라는 뜻은 아니다. init 함수가 실패했거나 provider dependency가 준비되지 않았을 수 있다. 이 실수는 board porting 초기에 특히 자주 나온다.
Linux와 비교해서 헷갈리는 지점
runtime discovery보다 build-time composition이 강하다
Linux는 bus, driver core, module, sysfs, hotplug처럼 런타임 discovery와 policy가 두껍다. Zephyr도 device model이 있지만, 많은 board에서는 devicetree와 Kconfig가 미리 가능한 세계를 작게 만든다. 그래서 Kconfig 분석에서는 "왜 런타임에 못 찾지?"보다 "빌드 결과에 애초에 들어왔나?"를 먼저 물어야 한다.
메모리와 시간의 upper bound가 더 노골적이다
Zephyr는 flash/RAM이 작은 MCU부터 대상으로 한다. 따라서 queue depth, stack size, slab count, workqueue 처리 시간 같은 값이 설계의 일부다. Linux에서라면 성능 튜닝 항목으로 넘길 수 있는 수치가 Zephyr에서는 correctness 조건이 된다.
debugging은 generated file을 포함한다
원본 C 파일만 보면 절반만 본 것이다. Zephyr는 build/zephyr/zephyr.dts, build/zephyr/.config, generated headers, linker map이 원본 코드 해석의 일부다. 문C식으로 그림과 코드를 나란히 놓을 때도 이 산출물을 한 축으로 넣어야 한다.
시간 축으로 읽는 실행 흐름
마지막으로 Kconfig를 시간 축 위에 올려 본다. 이 표는 실제 코드 라인보다 추상적이지만, bring-up 중 멈춘 위치를 찾을 때 훨씬 빠르다.
| 시간 | 무슨 일이 일어나는가 | 확인할 파일/산출물 | 실패 시 보이는 현상 |
|---|---|---|---|
| T0 configure | Kconfig와 devicetree 입력이 합쳐진다 | prj.conf, board defconfig, overlay | 원하는 symbol/node가 최종 결과에 없음 |
| T1 generate | generated header와 merged DTS가 만들어진다 | build/zephyr/include/generated, zephyr.dts | macro는 있는데 값이 예상과 다름 |
| T2 link | section과 object가 image에 배치된다 | zephyr.map, linker script | init entry 순서가 dependency와 맞지 않음 |
| T3 boot | 초기 init level과 kernel object 준비가 진행된다 | init log, early printk | 초기화 중 assert 또는 silent fail |
| T4 run | thread, ISR, workqueue, device API가 실제 부하를 처리한다 | runtime log, tracing, counters | timeout, latency spike, queue full |
| T5 recover | 실패를 return code, retry, reset, safe state로 처리한다 | error path, watchdog policy | 복구 불가 hang 또는 반복 reboot |
이 표를 사용하는 방법
문제가 생기면 먼저 마지막으로 확실히 통과한 시간을 표시한다. T1까지 맞는데 T3에서 실패하면 init ordering 문제일 가능성이 크고, T4에서만 실패하면 runtime pressure나 concurrency 문제일 가능성이 크다. Kconfig 분석도 이 시간 축으로 나누면 "코드가 틀렸다"라는 넓은 문장이 훨씬 작은 질문으로 줄어든다.
실전 bring-up 시나리오
이 섹션은 실제 보드에서 Kconfig 문제가 났다고 가정하고 읽는 부분이다. 문C식 글의 장점은 그림과 코드가 끝난 뒤에도, "그래서 보드에서 어디를 보면 되는가"까지 내려간다는 데 있다. 아래 네 가지 상황은 Zephyr bring-up에서 반복해서 만나는 실패 패턴이다.
시나리오 A. 빌드는 되는데 런타임에서 아무 일도 안 일어난다
첫 번째로 볼 것은 함수 body가 아니라 빌드 산출물이다. Kconfig symbols 단계가 source에는 있어도 최종 image에 들어가지 않았을 수 있다. Kconfig가 꺼져 있거나, devicetree node가 disabled이거나, macro expansion 조건이 맞지 않으면 C 파일을 아무리 읽어도 실행 지점에 도달하지 않는다.
grep -n "CONFIG_" build/zephyr/.config
grep -n "status = \"okay\"" build/zephyr/zephyr.dts
grep -n "Kconfig" build/zephyr/zephyr.map
설명: 이 세 줄은 source, hardware description, linker 결과를 한 번에 확인한다. Zephyr에서는 이 셋이 맞아야 원본 코드가 실제 firmware 동작으로 이어진다.
시나리오 B. 한 번은 동작하고 두 번째부터 멈춘다
이 경우에는 lifetime과 ownership을 먼저 의심한다. stack에 둔 object 주소를 workqueue나 ISR이 나중에 사용하거나, static object라고 생각한 buffer가 실제로는 producer가 다시 덮어쓰는 scratch buffer일 수 있다. Kconfig에서는 특히 "누가 object를 소유하고 언제 넘기는가"가 중요하다.
| 의심 지점 | 확인 질문 | 관찰 방법 |
|---|---|---|
| stack object | 비동기 처리 뒤에도 살아 있는가 | 주소 범위와 thread stack dump 확인 |
| static object | 동시 접근을 lock으로 보호하는가 | CONFIG_ASSERT와 race 재현 부하 |
| device pointer | ready 확인 뒤 사용하는가 | device_is_ready 실패 로그 추가 |
시나리오 C. latency가 간헐적으로 튄다
평균 latency가 아니라 tail latency를 본다. RTOS에서 문제는 "대체로 빠르다"가 아니라 "가끔 deadline을 넘는다"인 경우가 많다. interrupt, scheduler, workqueue, heap allocation, logging이 섞이면 간헐적인 긴 구간이 생긴다. Kconfig를 분석할 때는 context 전환과 timeout queue를 시간 축 위에 표시한다.
uint32_t t0 = k_cycle_get_32();
critical_path();
uint32_t dt = k_cycle_get_32() - t0;
LOG_INF("critical path cycles=%u", dt);
설명: 단순 로그도 처음에는 충분하다. 이후에는 tracing backend나 board counter를 붙여 ISR entry, work submit, thread wake-up 사이 간격을 따로 잰다.
시나리오 D. 설정 하나 바꿨더니 전혀 다른 subsystem이 깨진다
Zephyr에서는 Kconfig symbol 하나가 code size, stack size, device dependency, logging path, userspace permission을 동시에 바꿀 수 있다. 설정 변경 뒤에는 source diff만 볼 것이 아니라 .config, zephyr.dts, map file의 변화를 같이 봐야 한다. 작은 RTOS일수록 build-time composition의 파급이 크다.
cp build/zephyr/.config before.config
west build -p auto -b <board> <app>
diff -u before.config build/zephyr/.config
설명: 변경 전후의 최종 설정을 비교하면, 내가 직접 바꾼 symbol이 아닌 간접 dependency가 드러난다.
박사 과정 기준으로 더 파고들 질문
아래 질문들은 단순 사용법을 넘어서 Kconfig를 연구 주제로 다룰 때 필요한 관찰 축이다. 실제 논문이나 제품 안정화 보고서를 쓴다면 API 호출 예제보다 이런 질문이 본문을 만든다.
1. 결정성 determinism
이 경로의 worst-case execution time은 어디서 결정되는가. interrupt disable 구간, spinlock 보유 시간, ready queue scan, memory allocation, logging backend가 upper bound를 흐리게 만들지는 않는가. Kconfig의 각 API는 평균 비용보다 최악 비용을 먼저 적어야 한다.
Zephyr LTS를 쓰는 이유도 여기와 맞닿아 있다. 장기 지원 branch에서는 API와 내부 정책 변화가 줄어들어, 측정한 worst-case가 제품 수명 동안 유지될 가능성이 높아진다. 최신 stable을 따라가는 실험 branch와 LTS product branch를 분리해 분석하는 편이 좋다.
2. memory footprint와 fragmentation
정적 object는 예측 가능하지만 낭비가 생길 수 있고, 동적 object는 평균 footprint를 줄일 수 있지만 fragmentation과 allocation latency를 만든다. Kconfig에서 buffer, queue, stack, slab, heap 중 무엇을 쓰는지에 따라 실패 모드가 달라진다.
분석할 때는 RAM 총량이 아니라 peak 동시 사용량을 본다. ISR burst, network burst, workqueue backlog, nested callback이 겹치는 순간을 worst-case scenario로 잡고, 그때 object 수와 stack depth가 버티는지 계산한다.
3. isolation과 fault containment
userspace, memory domain, MPU, stack guard, assertion은 모두 비용이 있다. 하지만 제품에서 한 driver의 pointer bug가 전체 firmware를 망가뜨리지 않게 하려면 isolation boundary가 필요하다. Kconfig를 읽을 때도 기능이 아니라 fault containment 관점으로 다시 그릴 수 있어야 한다.
이 관점에서는 "성공 경로가 빠른가"보다 "실패했을 때 어디서 멈추고 어떤 정보를 남기는가"가 중요하다. panic, fatal error, assert, return code, log level을 하나의 정책으로 맞추는 것이 필요하다.
4. Linux와 다른 검증 방식
Linux kernel은 dynamic workload와 복잡한 runtime policy를 견디도록 설계되어 있다. Zephyr는 더 작은 구성 공간을 전제로 compile-time 결정을 적극적으로 사용한다. 따라서 검증도 runtime fuzzing만이 아니라 configuration matrix와 generated artifact diff가 중요하다.
예를 들어 board A와 board B가 같은 application source를 쓰더라도 devicetree와 Kconfig가 다르면 실제 image는 전혀 다른 system이 된다. 이 차이를 source-level review만으로 잡으려 하면 놓치는 것이 많다.
코드 리뷰 질문
아래 질문들은 Kconfig 관련 코드를 리뷰할 때 그대로 체크리스트로 쓸 수 있다. 코드가 돌아가는지보다, 보드가 바뀌고 부하가 걸리고 설정이 바뀌어도 유지되는지 보는 질문들이다.
configuration 질문
- 이 기능이 켜지는 Kconfig symbol은 무엇이며, hidden dependency가 있는가.
- board defconfig와 application prj.conf 중 어느 쪽이 최종 값을 결정하는가.
- 설정이 꺼졌을 때 compile error가 나는가, 조용히 기능이 빠지는가.
- LTS branch에서 이 symbol의 default나 dependency가 stable하다고 볼 수 있는가.
devicetree 질문
- consumer가 참조하는 node가 최종 merged DTS에서
status = "okay"인가. - alias/chosen을 쓰는 경우 board별 override가 의도대로 적용되는가.
- binding property 이름과 C macro 이름이 정확히 대응하는가.
- pinctrl, clock, reset, interrupt property가 provider driver 초기화보다 늦게 소비되지 않는가.
concurrency 질문
- 이 API는 ISR에서 호출 가능한가. 가능하다면 내부에서 sleep하지 않는가.
- 공유 object는 mutex, spinlock, atomic, workqueue serialization 중 무엇으로 보호되는가.
- timeout은 정상적인 backpressure인가, 장애를 감추는 임시방편인가.
- priority inversion이 생겼을 때 상한을 계산할 수 있는가.
operability 질문
- 실패했을 때 return code와 log가 원인을 구분할 만큼 남는가.
- assert를 끈 production build에서도 복구 또는 안전 정지가 가능한가.
- stack, heap, slab, queue 사용량을 계측할 방법이 있는가.
- Kconfig가 깨졌을 때 field engineer가 확인할 수 있는 최소 로그는 무엇인가.
디버깅과 계측 포인트
빌드 결과 확인
west build -b <board> <app>
grep CONFIG_ build/zephyr/.config
grep -n "<node-or-compatible>" build/zephyr/zephyr.dts
설명: Kconfig가 기대하는 symbol과 node가 최종 build에 들어왔는지 먼저 본다. source tree의 DTS/Kconfig 원본이 아니라 build directory의 merged 결과가 기준이다.
런타임 관찰
CONFIG_LOG=y
CONFIG_ASSERT=y
CONFIG_THREAD_NAME=y
CONFIG_INIT_STACKS=y
설명: 로그와 assert는 footprint 비용이 있지만 bring-up 단계에서는 시간을 아낀다. thread 이름, stack 초기화, assert를 켜면 잘못된 context 호출이나 stack 부족이 훨씬 빨리 드러난다.
실험 과제
- Kconfig에서 사용하는 object 하나를 의도적으로 disabled 상태로 만들고 어떤 build/runtime 에러가 나는지 본다.
- timeout을
K_NO_WAIT,K_MSEC(1),K_FOREVER로 바꿔 latency와 실패 양상이 어떻게 달라지는지 기록한다. - map file 또는 generated header에서 이 글의 원본 코드 조각이 어떤 symbol로 남는지 확인한다.
실험 로그 템플릿
나중에 같은 문제를 다시 만나지 않으려면 bring-up 로그를 일정한 형식으로 남겨야 한다. 아래 템플릿은 Kconfig를 실험할 때 그대로 복사해서 쓸 수 있는 형태로 넣었다.
[실험 주제] Kconfig
[Zephyr 버전] Zephyr 3.7.0 LTS3
[보드/SoC]
[west manifest revision]
[관련 Kconfig]
[관련 devicetree node]
[관찰한 원본 코드]
[예상 상태 전이]
[실제 상태 전이]
[실패 시 return/log/assert]
[latency 또는 memory 측정값]
[다음 실험]
설명: 이 양식은 단순 메모가 아니라 원인 분석의 좌표계다. 특히 west revision, 최종 .config, merged zephyr.dts를 같이 남기면 몇 달 뒤에도 같은 build를 재현할 수 있다.
중요한 것은 한 번에 완벽한 분석을 쓰는 게 아니다. 원본 코드에서 본 가정, 빌드 산출물에서 확인한 사실, 런타임에서 관찰한 결과를 구분해서 적는 것이다. 이 세 층을 섞어 쓰면 다음 디버깅 때 같은 함정에 다시 빠진다.
용어를 이 글 기준으로 다시 정리
아래 용어는 사전식 정의가 아니라 Kconfig를 읽을 때 실제로 어떤 질문으로 바뀌는지를 적은 것이다. 커널 글을 길게 쓰는 이유는 이런 단어들이 코드 안에서 서로 섞이는 순간을 놓치지 않기 위해서다.
| 용어 | 이 글에서의 의미 |
|---|---|
build-time composition | Kconfig, devicetree, CMake, linker section이 firmware image의 구조를 실행 전에 결정하는 방식 |
runtime state | device ready, thread ready, timeout pending, queue occupancy처럼 실행 중 계속 바뀌는 상태 |
context boundary | ISR, cooperative thread, preemptive thread, workqueue thread처럼 API 허용 범위가 달라지는 경계 |
object lifetime | static storage, stack storage, heap/slab block, device object가 언제부터 언제까지 유효한지에 대한 계약 |
backpressure | queue full, slab exhaustion, timeout, drop처럼 producer 속도를 consumer 처리 능력에 맞추는 신호 |
읽는 순서
처음에는 API 이름을 외우지 말고, Kconfig 근처에서 시작해 빌드 산출물과 runtime object를 왕복한다. 그 다음 같은 기능을 enabled/disabled로 바꿔 보고, 어떤 symbol과 section이 사라지는지 확인한다. 마지막으로 timeout과 priority를 바꿔 latency가 어떻게 달라지는지 측정한다.
이 과정을 한 번 거치면 Kconfig가 단순 기능 묶음이 아니라, 제한된 RAM과 deadline 안에서 어떤 불변조건을 지키려는 설계인지 보이기 시작한다. 그 지점부터 원본 코드의 macro와 작은 helper 함수들이 의미를 갖는다.