스택 확장
프로젝트 2에서는 스택이 USER_STACK에서 시작하는 단일 페이지였으며, 프로그램 실행은 이 크기로 제한되었습니다. 이제 스택이 현재 크기를 초과하면 필요한 만큼 추가 페이지를 할당합니다.
추가 페이지는 스택 접근으로 "보이는" 경우에만 할당하십시오. 스택 접근과 다른 접근을 구분하려는 휴리스틱을 고안하세요.
스택 접근과 다른 접근을 구분하려는 휴리스틱을 고안하지 않으면 어떻게 되지?
스택 접근과 다른 접근을 구분하려는 휴리스틱을 고안하지 않으면 다음과 같은 문제가 발생할 수 있습니다:
- 잘못된 메모리 할당: 스택 접근으로 착각하여 불필요한 메모리를 할당할 수 있습니다. 예를 들어, 프로그램이 의도치 않은 메모리 영역에 접근할 때 이를 스택 접근으로 오인하여 메모리 페이지를 계속 할당하게 되면, 불필요한 메모리 낭비가 발생할 수 있습니다.
- 보안 문제: 공격자가 고의로 특정 메모리 접근을 유발하여 스택 성장을 악용할 수 있습니다. 이렇게 되면 메모리 보호 메커니즘이 무력화될 수 있으며, 시스템의 안정성과 보안에 심각한 위협이 될 수 있습니다.
- 시스템 성능 저하: 불필요한 메모리 할당은 시스템의 전반적인 성능을 저하시킬 수 있습니다. 메모리 할당과 해제는 시스템 리소스를 소비하는 작업이므로, 잘못된 메모리 할당이 반복되면 시스템의 성능에 부정적인 영향을 미칠 수 있습니다.
- 버그 및 예외 상황 증가: 스택 접근과 다른 접근을 구분하지 않으면 잘못된 메모리 접근으로 인해 예외 상황이 증가할 수 있습니다. 이러한 예외 상황을 처리하는 과정에서 예상치 못한 버그가 발생할 가능성도 높아집니다.
- 디버깅 어려움: 잘못된 메모리 할당으로 인해 디버깅이 어려워질 수 있습니다. 프로그램이 비정상적으로 동작하거나 예기치 않은 메모리 할당이 발생하면, 이를 추적하고 원인을 파악하는 것이 매우 어려워질 수 있습니다.
일반적으로 실제 운영 체제는 프로세스에 "신호"를 전달하기 위해 언제든지 프로세스를 중단시킬 수 있기 때문에 사용자 프로그램이 스택 포인터 아래에 스택을 쓰는 경우 버그가 있습니다. 그러나 x86-64 PUSH 명령은 스택 포인터를 조정하기 전에 접근 권한을 확인하므로 스택 포인터 아래 8바이트에 페이지 폴트를 일으킬 수 있습니다.
그러나 x86-64 PUSH 명령은 스택 포인터를 조정하기 전에 접근 권한을 확인하므로 스택 포인터 아래 8바이트에 페이지 폴트를 일으킬 수 있습니다. <- 뭔 소린지 모르겠다
이 부분의 의미는 x86-64 아키텍처에서 `PUSH` 명령이 어떻게 동작하는지를 설명하는 것입니다. 더 쉽게 풀어서 설명하면 다음과 같습니다:
`PUSH` 명령은 데이터를 스택에 추가할 때 사용됩니다. 스택에 데이터를 추가할 때, 스택 포인터 (`rsp`)는 먼저 조정된 다음 데이터를 쓰는 것이 일반적입니다. 하지만 x86-64 아키텍처에서는 `PUSH` 명령이 실행되기 전에 먼저 메모리 접근 권한을 확인합니다. 이 말은 스택 포인터가 조정되기 전에 해당 위치에 접근 권한이 있는지를 검사한다는 것입니다. 만약 접근 권한이 없다면 페이지 폴트가 발생합니다.
특히 `PUSH` 명령은 데이터를 스택에 추가하기 전에 스택 포인터 (`rsp`)를 감소시키므로, 스택 포인터보다 8바이트 아래의 메모리에 접근 권한이 있는지를 먼저 확인합니다. 만약 이 위치에 접근 권한이 없다면, 스택 포인터가 조정되기 전에 페이지 폴트가 발생합니다. 따라서 스택 포인터보다 8바이트 아래에 접근할 때 페이지 폴트가 발생할 수 있다는 뜻입니다.
이를 요약하면:
- `PUSH` 명령은 데이터를 스택에 추가하기 전에 접근 권한을 확인합니다.
- 접근 권한이 없으면 페이지 폴트가 발생합니다.
- `PUSH` 명령은 스택 포인터보다 8바이트 아래에 대해 먼저 접근 권한을 확인합니다.
이 부분을 고려하여 스택 확장 기능을 구현할 때 페이지 폴트를 적절히 처리할 수 있도록 해야 합니다.
사용자 프로그램의 현재 스택 포인터 값을 얻을 수 있어야 합니다.
사용자 프로그램이 생성한 시스템 호출 또는 페이지 폴트 내에서 syscall_handler() 또는 page_fault()에 전달된 struct intr_frame의 rsp 멤버에서 이를 검색할 수 있습니다.
// syscall.c
void syscall_handler(struct intr_frame *f UNUSED)
{
int syscall_n = f->R.rax; /* 시스템 콜 넘버 */
#ifdef VM
thread_current()->rsp = f->rsp;
#endif
switch (syscall_n)
{
case SYS_HALT:
잘못된 메모리 접근을 감지하기 위해 페이지 폴트에 의존하는 경우, 커널에서 페이지 폴트가 발생하는 또 다른 사례를 처리해야 합니다.
이 문장의 의미는, 잘못된 메모리 접근을 감지하기 위해 페이지 폴트에 의존하는 경우, 커널 모드에서 페이지 폴트가 발생하는 상황도 처리해야 한다는 것입니다. 조금 더 자세히 설명하자면:
### 페이지 폴트란?
페이지 폴트는 프로그램이 메모리에 접근하려 할 때, 해당 메모리 페이지가 현재 메모리에 로드되어 있지 않거나 접근 권한이 없는 경우에 발생하는 예외 상황입니다. 페이지 폴트는 운영 체제가 메모리를 관리하는 중요한 메커니즘 중 하나입니다.
### 커널 모드와 사용자 모드
운영 체제는 보통 두 가지 모드로 실행됩니다:
- **사용자 모드**: 일반 사용자 프로그램이 실행되는 모드로, 시스템 리소스에 제한된 접근만 허용됩니다.
- **커널 모드**: 운영 체제의 핵심 부분이 실행되는 모드로, 모든 시스템 리소스에 접근할 수 있습니다.
### 문장의 의미
페이지 폴트를 이용해 잘못된 메모리 접근을 감지하려면, 사용자 모드에서 발생하는 페이지 폴트뿐만 아니라 커널 모드에서 발생하는 페이지 폴트도 처리해야 한다는 것입니다. 구체적으로:
1. **사용자 모드에서의 페이지 폴트 처리**:
- 사용자 프로그램이 잘못된 메모리 접근을 시도했을 때 페이지 폴트가 발생합니다.
- 이 경우 운영 체제는 페이지 폴트를 감지하고, 이를 처리하여 필요한 경우 스택을 확장하거나 적절한 예외 처리를 합니다.
2. **커널 모드에서의 페이지 폴트 처리**:
- 커널 코드가 실행되는 동안에도 페이지 폴트가 발생할 수 있습니다.
- 예를 들어, 사용자 프로그램에서 커널 모드로 전환되는 과정에서 페이지 폴트가 발생할 수 있습니다.
- 이 경우, 프로세서가 예외 상황을 처리하기 위해 커널 모드로 전환할 때 스택 포인터(`rsp`)를 저장합니다.
- 사용자 모드에서 커널 모드로 전환할 때, 스택 포인터가 올바르게 저장되지 않으면 페이지 폴트 처리가 복잡해질 수 있습니다.
따라서, 페이지 폴트에 의존하여 잘못된 메모리 접근을 감지하는 경우, 커널 모드에서 발생하는 페이지 폴트도 적절히 처리해야 한다는 의미입니다. 이를 위해 커널 모드로 전환할 때 사용자 모드의 스택 포인터 값을 저장하는 등의 추가적인 조치가 필요할 수 있습니다.
프로세서가 사용자 모드에서 커널 모드로 전환될 때 예외가 발생하면 스택 포인터를 저장하기 때문에 page_fault()에 전달된 struct intr_frame에서 rsp를 읽으면 정의되지 않은 값이 나타나 사용자 스택 포인터가 아닙니다. 사용자 모드에서 커널 모드로 처음 전환할 때 rsp를 struct thread에 저장하는 등의 다른 방법을 마련해야 합니다.
// thread.h
#ifdef VM
/* Table for whole virtual memory owned by thread. */
struct supplemental_page_table spt;
void *rsp;
#endif
스택 확장 기능을 구현하세요. 이를 구현하려면 먼저
vm/vm.c에서 vm_try_handle_fault를 수정하여 스택 성장을 식별해야 합니다.
스택 성장을 식별한 후, vm_stack_growth를 호출하여 스택을 확장해야 합니다.
vm_stack_growth를 구현하세요.
/* Return true on success */
bool vm_try_handle_fault(struct intr_frame *f UNUSED, void *addr UNUSED,
bool user UNUSED, bool write UNUSED, bool not_present UNUSED)
{
struct supplemental_page_table *spt UNUSED = &thread_current()->spt;
struct page *page = NULL;
if (addr == NULL)
return false;
if (is_kernel_vaddr(addr))
return false;
if (not_present) // 접근한 메모리의 physical page가 존재하지 않은 경우
{
/* TODO: Validate the fault */
// todo: 페이지 폴트가 스택 확장에 대한 유효한 경우인지를 확인해야 합니다.
void *rsp = f->rsp; // user access인 경우 rsp는 유저 stack을 가리킨다.
if (!user) // kernel access인 경우 thread에서 rsp를 가져와야 한다.
rsp = thread_current()->rsp;
// 스택 확장으로 처리할 수 있는 폴트인 경우, vm_stack_growth를 호출
if (USER_STACK - (1 << 20) <= rsp - 8 && rsp - 8 == addr && addr <= USER_STACK)
vm_stack_growth(addr);
else if (USER_STACK - (1 << 20) <= rsp && rsp <= addr && addr <= USER_STACK)
vm_stack_growth(addr);
page = spt_find_page(spt, addr);
if (page == NULL)
return false;
if (write == 1 && page->writable == 0) // write 불가능한 페이지에 write 요청한 경우
return false;
return vm_do_claim_page(page);
}
return false;
}
이 함수는 페이지 폴트 예외를 처리하는 동안 userprog/exception.c의 page_fault에서 호출됩니다. 이 함수에서는 페이지 폴트가 스택 성장을 위한 유효한 사례인지 확인해야 합니다. 폴트가 스택 성장으로 처리될 수 있는지 확인한 경우, 폴트가 발생한 주소로 vm_stack_growth를 호출하세요.
static void
vm_stack_growth(void *addr UNUSED)
{
// todo: 스택 크기를 증가시키기 위해 anon page를 하나 이상 할당하여 주어진 주소(addr)가 더 이상 예외 주소(faulted address)가 되지 않도록 합니다.
// todo: 할당할 때 addr을 PGSIZE로 내림하여 처리
vm_alloc_page(VM_ANON | VM_MARKER_0, pg_round_down(addr), 1);
}
/* Handle the fault on write_protected page */
주소가 더 이상 폴트가 발생하지 않도록 익명 페이지를 하나 이상 할당하여 스택 크기를 증가시킵니다. 할당을 처리할 때 addr를 PGSIZE로 내림하여 처리하는 것을 잊지 마세요.
대부분의 운영 체제는 스택 크기에 절대적인 제한을 둡니다. 일부 운영 체제는 예를 들어 많은 유닉스 시스템에서 ulimit 명령으로 사용자가 조정할 수 있는 제한을 둡니다. 많은 GNU/Linux 시스템에서는 기본 제한이 8MB입니다. 이 프로젝트에서는 스택 크기를 최대 1MB로 제한해야 합니다.
이제 모든 스택 성장 테스트 케이스를 통과해야 합니다.
예상과는 다르게 stack growth를 구현했음에도 exit(-1)으로 프로세스가 뒈져버리는 현상은 고쳐지지 않았다. 도대체 어디가 문제란 말인가 하 신이시여 핀토스보다 천배 만배 길고 복잡한 리눅스/윈도우 커널은 대체 어떻게 오류 없이 - 가만 생각해보니 없진 않다. 뻑나고 블루스크린뜨고 근데 그건 가끔이잖아 여하튼 전반적으로는 문제 없이 - 스무스하게 작동하는 것인가 내 머리가 감자인 것인가 리누스 토발ㄹㄹ즈/빌ㄹㄹ 게이츠의 머리가 대천재인 것인가 아니, 둘 다인가? 헤헤 나는 감자 대홍단 감자
'크래프톤 정글 일지' 카테고리의 다른 글
대한상공회의소 KDT 멘토멘티단 <Team-Mate> 면접 후기 (1) | 2024.07.18 |
---|---|
[PintOS] project 3.3.1 - stack growth 디버깅 (2) | 2024.06.08 |
[PintOS] project 3.2.1 - anonymous 디버깅 (1) | 2024.06.07 |
[PintOS] project 3.2.0 - anonymous page, vm.c, process.c (0) | 2024.06.07 |
[PintOS] project 3.1 - hash.h, memory management (0) | 2024.06.06 |