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

배재준·2025년 5월 21일

크래프톤 정글 - TIL

목록 보기
65/93
post-thumbnail

2025.05.21

TIL(TODAY I LEARN)


  • 오늘한 내용 : PintOS - Project2: UserProgram - User Memory, System calls
    - 기본 System calls 구현 (halt, exit, wirte (fd=1), wait 기본형태)
    사용자 메모리 접근 보호

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


System calls

  • 사용자 프로그램에서 main() 함수 내에서 시스템 콜을 호출했을 때, 그 호출이 커널에서 어떻게 처리되는지를 담당하는 부분.
  • 사용자 코드에서 시스템 콜을 호출하면 인터럽트를 통해 커널 모드로 전환되고, 등록된 syscall_handler()가 호출된다. 이 핸들러는 시스템 콜 번호를 읽고, 그에 해당하는 커널 함수를 호출하여 작업을 수행한다.
    • 인터럽트(시스템 콜)가 발생하면 CPU는 자동으로 커널 모드로 전환된다.
  • 이 과정에서 사용자 메모리를 접근할 경우 유효성을 검사해야 하며, 시스템 안정성을 위해 보호 로직이 반드시 필요하다.
  • 우선 접근 보호 함수핸들러 구조를 구현해 이 모든 흐름의 기반을 마련해야 함.

왜 사용자 메모리 접근 보호가 필요한가?

1. 사용자는 잘못된 포인터를 줄 수 있다

  • 사용자 프로그램이 시스템 콜을 호출하면서 NULL, 커널 영역 주소, 혹은 매핑되지 않은 가상 주소를 넘길 수 있다.
  • 예: write(1, NULL, 100) 같이 잘못된 buffer를 넘기면, 커널이 그 포인터를 그대로 접근하면 page fault가 발생하고 운영체제 전체가 죽을 수 있다.

2. 커널은 신뢰할 수 없는 사용자 입력을 절대 믿으면 안 된다

  • 보안적인 측면에서 커널은 항상 악의적이거나 버그가 있는 사용자 입력에 방어적이어야 한다.
  • 사용자 모드에서는 커널 메모리에 직접 접근할 수 없지만, 커널 모드에서는 사용자 메모리에 접근 가능하기 때문에 오히려 더 조심해야 한다.

3. 운영체제의 안정성과 보안을 위해 필수적인 보호막이다

  • 시스템 콜 인터페이스는 사용자와 커널의 경계선이다.
  • 이 인터페이스에서 사용자 주소를 엄격히 검증하지 않으면 커널 침해(Exploit), 시스템 다운, 예기치 않은 동작이 발생할 수 있다.

User Memory 접근 보호 구현

유저가 준 포인터가 (1) 유저 공간에 속하고, (2) 실제 물리 페이지가 준비되어 있는” 경우에만 메모리 접근을 허용하고, 그렇지 않으면 exit(-1) 등으로 프로세스를 강제 종료

(1)void check_user_address(const void *uaddr);
(2)void check_user_buffer(char *buffer, size_t size);

check_user_address(const void *uaddr)

  • 널 포인터 차단: !uaddrNULL 이면 즉시 프로세스 종료
  • 유저 영역 검사: !is_user_vaddr(uaddr) ⇒ 주소가 PHYS_BASE 이상(커널 영역)에 있으면 종료
  • 매핑 여부 검사: pml4_get_page(..., uaddr) == NULL ⇒가상→물리 매핑이 안 돼 있으면 종료

check_user_buffer(char *buffer, size_t size)

  • 버퍼 전체를 페이지 단위로 검사
    1. ptr 을 바이트 단위 주소로 보고
    2. ofs 만큼 오프셋을 늘려 가며
      • check_user_address(addr) 호출
      • 한 페이지(PGSIZE) 남은 크기(left)만큼 ofs += left
  • 이렇게 하면 버퍼가 페이지 경계를 넘어 걸쳐 있어도, 각 페이지 첫 주소만 검사해도 그 페이지 전체가 안전한지 보장 가능

핸들러 구조 구현


	void syscall_handler(struct intr_frame *f UNUSED){
		int syscall_num = (int)f->R.rax;
	
		switch (syscall_num)
		{
		case SYS_EXIT:
			break;
		case SYS_WRITE:
			break;
		case ....:
			break;
		default:
			break;
		}
	}

System Call 구현

"movq    %1, %%rax   \n" // 시스템콜 번호
"movq    %2, %%rdi   \n" // 첫 번째 인자
"movq    %3, %%rsi   \n" // 두 번째 인자
"movq    %4, %%rdx   \n" // 세 번째 인자
"syscall             \n"
"movq    %%rax, %0   \n" // 반환값
  • void exit(int status); 호출 시
    • 프로세스 종료를 처리하는 기본적인 콜
    • exit(status) 호출 → 레지스터에 SYS_EXITstatus를 담고 syscall 실행
    • sys_exit() 구현 필요 → thread_exit() 수행
  • write(1,buffer,len) 호출 시
    • 파일 디스크립터 fd에 버퍼 buffer의 내용을 size만큼 출력한다.
    • write(fd, buf, size) 호출 → SYS_WRITE, fd, buf, size를 담고 syscall 실행
    • sys_write() 구현 필요 → fd == 1 일때 putbuf(), 나머지 추후 구현
  • int wait(tid_t tid); 호출 시
    • 프로세스 ID tid의 자식 프로세스가 종료될 때까지 대기(block)하고, 그 종료 상태(exit status)를 반환한다.
    • wait(tid) 호출 → SYS_WAIT, tid를 담고 syscall 실행
    • process_wait() 수정 필요
  • void halt(void); 호출 시
    • 시스템을 즉시 종료(power off)하는 콜
    • halt() 호출 → 레지스터에 SYS_HALT를 담고 syscall 실행
    • sys_halt() 구현 필요 → power_off() 호출

0개의 댓글