
2025.05.20
오늘한 내용 : PintOS - Project2: UserProgram - Argument Passing
WEEK 10 : 정글 끝까지(PintOS) - UserProgram
프로세스가 만들어져서 사용자 코드가 실행되고, 다시 종료될 때까지의 주요 흐름을 단계별로 정리하면 다음과 같다. 약간 추상화한 그림이지만, Project 2 범위—즉 “유저 모드 프로세스 생성·실행·시스템콜·종료”—를 모두 아우른다.
pintos run foo arg1 arg2 등을 실행process_execute("foo arg1 arg2") 호출thread_create("foo", PRI_DEFAULT, initd, fn_copy)로 새 커널 스레드(유저 프로세스) 생성initd)initd(fn_copy)로 시작process_init() (필요한 초기화)process_exec(fn_copy) 호출 → 실제 ELF 바이너리 로드 및 실행 준비process_exec)process_cleanup()로 이전 주소 공간·자원 해제load(file_name, &_if) 호출_if.rip ← 프로그램 진입점, _if.rsp ← 초기 스택 포인터 설정argc, argv[] 구성_if.rsp 기준으로 문자열·포인터 배열·가짜 리턴 주소 푸시_if.R.rdi = argc, _if.R.rsi = argv 설정palloc_free_page(file_name)로 임시 버퍼 해제do_iret(&_if) → 커널에서 유저 모드로 점프_if.rip(_start)로 이동_start(argc, argv)가 main(argc, argv) 호출main() 내부에서 read(), write(), open(), fork(), exec(), wait() 등 호출syscall 명령을 통해 커널 syscall_handler()로 진입f->R.rax에서 콜 번호, f->R.rdi~에서 인자 획득f->R.rax에 저장iret으로 유저 모드 복귀timer_interrupt() → thread_tick() → schedule()process_activate()로 페이지 테이블 변경do_iret()로 다시 유저 모드 재개exit)exit(status) 호출 → sys_exit(status) 실행printf("%s: exit(%d)\n", name, status)로 종료 메시지 출력thread_exit() 호출로 스레드 완전 종료wait)wait(child_tid) 호출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) | 핸들러가 복원해 둔 레지스터 상태를 꺼내 다시 유저 모드로 복귀 | — |
프로세스 실행 흐름
process_execute("foo arg1 arg2") 호출 (커널 모드)process_exec() 내부load()로 ELF 로드 → _if.rip, _if.rsp 초기화file_name을 strtok_r로 토큰화 → argc, argv[] 생성 → strtok_r 사용_if.rsp부터argv 포인터 배열 + NULL 푸시_if.R.rdi = argc, _if.R.rsi = &argv[0], _if.rsp = 새로운 espdo_iret(&_if) 호출 → CPU 레지스터 복원 → 유저 모드 _start(argc, argv)main) 실행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"
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'
───────────────────────────────────esp = (char *)((uintptr_t)esp & ~(uintptr_t)0x7);esp &= ~0x7; 의미esp) → 0x7FFFFF0argv 포인터 배열 + 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을 넣어 두는 것.esp -= sizeof(void *)(void **)esp = NULLvoid * 타입(=포인터)로 보고, NULL 값을 쓴다.RDI 레지스터_if.R.rdi = argc;RSI 레지스터_if.R.rsi = (uintptr_t)(esp + sizeof(void *));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 세팅)