LK · source analysis

console parser와 정적 command table

입력 line을 token으로 나누고 linker section의 command table에서 handler를 찾는 과정을 읽습니다.

표의 버전은 소스 분석 기준이며 최신 보안 버전 권고가 아닙니다. TF-A LTS 2.14.3 이후에는 보안 수정이 있습니다. 제품 적용 시 공식 변경 기록과 사용 플랫폼의 errata를 확인하세요.

Series
13 / 20
Baseline
upstream 1eb9917
Commit
1eb99171efae
Source
lib/console/console.c:726-814 (발췌 범위)

console parser와 정적 command table 단계에서 실제로 바뀌는 상태는 무엇인가?

고정 길이 line buffer, quoting, command 재진입과 lock을 본다. 디버그 shell이라도 입력 길이와 handler의 blocking 특성을 무시하면 부트 경로를 멈출 수 있다.

parser가 handler에 넘기는 argc/argv는 command 실행이 끝날 때까지 유효하고 buffer 경계를 넘지 않아야 한다. 이 문장을 기준으로 코드를 위에서 아래로 읽으면, 함수 이름을 외우는 대신 어느 시점에 어떤 상태를 신뢰할 수 있는지 판단할 수 있다.

객체와 주소가 놓이는 구조

그림 1. console parser와 정적 command table에서 입력, 내부 상태, 출력의 경계
입력과 전제내부 상태외부로 공개되는 결과
01console buffer
02argv tokens
03cmd table
04command context

console_t / console_cmd / argv buffer를 중심에 놓고 왼쪽의 입력이 어떤 검사를 거쳐 오른쪽 결과로 공개되는지 표시했다. 실제 디버깅에서는 각 블록의 주소와 크기를 로그에 대입한다.

실행 흐름

그림 2. 정상 경로의 주요 호출과 상태 전달
UART input
line reader
tokenize
command lookup
handler return

화살표는 단순 호출 순서만 뜻하지 않는다. 각 단계가 성공을 반환할 때 다음 단계가 읽을 수 있는 상태가 무엇인지 함께 확인한다. 오류 반환이 발생하면 바로 다음 화살표로 진행하지 않고 해당 단계의 정리 경로를 따라간다.

단계별 입력과 출력

각 단계의 입력과 처리 후 바뀌는 상태를 정리한 표다. 사용 중인 소스 버전에서 다른 필드를 변경한다면 그에 맞춰 표도 수정한다.

#단계진입 시 신뢰할 상태성공 뒤 남아야 할 상태다음 소비자
01UART input부트 CPU의 동기 실행 문맥console bufferline reader
02line readerUART input 완료 상태argv tokenstokenize
03tokenizeline reader 완료 상태cmd tablecommand lookup
04command lookuptokenize 완료 상태command contexthandler return
05handler returncommand lookup 완료 상태command context이 단계의 처리 결과를 사용하는 코드

분석 범위의 전제와 비교해, 다음 단계에 잘못된 입력을 넘긴 부분부터 조사한다.

주소와 객체의 이동을 그림으로 읽기

아래 그림은 주소, 객체를 사용하는 기간, 제어권을 넘기는 시점을 중심으로 정리했다. 실제 주소와 크기는 사용 중인 보드의 bdinfo, 맵 파일과 링커 심벌로 확인한다.

그림 3. console 입력에서 command handler 반환까지

호출 이름만 나열하지 않고, 각 단계가 다음 단계에 넘기는 상태를 기준으로 실행 순서를 표시한다.

01UART/console input
02line editing
03tokenize argv
04command section lookup
05handler 호출
06status 출력
07다음 prompt
그림 4. command 등록 정보와 한 번의 invocation

두 열의 객체는 이름이 비슷해도 생성 시점, 사용 가능한 API와 해제 책임이 다르다.

Static command

01name·help

02function pointer

03linker section

04image lifetime

05Kconfig 포함 여부

Invocation

01argc·argv

02console thread context

03임시 parse buffer

04handler return

05ctrl-c 상태

원본 코드

아래 코드는 LK upstream 1eb9917의 lib/console/console.c에서 726-814줄을 그대로 가져온 것이다. 설명을 위해 실제 코드를 가짜 의사 코드로 바꾸지 않았다.

commit1eb99171efae98867d4be4a5bd6479ea03f43ac9anchor · L750static int console_run_script_etc
726
727    // we're done
728    if (lineread->string[lineread->pos] == 0)
729        return -1;
730
731    size_t bufpos = 0;
732    while (lineread->string[lineread->pos] != 0) {
733        if (lineread->string[lineread->pos] == '\n') {
734            lineread->pos++;
735            break;
736        }
737        if (bufpos == (lineread->buflen - 1))
738            break;
739        lineread->buffer[bufpos] = lineread->string[lineread->pos];
740        lineread->pos++;
741        bufpos++;
742    }
743    lineread->buffer[bufpos] = 0;
744
745    *buffer = lineread->buffer;
746
747    return bufpos;
748}
749
750static int console_run_script_etc(console_t *con, const char *string, bool locked) {
751    struct line_read_struct lineread;
752
753    lineread.string = string;
754    lineread.pos = 0;
755    lineread.buffer = malloc(LINE_LEN);
756    lineread.buflen = LINE_LEN;
757
758    command_loop(con, &fetch_next_line, (void *)&lineread, false, locked);
759
760    free(lineread.buffer);
761
762    return con->lastresult;
763}
764
765int console_run_script(console_t *con, const char *string) {
766    if (!con) {
767        con = console_get_current();
768    }
769    return console_run_script_etc(con, string, false);
770}
771
772int console_run_script_locked(console_t *con, const char *string) {
773    if (!con) {
774        con = console_get_current();
775    }
776    return console_run_script_etc(con, string, true);
777}
778
779console_cmd_func console_get_command_handler(const char *commandstr) {
780    const console_cmd *command = match_command(commandstr, CMD_AVAIL_NORMAL);
781
782    if (command)
783        return command->cmd_callback;
784    else
785        return NULL;
786}
787
788static int cmd_help_impl(uint8_t availability_mask) {
789    printf("command list by block:\n");
790
791    for (const console_cmd_block *block = &__start_commands; block != &__stop_commands; block++) {
792        const console_cmd *curr_cmd = block->list;
793        printf("  [%s]\n", block->name);
794        for (size_t i = 0; i < block->count; i++) {
795            if ((availability_mask & curr_cmd[i].availability_mask) == 0) {
796                // Skip commands that aren't available in the current shell.
797                continue;
798            }
799            if (curr_cmd[i].help_str)
800                printf("\t%-16s: %s\n", curr_cmd[i].cmd_str, curr_cmd[i].help_str);
801        }
802    }
803
804    return 0;
805}
806
807static int cmd_help(int argc, const console_cmd_args *argv) {
808    return cmd_help_impl(CMD_AVAIL_NORMAL);
809}
810
811static int cmd_help_panic(int argc, const console_cmd_args *argv) {
812    return cmd_help_impl(CMD_AVAIL_PANIC);
813}
814

발췌 범위와 읽기 기준

원문 726-814줄에는 앞뒤 문맥이 포함됩니다. 기준 구문 static int console_run_script_etc은 L750에 있습니다. 발췌 첫 줄을 함수 정의의 시작으로 해석하지 않습니다.

고정 길이 line buffer, quoting, command 재진입과 lock을 본다. 디버그 shell이라도 입력 길이와 handler의 blocking 특성을 무시하면 부트 경로를 멈출 수 있다.

함께 읽어야 하는 원본 코드

첫 코드 조각만으로 동작이 완성되지 않는 경우 호출 매크로, 객체 정의와 실제 실행 목록을 같은 페이지에 묶었다. 각 조각은 같은 기준 commit에서 가져왔다.

01. 입력 한 줄을 argc와 argv로 분해하는 tokenizer

lib/console/console.c 367-455줄이다.

filelib/console/console.canchor · L377static int tokenize_command
367    // add to history
368    add_history(con, buffer);
369#endif
370
371    // return a pointer to our buffer
372    *outbuffer = buffer;
373
374    return pos;
375}
376
377static int tokenize_command(const char *inbuffer, const char **continuebuffer, char *buffer, size_t buflen, console_cmd_args *args, int arg_count) {
378    int inpos;
379    int outpos;
380    int arg;
381    enum {
382        INITIAL = 0,
383        NEXT_FIELD,
384        SPACE,
385        IN_SPACE,
386        TOKEN,
387        IN_TOKEN,
388        QUOTED_TOKEN,
389        IN_QUOTED_TOKEN,
390        VAR,
391        IN_VAR,
392        COMMAND_SEP,
393    } state;
394    char varname[128];
395    int varnamepos;
396
397    inpos = 0;
398    outpos = 0;
399    arg = 0;
400    varnamepos = 0;
401    state = INITIAL;
402    *continuebuffer = NULL;
403
404    for (;;) {
405        char c = inbuffer[inpos];
406
407//      dprintf(SPEW, "c 0x%hhx state %d arg %d inpos %d pos %d\n", c, state, arg, inpos, outpos);
408
409        switch (state) {
410            case INITIAL:
411            case NEXT_FIELD:
412                if (c == '\0')
413                    goto done;
414                if (isspace(c))
415                    state = SPACE;
416                else if (c == ';')
417                    state = COMMAND_SEP;
418                else
419                    state = TOKEN;
420                break;
421            case SPACE:
422                state = IN_SPACE;
423                break;
424            case IN_SPACE:
425                if (c == '\0')
426                    goto done;
427                if (c == ';') {
428                    state = COMMAND_SEP;
429                } else if (!isspace(c)) {
430                    state = TOKEN;
431                } else {
432                    inpos++; // consume the space
433                }
434                break;
435            case TOKEN:
436                // start of a token
437                DEBUG_ASSERT(c != '\0');
438                if (c == '"') {
439                    // start of a quoted token
440                    state = QUOTED_TOKEN;
441                } else if (c == '$') {
442                    // start of a variable
443                    state = VAR;
444                } else {
445                    // regular, unquoted token
446                    state = IN_TOKEN;
447                    args[arg].str = &buffer[outpos];
448                }
449                break;
450            case IN_TOKEN:
451                if (c == '\0') {
452                    arg++;
453                    goto done;
454                }
455                if (isspace(c) || c == ';') {

내부 동작을 더 깊게 읽기

01

진입 조건을 먼저 고정한다

UART input에서 들어온 실행은 부트 CPU의 동기 실행 문맥에 놓여 있다. 이때 interrupt, MMU/cache, stack, heap 중 무엇이 이미 준비되었는지 소스의 호출자까지 올라가 확인한다. 같은 함수라도 SPL, relocation 전후, app thread처럼 호출 문맥이 달라지면 허용되는 API와 지연 시간이 달라진다.

고정 길이 line buffer, quoting, command 재진입과 lock을 본다. 디버그 shell이라도 입력 길이와 handler의 blocking 특성을 무시하면 부트 경로를 멈출 수 있다.

02

중심 객체의 생성과 공개를 나눈다

이 글의 중심 객체는 console_t / console_cmd / argv buffer다. 메모리를 확보한 시점, 필드를 채운 시점, 전역 list나 다른 subsystem에 공개한 시점을 구분한다. 공개 뒤 오류가 발생한다면 목록에서 제거하고 child, buffer, reference를 역순으로 정리하는지 확인한다.

빌드 산출물 관점에서는 최종 부트로더 이미지 안에 해당 symbol과 section이 실제로 포함되었는지도 map과 objdump로 검증한다.

03

주소, 크기와 정렬을 계산한다

부트 코드에서는 주소 계산 오류를 주의해야 한다. 원본과 목적지의 주소 범위, 헤더에 기록된 데이터 크기, 블록·페이지 단위를 표로 정리하고 각 구간의 끝 주소를 계산한다. 끝 주소가 start + size - 1처럼 마지막 바이트를 가리키는지, 구간 바로 다음 주소를 가리키는지는 API 규약에서 확인한다.

04

성공 flag와 실제 완료 시점을 맞춘다

line reader → tokenize → command lookup 구간에서는 부분 초기화 상태가 생길 수 있다. flag, list insertion, callback 등록, storage write 완료 중 무엇이 성공의 기준인지 찾는다. hardware write나 DMA가 포함되면 함수 반환과 장치 완료가 같은 시점인지도 확인한다.

다른 CPU, 인터럽트 처리기, USB 완료 콜백, 셸 명령에서도 이 상태를 읽는다면 메모리 접근 순서와 잠금 범위를 함께 확인한다.

05

제어권을 넘기기 전에 확인할 조건을 정리한다

정상 경로는 handler return에서 끝난다. 이 단계가 함수 반환인지, scheduler 전환인지, 다른 image로의 비복귀 분기인지 구분한다. 비복귀 handoff라면 cache clean/invalidate, interrupt disable, 장치 quiesce, argument register와 FDT 또는 image address가 최종 점검 항목이다.

호출한 함수로 돌아오는 경로라면 오류와 일부 처리만 완료된 상태를 구분하는지 확인한다. 이어서 다음 후보를 시도할지, 복구 절차를 실행할지 살펴본다.

구현을 읽을 때 놓치기 쉬운 부분

01

command table도 linker가 수집하는 정적 registry다

command macro가 name, help와 callback을 section에 놓고 parser가 이름으로 검색한다. 같은 이름 중복, section garbage collection과 module 미포함을 map과 help 출력으로 구분한다.

callback pointer와 descriptor 수명은 image 전체이지만 argv buffer는 한 command 실행 동안만 유효하다.

02

tokenizer의 quote와 길이 규칙이 입력 경계다

공백 분리, quote, escape와 최대 argument 수를 확인한다. 긴 line이 truncate된 뒤 유효한 다른 command로 해석되지 않도록 오류 처리를 본다.

숫자 변환은 base, signedness와 overflow를 검사하고 address command는 접근 가능한 range를 제한한다.

03

command callback의 실행 문맥을 고정한다

console thread에서 실행되는 blocking command는 다른 shell 입력을 막고 lock을 잡은 채 device completion을 기다릴 수 있다. interrupt나 timer callback에서 command를 직접 호출하지 않는다.

ctrl-c 취소, timeout과 부분 hardware 변경의 rollback을 command별로 시험한다.

객체와 수명

대상만들어지는 시점유효 범위확인할 조건
console_t / console_cmd / argv bufferline readerhandler return 또는 오류 정리 완료까지진입 전제와 상태 계약
입력 buffer / metadataUART inputparse와 검증이 끝날 때까지길이, 정렬, 소유권, 변조 가능성
등록된 list / descriptortokenize하위 시스템을 종료하거나 다음 이미지에 제어권을 넘길 때까지중복 등록, 해제된 객체를 가리키는 포인터, 오류 후 자원 정리
hardware 또는 persistent state실제 write/probe가 완료된 뒤reset 또는 명시적 해제·갱신까지쓰기 완료, 시간 초과, 전원 차단, 이전 상태 복원
최종 부트로더 이미지link/image 생성 시점다음 stage가 새 image로 교체할 때까지적재 주소, 진입점, 섹션, 헤더의 일치 여부

실패 지점과 증상

#조건관찰되는 증상먼저 볼 단계
01긴 입력buffer truncation/overflowline reader
02console lock 재진입deadlocktokenize
03알 수 없는 command 처리 오류stale argv 사용command lookup

로그가 끊긴 함수뿐 아니라 그 함수에 주소, 크기, 플래그를 전달한 앞 단계도 조사한다. 오류 처리 후 등록된 객체나 하드웨어 설정이 그대로 남아 있는지도 확인한다.

소스 밖에서 확인할 증거

소스에서 파악한 동작을 실제 빌드 결과와 대상 보드의 로그로 확인한다. 아래 명령의 보드 이름과 툴체인 접두사는 사용 중인 빌드 환경에 맞게 바꾼다.

#목적명령 또는 계측판정 기준
01빌드 구성make <project> -j1platform, target, app 선택과 linker 입력을 로그로 남긴다.
02symbol과 주소nm -n build-*/lk.elf | grep 'static int console_run_script_etc'init hook, app descriptor와 함수가 어느 주소에 놓였는지 확인한다.
03어셈블리 대조objdump -drS build-*/lk.elfthread 전환과 interrupt disable 구간의 실제 명령을 확인한다.
04초기화 순서init level 전후에 current_time_hires()와 함수 이름 기록level 내부 순서와 지연을 serial log로 남긴다.
05문맥 확인current_thread, interrupt state, stack pointer 기록함수가 bootstrap thread인지 scheduler 이후 thread인지 구분한다.

직접 확인할 실험

  1. 01
    최대 길이 직전/초과 입력 비교

    line reader 진입 전후에 console buffer의 주소·크기·반환값과 timestamp를 함께 남긴다.

  2. 02
    따옴표와 공백 token case 작성

    tokenize 진입 전후에 argv tokens의 주소·크기·반환값과 timestamp를 함께 남긴다.

  3. 03
    command 안에서 script 실행 시 locked API 확인

    command lookup 진입 전후에 cmd table의 주소·크기·반환값과 timestamp를 함께 남긴다.

원문과 다음 글