PROJECT-2 (User Programs)

박지성 ·2025년 5월 17일
post-thumbnail

사용자 프로그램을 커널 위에서 실행 가능하게 만드는 것이 목적

기본 개념

사용자 프로그램(User Programs)

사용자 프로그램은 커널이 아닌 비특권 모드에서 실행되는 일반적인 프로그램. Linux로 치면 우리가 작성해서 gcc로 컴파일하고 실행하는 C 프로그램들.

Pintos에서는 이런 사용자 프로그램들을 실행시킬 수 있도록 운영체제를 확장해나가는 작업을 진행

프로세스(Process)와 주소 공간(Address Space)

  • 유저 프로그램은 프로세스로 실행
  • 각 프로세스는 독립된 가상 주소 공간을 가지고 있다
  • 이 공간에는 코드, 데이터, 스택 등이 들어간다

시스템 콜(System Call)

  • 사용자 프로그램은 직접 커널을 호출할 수 없음.
  • 대신 시스템 콜 인터페이스를 통해 커널 기능을 요청
  • 예: exit()write()read() 등의 기본 함수들은 시스템 콜

인터럽트와 트랩(Trap)

  • 시스템 콜은 소프트웨어 인터럽트(예: int 0x30)로 커널에 제어를 넘긴다.
  • 이는 threads/interrupt.c의 인터럽트 핸들러에서 처리된다

유저 메모리 접근 검증

  • 커널은 신뢰할 수 없는 유저 프로그램이 유효한 주소만 접근하도록 해야 한다
  • 사용자 프로그램이 넘긴 포인터가 커널 메모리나 NULL을 가리키면 안된다

스택(Stack)과 인자 전달

  • main(int argc, char **argv) 형식으로 인자를 전달하기 위해, 커널은 프로세스의 스택을 직접 구성해야 한다..
  • 문자열 복사, 정렬, 정렬된 포인터 push 등을 직접 구현해야 한다.

ELF 실행 파일 포맷

  • Pintos의 유저 프로그램은 ELF(Executable and Linkable Format)으로 컴파일된다.
  • 이 바이너리 포맷을 파싱해서 코드 섹션을 메모리에 로딩하고 시작 지점으로 점프합니다

파일 디스크립터(File Descriptor)

  • open()read()write() 등에서 사용하는 정수형 핸들.
  • 프로세스마다 독립된 파일 디스크립터 테이블이 있어야 한다.

프로세스 종료와 부모-자식 관계

  • process_wait() 구현을 위해, 부모가 자식의 종료 상태를 기다릴 수 있어야 한다
  • 각 스레드/프로세스는 자신의 자식 목록을 추적해야 한다.

테스트 케이스

Argument Passing

테스트 이름설명
args-none인자가 없는 실행 확인
args-single인자 1개 전달 확인
args-multiple여러 개 인자 전달 확인
args-dbl-space중복 공백이 있을 때 인자 처리 확인
args-many많은 수의 인자 처리 테스트

System Call: Process Control

테스트 이름설명
halthalt() 호출 시 시스템 종료 확인
exitexit(status)가 정상 작동하고 부모가 상태 수신하는지 확인
wait-simple기본적인 wait() 동작 확인
wait-twice동일 자식에 대해 두 번 wait 시 -1 반환
wait-killed자식이 예외로 종료된 경우 wait() 결과 -1 확인

System Call: exec & load

테스트 이름설명
exec-onceexec()로 한 번 실행 후 종료 여부 확인
exec-argexec() 호출로 인자 전달 여부 확인
exec-missing존재하지 않는 파일을 exec() 시도 시 실패 처리
exec-bad-addr잘못된 포인터를 exec()에 넘길 경우 예외 처리 확인
exec-read실행 중 프로그램이 자신의 파일 읽기 시도 가능 여부

File I/O

테스트 이름설명
create파일 생성 시스템 콜 create() 테스트
open파일 열기 테스트 (open())
close파일 디스크립터 닫기 (close())
read-normal정상 read() 호출 후 내용 읽기 확인
read-bad-ptr잘못된 포인터로 읽기 요청 시 예외 발생
read-stdinread(0, ...)을 통한 입력 읽기 처리
write-normalwrite()로 파일 또는 stdout 출력 확인
write-bad-ptr잘못된 포인터로 출력 요청 시 예외 처리
write-stdoutwrite(1, ...)으로 콘솔 출력 확인
filesizefilesize(fd)로 파일 크기 확인
seektell파일 오프셋 조작 및 현재 위치 반환 확인

System Call: Robustness / Fault

테스트 이름설명
sc-boundary포인터가 페이지 경계에 걸칠 때 처리
sc-bad-addr주소가 물리 메모리 외일 때 예외 발생 확인
sc-nullNULL 포인터 접근 처리 확인
sc-bad-sp잘못된 스택 포인터로 시스템 콜 접근 시 처리
rox-simplerox-childrox-multichild실행 중인 파일에 write() 시도 시 거부 여부 확인

Memory Pressure / Stress Test

테스트 이름설명
multi-recurseexit-recurse깊은 재귀 호출과 정상 종료 여부 확인
multi-oom많은 프로세스 생성 시 메모리 부족 대응 테스트

폴더 구조 및 설명

파일명설명
process.c / process.h프로세스 생성과 로딩, 종료 처리를 담당합니다. ELF 실행 파일을 로드하고 초기 스택을 설정하는 코드가 들어 있습니다. 인자 전달도 이 파일에서 구현합니다.
syscall.c / syscall.h시스템 콜 인터페이스 처리: 유저 프로그램이 커널 기능을 요청할 때 인터럽트를 받아 분기 처리하는 메인 로직입니다. syscall_handler()가 핵심입니다.
exception.c / exception.h예외 처리: 유저 프로그램이 잘못된 메모리에 접근했을 때 발생하는 page fault 등을 처리합니다. page_fault() 수정 가능성이 있습니다.
pagedir.c / pagedir.h페이지 테이블 관리 유틸리티: 유저 메모리 접근 시 페이지가 유효한지 확인할 때 사용합니다. 직접 수정은 드물지만 함수 호출은 자주 합니다.
gdt.c / gdt.hGlobal Descriptor Table 설정: 세그먼트 기반 보호 모드 설정에 필요한 코드입니다. 수정할 필요는 거의 없습니다.
tss.c / tss.hTask-State Segment 설정: 인터럽트 진입 시 유저에서 커널 스택으로 전환하기 위한 구조입니다. 마찬가지로 읽기만 하면 되고, 수정하지 않아도 됩니다.

진행 순서

  1. 시스템 콜 인터페이스 열기
  2. 프로세스 생성 (process_exec)
  3. 인자 전달 처리
  4. 시스템 콜 핸들러 구현 (syscall.c)
  5. 프로세스 종료 메시지
  6. 파일 접근 제한 (deny_write)
  7. (선택) stdin/stdout 닫기 및 dup2() 지원

시작

1. 시작 전 준비

  • 코드 베이스 확인userprog 디렉토리에서 작업을 시작합니다.
  • 테스트 환경 구축make를 통해 빌드하고, 테스트를 실행할 수 있는지 확인합니다.

2. 시스템 콜 핸들러 구현

  • 핵심 파일userprog/syscall.c
  • 할 일:
    • 시스템 콜 번호를 읽고 해당 핸들러로 분기하는 로직 구현
    • 사용자 메모리 접근 시 유효성 검사 추가
  • 관련 테스트 케이스:
    • syscall-read-bad-ptr
    • syscall-write-bad-ptr

3. 기본 시스템 콜 구현

  • 핵심 파일userprog/syscall.c
  • 할 일:
    • halt: Pintos 종료
    • exit: 현재 프로세스 종료 및 상태 반환
    • write: 콘솔 출력 구현
  • 관련 테스트 케이스:
    • halt
    • exit
    • write-normal
    • write-bad-ptr

4. 인자 전달 (Argument Passing) 구현

  • 핵심 파일userprog/process.c
  • 할 일:
    • process_execute에서 프로그램 이름과 인자를 분리
    • setup_stack에서 스택에 인자들을 올바르게 배치
  • 관련 테스트 케이스:
    • args-single
    • args-multiple
    • args-many

5. 프로세스 종료 및 대기 구현

  • 핵심 파일userprog/process.c
  • 할 일:
    • process_wait: 자식 프로세스의 종료를 대기하고 상태 반환
    • process_exit: 프로세스 종료 시 자원 정리
  • 관련 테스트 케이스:
    • wait-simple
    • wait-twice
    • wait-killed

6. 파일 시스템 관련 시스템 콜 구현

  • 핵심 파일userprog/syscall.cfilesys/file.c
  • 할 일:
    • createremoveopenfilesizereadwriteseektellclose 등의 시스템 콜 구현
    • 파일 디스크립터 테이블 관리
  • 관련 테스트 케이스:
    • open-missing
    • close-twice
    • read-normal
    • write-normal

7. 파일 디스크립터 관리

  • 핵심 파일userprog/syscall.c
  • 할 일:
    • 각 프로세스마다 파일 디스크립터 테이블 유지
    • 파일 디스크립터와 실제 파일 구조체 간의 매핑 관리
  • 관련 테스트 케이스:
    • open-twice
    • close-stdin
    • close-stdout

8. 동기화 및 예외 처리

  • 핵심 파일userprog/syscall.cthreads/synch.c
  • 할 일:
    • 파일 시스템 접근 시 동기화 처리 (락 사용)
    • 잘못된 포인터 접근 시 예외 처리
  • 관련 테스트 케이스:
    • multi-oom
    • read-bad-ptr
    • write-bad-ptr

테스트 실행 방법

  • 전체 테스트 실행:
    make check
  • 특정 테스트 실행:
    pintos -q run 'test-name'
    예를 들어, args-single 테스트를 실행하려면:
    pintos -q run 'args-single'

추가 (공식 문서 순서)

  1. Argument Passing (인자 전달)
    • 모든 사용자 프로그램은 실행 초기에 argc, argv를 읽기 때문에 스택에 올바르게 인자들을 설정하지 않으면 페이지 폴트(page fault) 가 발생하게 됩니다.
    • 간단한 테스트를 위해 *esp = PHYS_BASE - 12;로 변경하여 시작할 수도 있어요.

  2. User Memory Access (사용자 메모리 접근)
    • 사용자 스택에서 시스템 콜 번호 및 인자를 읽으려면, 사용자 메모리에 접근할 수 있어야 합니다.
    • get_user()와 put_user() 함수로 안전하게 접근하도록 구현하세요.

  3. System Call Infrastructure 구축
    • 시스템 콜 번호를 읽고, 해당하는 핸들러로 디스패치하는 기본 골격을 syscall.c에 구축합니다.
    • 이 구조를 먼저 만들어 놓으면 이후 syscall 들을 하나씩 붙이기 쉬워요.

  4. exit 시스템 콜
    • 모든 프로그램은 종료 시 exit()을 호출하므로, 가장 먼저 구현되어야 할 syscall 중 하나입니다.

  5. write 시스템 콜
    • 대부분의 테스트 프로그램은 콘솔 출력이 필요하기 때문에, write() (특히 fd 1, 즉 콘솔)을 지원해야 정상 작동합니다.

  6. process_wait() 무한 루프화
    • 초기에는 process_wait()를 무한 루프로 구현하여, 부모 프로세스가 자식 프로세스를 기다리는 상황을 단순화하세요.

profile
개발 블로그 맞음.

2개의 댓글

comment-user-thumbnail
2025년 5월 19일

항상 잘 보고있습니다. 감사합니다 지성님.

1개의 답글