syscall로 알아보는 시스템콜 (with pintOS)

쏭웬·2023년 10월 9일
0

정글

목록 보기
10/11

(지적 환영)

유저 프로그램을 실행하다

유저 프로그램이란 뭘까?

유저프로그램은 (대부분) (컴파일된) 실행파일이다. 컴퓨터의 하드디스크 어딘가에 실행파일이 저장되어있고 운영체제는 그 파일을 실행시킴으로써 유저프로그램이 시작된다.

pintos에서 유저프로그램 실행시키기

우리 핀토스 운영체제는 깡통 가상머신위에 세팅한 거라 유저프로그램이고 뭐고 그냥 아무것도 없다. 운영체제 이미지로 OS 올리는 것 부터 해줘야 한다. OS 올리는 게 make 할 때 일어나는 일들이다.

핀토스문서 발췌

핀토스 문서를 읽어봐서 알겠지만 저번주에 우리는 커널위에서 돌아가는 코드를 짰다. 하지만 이번엔 유저프로그램을 실행시켜야 하기때문에 저번주와 테스트 실행 명령부터 다르다. 첫번째 줄이 1주차 때 실행했던 스레드 테스트고 두번째 줄이 2주차에 실행했던 유저프로그램 테스트다.

테스트명령어

눈에 띄게 명령어가 길어졌다. 길어진 이유는 크게 2가지.

  1. -p 옵션으로 파일을 올려줬다.
  2. --fs-disk=10로 파일 시스템 디스크 사이즈를 설정해주었다.

tests/userprog/args-single 이 경로에 있는 파일을 arg-single이라는 실행파일로만들어줬다. 파일을 올리기 때문에 파일시스템디스크 크기를 기본값이 아닌 좀 더 크게 10MB로 설정했다.

이제 우리의 깡통 가상머신에 make와 pintos 명령어로 운영체제와 유저프로그램을 올려주었다. run 명령어로 그 유저프로그램을 실행시킬 것이다.

시스템 콜의 발생


void
test_main (void) 
{
  pid_t children[CHILD_CNT];
  int fd;

  CHECK (create (file_name, sizeof buf), "create \"%s\"", file_name);
  CHECK ((fd = open (file_name)) > 1, "open \"%s\"", file_name);
  random_bytes (buf, sizeof buf);
  CHECK (write (fd, buf, sizeof buf) > 0, "write \"%s\"", file_name);
  msg ("close \"%s\"", file_name);
  close (fd);

  exec_children ("child-syn-read", children, CHILD_CNT);
  wait_children (children, CHILD_CNT);
}

유저 프로그램 파일의 내용을 읽다보니가 create, open, write 등의 명령어를 만났다. 얘들은 우리가 일반적으로 시스템 콜이라고 당연하게 생각하고 있었지만 이번엔 왜 시스템 콜인지 그리고 시스템 콜이 뭔지 알아보자.

시스템 콜

운영 체제의 커널이 제공하는 서비스에 대해, 응용 프로그램의 요청에 따라 커널에 접근하기 위한 인터페이스

그러니까 일반적인 사용자프로그램은 물리주소를 확정할 수도 없고 cpu 권한 수준도 사용자 공간에 한정되어 있다. 하지만 파일 읽고 쓰는 파일시스템이나 메모리를 할당하는 일들은 실제로 물리주소에 접근해야 하는데 이땐 응용프로그램 수준에서 할 수 없으니까 커널을 부르는 과정인 것이다.

핀토스에서 시스템 콜이 이루어지는 과정

x86 아키텍처에서는 "syscall" 이라는 인스트럭션을 이용해서 시스템콜을 할 수 있다. syscall 설명을 읽어보면 권한수준을 가장 높은 0으로 만들어 주는 명령이라고 적혀있습니다. 레벨 0은 모든 리소스에 다 접근할 수 있는 커널 모드를 말한다.

syscall설명

출처 : https://www.felixcloutier.com/x86/syscall

1. syscall

우리 핀토스에서는 /lib/user/syscall.c 파일을 보면 syscall을 호출하는 어셈블리어를 볼 수 있다.


__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_;
	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;
}

이 파일 아래에는 어떤 함수들이 syscall()을 부르는 지 적혀있다. 우리가 흔히 시스템 콜이라고 생각했던 함수들이다.

시스콜하는함수들

2. syscall이 발생하면 어떻게 되나

그럼 운영체제는 x86 인스트럭션 syscall이 발생하면 어떤 일을 할까? 그것도 운영체제를 만드는 사람들이 다 정의를 해 놓았다. syscall_init()이라는 함수에 적혀있는데 syscall_init() 함수는 init() 할 때 실행된다.


int main (void) {
	...

#ifdef USERPROG
	exception_init ();
	syscall_init ();
#endif
	
	...

}
void syscall_init(void)
{
	sema_init(&syn_sema, 1);
	write_msr(MSR_STAR, ((uint64_t)SEL_UCSEG - 0x10) << 48 |
							((uint64_t)SEL_KCSEG) << 32);
	write_msr(MSR_LSTAR, (uint64_t)syscall_entry);

	/* The interrupt service rountine should not serve any interrupts
	 * until the syscall_entry swaps the userland stack to the kernel
	 * mode stack. Therefore, we masked the FLAG_FL. */
	write_msr(MSR_SYSCALL_MASK,
			  FLAG_IF | FLAG_TF | FLAG_DF | FLAG_IOPL | FLAG_AC | FLAG_NT);
}

여기를 보면 syscall_entry 라는 거랑 연결되어 있다고 적혀있다. 이 파일도 역시 주어져 있는데 뜯어보면 결국 syscall_handler와 연결되어 있다.

syscall_entry

3. syscall_handle

syscall_handler는 우리가 이번주에 열심히 만들었던 바로 그 함수다.

syscall_handler

여기서 해당 명령이 들어올 때 어떻게 처리할 지 운영체제 입장에서 적어주는 게 이번주에 했던 것이다. 적절하게 원하는 바를 처리해줄 뿐만 아니라 하드웨어를 파괴하는 명령은 적당히 컷해주고 이상한 주소값에 접근하는 명령도 거부해주면 좋은 운영체제를 만들 수 있게 된다.

profile
중꺽그마

0개의 댓글