
지난 포스팅에 이어 이번에는 syscall.c, process.c 의 수정 사항들에 대해서 알아보자
syscall.c우리는 syscall 함수들을 구현할 때, 인자값으로 포인터를 받아오는 것들에 대한 검사를 해주었다
아마 check_address를 많이 구현했을 듯 싶은데, 본인의 이전 팀은 validate.c 파일을 따로 만들어서
validate_ptr 함수를 따로 만들어 사용했다.
그 점을 짚고 시작해보겠다. 아마 check_address도 비슷하게 구현하면 되지 않을까?(진짜 모름)
.
.
.
모든 구현을 모두 마치고 테스트를 돌려보니 read_boundary 부분에서 Fail이 계속해서 발생했다 !
그래서 먼저 구현을 마친 팀원에게 이유를 물어보니 check_address 함수에서 조건 분기를 추가해줘야 한다고 이야기를 해줬다.
그런데 이게 웬걸 나의 코드에는 check_address 함수는 구현해놓았지만, 각 시스템 콜 함수에서는 호출하지 않고
위에서 얘기했던 validate_ptr 함수를 사용하고 있었다.
validate.cvalidate_ptr 함수void
validate_ptr (const void *uaddr, size_t size) {
if (size == 0) return; // 검사할 바이트 수가 0이면 return
const uint8_t *usr = uaddr; // 현재 검사할 위치 (byte 단위 포인터)
size_t left = size; // 검사해야 할 남은 바이트 수
// 검사할 전체 영역을 페이지 단위로 나누어 한 페이지씩 접근 가능 여부를 확인
while (left > 0) {
// 현재 usr 포인터가 가리키는 주소가 사용자 영역에 있고
// 실제로 물리 메모리에 매핑되어 있는지 확인
if (!check_page (usr))
sys_exit (-1); // 잘못된 주소일 경우, 즉시 프로세스 종료
// 현재 페이지에서 끝까지 남은 바이트 수 계산
size_t page_left = PGSIZE - pg_ofs (usr);
// 남은 전체 바이트와 현재 페이지에서 가능한 바이트 중 더 작은 만큼만 이동
size_t chunk = left < page_left ? left : page_left;
usr += chunk; // 검사할 포인터를 다음 영역으로 이동
left -= chunk; // 검사해야 할 남은 바이트 수 갱신
}
}
본인은 이 코드에서 while 문 안에서 check_page라는 함수를 호출하는 것을 발견했다 !
그래서 check_page() 함수를 확인해보았다.
check_page() 함수static bool
check_page (const void *uaddr) {
return uaddr != NULL &&
is_user_vaddr(uaddr) &&
pml4_get_page (thread_current ()->pml4, uaddr) != NULL;
}
기존에 구현해놓은 로직을 보면
현재 프로세스의 PML4 페이지 테이블에서 해당 주소가 실제로 매핑되었는가 검사를 한다.
즉, 이 함수는 uaddr -> 커널 주소로 변환이 가능한지 판단한다.
이 로직의 한계는
아직 claim되지 않은 페이지(Lazy Load, Swap Out된 페이지 등)는 거짓으로 판단된다 !
vm_claim_page() 이전의 정상적인 유저 주소들도 거부를 당함 ㅠㅠ
이러한 문제들을 발견하여
check_page() 함수static bool
check_page (const void *uaddr) {
if (uaddr == NULL || !is_user_vaddr(uaddr))
return false;
void *va = pg_round_down(uaddr);
struct supplemental_page_table *spt = &thread_current()->spt;
return spt_find_page(spt, va) != NULL;
}
이런 식으로 구조를 바꾸었다 :
주소가 유저 영역인지 확인 후, SPT에 등록된 페이지인지 여부만 판단한다
아직 매핑되지 않았더라도 → lazy load 예정, swap-out 상태라면 → OK
로직 변경으로
가상 메모리 시스템의 전체 설계와 일관되게 동작하고
Lazy Load, Swap In 등 페이지 폴트 후 로딩될 수 있는 주소도 안전하다고 판단이 가능하게 되었고
vm_claim_page() 호출을 유도하는 조건 검사로써 적절하게 되었다 !
| 항목 | 기존 버전 | 변경된 버전 |
|---|---|---|
| 판단 기준 | PML4 매핑 여부 | SPT 등록 여부 |
| Lazy Page 지원 | ❌ 거부함 | ✅ 허용 |
| Swap-out된 페이지 | ❌ 거부함 | ✅ 허용 |
| 페이지 폴트 전 허용 | ❌ 안됨 | ✅ 가능 |
| 보안성 | 낮음 (직접 매핑 확인이 전부) | 높음 (VM 상태 기반 정밀 판단) |
process.cprocess.c 내부에 -DVM 플래그가 있을 시에 동작하는 코드 영역이 있는데 그 부분을 추가 구현 & 수정 해주었다 !
먼저 알아볼 함수는 lazy_load_segment() 함수이다
lazy_load_segment() - 페이지 폴트 시 파일로부터 세그먼트 로딩이 함수는 페이지가 처음 접근되었을 때(
vm_claim_page()내부 →swap_in()호출)
해당 페이지의 실제 데이터를 실행 파일로부터 읽어와 물리 프레임에 로드하는 역할을 수행한다.즉, "지금은 로딩하지 않고, 나중에 fault 날 때 로딩할게요"라는
Lazy Loading(지연 로딩) 전략의 구현체다.
우리는 여기서 load_info 라는 구조체를 선언해주어야 한다.
struct load_infostruct load_info {
struct file *file; // ELF 실행 파일 포인터
off_t ofs; // 해당 데이터의 파일 내 오프셋
uint32_t read_bytes; // 파일에서 읽어야 할 바이트 수
uint32_t zero_bytes; // 이후 0으로 채울 바이트 수
};
이 구조체는 페이지 초기화 시 aux 포인터로 전달된다
read_bytes + zero_bytes == PGSIZE 를 만족
struct load_info *load_info = (struct load_info *)aux;
if (load_info->read_bytes > 0) {
file_seek(load_info->file, load_info->ofs);
if (file_read(load_info->file, page->frame->kva, load_info->read_bytes)
!= (int)(load_info->read_bytes)) {
palloc_free_page(page->frame->kva);
return false;
}
}
파일의 ofs 위치부터 read_bytes만큼 현재 페이지 프레임(kva)에 읽어옴
읽기에 실패하면 프레임 해제 후 실패 처리
memset(page->frame->kva + load_info->read_bytes, 0, load_info->zero_bytes);
ELF Segment는 일부만 실제로 파일에 존재하고, 나머지는 BSS같은 빈 공간일 수 있음
이를 0으로 채우는 작업 수행
return true;
true반환 → 페이지가 메모리에 완전히 로딩됨load_segment() → vm_alloc_page_with_initializer(..., lazy_load_segment, aux)
→ page fault 발생 → vm_claim_page()
→ vm_do_claim_page() → swap_in()
→ page->operations->swap_in() == lazy_load_segment() 호출
load_segment() - ELF 파일의 세그먼트를 Lazy Loading으로 등록유저 프로그램을 실행할 때, 실행 파일의 각 segment (code/data 영역)를 메모리에 올려야 한다.
하지만 모든 데이터를 미리 올리면 비효율적이므로, 실제 접근 시점에 로딩하는 Lazy Loading 방식을 사용한다.
이 함수는 ELF 로더에서 load_segment()로 호출되며,
가상 주소 공간에 필요한 페이지들을 Lazy Load 방식으로 등록한다.
이 함수도 인자값에 대해 알아보자
static bool
load_segment (struct file *file, off_t ofs, uint8_t *upage,
uint32_t read_bytes, uint32_t zero_bytes, bool writable)
| 인자 | 설명 |
|---|---|
file | ELF 실행 파일 포인터 |
ofs | 해당 세그먼트의 파일 내 오프셋 |
upage | 매핑을 시작할 유저 가상 주소 |
read_bytes | 파일로부터 읽어야 할 총 바이트 수 |
zero_bytes | 그 외 0으로 채워야 할 바이트 수 |
writable | 쓰기 가능 여부 |
ASSERT((read_bytes + zero_bytes) % PGSIZE == 0);
ASSERT(pg_ofs(upage) == 0);
ASSERT(ofs % PGSIZE == 0);
총 페이지 크기와 정렬이 정확해야 가상 메모리로 올릴 수 있다
페이지 단위로 올라가야 하기 때문에 엄격하게 체크해야 함 !
while (read_bytes > 0 || zero_bytes > 0)
각 반복마다 :
PGSIZE가 되도록)size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
size_t page_zero_bytes = PGSIZE - page_read_bytes;
load_info)를 생성해서 aux로 넘김struct load_info *load_info = malloc(sizeof(struct load_info));
load_info->file = file_duplicate(file);
load_info->ofs = ofs;
load_info->read_bytes = page_read_bytes;
load_info->zero_bytes = page_zero_bytes;
file_duplicate()를 사용하는 이유:Lazy load 시점이 훨씬 나중일 수도 있으므로,
현재 열린 파일이 닫혀도 해당 파일을 안전하게 참조할 수 있도록 하기 위함.
vm_alloc_page_with_initializer() 호출if (!vm_alloc_page_with_initializer(VM_FILE, upage, writable, lazy_load_segment, load_info))
return false;
lazy_load_segment()는 페이지 폴트 시점에 호출될 함수
여기서는 실제 프레임을 만들지 않고 예약만 해두는 상태
read_bytes -= page_read_bytes;
zero_bytes -= page_zero_bytes;
upage += PGSIZE;
ofs += page_read_bytes;
ELF의 한 segment를 여러 페이지로 분할하여
각 페이지를 lazy loading 방식으로 SPT에 등록
실제 로딩은 vm_claim_page() → swap_in() → lazy_load_segment() 순으로 이루어짐
load() → load_segment() → vm_alloc_page_with_initializer() → lazy_load_segment()
ELF Loader 내부에서 사용
실행 파일의 .text, .data, .bss 등을 메모리에 올릴 준비를 하는 함수
setup_stack() - 유저 스택 초기 페이지 설정PintOS에서 유저 프로그램은 USER_STACK (0x47480000)에서 스택을 시작한다.
스택은 페이지 단위로 아래로 확장되며,
처음 실행 시점에는 최상단 한 페이지만 미리 확보되어 있어야 한다.
이 함수는 그 한 페이지를:
SPT에 등록하고,
즉시 물리 메모리를 할당 및 매핑
rsp를 USER_STACK으로 설정
하는 역할을 한다.
void *stack_bottom = (void *)(((uint8_t *)USER_STACK) - PGSIZE);
USER_STACK 바로 아래 주소를 스택의 바닥(bottom)으로 설정
이 한 페이지만 미리 확보함 (이후 자동 확장은 stack growth에서 처리)
if (vm_alloc_page_with_initializer(VM_ANON | VM_MARKER_0,
stack_bottom, 1, NULL, NULL))
익명 페이지로 등록 (VM_ANON)
추가적으로 VM_MARKER_0를 OR하여 이 페이지는 "스택이다" 라는 표시를 남김
이후 vm_try_handle_fault() 등에서 stack growth 여부 판단 시 사용
lazy-load가 아니므로 init, aux는 NULL
success = vm_claim_page(stack_bottom);
페이지를 즉시 확보
물리 프레임을 할당하고 PML4에 매핑 → swap_in() 호출됨
if (success)
if_->rsp = USER_STACK;
rsp (유저 스택 포인터)를 스택 최상단으로 초기화
이 주소부터 인자 스택, 환경 변수 등을 push 할 수 있게 됨.
process_exec() → load() → setup_stack()
페이지가 스택인지 여부를 식별하는 flag 역할
vm_claim_page()나 vm_try_handle_fault()에서 해당 페이지가 스택인 경우
stack growth 조건을 활성화하는 데 사용됨
__do_fork()fork 시 자식 프로세스는 부모의 주소 공간을 그대로 복사해야 하므로,
보조 페이지 테이블(SPT)도 함께 복제해야 한다
PintOS에서는 VM 기능이 켜져 있을 때(#ifdef VM)만 SPT 관련 코드가 활성화되므로,
다음 코드를 __do_fork() 함수 내부의 #ifdef VM 블록 안에 작성해야 한다 :
#ifdef VM
// 보조 페이지 테이블 초기화 및 복사 (VM 기능이 켜져 있는 경우)
supplemental_page_table_init(¤t->spt);
supplemental_page_table_copy(¤t->spt, &parent->spt);
#else
supplemental_page_init()
→ 자식 프로세스의 SPT 구조를 빈 상태로 초기화
supplemental_page_table_copy()
→ 부모의 SPT를 순회하며 자식에게 그대로 복제
lazy 상태인 페이지는 lazy로 복사
초기화된 페이지는 프레임까지 확보해서 memcpy()로 복사
supplemental_page_table_* 함수들은 VM 활성화 조건 하에서만 정의됨
만약 이 코드가 #ifdef VM 밖에 있으면 컴파일 오류가 발생한다
따라서 fork에서 테스트를 통과하려면 반드시 이 조건부 컴파일 블록 내에 있어야 한다 !
이전 포스팅과 이번 포스팅까지 잘 따라온다면
pass tests/userprog/args-none
pass tests/userprog/args-single
pass tests/userprog/args-multiple
pass tests/userprog/args-many
pass tests/userprog/args-dbl-space
pass tests/userprog/halt
pass tests/userprog/exit
pass tests/userprog/create-normal
pass tests/userprog/create-empty
pass tests/userprog/create-null
pass tests/userprog/create-bad-ptr
pass tests/userprog/create-long
pass tests/userprog/create-exists
pass tests/userprog/create-bound
pass tests/userprog/open-normal
pass tests/userprog/open-missing
pass tests/userprog/open-boundary
pass tests/userprog/open-empty
pass tests/userprog/open-null
pass tests/userprog/open-bad-ptr
pass tests/userprog/open-twice
pass tests/userprog/close-normal
pass tests/userprog/close-twice
pass tests/userprog/close-bad-fd
pass tests/userprog/read-normal
pass tests/userprog/read-bad-ptr
pass tests/userprog/read-boundary
pass tests/userprog/read-zero
pass tests/userprog/read-stdout
pass tests/userprog/read-bad-fd
pass tests/userprog/write-normal
pass tests/userprog/write-bad-ptr
pass tests/userprog/write-boundary
pass tests/userprog/write-zero
pass tests/userprog/write-stdin
pass tests/userprog/write-bad-fd
pass tests/userprog/fork-once
pass tests/userprog/fork-multiple
pass tests/userprog/fork-recursive
pass tests/userprog/fork-read
pass tests/userprog/fork-close
pass tests/userprog/fork-boundary
pass tests/userprog/exec-once
pass tests/userprog/exec-arg
pass tests/userprog/exec-boundary
pass tests/userprog/exec-missing
pass tests/userprog/exec-bad-ptr
pass tests/userprog/exec-read
pass tests/userprog/wait-simple
pass tests/userprog/wait-twice
pass tests/userprog/wait-killed
pass tests/userprog/wait-bad-pid
pass tests/userprog/multi-recurse
pass tests/userprog/multi-child-fd
pass tests/userprog/rox-simple
pass tests/userprog/rox-child
pass tests/userprog/rox-multichild
pass tests/userprog/bad-read
pass tests/userprog/bad-write
pass tests/userprog/bad-read2
pass tests/userprog/bad-write2
pass tests/userprog/bad-jump
pass tests/userprog/bad-jump2
FAIL tests/vm/pt-grow-stack
pass tests/vm/pt-grow-bad
FAIL tests/vm/pt-big-stk-obj
pass tests/vm/pt-bad-addr
pass tests/vm/pt-bad-read
pass tests/vm/pt-write-code
FAIL tests/vm/pt-write-code2
FAIL tests/vm/pt-grow-stk-sc
pass tests/vm/page-linear
pass tests/vm/page-parallel
pass tests/vm/page-merge-seq
FAIL tests/vm/page-merge-par
FAIL tests/vm/page-merge-stk
FAIL tests/vm/page-merge-mm
pass tests/vm/page-shuffle
FAIL tests/vm/mmap-read
FAIL tests/vm/mmap-close
FAIL tests/vm/mmap-unmap
FAIL tests/vm/mmap-overlap
FAIL tests/vm/mmap-twice
FAIL tests/vm/mmap-write
FAIL tests/vm/mmap-ro
FAIL tests/vm/mmap-exit
FAIL tests/vm/mmap-shuffle
FAIL tests/vm/mmap-bad-fd
FAIL tests/vm/mmap-clean
FAIL tests/vm/mmap-inherit
FAIL tests/vm/mmap-misalign
FAIL tests/vm/mmap-null
FAIL tests/vm/mmap-over-code
FAIL tests/vm/mmap-over-data
FAIL tests/vm/mmap-over-stk
FAIL tests/vm/mmap-remove
FAIL tests/vm/mmap-zero
FAIL tests/vm/mmap-bad-fd2
FAIL tests/vm/mmap-bad-fd3
FAIL tests/vm/mmap-zero-len
FAIL tests/vm/mmap-off
FAIL tests/vm/mmap-bad-off
FAIL tests/vm/mmap-kernel
FAIL tests/vm/lazy-file
pass tests/vm/lazy-anon
FAIL tests/vm/swap-file
FAIL tests/vm/swap-anon
FAIL tests/vm/swap-iter
pass tests/vm/swap-fork
pass tests/filesys/base/lg-create
pass tests/filesys/base/lg-full
pass tests/filesys/base/lg-random
pass tests/filesys/base/lg-seq-block
pass tests/filesys/base/lg-seq-random
pass tests/filesys/base/sm-create
pass tests/filesys/base/sm-full
pass tests/filesys/base/sm-random
pass tests/filesys/base/sm-seq-block
pass tests/filesys/base/sm-seq-random
pass tests/filesys/base/syn-read
pass tests/filesys/base/syn-remove
pass tests/filesys/base/syn-write
pass tests/threads/alarm-single
pass tests/threads/alarm-multiple
pass tests/threads/alarm-simultaneous
pass tests/threads/alarm-priority
pass tests/threads/alarm-zero
pass tests/threads/alarm-negative
pass tests/threads/priority-change
pass tests/threads/priority-donate-one
pass tests/threads/priority-donate-multiple
pass tests/threads/priority-donate-multiple2
pass tests/threads/priority-donate-nest
pass tests/threads/priority-donate-sema
pass tests/threads/priority-donate-lower
pass tests/threads/priority-fifo
pass tests/threads/priority-preempt
pass tests/threads/priority-sema
pass tests/threads/priority-condvar
pass tests/threads/priority-donate-chain
FAIL tests/vm/cow/cow-simple
37 of 141 tests failed.
Project 2의 테스트는 전부 pass가 나온다 !