(TIL)요즘 하루종일 디버깅만 하다 끝나는 것 같다~

낚시하는 곰·2025년 6월 9일
1

jungle TIL

목록 보기
13/20

mmap-read test

void
test_main (void)
{
  char *actual = (char *) 0x10000000;
  int handle;
  void *map;
  size_t i;

  CHECK ((handle = open ("sample.txt")) > 1, "open \"sample.txt\"");
  CHECK ((map = mmap (actual, 4096, 0, handle, 0)) != MAP_FAILED, "mmap \"sample.txt\"");

  /* Check that data is correct. */
  if (memcmp (actual, sample, strlen (sample)))
    fail ("read of mmap'd file reported bad data");

  /* Verify that data is followed by zeros. */
  for (i = strlen (sample); i < 4096; i++)
    if (actual[i] != 0)
      fail ("byte %zu of mmap'd region has value %02hhx (should be 0)",
            i, actual[i]);

  munmap (map);
  close (handle);
}
1. 파일 내용이 메모리에 제대로 매핑되었는가?

if (memcmp (actual, sample, strlen (sample)))
  fail ("read of mmap'd file reported bad data");

	•	sample.txt의 앞부분이 메모리 주소 0x10000000에 정확히 복사되었는지 확인합니다.memcmp()을 통해 매핑된 메모리 영역(actual)과 사전에 정의된 참조 데이터 sample을 비교합니다.
	•	파일에서 읽은 내용이 기대한 문자열과 다르면 실패 처리합니다.

⸻

✅ 2. 매핑된 페이지의 나머지 영역이 0으로 초기화되었는가?

for (i = strlen (sample); i < 4096; i++)
  if (actual[i] != 0)
    fail ("byte %zu of mmap'd region has value %02hhx (should be 0)", i, actual[i]);strlen(sample) 이후의 메모리 공간이 전부 0으로 채워져 있어야 합니다.
	•	이는 파일의 크기가 4096바이트보다 작을 경우, 남은 부분을 0으로 채워야 한다는 mmap의 요구사항을 테스트하는 것입니다.
	•	예를 들어, 파일이 100바이트라면, 나머지 3996바이트는 0이 되어야 합니다.

디버깅

nterrupt 0x0d (#GP General Protection Exception) at rip=8004221fe0
 cr2=0000000010000000 error=               0
rax ccccccccccccccb4 rbx 00008004247f1800 rcx 0000000000000011 rdx 0000008004244000
rsp 0000008004244e90 rbp 0000008004244ee0 rsi 0000000000000251 rdi 0000008004244070
rip 0000008004221fe0 r8 0000008004244d18  r9 000000800421b557 r10 0000000000000000
r11 0000000000000216 r12 000000800421d777 r13 0000010424400000 r14 0000800424400000
r15 0000800422bdd800 rflags 00000286
es: 001b ds: 001b cs: 0008 ss: 0010

rax의 값을 보면 이상한 쓰레기 값을 반환하고 있다. 문제 발생 근원지를 찾기 위해 로그를 찍어봤더니

// spt remove page start
				spt_remove_page(spt, page);
				dprintfg("[do_munmap] spt remove() 실행 후 다음 line으로 넘어가나요??\n");
				unmap_addr += PGSIZE;
    			length -= PGSIZE; 

do_munmap()의 spt page를 해제해주는 부분에서 문제가 발생했다.

struct hash_elem *
hash_delete (struct hash *h, struct hash_elem *e) {
	dprintfg("[hash_delete] hash func addr: %p, less func addr: %p\n", h->hash, h->less);
	struct hash_elem *found = find_elem (h, find_bucket (h, e), e);

find_elem에서 hash elem의 주소가 Null값이 반환되었기 때문이라고 예상하고, page→hash_elem 주소가 깨질 수 있는 경우를 생각해봤다.

if (!vm_alloc_page_with_initializer(VM_FILE, addr, writable, lazy_load_file_backed, aux))

do_mmap()에서 file backed page를 lazy load방식으로 할당해주는 부분이 있는데 흐름을 타고 들어가면

initializer = file_backed_initializer;

file_backed initializer()를 호출하는 걸 볼 수 있다. 왜 file backed 구조체 멤버 값들을 초기화 해줘야한다는 생각을 하지 못했을까? 그런데 아직도 do_mmap()에서 lazy load file backed()함수를 호출하면서 lazy load 구조체를 초기화 해주기 때문에 file_backed_initializer()에서 초기화 해주는 것이 의미가 있나 싶다.

여전히 문제가 해결되지 않았고, 원인도 모르겠어서 gpt의 도움을 받았다.

/* Do the munmap */
void do_munmap(void *addr)
{
	// 프로세스가 종료되면 매핑 자동해제. munmap할 필요는 없음.
	// 매핑 해제 시 수정된 페이지는 파일에 반영
	// 수정되지 않은 페이지는 반영할 필요 없음
	// munmap 하고 spt제거?
	// 파일 close, remove는 매핑에 반영되지 않음( 프레임은 가마니)
	// 한 파일을 여러번 mmap하는 경우에는 file_reopen을 통해 독립된 참조. -> 하나의 file이 여러번 mmap 되어 있는 걸 어떻게 알지?
	struct thread *curr = thread_current();
	struct supplemental_page_table *spt = &curr->spt; // 현재 스레드의 spt 정보 참조

	for(struct list_elem *e = list_begin(&curr->mmap_list); e != list_end(&curr->mmap_list); e = list_next(e)){
		struct mmap_file *mmap_file = list_entry(e, struct mmap_file, elem);
		void *unmap_addr = mmap_file->start_addr;
		size_t length = mmap_file->start_length;

		if(mmap_file->start_addr == addr){
			dprintfg("[do_mmap] start_addr : %p, addr : %p\n", mmap_file->start_addr, addr);

			while(length > 0){
				dprintfg("[do_mmap] while() 안의 length : %d\n", length);
				struct page *page = spt_find_page(spt, unmap_addr); // spt정보를 가져온다.
				dprintfg("[do_mmap] spt find page : %p\n", page);

				// spt remove page debug log
				if (page == NULL) {
					dprintfg("[do_munmap] spt_find_page() 결과 NULL! 잘못된 주소일 가능성\n");
					break;
				}
				dprintfg("[debug] page: %p, &page->hash_elem: %p, page->va: %p\n", page, &page->hash_elem, page->va);

				dprintfg("[debug] hash_elem addr: %p, 예상 page addr: %p\n", &page->hash_elem, hash_entry(&page->hash_elem, struct page, hash_elem));

				// spt remove page start
				spt_remove_page(spt, page);
				dprintfg("[do_munmap] spt remove() 실행 후 다음 line으로 넘어가나요??\n");
				unmap_addr += PGSIZE;
    			length -= PGSIZE; 
			}

			list_remove(&mmap_file->elem);
			free(mmap_file);
		}
	}
}

위 코드의 for()문에서 문제가 발생했는데 문제는

free(mmap_file);

메모리 누수를 막기 위해서 mmap list의 mmap_file을 free()해주고 나서 다시 loop()을 도는데

e != list_end(&curr->mmap_list); e = list_next(e)

mmap list의 end인지 check하고 아니라면 list next()로 넘어가는 흐름인데 next()로 포인터가 이동하기도 전에 mmap_file을 free()해준 것이다. 따라서 이미 free()된 주소를 다시 참조하면서 문제가 발생한 것이다.

/* Do the munmap */
void do_munmap(void *addr)
{
	// 프로세스가 종료되면 매핑 자동해제. munmap할 필요는 없음.
	// 매핑 해제 시 수정된 페이지는 파일에 반영
	// 수정되지 않은 페이지는 반영할 필요 없음
	// munmap 하고 spt제거?
	// 파일 close, remove는 매핑에 반영되지 않음( 프레임은 가마니)
	// 한 파일을 여러번 mmap하는 경우에는 file_reopen을 통해 독립된 참조. -> 하나의 file이 여러번 mmap 되어 있는 걸 어떻게 알지?
	struct thread *curr = thread_current();
	struct supplemental_page_table *spt = &curr->spt; // 현재 스레드의 spt 정보 참조

	struct list_elem *e = list_begin(&curr->mmap_list);
    while (e != list_end(&curr->mmap_list)) {
		struct list_elem *next = list_next(e);
		struct mmap_file *mmap_file = list_entry(e, struct mmap_file, elem);
		void *unmap_addr = mmap_file->start_addr;
		size_t length = mmap_file->start_length;

		if(mmap_file->start_addr == addr){
			dprintfg("[do_munmap] start_addr : %p, addr : %p\n", mmap_file->start_addr, addr);

			while(length > 0){
				dprintfg("[do_mmap] while() 안의 length : %d\n", length);
				struct page *page = spt_find_page(spt, unmap_addr); // spt정보를 가져온다.
				dprintfg("[do_munmap] spt find page : %p\n", page);

				// spt remove page debug log
				if (page == NULL) {
					dprintfg("[do_munmap] spt_find_page() 결과 NULL! 잘못된 주소일 가능성\n");
					break;
				}
				dprintfg("[do_munmap] page: %p, &page->hash_elem: %p, page->va: %p\n", page, &page->hash_elem, page->va);

				dprintfg("[do_munmap] hash_elem addr: %p, 예상 page addr: %p\n", &page->hash_elem, hash_entry(&page->hash_elem, struct page, hash_elem));

				// spt remove page start
				spt_remove_page(spt, page);
				dprintfg("[do_munmap] spt remove() 실행 후 다음 line으로 넘어가나요??\n");
				unmap_addr += PGSIZE;
    			length -= PGSIZE; 
			}

			list_remove(&mmap_file->elem);
			dprintfg("[do_munmap] list remove(mmap file -> elem) success\n");
			free(mmap_file);
			dprintfg("[do_munmap] free(mmap file) success\n");
        }
        e = next;
    }
}

위와 같이 수정하니 null값을 참조하는 문제는 해결되었다. 위 코드는 next() 주소를 미리 저장해놨다가 e != list_end(&curr->mmap_list)조건을 검사하기 전에 할당해주는 방식이다.

profile
취업 준비생 낚곰입니다!! 반갑습니다!!

2개의 댓글

comment-user-thumbnail
2025년 6월 9일

저도 하루종일 디버깅하다가 자러갑니다

1개의 답글