정글사관학교 개발일지/운영체제-PintOS

PintOS Project 2 - User Program (6) System Call(정글사관학교 68일차 TIL) - system call 흐름 정리

Woonys 2022. 1. 6. 18:03
반응형

시스템 콜 과제 이어서. 한양대 핀토스 pdf에서는 이 챕터를 process hierachy라고 명명하는데, 그냥 system call로 이어서 하겠다.

 

지난 과제에서는 halt(), create(), remove(), exit()에 대해 정리했다. 여기서 잠시 리마인드해보기. 이 과제를 왜 하고 있지? 지금 우리가 하는 일은 시스템 콜 핸들러를 구현하는 것이다. 왜 이게 필요하지? 유저 프로그램에서 운영체제 혹은 하드웨어에 직접 접근하지 않고 커널에다가 요청(시스템 콜)만 하면 운체가 시스템 콜 핸들러를 이용해 내부적으로 작업하고 결과값만 휘리릭~하고 넘겨주기 위한 것이다. 이 작업으로 유저 프로그램은 복잡한 아랫단까지 내려갈 것 없이 결과만 호로록 받으면 되는 것! 뿐만 아니라 이렇게 작업하면 유저 프로그램이 다른 유저 프로그램에도 접근할 수 없기에(isolation) 보안 차원에서도 장점이다.

 

시스템 콜의 동작 원리에 대해 좀 더 깊이 파보자. 정글에서 중요한 건 내가 이 과제를 구현했나 아니냐도 있지만 그보다 이걸 배우기 위해 어떻게 접근했고 어떤 과정을 거쳤는가이다. 그럼 스타뚜.

 

1.  Pintos 부팅 ~ 파일 실행 전까지

pintos --fs-disk=10 -p tests/userprog/args-single:args-single -- -q -f run 'args-single onearg'

먼저, 우리가 user program에서 테스트하는 코드부터 다시 찬찬히 뜯어보기로 했다.  위 코드를 실행하면 아래와 같은 결과가 나온다. 이전까지는 대충 넘어갔는데, 이걸 찬찬히 살펴보자. 출력하는 라인 위주로 뜯어볼 예정.

 

1. qemu 에뮬레이터 실행: qemu-system-x86_64

 

코드를 입력하면 가장 먼저 실행되는 건 qemu-system-x86 64이다. 이는 에뮬레이터로, 우리가 빌린 리눅스 컴퓨터 내에서 또 하나의 OS인 pintos를 띄우기 위해 가상화를 지원하는 에뮬레이터라고만 알자.

 

 

2. int main() 실행: kernel command line: -q -f put args-single run 'args-single onearg'

에뮬레이터가 실행되고 나면 곧바로 Pintos 부팅이 시작된다. pintos 부팅은 init.c를 실행하면서 시작된다. int main()으로 가보자. 

 

/* Pintos main program. */
int
main (void) {
	...

	/* Clear BSS and get machine's RAM size. */
	bss_init ();

	/* Break command line into arguments and parse options. */
	argv = read_command_line ();
	argv = parse_options (argv);

	/* 스레드, 콘솔, 말록, 페이징, tss, gdt 등등 초기화 */
	thread_init ();
	console_init ();

	mem_end = palloc_init ();
	malloc_init ();
	paging_init (mem_end);

#ifdef USERPROG
	tss_init ();
	gdt_init ();
#endif

	/* Initialize interrupt handlers. */
	intr_init ();
	timer_init ();
	kbd_init ();
	input_init ();
#ifdef USERPROG
	exception_init ();
	syscall_init ();
#endif
	/* Start thread scheduler and enable interrupts. */
	thread_start ();
	serial_init_queue ();
	timer_calibrate ();


	printf ("Boot complete.\n");

	/* Run actions specified on kernel command line. */
	run_actions (argv);

	/* Finish up. */
	if (power_off_when_done)
		power_off ();
	thread_exit ();
}

 

위에서 argv = read_command_line()이라는 코드가 있는데, 이 read_command_line() 함수에서 우리가 입력한 input 코드를 읽어들인 다음 kernel command line: -q -f put args-single run 'args-single onearg'를 출력한다. 보면 뭔가 이상한 게, "아니 이미 parsing이 구현되어 있는데?!" -> 여기서 parsing은 우리가 raw하게 입력한 명령어에서 실제 필요한 부분만 잘라내는 역할이다.

 

이 read_command_line()에 가보면 ptov 라는 매크로가 있는데, 얘의 역할은 물리 주소를 가상 주소로 변환하는 역할이다. 보는 것처럼 물리주소에 KERN_BASE만큼 더해서 가상주소로 변경해준다. 이는 이전에 정리했던 베이스 & 바운드 방식(physical addr = virtual addr + base)이다.

/* Returns kernel virtual address at which physical address PADDR
 *  is mapped. */
#define ptov(paddr) ((void *) (((uint64_t) paddr) + KERN_BASE))

 

이어서 나오는 출력값인 숫자의 나열은 패스하고, 다음 줄로 넘어간다.

 

3. thread_init() 실행: main thread 실행

스레드를 초기화하는 함수. 여기서 main 스레드가 실행된다. 코드를 보자. "main"이라는 이름을 인자로 넣어 init_thread()가 실행되는 것을 볼 수 있다. init_thread()에서는 해당 스레드에 메모리를 세팅해주고 스레드 구조체 내 interrupt frame 멤버에 해당하는 tf의 하위 멤버이 rsp에 커널 스택 포인터 위치를 저장한다. 현재 t는 스택의 가장 아래 위치를 나타낸다. 여기에 page size- sizeof(void *)를 계산하면 스택의 가장 끝 영역을 가리키는데 여기가 커널 스택 포인터의 위치이다. 여기서의 스택은 main 스레드의 커널 스택에 해당한다. 왜 이 값을 rsp에 저장하는지는 이 부분은 아래 tss를 설명할 때 나온다.

 

빨간색 표시. 여기서 커널 스택에 뭐가 쌓이면 포인터가 계속 내려오겠지.

 

void
thread_init (void) {
	ASSERT (intr_get_level () == INTR_OFF);

	...
    
	/* Set up a thread structure for the running thread. */
	initial_thread = running_thread ();
	init_thread (initial_thread, "main", PRI_DEFAULT);
	initial_thread->status = THREAD_RUNNING;
	initial_thread->tid = allocate_tid ();
}

static void
init_thread (struct thread *t, const char *name, int priority) {
	
    ...

	memset (t, 0, sizeof *t);
	t->status = THREAD_BLOCKED;
	strlcpy (t->name, name, sizeof t->name);
	t->tf.rsp = (uint64_t) t + PGSIZE - sizeof (void *);
	t->priority = priority;
	t->magic = THREAD_MAGIC;

	/* --- Project 1.4 priority donation --- */
	/* --- 자료구조 초기화 --- */
	t->init_priority = priority;
	t->wait_on_lock = NULL;
	list_init(&t->donations);
	/* project 2: process hierachy */
	list_init(&t->child_list);
	t->exit_status = 0;

 

 

 

4. palloc_init() 실행: Pintos booting with: ~~ / base_mem: ~~ / ext_mem: ~~

명령어를 읽어들이는 작업을 하고 나면 각종 초기화 세팅을 해주는 작업을 거친다. 스레드부터 시작해서 콘솔, malloc 초기화를 거치면 다음에 페이지 할당 초기화 작업을 수행한다. 함수로 가보자.

 

/* Initializes the page allocator and get the memory size */
uint64_t
palloc_init (void) {
  /* End of the kernel as recorded by the linker.
     See kernel.lds.S. */
	extern char _end;
	struct area base_mem = { .size = 0 };
	struct area ext_mem = { .size = 0 };

	resolve_area_info (&base_mem, &ext_mem);
	printf ("Pintos booting with: \n");
	printf ("\tbase_mem: 0x%llx ~ 0x%llx (Usable: %'llu kB)\n",
		  base_mem.start, base_mem.end, base_mem.size / 1024);
	printf ("\text_mem: 0x%llx ~ 0x%llx (Usable: %'llu kB)\n",
		  ext_mem.start, ext_mem.end, ext_mem.size / 1024);
	populate_pools (&base_mem, &ext_mem);
	return ext_mem.end;
}

우리가 쓸 수 있는 메모리(base memory, external memory)가 얼마나 되는지 표시해준다.

 

 

5. tss_init() 실행

위 출력값에 들어가지는 않으나 필요한 개념이라 하나 추가한다. 이후에 시스템 콜에서 나오는 개념이기 때문이다. 위에서 tss_init()을 실행하는데, tss는 task-state segment를 뜻한다. 여기서 task라는 개념이 나오는데,  task는 프로세서가 수행하는 작업의 최소 단위를 뜻한다. task는 곧 process이자 thread가 될 수도 있는데(좀 애매하긴 하다. 링크 참고), 인터럽트 혹은 exception 핸들러, 커널 등의 OS 서비스 작업을 수행하는데 사용한다. 이 task는 두 가지 파트로 구성되어 있는데, 하나는 태스크 수행 공간(task execution space)으로, 태스크 역시 코드/데이터/스텍 세그먼트를 가진다. 그리고 이 태스크의 상태를 관리하는 세그먼트가 하나 더 있는데, 그게 tss이다. 이 친구는 예전(x86 32비트)에는 task switching이라고 해서 작업 간 switching을 수행할 때 사용됐다. 하지만 여기 x86-64에서는 task switching이 사용되지 않는다고 한다. context switching이 이미 이 역할을 해주고 있기 때문에 사라진 게 아닐까. x86에서는 뭔가 task와 process/thread 간에 명확한 분리가 있던 것 같은데 64비트로 넘어오면서 이 개념이 모호해진듯. 그래서 빠진 것 같다.

 

그럼 x86-64에서 tss의 역할은 무엇일까? Gitbook에서는 이렇게 나와있다.

"TSS(Task-State Segment)는 x86 아키텍처 task switching에 사용되었습니다. 그러나 x86-64에서는 task switching이 더 이상 사용되지 않습니다. 그럼에도 불구하고 TSS는 링 스위칭 동안 스택 포인터를 찾기 위해 여전히 존재합니다.

이것은 사용자 프로세스가 인터럽트 핸들러에 들어갈 때 하드웨어가 커널의 스택 포인터를 찾기 위해 tss를 참조한다는 것을 의미합니다."

 

이게 무슨 말일까? 사용자 프로세스에서 시스템 콜을 호출하면 인터럽트 핸들러로 들어간다. 이때 커널의 스택 포인터를 찾아서 거기에 이제까지 사용자 프로세스에서 진행했던 작업을 쌓아야 하는데, 이때 커널 스택을 찾는데 드는 오버헤드를 줄이기 위한 용도로 tss를 사용한다. 즉, tss는 해당 유저 프로세스에 대응하는 커널 프로세스에 대해 해당 커널 스택 포인터 끝을 가리키고 있다는 뜻이다! 아래 코드를 보자.

 

/* Initializes the kernel TSS. */
void
tss_init (void) {
	/* Our TSS is never used in a call gate or task gate, so only a
	 * few fields of it are ever referenced, and those are the only
	 * ones we initialize. */
	tss = palloc_get_page (PAL_ASSERT | PAL_ZERO);
	tss_update (thread_current ());
}

/* Returns the kernel TSS. */
struct task_state *
tss_get (void) {
	ASSERT (tss != NULL);
	return tss;
}

/* Sets the ring 0 stack pointer in the TSS to point to the end
 * of the thread stack. */
void
tss_update (struct thread *next) {
	ASSERT (tss != NULL);
	tss->rsp0 = (uint64_t) next + PGSIZE;
}

 

tss_init을 보면 tss에 초기화(페이지를 할당)해주고 현재 스레드(initial(main) 스레드로 부팅 중인 상황임)에 대해 rsp0 레지스터에다가 스레드의 주소 + PGSIZE를 더한다. 여기서 tss_update에 들어가는 thread는 thread_current()로 부팅 작업을 하고 있는 커널 스레드이다. 즉, 커널 스레드 주소의 시작 부분에 페이지 크기만큼 더하면 커널 페이지의 끝 부분을 가리킨다는 것을 알 수 있다.

 

참고로 아래 이미지를 보면 알겠지만, rsp 레지스터에는 스택 포인터 값이 들어가있다. 이때 스택 포인터가 바로 커널 스택 포인터! 이를 tss 구조체 내에 저장한다는 의미다. 따라서 우리는 일일이 특정 사용자 프로세스를 잡을 때 이에 대응하는 커널을 찾을 필요 없이 tss에 들어가면 바로 커널 스택 끝 주소를 알아낼 수 있다는 뜻이 된다. 이 커널 스택 포인터를 찾으면 곧바로 %rsp에 이 값을 넣어주면 되겠지.

 

 

이후에 timer, 하드 디스크 초기화까지 진행하고 나면 Boot complete이 뜬다. 이제 본격적으로 입력한 값을 실행한다.

 

6. fsutil_put() 실행: Putting 'args-single' into the file system...

 

이건 그냥 가볍게 패스. 하드 디스크 내 파일 시스템에 명령어를 복사해서 넣어준다 정도로만 이해하면 될듯.

 

7. run_action() -> run_task() 실행: Executing 'args-single onearg'

대망의 run_task()를 실행한다. 이제 거의 다 와간다! 인자로 들어가는 값(args-single onearg)을 보면 알겠지만 그 전까지 입력해줬던 값은 이미 parsing되어 잘려나갔다.

static void
run_task (char **argv) {
	const char *task = argv[1];

	printf ("Executing '%s':\n", task);
#ifdef USERPROG
	if (thread_tests){
		run_test (task);
	} else {
		process_wait (process_create_initd (task));
	}
#else
	run_test (task);
#endif
	printf ("Execution of '%s' complete.\n", task);

여기서 process_create_initd()를 실행해 우리가 입력해준 'args-single onearg'에 대한 프로세스를 생성한다. 이어서 process_wait()으로 반환값이 인자로 들어가며 대기한다. 이때 들어가는 인자 task는 argv[1] = 'args-single onearg'이다. argv[0]은 뭔가 싶어 찍어보니 "run"이더라.

 

여기서 wait()을 통해 무한 대기 루프로 들어가는데, 우리는 이전에 timer_init()으로 타이머 인터럽트를 초기화해줬다. 정해진 시간 동안 기다리다가 타이머가 작동하면 initial 스레드에서 우리가 실행하고자 하는 사용자 프로세스에 대응하는 커널 스레드(아직 운영체제가 cpu를 잡고 있는 것은 동일)로 프로세스가 switch된다. 당연히 내부 레지스터 역시 main 스레드 내용에서 args 커널 스레드로 변경(context switching).

 

8. process_create_initd() -> initd() 실행: 첫 유저 프로세스 실행

 

여기서 'args-single onearg'에 대한 프로세스를 생성하는데, thread_create()에서 initd()를 실행하고 해당 파일 이름으로 스레드를 생성한다. initd()는 첫번째 유저 프로세스를 실행하는 함수이다. 왜 첫번째라고 명시하냐면, 그 다음부터는 fork()를 통해 프로세스를 생성하면 되기 때문! 첫 프로세스를 생성할 때만 initd()를 사용한다. initd()에서는 process_init()에 이어 process_exec()을 실행한다.

 

 

tid_t
process_create_initd (const char *file_name) {
	char *fn_copy;
	tid_t tid;
	/* Make a copy of FILE_NAME.
	 * Otherwise there's a race between the caller and load(). */
	fn_copy = palloc_get_page (0);
	if (fn_copy == NULL)
		return TID_ERROR;
	strlcpy (fn_copy, file_name, PGSIZE);
	/* project 2. command line parsing */
	// char *token, *last;
	// token = strtok_r(file_name, " ", &last);
	// tid = thread_create (token, PRI_DEFAULT, initd, fn_copy);
	/* project 2. command line parsing */


	/* Create a new thread to execute FILE_NAME. */
	tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
	if (tid == TID_ERROR)
		palloc_free_page (fn_copy);
	return tid;
}

/* A thread function that launches first user process. */
static void
initd (void *f_name) {
#ifdef VM
	supplemental_page_table_init (&thread_current ()->spt);
#endif

	process_init ();

	if (process_exec (f_name) < 0)
		PANIC("Fail to launch initd\n");
	NOT_REACHED ();
}

 

 

8. process_exec() 실행

 이전 과제에서 했던 작업인 argument parsing 및 유저 커널 스택에 정보를 올리는 작업(load)을 수행한다. 이 작업을 수행하는 주체 역시도 커널 스레드라는 점을 헷갈리지 말자. load()를 통해 사용자 프로세스 작업을 수행하기 위한 인터럽트 프레임 구조체 내 정보를 유저 커널 스택에 쌓는다. 여기 내부에서 tss_update()도 실행하는데, 자세한 건 패스. 이후 argument_stack() 함수를 실행해 입력받은 인자들 역시 이 스택에 쌓는다.

 

이 과정이 끝나면 hex_dump()를 실행하는데, 그러면 우리가 입력한 값이 parsing이 잘 되는지를 확인하는 게 여기서이다.

 

 

그러고 나면 do_iret()을 수행한다. (여기서 진짜 사용자 프로세스로 cpu가 넘어가는 상황!) 위에서 지역 구조체로 _if를 생성했다. 얘는 현재 스레드인 arg-single onearg 에 유저 메모리에 관련된 정보이다. exec() 자체가 새로운 프로세스로 탈바꿈 하는 애이니, 여기서 do_iret()에 exec() 내에서 만들어준 _if 구조체 내 값으로 레지스터 값을 수정한다. 여기서 SEL_UDSEG, SEL_UCSEG는 각각 유저 메모리의 데이터, 코드 선택자로 유저 메모리에 있는 데이터, 코드 세그먼트를 가리키는 주소값이다.

 

int
 process_exec (void *f_name) { // 유저가 입력한 명령어를 수행하도록 프로그램을 메모리에 적재하고 실행하는 함수. 여기에 파일 네임 인자로 받아서 저장(문자열) => 근데 실행 프로그램 파일과 옵션이 분리되지 않은 상황.
	
    ...(인자 파싱 작업)...

	/* We cannot use the intr_frame in the thread structure.
	 * This is because when current thread rescheduled,
	 * it stores the execution information to the member. */
	struct intr_frame _if; // intr_frame 내 구조체 멤버에 필요한 정보를 담는다.
	_if.ds = _if.es = _if.ss = SEL_UDSEG; // user data selector
	_if.cs = SEL_UCSEG; // user code selector
	_if.eflags = FLAG_IF | FLAG_MBS;

	/* And then load the binary */
	success = load (file_name_copy, &_if); // file_name, _if를 현재 프로세스에 load.
	
    (...)

	hex_dump(_if.rsp, _if.rsp, USER_STACK - _if.rsp, true);

    (...)
	/* Start switched process. */
	
	do_iret (&_if);
	NOT_REACHED ();
}

 

 

9. do_iret() 실행: arg.c로 넘어간다!

 

진짜 너무 딥하다..하지만 포기할 수 없다..

 

왼쪽 do_iret()을 실행하면 아래 어셈블리어 명령을 수행한다. 보면 처음에 %0을 rsp에 입력해준다. 이러면 해당 구조체가 들어있는 메모리에서 가장 낮은 값의 주소를 갖는다. (가상주소) 오른쪽 이미지를 보면 rsp는 intr_frame 구조체에서 가장 윗단에 있는 gp_registers R을 가리킨다. 이 R 안에 r15, r14, ... ,rax까지 들어있다. 동시에 이 r15, ..., rax는 레지스터의 이름이기도 하다!

 

따라서 왼쪽 명령어는 "인터럽트 프레임 tf에 있는 %%rsp가 가리키는 값들을 레지스터에 넣으세요!"이다.

 

"movq 숫자(%%rsp)"에서 숫자값은 바이트 크기다. 우리가 작업하고 있는 영역은 메모리인데, r15에서 r14로 내려가려면 인터럽트 프레임 구조체 내에서 r15 멤버 크기가 64비트(8바이트)이니 8바이트를 내리면 해당 포인터는 r14를 가리키게 된다. 이렇게 8의 배수로 쭉 내리면서 인터럽트 프레임 내 gp_register 값들을 하나하나 레지스터에 넣어주는 것.

 

다 넣고 나면 (여기는 아직 잘 모르겠는데) addq 작업을 통해 우리는 이제 arg.c로 이동해 main 함수를 실행한다.

 

10. args.c -> main() 실행

 

거진 지하 158층에 도달했다..어질하네.. 우리는 args라는 값을 실행하라고 어셈블리어 명령을 통해 레지스터에 넣어줬고 따라서 arg.c를 실행하게 된다. 아래 코드가 arg.c 코드인데, 곧바로 main()을 실행한다. 여기서 msg 함수를 실행한다. if 문 안의 함수든 그 바깥의 msg(begin)이든 상관없다. 이 msg()가 중요하고 이제 진짜 끝났다. msg()를 타고 가보자.

 

#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 ("argc = %d", argc);
  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;
}

 

 

11. msg() -> vmsg() -> write(): 드디어 시스템 콜!!!!

아래처럼 msg를 실행하면 쭉 가다가 vmsg()라는 놈이 나온다. 바로 넘어가면 vmsg()에서 우리가 그토록 기다리던 시스템 콜인 write()이 나온다!!! 참고로 여기서 write()은 우리가 구현해야 할 write()이긴 한데 그전에 아직 유저 모드에서 시스템 콜을 하는 write()에 해당한다. 즉, 커널 코드가 아닌 유저 코드(write()은 시스템 콜을 요청하는 유저 코드임!). 따라서 lib/user/syscall.c에 위치해있다. (syscall.c는 두 개가 있는데 하나는 user 디렉토리, 다른 하나는 userprog 디렉토리. 후자가 우리가 작업하는 커널 코드.)

 

@/lib/user/syscall.c

int
write (int fd, const void *buffer, unsigned size) {
	return syscall3 (SYS_WRITE, fd, buffer, size);
}

... 위에서 아래 매크로 호출


#define syscall3(NUMBER, ARG0, ARG1, ARG2) ( \
		syscall(((uint64_t) NUMBER), \
			((uint64_t) ARG0), \
			((uint64_t) ARG1), \
			((uint64_t) ARG2), 0, 0, 0))

아래 매크로에 들어있는 syscall3()를 통해 syscall()을 호출하면 아래 syscall() 함수로 이동한다. (여기까지 따라왔다면 그냥 그러려니하고 읽자..^__^)

 

 

12. syscall(): 어셈블리어로 진입

 

시스템 콜을 요청하면 인자로 받은 값들을 레지스터에 하나씩 입력한다. write()함수는 syscall3, 즉 인자가 3개인 함수이다. 따라서 인자 세개를 넣고 나머지 값은 0으로 레지스터에 넣는다.

 

그러고 나면 x86-64에서 갖고 있는 어셈블리어 명령인 "syscall\n"을 호출한다.(어셈블리어임!!)

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

 

13. syscall-entry.s: 어셈블리어로 진입

 

복잡해보이지만 간단하다. 그냥 인터럽트 프레임 구조체에 있던 값들을 레지스터에 옮기고 계산하는 작업이다.

 

여기서 주목해야 할 부분은 두 가지가 있다.

 

movabs ($tss) %r12: 이제 커널 스택 포인터를 찾아야 한다. 왜냐면 아래에서 할 작업이 커널을 호출해 커널 스택에 push/pop을 해야 하는 것인데, 이 작업을 수행하려면 커널 스택을 가리키고 있는 포인터를 알아야 하기 때문. 이때문에 위에서 tss가 있던 것이다...!!!!! 그래서 이 어셈블리어 명령을 통해 우리는 커널 스택 포인터를 찾고 

 

 movq 4(r%12) %rsp 작업을 하면 (정확한 원리는 모르겠으나) tss 값(얘는 커널 스택 포인터)을 %rsp에 넣어준다. 이를 통해 우리는 이제 커널 스택 포인터로 이동할 수 있게 된다. 이때부터 커널 모드로 진입하여 ring0의 특권을 갖는다. 작업이 끝나면 sysretq를 반환한다.

 

참고 글: 시스템 콜 흐름

#include "threads/loader.h"

.text
.globl syscall_entry
.type syscall_entry, @function
syscall_entry:
	movq %rbx, temp1(%rip)
	movq %r12, temp2(%rip)     /* callee saved registers */
	movq %rsp, %rbx            /* Store userland rsp    */
	movabs $tss, %r12
	movq (%r12), %r12
	movq 4(%r12), %rsp         /* Read ring0 rsp from the tss */
	/* Now we are in the kernel stack */
	push $(SEL_UDSEG)      /* if->ss */
	push %rbx              /* if->rsp */
	push %r11              /* if->eflags */
	push $(SEL_UCSEG)      /* if->cs */
	push %rcx              /* if->rip */
	subq $16, %rsp         /* skip error_code, vec_no */
	push $(SEL_UDSEG)      /* if->ds */
	push $(SEL_UDSEG)      /* if->es */
	push %rax
	movq temp1(%rip), %rbx
	push %rbx
	pushq $0
	push %rdx
	push %rbp
	push %rdi
	push %rsi
	push %r8
	push %r9
	push %r10
	pushq $0 /* skip r11 */
	movq temp2(%rip), %r12
	push %r12
	push %r13
	push %r14
	push %r15
	movq %rsp, %rdi
    
    ...
    sysretq

 

 

13. syscall_handler(): 진짜진짜최종으로 "system call!" 출력

 

와..이젠 진짜.. 위 작업이 끝나면 syscall_handler()를 호출한다. 이때 시스템 콜 넘버를 보고서 write()로 넘겨주는데, 우리는 아직 write()를 구현하지 않았으니 "system call!"만 출력하고 무한 대기 상태로 돌입한다.

 

void
syscall_handler (struct intr_frame *f UNUSED) {
	...

	// TODO: Your implementation goes here.
	switch(sys_number) {
		...
        
		case SYS_WRITE:
			write(f->R.rdi, f->R.rsi, f->R.rdx);		
		...
	}
	printf ("system call!\n");
	printf("%d", sys_number);
	thread_exit ();

 

그래서...아래처럼 system call!이 출력되는 것이었따...

 

 

 

 

반응형