[SW사관학교 정글 6기]WIL - Pintos Project 2 : User Program

ssh00n·2023년 5월 16일
0

SW사관학교 정글

목록 보기
7/8
post-thumbnail

Pintos 두 번째 프로젝트는 User Program이다. 이전까지 Pintos에서의 모든 코드는 OS kernel의 일부였다. 이 말인 즉 system의 중요한 자원에 접근할 때 모든 privilege를 가진 채 프로그램이 실행되었다는 의미이다. 그러나 OS 위에서 유저 프로그램을 실행시킬 경우 이러한 privilege를 갖지 못한다. 운영체제는 기본적으로 운영체제가 아닌 프로그램은 신뢰하지 않고, 따라서 privilege를 주지 않는다.


https://en.wikipedia.org/wiki/Protection_ring

이제부터는 Ring 3 영역에서 유저 프로그램이 실행될 것이다. 이 때 유저 프로그램은 자신의 User virtual memory에만 접근할 수 있다. 만약 유저 프로그램이 Kernel virtual memory에 접근을 시도하면, page fault를 야기하고 프로세스가 종료된다.

Virtual Memory Layout

USER_STACK +----------------------------------+
           |             user stack           |
           |                 |                |
           |                 |                |
           |                 V                |
           |           grows downward         |
           |                                  |
           |                                  |
           |                                  |
           |                                  |
           |           grows upward           |
           |                 ^                |
           |                 |                |
           |                 |                |
           +----------------------------------+
           | uninitialized data segment (BSS) |
           +----------------------------------+
           |     initialized data segment     |
           +----------------------------------+
           |            code segment          |
 0x400000  +----------------------------------+
           |                                  |
           |                                  |
           |                                  |
           |                                  |
           |                                  |
       0   +----------------------------------+

유저 가상 메모리는 위 그림과 같은 레이아웃을 가진다.

시스템 콜

유저 프로그램이 실행되던 도중 interrupt가 발생하는 순간 CPU는 커널 모드로 바꾸고 운영체제는 발생한 interrupt를 handling 한다.
즉, interrupt가 발생하면 유저 모드에서 커널 모드로 변환이 일어난다고 할 수 있다.

운영체제의 구성 상, 커널과 Application은 CPU의 권한 수준이나 하드웨어 접근 능력이 다르다. 유저 프로그램이 주어진 instruction들을 수행하다가, Direct I/O와 같이 privilege가 필요한(즉, OS 커널만 수행할 수 있는) 작업이 필요한 경우, 커널 모드로의 전환이 필요하다.
이 때 유저 모드 -> 커널 모드로 변하게 만드는 다른 한 가지 방법이 바로 시스템 콜(system call)이다.

시스템 콜(System call)
👉 운영체제의 커널이 제공하는 서비스에 대해, Application의 요청에 따라 커널에 접근하기 위한 Interface

유저 프로그램은 시스템 콜을 호출하여 커널 모드로 전환하여 필요한 작업들을 수행하고, 다시 유저 모드로 돌아간다. 이 때 사용되는 low-level instruction이 syscall이라는 instruction 이다. syscall instruction을 사용하기 위해서는 적절한 시스템 콜 number가 CPU 레지스터에 로드되어야 한다. pintos에서 유저프로그램은 시스템 콜 번호를 %rax에, 1~6번째 인자는 각각 %rdi, %rsi, ...%r10에 저장한 후, 시스템 콜을 만들기 위해 syscall을 호출한다.

시스템 콜 핸들러 syscall_handler() 가 제어권을 얻으면 시스템 콜 번호는 rax 에 있고, 인자는 %rdi, %rsi, %rdx, %r10, %r8, %r9 순서로 전달

이 때 커널 스택에 있는 interrupt frame이 전달되므로 시스템 콜 핸들러를 호출한 프로세스의 레지스터에 접근할 수 있다. 시스템 콜 핸들링을 마치고 리턴되는 값은 rax에 저장된다.

__attribute__((always_inline))
static __inline int64_t syscall (uint64_t num_, uint64_t a1_, uint64_t a2_,
		uint64_t a3_, uint64_t a4_, uint64_t a5_, uint64_t a6_) {
	int64_t ret;
	register uint64_t *num asm ("rax") = (uint64_t *) num_;			// system call number
	register uint64_t *a1 asm ("rdi") = (uint64_t *) a1_;
	register uint64_t *a2 asm ("rsi") = (uint64_t *) a2_;
	register uint64_t *a3 asm ("rdx") = (uint64_t *) a3_;
	register uint64_t *a4 asm ("r10") = (uint64_t *) a4_;
	register uint64_t *a5 asm ("r8") = (uint64_t *) a5_;
	register uint64_t *a6 asm ("r9") = (uint64_t *) a6_;

	__asm __volatile(
			"mov %1, %%rax\n"
			"mov %2, %%rdi\n"
			"mov %3, %%rsi\n"
			"mov %4, %%rdx\n"
			"mov %5, %%r10\n"
			"mov %6, %%r8\n"
			"mov %7, %%r9\n"
			"syscall\n"
			: "=a" (ret)
			: "g" (num), "g" (a1), "g" (a2), "g" (a3), "g" (a4), "g" (a5), "g" (a6)
			: "cc", "memory");
	return ret;
}

Argument passing

유저 프로그램이 올바르게 실행되기 위해서는, User stack을 Setting하고, 실행할 프로그램의 argument들을 User stack에 적절히 넣어주어야 한다.

예를 들어 /bin/ls -l foo bar 와 같은 명령이 주어졌을 때, 아래와 같이 stack에 푸쉬해야 한다. 이를 위해서

1) 주어진 명령을/bin/lslfoobar 과 같이 실행 파일과 argument들로 분리한다.
2) parsing된 arg들을 스택의 맨 처음 부분에 넣는다. 이 arg들은 char pointer(char *)로 참조할 것이다.
3) 각 문자열의 주소 + 경계조건을 위한 널포인터를 스택에 오른쪽→왼쪽 순서로 푸시한다.
이 null pointer는 argv[argc]가 null pointer라는 사실을 보장해준다. 또한 이 순서는 argc-> argc-1 -> argc-2, ... 순으로 내려가면서 argv[0]이 가장 낮은 가상 주소를 가진다는 사실을 보장해준다.

word-align을 수행하는 이유는 word-size로 정렬된 접근의 속도가 더 빠르기 때문이다. 따라서 스택 포인터를 word-size(8의 배수)로 반올림하기 위해 rsp가 8의 배수를 가질 때 까지 null pointer를 삽입한다.

4) %rsiargv 주소(argv[0]의 주소)를 가리키게 하고, %rdiargc 로 설정한다.

5) 마지막으로 fake “return address”를 푸시한다. 이는 해당 모든 스택 프레임이 동일한 구조를 갖도록 하기 위함이다.

위 과정을 통해 전달 받은 /bin/ls -l foo bar 명령이 아래와 같이 User stack에 쌓는다.

AddressNameDataType
0x4747fffcargv[3][...]'bar\0'char[4]
0x4747fff8argv[2][...]'foo\0'char[4]
0x4747fff5argv[1][...]'-l\0'char[3]
0x4747ffedargv[0][...]'/bin/ls\0'char[8]
0x4747ffe8word-align0uint8_t[]
0x4747ffe0argv[4]0char *
0x4747ffd8argv[3]0x4747fffcchar *
0x4747ffd0argv[2]0x4747fff8char *
0x4747ffc8argv[1]0x4747fff5char *
0x4747ffc0argv[0]0x4747ffedchar *
0x4747ffb8return address0void (*) ()

Implementation : Argument Passing

void push_args(char **argv, int argc, struct intr_frame *if_){
	char *arg_address[128];
	
	for (int i=argc-1; i>=0; i--){
		size_t arg_size = strlen(argv[i]) + 1;  // include sentinel (\0)
		if_->rsp -= arg_size;        // 인자 크기만큼 스택을 늘려줌
		memcpy(if_->rsp, argv[i], arg_size);
		arg_address[i] = if_->rsp;	// arg_address에 인자를 복사해준 주소값을 저장
	}

	while((uintptr_t)if_->rsp % 8 != 0){
		if_->rsp--;
		*(uint8_t *)if_->rsp = 0;
	}
	
	for (int i=argc; i>=0; i--){
		if_->rsp -= sizeof(char *);

		if (i == argc){
			memset(if_->rsp, 0, sizeof(char *));
		}
		else{
		memcpy(if_->rsp, &arg_address[i], sizeof(char *));
		}
	}

	if_->rsp -= sizeof(void *);
	memset(if_->rsp, 0, sizeof(void (*)));

	if_->R.rdi = argc;
	if_->R.rsi = if_->rsp + sizeof(void *);

}

User Memory Access

Pintos Project 2에서 시스템 콜 기능을 구현하고, 유저 가상 주소 공간에 데이터를 읽고 쓰는 과정이 필요하다. 이 과정에서 만약 User mode에서 실행하던 프로그램이 유효하지 않은 메모리 영역(커널 영역 또는 할당받지 않은 영역)을 가리킬 경우 이를 처리해주어야 한다.

이것을 Page fault라고 하는데, Page fault에 대한 handling은 Project 3 : Virtual Memory에서 다루기 때문에 현재로서는 page fault handler에서 exit(-1)을 해줌으로써, 유저 프로그램이 유효하지 않은 주소에 접근하면 프로세스를 종료시키도록 한다.

System Call

Pintos Project 2 : User program의 핵심이다. 시스템 콜의 기반이 되는 구조와 기능들을 구현한다. 리눅스에서 사용하는 시스템 콜은 300개가 넘는데, pintos에서는 핵심 시스템 콜 함수들을 구현한다.

void
syscall_handler (struct intr_frame *f UNUSED) {
	/* Arguments: %rdi %rsi %rdx %r10 %r8 %r9 */
	switch(f->R.rax){
		case SYS_HALT:
		{
			halt();
		}
		case SYS_EXIT:
		{
			int status = f->R.rdi;
			exit(status);
		}
		case SYS_FORK:
		{
			f->R.rax = fork(f);
			break;
		 }
		case SYS_EXEC:
		{
			const char *file = f->R.rdi;
			f->R.rax = exec(file);
			break;
		}
		case SYS_WAIT:
		{
			pid_t pid = f->R.rdi;
			f->R.rax = wait(pid);
			break;
		}
		case SYS_CREATE:
		{
			const char *file = f->R.rdi;
			unsigned int initial_size = f->R.rsi;
			f->R.rax = create(file, initial_size);
			break;
		}
		case SYS_REMOVE:
		{
			const char *file = f->R.rdi;
			f->R.rax = remove(file);
			break;
		}
		case SYS_OPEN:
		{
			const char *file = f->R.rdi;
			int fd = open(file);
			f->R.rax = fd;
			break;
		}
		case SYS_FILESIZE:
		{
			int fd = f->R.rdi;
			int file_size = filesize(fd);
			f->R.rax = file_size;
			break;
		}
		case SYS_READ:
		{
			int fd = f->R.rdi;
			void *buffer = f->R.rsi;
			unsigned int size = f->R.rdx;
			f->R.rax = read(fd, buffer, size);
			break;
		}
		case SYS_WRITE:
		{
			int fd = f->R.rdi;
			void *buffer = f->R.rsi;
			unsigned int size = f->R.rdx;
			f->R.rax = write(fd, buffer, size);
			break;
		}
		case SYS_SEEK:
		{
			int fd = f->R.rdi;
			unsigned position = f->R.rsi;
			seek(fd, position);
			break;	
		}
		case SYS_TELL:
		{
			int fd = f->R.rdi;
			unsigned result = tell(fd);

			f->R.rax = result;
			break;
			
		}
		case SYS_CLOSE:
		{	
			int fd = f->R.rdi;
			close(fd);
			break;
		}
		default:
		{
			thread_exit();
		}
	}
}

모든 시스템 콜 함수를 구현하는데 성공했다. File-related 시스템 콜들은 file을 다루는 기존 함수들이 존재해서 file descriptor table을 만든 뒤 fd를 할당 하고 예외 처리를 해주면 모든 테스트 케이스들이 통과했다.

다만 Project 2에서는 예외처리가 촘촘히 되었는지 확인하기 어렵다. 그 이유는 유저가 유효하지 않은 주소(할당받지 않았거나, 유저 영역이 아닌 곳)에 접근했을 때 page_fault handler에서 exit(-1)을 수행하도록 했기 때문에, 예외 상황에서 대부분 exit(-1)이 되면 pass가 출력되기 때문이다.

또한 공유 자원을 사용하기 위해 임계 영역(critical section)에 들어 왔을 때, file을 위한 lock을 만들어 사용 했지만, load 함수가 호출 될 때는 global lock을 사용하지 않았음에도 모든 테스트 케이스들이 통과했다. 물론 테스트 케이스를 통과하는 것이 전부가 아니지만, 일단은 여기까지 하고 Project 3,4에 넘어갔을 때 문제가 생기면 그 때 해결하도록 할 것이다.

profile
Whatever I want

0개의 댓글