[TIL/크래프톤 정글] DAY 72

배재준·2025년 5월 20일

크래프톤 정글 - TIL

목록 보기
64/93
post-thumbnail

2025.05.20

TIL(TODAY I LEARN)


  • 오늘한 내용 : PintOS - Project2: UserProgram - Argument Passing

  • WEEK 10 : 정글 끝까지(PintOS) - UserProgram


전체 작동 흐름

프로세스가 만들어져서 사용자 코드가 실행되고, 다시 종료될 때까지의 주요 흐름을 단계별로 정리하면 다음과 같다. 약간 추상화한 그림이지만, Project 2 범위—즉 “유저 모드 프로세스 생성·실행·시스템콜·종료”—를 모두 아우른다.


전체 프로세스 실행 흐름

  1. 프로세스 생성 요청
    • 터미널 또는 Pintos 스크립트가 pintos run foo arg1 arg2 등을 실행
    • 내부적으로 process_execute("foo arg1 arg2") 호출
    • thread_create("foo", PRI_DEFAULT, initd, fn_copy)로 새 커널 스레드(유저 프로세스) 생성
  2. 초기 사용자 프로세스 진입 (initd)
    • 새 스레드가 initd(fn_copy)로 시작
    • process_init() (필요한 초기화)
    • process_exec(fn_copy) 호출 → 실제 ELF 바이너리 로드 및 실행 준비
  3. ELF 로드 및 스택 준비 (process_exec)
    1. process_cleanup()로 이전 주소 공간·자원 해제
    2. load(file_name, &_if) 호출
      • ELF 헤더 검사 → 세그먼트별로 페이지 단위 로드
      • _if.rip ← 프로그램 진입점, _if.rsp ← 초기 스택 포인터 설정
    3. 인자 전달
      • 커맨드라인 문자열 토큰화 → argc, argv[] 구성
      • _if.rsp 기준으로 문자열·포인터 배열·가짜 리턴 주소 푸시
      • _if.R.rdi = argc, _if.R.rsi = argv 설정
    4. palloc_free_page(file_name)로 임시 버퍼 해제
    5. do_iret(&_if) → 커널에서 유저 모드로 점프
  4. 유저 모드 첫 진입
    • CPU가 _if.rip(_start)로 이동
    • _start(argc, argv)main(argc, argv) 호출
  5. 유저 코드 실행 & 시스템 콜
    • main() 내부에서 read(), write(), open(), fork(), exec(), wait() 등 호출
    • 각 호출은 syscall 명령을 통해 커널 syscall_handler()로 진입
      1. f->R.rax에서 콜 번호, f->R.rdi~에서 인자 획득
      2. 해당 시스템 콜 구현 함수 실행 → 결과를 f->R.rax에 저장
      3. iret으로 유저 모드 복귀
  6. 스케줄링 & 컨텍스트 스위치
    • 타이머 인터럽트 → timer_interrupt()thread_tick()schedule()
    • 다른 스레드 선택 후 process_activate()로 페이지 테이블 변경
    • do_iret()로 다시 유저 모드 재개
  7. 프로세스 종료 (exit)
    • 유저 코드에서 exit(status) 호출 → sys_exit(status) 실행
      1. printf("%s: exit(%d)\n", name, status)로 종료 메시지 출력
      2. 열린 파일 닫기, 페이지 테이블 해제 등 자원 정리
      3. thread_exit() 호출로 스레드 완전 종료
  8. 부모의 대기 (wait)
    • 부모가 wait(child_tid) 호출
      1. 자식이 살아 있으면 대기
      2. 자식 종료 시 저장된 종료 상태값 반환
      3. 같은 자식에 대해 중복 대기는 실패
  9. 시스템 완전 종료
    • 초기 프로세스(initd)가 exit()될 때까지 반복 관리
    • 마지막으로 halt() 시스템 콜로 머신 전원 종료

이 흐름대로 “프로세스 생성 → ELF 로드 & 스택 세팅 → 유저 모드 진입 → 시스템 콜 & 인터럽트 → 종료 → 부모 대기 → 전체 종료”가 차례로 이루어진다.


추가)

  • 레지스터 프레임(_if) 역할
    • struct intr_frame _if“모드 전환(유저 ↔ 커널) 시 CPU가 가져야 할 레지스터 상태”를 모두 담는 상자다.

    • 주요 필드:
      - rip → 복귀 또는 진입할 명령 주소
      - rsp → 스택 포인터
      - R.rdi, R.rsi → 함수 인자용 레지스터
      - cs, ss, ds, es → 코드·스택·데이터 세그먼트 셀렉터
      - eflags → 인터럽트 플래그 등

      do_iret(&_if) 또는 인터럽트 진입 코드가 이 구조체를 통해 레지스터를 복원하거나 저장한다.

  • 커널 ↔ 유저 모드 전환 흐름
    방향진입 명령어 / 함수내부 동작복귀 명령어 / 함수
    커널 → 유저do_iret(&_if)_if 안의 rip,rsp,cs,ss,eflags 등을 레지스터에 복원 후 유저 모드 전환
    유저 → 커널syscall / 인터럽트CPU가 자동으로 레지스터 상태를 스택에 저장 → syscall_handler() 등 진입iretq
    커널 → 유저iretq (또는 do_iret)핸들러가 복원해 둔 레지스터 상태를 꺼내 다시 유저 모드로 복귀

Argument Passing

프로세스 실행 흐름

  1. 사용자 요청
    • process_execute("foo arg1 arg2") 호출 (커널 모드)
  2. 커널 (Argument Passing)
    • process_exec() 내부
      • load()로 ELF 로드 → _if.rip, _if.rsp 초기화
      • 구현부 : Argument Passing:
        1. file_namestrtok_r로 토큰화 → argc, argv[] 생성 → strtok_r 사용
        2. _if.rsp부터
          1. 문자열 복사
          2. 8바이트 정렬
          3. argv 포인터 배열 + NULL 푸시
          4. 가짜 리턴 주소 푸시
        3. _if.R.rdi = argc, _if.R.rsi = &argv[0], _if.rsp = 새로운 esp
  3. 커널→유저 모드 전환
    • do_iret(&_if) 호출 → CPU 레지스터 복원 → 유저 모드 _start(argc, argv)
  4. 유저 모드 (main) 실행

구현부

1. filename 토큰화

strtok_r (char *s, const char *delim, char **save_ptr)
--------------------------------------------
ex) filename = "foo arg1 arg2"
argc = 3
argv[0] = "foo"
argv[1] = "arg1"
argv[2] = "arg2"

2. 스택에 넣기

  • 문자열 복사
    ex) argc = 3, argv = ["foo","arg1","arg2"]일 때,
    
    char *esp = (char *) _if.rsp;  // USER_STACK 예: 0x800000
    // 역순으로 문자열 복사
    for (i = 2; i >= 0; i--) {}
    복사 과정을 간략히 주소 오프셋으로 표현한 그림. (스택은 높은 주소에서 낮은 주소로 내려감)
    주소           내용
    ───────────────────────────────────
    0x800000       ← USER_STACK (초기 _if.rsp)
    ───────────────────────────────────
    // i = 2 ("arg2")
    0x7FFFFFF     '\0'
    0x7FFFFFE     '2'
    0x7FFFFFD     'g'
    0x7FFFFFC     'r'
    0x7FFFFFB     'a'
    ───────────────────────────────────
    // i = 1 ("arg1")
    0x7FFFFFA     '\0'
    0x7FFFFF9     '1'
    0x7FFFFF8     'g'
    0x7FFFFF7     'r'
    0x7FFFFF6     'a'
    ───────────────────────────────────
    // i = 0 ("foo")
    0x7FFFFF5     '\0'
    0x7FFFFF4     'o'
    0x7FFFFF3     'o'
    0x7FFFFF2     'f'
    ───────────────────────────────────
  • 워드(8바이트) 정렬
    • esp = (char *)((uintptr_t)esp & ~(uintptr_t)0x7);
      • esp &= ~0x7; 의미
    • 하위 3비트가 0이 되도록 비트 연산으로 내림
    • 8바이트 경계를 맞춰서 esp가 8의 배수를 가리키도록
    • ex) 정렬 후 스택 포인터(esp) → 0x7FFFFF0
    • 포인터 위치가 조정된 후 사이의 메모리는 비어있는 공간으로 남음
  • argv 포인터 배열 + NULL 푸시
    • 포인터 크기는 8바이트

      esp -= 8;              // esp = 0x7FFFFF0 - 8 = 0x7FFFFE8
      *(char **)esp = NULL;  // [0x7FFFFE8] = 0x0000000000000000
      
      -----------------------------------------------------------
      주소       값
      ─────────────────
      0x7FFFFE8   NULL (argv[3])
      
      for문 반복하면서
      주소       값
      ─────────────────
      0x7FFFFE8   NULL      (argv[3])
      0x7FFFFE0   0x7FFFFFB ("arg2"(argv[2]))
      0x7FFFFD8   0x7FFFFF6 ("arg1"(argv[1]))
      0x7FFFFD0   0x7FFFFF2 ("foo"(argv[0]))
      
  • 가짜 리턴 주소 푸시
    • 스택 프레임의 맨 위(최하위 주소)에 리턴 주소가 있어야 하는데, 우리는 실제로 돌아갈 곳이 없으므로 NULL을 넣어 두는 것.
      • main 함수가 돌아갈 주소 자리를 채워주는 것!
    • esp -= sizeof(void *)
      • 스택 포인터를 8바이트(포인터 크기)만큼 내린다.
    • (void **)esp = NULL
      • 그 위치를 void * 타입(=포인터)로 보고, NULL 값을 쓴다.

3. 레지스터 및 스택 포인터 업데이트

  • 준비한 스택과 레지스터를 _if 프레임에 반영하는 부분
  • 첫 번째 정수 인자는 RDI 레지스터
    • _if.R.rdi = argc;
    • argc
  • 두 번째 인자는 RSI 레지스터
    • _if.R.rsi = (uintptr_t)(esp + sizeof(void *));
    • argv배열의 시작 주소를 가리키는 포인터( argv[0] )
  • 스택 포인터(RSP) 자체를 새롭게 세팅한 esp 값으로 - _if.rsp = esp;
    • do_iret(&_if)가 실행될 때 CPU의RSP레지스터를우리가 구축한 스택 프레임의 최하단(esp)으로 설정

최종 스택 그림

주소           내용
───────────────────────────────────
0x800000       ← USER_STACK (초기 _if.rsp)
───────────────────────────────────
// i = 2 ("arg2")
0x7FFFFFF     '\0'
0x7FFFFFE     '2'
0x7FFFFFD     'g'
0x7FFFFFC     'r'
0x7FFFFFB     'a'
───────────────────────────────────
// i = 1 ("arg1")
0x7FFFFFA     '\0'
0x7FFFFF9     '1'
0x7FFFFF8     'g'
0x7FFFFF7     'r'
0x7FFFFF6     'a'
───────────────────────────────────
// i = 0 ("foo")
0x7FFFFF5     '\0'
0x7FFFFF4     'o'
0x7FFFFF3     'o'
0x7FFFFF2     'f'
───────────────────────────────────
// 8바이트 정렬
0x7FFFFF0     ← esp &= ~0x7
───────────────────────────────────
// NULL 종단자 푸시 (argv[3])
0x7FFFFE8     NULL
───────────────────────────────────
// argv 포인터 푸시
0x7FFFFE0     0x7FFFFFB  (argv[2] → "arg2")
0x7FFFFD8     0x7FFFFF6  (argv[1] → "arg1")
0x7FFFFD0     0x7FFFFF2  (argv[0] → "foo")
───────────────────────────────────
// 가짜 리턴 주소 푸시
0x7FFFFCC     NULL  (fake return)
───────────────────────────────────
// 최종 스택 포인터
0x7FFFFCC     ← _if.rsp (이후 rdi=3, rsi=0x7FFFFD0 세팅)

0개의 댓글