21.3.1.2.1. NSH에서 입력된 문자열을 `argc`, `argv` 배열로 토크나이징(Tokenizing)하는 NuttX 내부 로직

21.3.1.2.1. NSH에서 입력된 문자열을 argc, argv 배열로 토크나이징(Tokenizing)하는 NuttX 내부 로직

우리가 QGroundControl이나 터미널 창에서 무심코 Enter 키를 누를 때, 픽스호크 내부의 RAM 공간에서는 긴 일직선의 문자열(String)이 여러 조각의 단어(Token) 포인터 배열로 쪼개지는 피 튀기는 해체 작업이 일어난다. 이 단원에서는 그 토크나이징(Tokenizing)의 현장을 NuttX 커널 소스 코드 단위의 시각으로 들여다본다.

1. NSH 파서(Parser)의 원시적인 문자열 자르기

사용자가 콘솔에 custom_app start -i 5 라고 치면, UART 시리얼 드라이버가 이 글자들을 임시 텍스트 버퍼 버퍼(Buffer)에 고스란히 저장한다.

메모리 상태 (버퍼): ['c', 'u', 's', 't', 'o', 'm', '_', 'a', 'p', 'p', ' ', 's', 't', 'a', 'r', 't', ' ', '-', 'i', ' ', '5', '\n']

이 긴 아날로그 감성의 텍스트는 C++ 메인 함수가 소화할 수 있는 안전한 포인터 배열 구조체인 argv[]로 변환되어야 한다. 이 과정은 리눅스 커널의 셸 파싱과 거의 동일한 메커니즘을 따른다.

1.1 단계: 스페이스바(공백) 추적과 널 문자의 주입

NSH 셸의 내부 파서(Parser)는 배열의 0번 인덱스부터 스르륵 읽어나가기 시작한다. 파서가 가장 눈을 불켜고 찾는 것은 단어와 단어 사이를 가르는 기호, 바로 ’공백 문자(Space, )’이다.

  1. 파서는 custom_app 문자열을 통과하다가 첫 번째 공백 문자를 발견한다.
  2. 널 바이트 덮어쓰기: 파서는 자비 없이 이 공백 문자를 **문자열의 끝을 알리는 널 종료 문자(\0, Null Byte)**로 덮어써 버린다.
    기존: ...app start... \rightarrow 치환 후: ...app\0start...
  3. 파서는 이 과정을 문자열 끝(개행 문자 \n)을 만날 때까지 끝없이 반복하며, 모든 스페이스바를 널 문자로 강제 치환해 버린다.

1.2 단계: argv 포인터 배열의 할당

이제 버퍼는 하나의 긴 문장이 아니라, 널 문자로 인해 칸칸이 단절된 4개의 짧은 문자열 덩어리들이 메모리에 일렬로 늘어선 기차 형태가 되었다.
이제 포인터 배열인 argv 껍데기를 만들어 각 객실(단어)의 주소를 연결해 줄 차례다.

  1. argv[0]에는 가장 첫 글자인 c의 메모리 주소가 담긴다.
  2. 첫 번째 널 문자 바로 다음 칸인 s의 주소를 따서 argv[1]에 집어넣는다.
  3. 두 번째 널 문자 다음인 -의 주소를 argv[2]에 넣는다.
  4. 세 번째 널 문자 다음인 5의 주소를 argv[3]에 넣는다.
  5. 더 이상 남은 글자가 없으므로(개행 혹은 원래의 널 문자 도달), 배열의 끝을 알리기 위해 argv[4]에는 절대적인 안전망인 NULL (0x00000000)을 넣고 토크나이징을 종료한다.

1.3 단계: argc 산출 및 스레드 던지기(Spawn)

포인터 배열 조립이 끝나면 굳이 세어보지 않아도 단어의 개수는 자명해진다. NULL 이전까지 배열 방이 4개 들어찼으므로, argc의 값은 4로 픽스된다.

이제 NuttX 커널은 대망의 스케줄러 API인 task_create()(또는 task_spawn()) 함수를 호출하면서 찾아둔 custom_app_main의 주소와 방금 예쁘게 포장한 파라미터 박스(argc=4, argv 포인터 배열)를 함께 던져버린다.

마침내, 이 포장 상자가 스레드의 껍데기 문법인 int custom_app_main(int argc, char *argv[]) 내부로 툭 떨어지는 것이다.

이제 여러분의 C++ 코드는 ఈ argcargv 텍스트 덩어리들을 열어보고 “사용자가 나한테 구체적으로 무슨 일을 하라고 시켰는가?“를 알아내야 한다. 그 텍스트 파싱을 무식한 if-else문 노가다가 아니라, 우아하고 범용적인 POSIX 규격으로 처리해 주는 getopt() 라이브러리의 활용법을 다음 단원(21.3.2)에서 본격적으로 배워본다.