SW 사관학교 정글(Jungle)/운영체제-PintOS

syscall-entry.S

jinsang-2 2024. 10. 8. 01:38

  • x86-64에서는 int 0x30이 아니라 `syscall`로 바뀌었다.

시스템 콜과 Ring 0: 커널 모드 스택 전환의 이해

운영체제의 핵심 개념 중 하나는 시스템 콜을 통해 사용자 모드에서 커널 모드로 전환하는 과정입니다. 이 글에서는 시스템 콜이 이루어지는 과정, 그리고 커널 모드에서의 스택 전환 과정을 구체적으로 설명합니다. 특히, 코드 예제를 바탕으로 레지스터의 역할, TSS(Task State Segment)의 의미, 그리고 Ring 0의 중요성에 대해 알아보겠습니다.

1. CPU 권한 레벨: Ring의 개념

현대의 컴퓨터는 보안안정성을 위해 CPU 권한을 여러 레벨로 나누어 관리합니다. 이를 "링(Ring)"이라고 부르며, x86-64 아키텍처에서는 일반적으로 4가지 권한 레벨을 사용합니다.

  • Ring 0: 가장 높은 권한을 가진 커널 모드. 운영체제의 커널이 실행되는 곳입니다. 모든 자원과 명령어를 제어할 수 있습니다.
  • Ring 1 & Ring 2: 운영체제의 일부 서비스나 드라이버가 사용할 수 있는 권한 레벨이지만, 현대 OS에서는 거의 사용하지 않습니다.
  • Ring 3: 가장 낮은 권한을 가진 사용자 모드. 애플리케이션 같은 사용자 프로그램이 실행됩니다. 제한된 명령어만 사용 가능하고, 하드웨어 자원에 직접 접근할 수 없습니다.

2. 시스템 콜: 사용자 모드에서 커널 모드로의 전환

사용자 모드(Ring 3)에서 실행 중인 프로그램이 운영체제의 기능에 접근하려면 시스템 콜을 통해 `커널 모드(Ring 0)`로 진입해야 합니다. 이때 중요한 과정은 스택 전환입니다. 사용자는 자신의 스택을 사용하지만, 커널은 자체 스택을 사용해야 합니다. 이는 보안안정성을 유지하기 위해 필수적입니다.

사용자 모드에서의 스택은 언제든지 사용자가 변경하거나 손상시킬 수 있기 때문에 커널이 그 스택을 사용하면 큰 문제가 발생할 수 있습니다. 그래서 커널 모드로 전환하면 커널 전용 스택을 사용해야 하며, 이 스택은 TSS에 의해 관리됩니다.

3. TSS(Task State Segment)란?

TSSTask State Segment의 약자로, CPU가 문맥 전환(Context Switch)을 할 때 필요한 정보를 저장해두는 구조체입니다. TSS에는 커널 모드에서 사용할 스택 포인터(즉, Ring 0의 스택 주소) 등 중요한 정보가 포함되어 있습니다.

시스템 콜이 발생하면 CPU는 TSS에서 Ring 0 스택의 주소를 읽어와 커널 모드에서 사용할 스택을 설정합니다.

movq 4(%r12), %rsp   /* TSS에서 Ring 0 스택 포인터를 읽어와 커널 스택을 사용하도록 설정 */

이 명령어는 TSS에 저장된 커널 모드 스택 주소%rsp에 저장함으로써, 이제부터 커널 전용 스택을 사용하도록 전환하는 과정입니다.

4. 레지스터 저장: 시스템 콜에서의 레지스터 관리

시스템 콜을 처리하는 동안 사용자 모드의 상태를 보존하고, 커널 모드에서 필요한 데이터를 관리하려면 레지스터의 값을 적절히 처리해야 합니다. 시스템 콜의 진입점에서 우리는 callee-saved 레지스터와 몇몇 중요한 레지스터를 커널 스택에 저장합니다.

movq %rbx, temp1(%rip)
movq %r12, temp2(%rip)     /* callee-saved registers */

여기서 callee-saved 레지스터란 함수 호출이 끝난 후에도 값이 유지되어야 하는 레지스터들입니다. 시스템 콜 처리 중에 레지스터 값이 변경될 수 있으므로, 이를 커널 모드로 전환하기 전에 따로 저장하는 작업이 필요합니다.

5. 스택 전환: 커널 모드로의 준비

다음으로 커널 모드에서 사용할 스택 포인터를 설정합니다. 이 과정에서 TSS로부터 커널 모드 스택을 불러옵니다.

movabs $tss, %r12
movq (%r12), %r12
movq 4(%r12), %rsp   /* TSS에서 커널 모드 스택 주소를 읽어와 %rsp에 저장 */

TSS에는 커널 모드에서 사용할 스택 주소가 저장되어 있으며, 이를 %rsp에 복사함으로써 커널 모드 스택을 사용할 준비를 마칩니다. 커널 모드로 진입한 이후에는 이 스택을 사용하여 안정적으로 시스템 콜을 처리할 수 있습니다.

6. 커널 모드에서의 스택 구성

스택 포인터 설정이 완료되면, 커널 모드로 진입하여 필요한 값들을 스택에 저장합니다. 여기서 중요한 점은 인터럽트 플래그세그먼트 레지스터, 명령어 포인터 등을 포함한 정보들을 스택에 저장해두는 것입니다. 이를 통해 시스템 콜이 끝나고 사용자 모드로 돌아갈 때, 정확히 원래 상태로 복원될 수 있습니다.

push $(SEL_UDSEG)      /* 사용자 모드의 세그먼트 레지스터(ss)를 저장 */
push %rbx              /* 사용자 모드의 스택 포인터(rsp)를 저장 */
push %r11              /* 인터럽트 플래그(eflags)를 저장 */
push $(SEL_UCSEG)      /* 사용자 모드의 코드 세그먼트(cs)를 저장 */
push %rcx              /* 사용자 모드의 명령어 포인터(rip)를 저장 */

이 명령어들은 시스템 콜이 끝난 후 다시 사용자 모드로 돌아갈 때 필요하므로, 스택에 저장해두고 있습니다.

마무리: 사용자 모드로 복귀

시스템 콜이 완료되면, 우리는 다시 사용자 모드로 돌아가기 위한 준비를 합니다. 이 과정에서는 저장했던 사용자 모드의 레지스터 값과 스택 포인터를 복원하고, sysretq 명령어를 통해 사용자 모드로 돌아갑니다.

sysretq   /* 사용자 모드로 복귀 */

이 명령어는 CPU를 다시 **Ring 3(사용자 모드)**로 전환하고, 시스템 콜이 발생하기 전의 상태로 돌아가도록 합니다.


전체적인 코드 흐름 정리

  1. 시스템 콜 발생: 사용자 모드에서 커널 모드로 전환. CPU는 커널이 정의한 시스템 콜 핸들러로 이동.
  2. 사용자 상태 저장: 사용자 모드에서 사용하던 스택 포인터, 레지스터, 세그먼트 정보 등을 커널 스택에 안전하게 저장.
  3. 커널에서 시스템 콜 처리: 커널이 시스템 콜을 처리. 예를 들어 파일 읽기, 쓰기, 메모리 할당 등.
  4. 사용자 상태 복구: 커널 스택에 저장해 두었던 레지스터 값들과 세그먼트 정보를 복구.
  5. 사용자 모드로 복귀: 시스템 콜 전의 상태로 돌아가서 프로그램을 이어서 실행.

이 흐름을 통해 사용자 프로그램은 커널에게 안전하게 시스템 자원을 요청할 수 있고, 커널은 정확하게 사용자 요청을 처리하고, 다시 원래의 상태로 돌아가 프로그램이 계속 실행될 수 있도록 하는 거야.

각 단계에서 메모리와 레지스터, 스택을 철저히 관리하는 것이 중요한 이유는, 시스템 콜 중에 사용자 모드와 커널 모드 사이에서 데이터를 교환하고 프로세스의 상태를 안전하게 유지해야 하기 때문이야.

1. 사용자 모드에서 커널 모드로 전환

  • 코드 부분: syscall_entry 함수의 초반부
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 */

설명:

  • 이 부분은 시스템 콜이 발생하면 처음 실행되는 부분이야.
  • 사용자 모드에서 커널 모드로 전환하기 전에 사용자 모드에서의 레지스터 상태를 저장해야 하기 때문에, 여기서 사용자 모드의 callee-saved 레지스터들(%rbx, %r12 등)스택 포인터(%rsp)를 저장해.
  • 그리고 TSS(Task State Segment)를 통해 커널 스택 포인터를 가져와 커널 스택으로 전환돼. 이게 커널 모드에서 작업을 할 수 있게 만드는 과정이야.

2. 커널에서 시스템 콜 처리

  • 코드 부분: 사용자 모드에서 넘어온 상태 정보를 저장하고, 커널 모드로 전환된 후 커널에서 작업을 처리하는 부분
    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 */

설명:

  • 이 부분은 사용자 모드의 레지스터와 세그먼트 레지스터들을 커널 스택에 저장하는 과정이야.
  • 스택 포인터(%rbx), 명령어 포인터(%rcx), 상태 플래그(%r11), 코드 세그먼트(SEL_UCSEG) 등을 저장해서 나중에 복구할 수 있도록 해.
  • 이렇게 저장해두면, 커널에서 작업이 끝난 후 다시 원래의 사용자 모드 상태로 정확히 복귀할 수 있어.

이 부분을 마치면 커널은 시스템 콜을 처리하는 핸들러 함수로 넘어가서, 실제로 시스템 콜을 처리하게 돼. 예를 들어, 파일을 읽거나, 메모리 작업을 하거나, 다른 자원을 관리하는 작업이 이뤄져.

3. 사용자 모드로 복귀

  • 코드 부분: 시스템 콜 처리가 완료된 후, 저장된 상태를 복구하고 사용자 모드로 돌아가는 부분
    popq %r15
    popq %r14
    popq %r13
    popq %r12
    popq %r11
    popq %r10
    popq %r9
    popq %r8
    popq %rsi
    popq %rdi
    popq %rbp
    popq %rdx
    popq %rcx
    popq %rbx
    popq %rax
    addq $32, %rsp
    popq %rcx              /* if->rip */
    addq $8, %rsp
    popq %r11              /* if->eflags */
    popq %rsp              /* if->rsp */
    sysretq

설명:

  • 이 부분은 커널 스택에 저장해두었던 레지스터 값을 복구하는 과정이야.
  • popq 명령어를 통해 순차적으로 레지스터 값을 다시 꺼내오고, 시스템 콜이 호출되기 전에 사용자 모드에서 사용하던 레지스터와 상태 정보를 다시 복구해.
  • 이 과정이 끝나면, sysretq 명령어를 통해 커널 모드에서 사용자 모드로 돌아가. 이때 CPU는 시스템 콜이 발생한 원래 위치로 돌아가서 프로그램을 이어서 실행하게 돼.

전체 요약

  1. 사용자 모드에서 커널 모드로 전환: 사용자 레지스터 값을 저장하고, 커널 스택으로 전환해 커널에서 안전하게 작업할 준비를 함.
  2. 커널에서 시스템 콜 처리: 사용자 모드에서 넘어온 상태 정보를 저장한 후, 커널 스택을 사용해 시스템 콜을 처리.
  3. 사용자 모드로 복귀: 시스템 콜 처리가 끝나면 저장했던 레지스터 값을 복구하고, 커널 모드에서 다시 사용자 모드로 돌아가서 프로그램을 이어서 실행.

이렇게 사용자 모드에서 커널 모드로 전환하고, 시스템 콜을 처리한 뒤, 다시 사용자 모드로 돌아가는 흐름이 전반적으로 잘 유지되도록 코드를 작성한 거야.

 

전체 코드

#include "threads/loader.h"

.text 
.globl syscall_entry
.type syscall_entry, @function
syscall_entry:
	movq %rbx, temp1(%rip)		/* callee registers 임시 저장*/	
	movq %r12, temp2(%rip)     /* callee registers 임시 저장*/
	movq %rsp, %rbx            /* user 영역 rsp 저장   */
	movabs $tss, %r12			/* tss의 절대주소를 r12레지스터에 저장*/
	movq (%r12), %r12			/* 위에서 주소값을 받아왔으니 tss 주소 안의 값을 받아오기 위함 */
	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

check_intr:
	/* 비트 테스트 및 설정 명령어, r11레지스터의 9번째 비트를 검사 
		r11의 9번째 비트가 1이면 sti(인터럽트 활성화), 0이면 인터럽트 복원x*/
	btsq $9, %r11
    
	/* jnb = ji-eun and byeung-chan is not -> jump if not below
		이전 조건 확인하고 조건 충족되지 않으면(0이면) no_sti로 점프. -> 인터럽트 복원 없이 점프할 것임 */ 
	jnb no_sti
    
	/* sti는 인터럽트를 활성화하는 명령어 btsq 결과 9번째 비트가 1이라면 
		sti 실행 -> 인터럽트 다시 화 */
	sti                    /* restore interrupt */
no_sti: // sti 를 안거치고 여기로 바로 점프하면 인터럽트 활성화x
	movabs $syscall_handler, %r12 // 절대주소 syscall_handler의 주소를 r12에 저장.
	call *%r12 // r12에 저장된 함수로 호출을 이동.
	// 레지스터 복구. 스택에서 값을 팝해서 레지스터에 복구하는 명령어.
	popq %r15
	popq %r14
	popq %r13
	popq %r12
	popq %r11
	popq %r10
	popq %r9
	popq %r8
	popq %rsi
	popq %rdi
	popq %rbp
	popq %rdx
	popq %rcx
	popq %rbx
	popq %rax

//시스템 호출 전의 상태를 복원하는 과정입니다. 시스템 호출이 끝나면 CPU는 원래 하던 작업(프로그램의 실행)으로 돌아가야 하므로, 이전 레지스터 상태와 스택 상태를 다시 복원하는 것
	/* 스택 포인터 rsp에 32바이트를 더해서 스택 정리(비워냄), 
복원하는 순서와 값은 미리 정해져 있기 때문에 그 값을 정확히 꺼내올 수 있음.*/
	addq $32, %rsp 
//if->rip은 시스템 호출을 처리한 후 다시 실행할 명령어의 주소입니다. 시스템 호출이 끝나면 원래 실행하던 프로그램의 다음 명령어로 돌아가야 하므로, 이 값을 복원합니다.
	popq %rcx              /* if->rip */
	addq $8, %rsp // rsp를 8바이트 위로 이동, 스택에 저장 된 값 중 하나를 무시하고 그냥 넘어가는 역할.
/* 시스템 호출 이전에 어떤 상태(플래그)가 있었는지 복원해야 합니다. eflags는 CPU의 다양한 상태를 나타내는 플래그를 저장하는데, 이 값을 복원해서 시스템 호출 이전의 상태로 되돌려 줍니다.*/
	popq %r11              /* if->eflags */
/* 시스템 호출을 처리하는 동안 스택이 사용되었지만, 이제 시스템 호출이 끝났으니 원래의 스택 포인터로 돌아가야 합니다. 이렇게 함으로써 스택의 상태가 시스템 호출 이전으로 복원됩니다. */
	popq %rsp              /* if->rsp */
	sysretq			/* 시스템 호출후 사용자 모드로 되돌아가는 명어 */

.section .data // 데이터 섹션 정의. 프로그램에서 전역변수나 초기화 된 변수가 저장되는 메모리 영역. 이 섹션에서 선언된 변수들은 프로그램이 실행되는 동안 메모리에 남아 있음.
.globl temp1 // temp1 전역변수 선언 -> 다른 파일에서도 사용할 수 있음.
temp1: // 변수의 메모리 주소를 나타내며, 8바이트 공간 차지.
.quad	0 // 8바이트 크기의 데이터정의, temp1에 0을 할당
.globl temp2 // temp2 전역변수 선언
temp2:
.quad	0