정글에서 온 개발자

정글pintOS project3 - Anonymous page 디버깅 본문

TIL

정글pintOS project3 - Anonymous page 디버깅

dev-diver 2023. 12. 23. 16:21

배경

  • pintos vm에서는 page를 보조테이블에 uninit_page 로 미리 넣어놓고, 페이지를 읽을 때 lazy_load_segment로 물리주소에 할당한다.
  • process를 시작할 때 읽어들이는 파일도 마찬가지인데, process_exec이 stack을 쌓고 do_iret()을 하면, 해당 rip로 가면서 rip가 가리키는 가상주소를 물리주소에 할당하기 시작한다.

  • 그런데 위와 같이 잘 읽어들이다가 0을 가리키는 곳으로 빠졌다.
  • 해당 fault가 특정 코드에서 나는 것이 아니기 때문에 fault 직전에 브레이크포인트를 잡고 디버깅할 수도 없어 이틀 정도를 헤멨다.
  • 'addr :  '   log는  page_fault_handler 진입하자마자 찍는 로그이다.

해결

  • 기존 코드에서는 아래 두 코드가 빠져있었다.
    • load_segment로  lazy_load를 세팅해주는 동안 offset (pos)을 계속 이동 시켜주는 코드
    • lazy_load segment 안에서 file_read전에 file_seek로 pos를 바꿔주는 코드
static bool
load_segment (struct file *file, off_t ofs, uint8_t *upage,
		uint32_t read_bytes, uint32_t zero_bytes, bool writable) {
	ASSERT ((read_bytes + zero_bytes) % PGSIZE == 0);
	ASSERT (pg_ofs (upage) == 0);
	ASSERT (ofs % PGSIZE == 0);

	while (read_bytes > 0 || zero_bytes > 0) {
		/* Do calculate how to fill this page.
		 * We will read PAGE_READ_BYTES bytes from FILE
		 * and zero the final PAGE_ZERO_BYTES bytes. */
		size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
		size_t page_zero_bytes = PGSIZE - page_read_bytes;

	 //arg 세팅
		struct seg_arg *args = calloc(1, sizeof(struct seg_arg));
		if(args == NULL){
			PANIC("Memory allocation failed\\n");
			return false;
		}

		args->file = file;
		args->ofs = ofs;
		args->page_read_bytes = page_read_bytes;
		args->page_zero_bytes = page_zero_bytes;
		void *aux = args;
		if (!vm_alloc_page_with_initializer (VM_ANON, upage,
					writable, lazy_load_segment, aux)){
			return false;
		}

		/* Advance. */    -> Advance라고 써있어서 낚였다.
		read_bytes -= page_read_bytes;
		zero_bytes -= page_zero_bytes;
		upage += PGSIZE;
		ofs += page_read_bytes; //이 부분 추가
	}
	return true;
}

기본 원리

  • load_segment를 하면 file을 read_bytes만큼 파일을 읽어들이는 설정을 하는데, 바로 읽지 않고vm_alloc_page_with_initializer로 lazy_load_segment를 uninit_page의 init 함수로 지정하고 넘어간다.
  • 이 때 aux 값으로 해당 페이지를 파일에서 읽어올 때 어느 offset에서 읽을지 지정해줘야 한다.
  • 한편 내부에서는 넘겨받은 aux값은 file_seek()를 통해서 파일에서 읽어야 하는 위치를 정해준다.

문제의 핵심

  • lazy_load_segment시 읽어오는 페이지들이,  load_segment 때 순차적으로 while문을 돌았던 것과 달리 순차적이 아니다.
  • 그래서 segment를 읽을 때, 어떤 offset부터 읽어야 하는지 정확히 지정해야 한다.
  • 최근 file_read로 바뀐 offset (pos)를 믿고 그대로 쓰게 하는 것은 정확히 지정해준 것이 아니다.
static bool
lazy_load_segment (struct page *page, void *aux) {

	struct seg_arg *args = (struct seg_arg *)aux;
	struct file *file = args->file;
	off_t ofs = args->ofs;
	size_t page_read_bytes = args->page_read_bytes;
	size_t page_zero_bytes = args->page_zero_bytes;

	file_seek(file, ofs); //이 부분
	uint8_t *kpage = page->frame->kva;

	/* Load this page. */
	if (file_read (file, kpage, page_read_bytes) != (int) page_read_bytes) {
		palloc_free_page (kpage);
		vm_dealloc_page(page);
		return false;
	}
	memset (kpage + page_read_bytes, 0, page_zero_bytes);

	free(aux);
	
	return true;
}

해결

고찰

이전엔 왜 에러가 났을까?

  • lazy_load는 파일의 해당 부분이 불러와질 때 frame과 연결되고, 물리 메모리에 할당된다.
    • do_iret 직후에는 해당 파일의 main의 주소로 이동한다.
    • 하지만 코드를 진행하면서 읽으려는 코드가 들어있는 가상주소에 해당 코드가 로드가 안 되어 있을 수 있다.
    • 그런데 그 접근하는 순서가 ‘순차적이 아니다’.  중간에 함수 호출, 조건 분기 등으로 page를 넘어서 코드를 건너 뛸 수 있다.
    • 예시에서는 2번째 lazy load를 하는데, 해당 코드가 0x605248에 있다.
    • 이 때 file_seek 를 하지 않고 file_read를 하면, read는 마지막 file_read의 최근 offset(0x400c7f+PAGE_SIZE)부터 시작하고, 가상주소 0x605248에 들어갔어야 할 파일 내용이 아닌 최근 offset의 파일이 로드되어 예상외의 동작을 하게 된다.
  • file_seek()만 넣어줬을 때도 문제가 생길 수 있다.
  • 예시에서 0x605248 페이지 실행을 무사히 마치고 다시 0x401076자리로 돌아간다.
  • 이 때 offset 이동 코드를 쓰지 않았다면, file의 offset은 마지막으로 file_read를 끝마친 offset( 0x604d20+PAGESIZE )을 가리키고, 가상주소 0x401076에 로드되어야 하는 파일의 내용에는 해당 offset의 내용이 로드되어서 이후에 예상하지 못한 방식으로 동작하게 된다.

미해결 의문

#include "tests/lib.h"

int
main (int argc, char *argv[]) 
{
  int i;

  test_name = "args";

  if (((unsigned long long) argv & 7) != 0)
    msg ("argv and stack must be word-aligned, actually %p", argv);

  msg ("begin"); //msg함수 코드의 위치 0x605248?
  msg ("argc = %d", argc); // 다시 0x40176?
  for (i = 0; i <= argc; i++) 
    if (argv[i] != NULL)
      msg ("argv[%d] = '%s'", i, argv[i]);
    else
      msg ("argv[%d] = null", i);
  msg ("end");

  return 0;
}
  • args-one 테스트를 돌리면 실제로 돌아가는 args.c 에서, 어디에서 점프를 뛰는지 알고 싶은데, msg가 유력한 후보인 것 같다.
  • msg실행 이후 다시 main 함수로 돌아와서 msg(”argc =%d”,argc) 를 실행하는 코드 (어셈블리어) 가 들어가 있는 부분이 0x40176 에 들어있는 것으로 추측된다.
  • 하지만 printf(”%p”,msg); 를 해본 결과 0x40번대가 출력되었다.
  • 어셈블리어의 라인을 이동하면서 디버깅 되는 툴이 있어야 해당 의문이 풀릴 것 같다