Pintos project의 시작
드디어 OS프로젝트을 시작하는 주차가 됐다. 이번 프로젝트를 진행하면서 이전의 다른 프로젝트들과는 확실히 다른 느낌을 받았다. 난이도는 물론 더 어려웠고, 함수의 진행을 따라가기만 했는데도 머리가 어지러웠다. 하지만 그런 난이도적인 차이보다 OS 프로젝트의 차별점으로 느껴진 것은 확실히 하드웨어와 더 가까운 거리에서 코딩을 한다는 것이었다.
이전의 내가 알던 코딩은 함수내에서 함수를 호출하는 것으로 진행이 이루어졌지만, 이번 프로젝트에서는 그 뿐만 아니라 레지스터의 값을 변화시키는 방법으로 진행의 흐름을 바꾸는 내용이 포함되어있었다.그리고 그 레지스터 값을 직접적으로 다루는 코드는 프로젝트 내에 어셈블리어로 작성되어 있었고, 그 코드를 이해하는 것이 다른 프로젝트와 가장 큰 차이점이라고 느꼈다. (덤으로 겁나게 어려웠다...)
개인적으로 직접 어셈블리어를 해석하고 조작해본 경험이 이번 주차에서 가장 인상깊고, 잊고 싶지 않은 경험이라 이번 일지에 정리하려고 한다.
Thread Context Switching - thread_launch()
이번주차에서 가장 많이 사용되는 함수중 하나는 thread_launch(struct thread *th)함수다. 함수의 기능을 요약하면 다음과 같다.
- 현재 실행중인 thread의 context, 즉 현재 thread가 진행되고있는 cpu의 레지스터 정보들을 메모리에 저장한다.
- 인자로 받은 포인터가 가리키는 thread를 깨워서 실행시킨다.
즉 Thread Context Switching을 하는 함수인데, 이 함수의 주요 동작 대부분이 어셈블리어로 이루어져있다. 이 함수에 대해 분석해 보자.
static void
thread_launch(struct thread *th)
{
uint64_t tf_cur = (uint64_t)&running_thread()->tf;
uint64_t tf = (uint64_t)&th->tf;
ASSERT(intr_get_level() == INTR_OFF);
__asm __volatile(
"push %%rax\n"
"push %%rbx\n"
"push %%rcx\n"
"movq %0, %%rax\n"
"movq %1, %%rcx\n"
"movq %%r15, 0(%%rax)\n"
"movq %%r14, 8(%%rax)\n"
"movq %%r13, 16(%%rax)\n"
"movq %%r12, 24(%%rax)\n"
"movq %%r11, 32(%%rax)\n"
"movq %%r10, 40(%%rax)\n"
"movq %%r9, 48(%%rax)\n"
"movq %%r8, 56(%%rax)\n"
"movq %%rsi, 64(%%rax)\n"
"movq %%rdi, 72(%%rax)\n"
"movq %%rbp, 80(%%rax)\n"
"movq %%rdx, 88(%%rax)\n"
"pop %%rbx\n" // Saved rcx
"movq %%rbx, 96(%%rax)\n"
"pop %%rbx\n" // Saved rbx
"movq %%rbx, 104(%%rax)\n"
"pop %%rbx\n" // Saved rax
"movq %%rbx, 112(%%rax)\n"
// "addq $120, %%rax\n"
"movw %%es, 120(%%rax)\n"
"movw %%ds, 128(%%rax)\n"
"addq $152, %%rax\n"
"call __next\n" // read the current rip.
"__next:\n"
"pop %%rbx\n"
"addq $(out_iret - __next), %%rbx\n"
"movq %%rbx, 0(%%rax)\n" // rip
"movw %%cs, 8(%%rax)\n" // cs
"pushfq\n"
"popq %%rbx\n"
"mov %%rbx, 16(%%rax)\n" // eflags
"mov %%rsp, 24(%%rax)\n" // rsp
"movw %%ss, 32(%%rax)\n"
"mov %%rcx, %%rdi\n"
"call do_iret\n"
"out_iret:\n"
: : "g"(tf_cur), "g"(tf) : "memory");
}
- line 4~5 : 현재 진행되고 있는 스레드의 tf필드의 주소를 tf_cur변수에 저장하고, 앞으로 현재 스레드와 교체할 스레드의 tf필드 주소도 tf변수에 저장한다.
- line 8~10 : 레지스터에 있는 값들을 메모리로 옮기는데 사용될 레지스터들인데, 이 rax,rbx,rcx레지스터의 값들도 메로리에 저장되어야 한다. 그래서 미리 백업해둔다.
- line 12~25 : %0와 %1은
__asm __volatile
의 문법을 알아야 한다. 간단히 설명하면__asm __volatile
은 인자로 쓰여진 어셈블리어 코드를 컴파일하지 말고 그대로 두라는 함수이다, 그리고 아래에 input으로 들어온 인자들(tf_cur,tf)을 순서에 따라 %0,%1로 쓸 수 있다. 이제 해석해보자면, tf_cur 시작 주소에서부터 8byte씩 움직이면서 현재 레지스터들의 값들을 차례로 넣어주는 부분이다.(tf는 intr_frame구조체로 선언되어 있는데 그 순서에 맞게 넣어준다.) - line 27~32 : 백업해둔 값들도 메모리에 넣어준다. (push로 넣어줬기 때문에 pop으로 꺼낸다.)
- line 34 : 기존 코드에서는 구조체 구성 순서를 나타내기 위해서 add를 써줬지만 개인적으로 코드 독해에 방해가 된다고 생각되어 주석처리를 하고 이후 add에 한번에 값을 더해줬다.(참고로 add는 2번째 인자 += 1번째 인자 라고 생각하면 된다.)
- line 35~36 : 계속 메모리에 현재 레지스터값을 저장한다.
- line 38~42 : rip레지스터의 값은 다른 레지스터들처럼 직접 접근하지 못하기 때문에, 이 부분처럼 우회해서 가져온다.
우회 과정 : call __next로 현재 rip를 stack에 넣고, __next: 진행중에 pop으로 바로 가져온다.
추가 과정 :addq $(out_iret - __next), %%rbx\n
를 해주는 이유는 next를 현재 진행중이고 끝낼 예정인데, 현재 thread가 지금은 쉬러가지만, 나중에 다시 일할 때 next를 또 하기 싫기 때문에 귀환 위치를 line38 -> line51로 수정하는 것이다. - line 44~46 : 플래그 레지스터도 rip 레지스터처럼 직접 접근 불가능해서 우회한다.
- line 49 : 50번째 줄에서
call do_iret
을 할건데 do_iret함수에 인자가 필요하다. 거기에 rcx에 저장된 값을 인자로 쓰기위한 부분(rdi는 1st argument를 저장하는 레지스터여서) - line 50 : do_iret함수로 rip위치를 옮긴다 -> 실행 흐름을 옮긴다.
위에서 메모리에 현재 상태를 저장하는 부분은 끝났다. 이제 메모리에서 레지스터로 데이터를 올리는 do_iret함수를 보자.
void do_iret(struct intr_frame *tf)
{
__asm __volatile(
// next_tf 주소를 rsp로 설정
"movq %0, %%rsp\n"
// 물리 레지스터에 next_tf의 gp레지스터의 값들 전부 올린다.
"movq 0(%%rsp),%%r15\n"
"movq 8(%%rsp),%%r14\n"
"movq 16(%%rsp),%%r13\n"
"movq 24(%%rsp),%%r12\n"
"movq 32(%%rsp),%%r11\n"
"movq 40(%%rsp),%%r10\n"
"movq 48(%%rsp),%%r9\n"
"movq 56(%%rsp),%%r8\n"
"movq 64(%%rsp),%%rsi\n"
"movq 72(%%rsp),%%rdi\n"
"movq 80(%%rsp),%%rbp\n"
"movq 88(%%rsp),%%rdx\n"
"movq 96(%%rsp),%%rcx\n"
"movq 104(%%rsp),%%rbx\n"
"movq 112(%%rsp),%%rax\n"
//"addq $120,%%rsp\n"
"movw 8(%%rsp),%%ds\n"
"movw (%%rsp),%%es\n"
"addq $152, %%rsp\n"
"iretq"
: : "g"((uint64_t)tf) : "memory");
}
- 상당히 별게 없다.
- 인자로 받은 메모리 위치에서 차례대로 레지스터값을 올리는 과정이다.
- 특이한 점이라면 rsp를 tf의 위치로 설정해서, 이전 rsp에 들어있던
call do_iret
의 위치는 사라진다는 점이다. 하지만 코드의 목적이 스레드를 바꾸는 것이지 끝내고 다시 돌아가거나 하는 것이 아니기 때문에 아무런 문제가 없다. - 또한 es 이후의 메모리 값들은 어떻게 레지스터로 올라는지 궁금할 수 있는데 iretq에서 다 해준다.
이번 주차에서 프로젝트를 진행하면서 정리를 너무 자유분방하게 해서, 공부한 내용을 하나로 엮는데 어려움이 있었다. 다음 주차는 TIL까진 아니더라도 적당히 하루의 끝에 정리를 해보도록 하자.
'SW 정글 일지' 카테고리의 다른 글
정글 개발일지 week10 핀토스의 마무리 (0) | 2024.05.28 |
---|---|
정글 개발일지week09 (1) | 2024.05.21 |
정글 개발일지 week08 (0) | 2024.05.14 |
정글 개발일지 week02 (0) | 2024.03.31 |
정글 에세이 : 과거와 앞으로의 정글 (1) | 2024.03.16 |
Pintos project의 시작
드디어 OS프로젝트을 시작하는 주차가 됐다. 이번 프로젝트를 진행하면서 이전의 다른 프로젝트들과는 확실히 다른 느낌을 받았다. 난이도는 물론 더 어려웠고, 함수의 진행을 따라가기만 했는데도 머리가 어지러웠다. 하지만 그런 난이도적인 차이보다 OS 프로젝트의 차별점으로 느껴진 것은 확실히 하드웨어와 더 가까운 거리에서 코딩을 한다는 것이었다.
이전의 내가 알던 코딩은 함수내에서 함수를 호출하는 것으로 진행이 이루어졌지만, 이번 프로젝트에서는 그 뿐만 아니라 레지스터의 값을 변화시키는 방법으로 진행의 흐름을 바꾸는 내용이 포함되어있었다.그리고 그 레지스터 값을 직접적으로 다루는 코드는 프로젝트 내에 어셈블리어로 작성되어 있었고, 그 코드를 이해하는 것이 다른 프로젝트와 가장 큰 차이점이라고 느꼈다. (덤으로 겁나게 어려웠다...)
개인적으로 직접 어셈블리어를 해석하고 조작해본 경험이 이번 주차에서 가장 인상깊고, 잊고 싶지 않은 경험이라 이번 일지에 정리하려고 한다.
Thread Context Switching - thread_launch()
이번주차에서 가장 많이 사용되는 함수중 하나는 thread_launch(struct thread *th)함수다. 함수의 기능을 요약하면 다음과 같다.
- 현재 실행중인 thread의 context, 즉 현재 thread가 진행되고있는 cpu의 레지스터 정보들을 메모리에 저장한다.
- 인자로 받은 포인터가 가리키는 thread를 깨워서 실행시킨다.
즉 Thread Context Switching을 하는 함수인데, 이 함수의 주요 동작 대부분이 어셈블리어로 이루어져있다. 이 함수에 대해 분석해 보자.
static void
thread_launch(struct thread *th)
{
uint64_t tf_cur = (uint64_t)&running_thread()->tf;
uint64_t tf = (uint64_t)&th->tf;
ASSERT(intr_get_level() == INTR_OFF);
__asm __volatile(
"push %%rax\n"
"push %%rbx\n"
"push %%rcx\n"
"movq %0, %%rax\n"
"movq %1, %%rcx\n"
"movq %%r15, 0(%%rax)\n"
"movq %%r14, 8(%%rax)\n"
"movq %%r13, 16(%%rax)\n"
"movq %%r12, 24(%%rax)\n"
"movq %%r11, 32(%%rax)\n"
"movq %%r10, 40(%%rax)\n"
"movq %%r9, 48(%%rax)\n"
"movq %%r8, 56(%%rax)\n"
"movq %%rsi, 64(%%rax)\n"
"movq %%rdi, 72(%%rax)\n"
"movq %%rbp, 80(%%rax)\n"
"movq %%rdx, 88(%%rax)\n"
"pop %%rbx\n" // Saved rcx
"movq %%rbx, 96(%%rax)\n"
"pop %%rbx\n" // Saved rbx
"movq %%rbx, 104(%%rax)\n"
"pop %%rbx\n" // Saved rax
"movq %%rbx, 112(%%rax)\n"
// "addq $120, %%rax\n"
"movw %%es, 120(%%rax)\n"
"movw %%ds, 128(%%rax)\n"
"addq $152, %%rax\n"
"call __next\n" // read the current rip.
"__next:\n"
"pop %%rbx\n"
"addq $(out_iret - __next), %%rbx\n"
"movq %%rbx, 0(%%rax)\n" // rip
"movw %%cs, 8(%%rax)\n" // cs
"pushfq\n"
"popq %%rbx\n"
"mov %%rbx, 16(%%rax)\n" // eflags
"mov %%rsp, 24(%%rax)\n" // rsp
"movw %%ss, 32(%%rax)\n"
"mov %%rcx, %%rdi\n"
"call do_iret\n"
"out_iret:\n"
: : "g"(tf_cur), "g"(tf) : "memory");
}
- line 4~5 : 현재 진행되고 있는 스레드의 tf필드의 주소를 tf_cur변수에 저장하고, 앞으로 현재 스레드와 교체할 스레드의 tf필드 주소도 tf변수에 저장한다.
- line 8~10 : 레지스터에 있는 값들을 메모리로 옮기는데 사용될 레지스터들인데, 이 rax,rbx,rcx레지스터의 값들도 메로리에 저장되어야 한다. 그래서 미리 백업해둔다.
- line 12~25 : %0와 %1은
__asm __volatile
의 문법을 알아야 한다. 간단히 설명하면__asm __volatile
은 인자로 쓰여진 어셈블리어 코드를 컴파일하지 말고 그대로 두라는 함수이다, 그리고 아래에 input으로 들어온 인자들(tf_cur,tf)을 순서에 따라 %0,%1로 쓸 수 있다. 이제 해석해보자면, tf_cur 시작 주소에서부터 8byte씩 움직이면서 현재 레지스터들의 값들을 차례로 넣어주는 부분이다.(tf는 intr_frame구조체로 선언되어 있는데 그 순서에 맞게 넣어준다.) - line 27~32 : 백업해둔 값들도 메모리에 넣어준다. (push로 넣어줬기 때문에 pop으로 꺼낸다.)
- line 34 : 기존 코드에서는 구조체 구성 순서를 나타내기 위해서 add를 써줬지만 개인적으로 코드 독해에 방해가 된다고 생각되어 주석처리를 하고 이후 add에 한번에 값을 더해줬다.(참고로 add는 2번째 인자 += 1번째 인자 라고 생각하면 된다.)
- line 35~36 : 계속 메모리에 현재 레지스터값을 저장한다.
- line 38~42 : rip레지스터의 값은 다른 레지스터들처럼 직접 접근하지 못하기 때문에, 이 부분처럼 우회해서 가져온다.
우회 과정 : call __next로 현재 rip를 stack에 넣고, __next: 진행중에 pop으로 바로 가져온다.
추가 과정 :addq $(out_iret - __next), %%rbx\n
를 해주는 이유는 next를 현재 진행중이고 끝낼 예정인데, 현재 thread가 지금은 쉬러가지만, 나중에 다시 일할 때 next를 또 하기 싫기 때문에 귀환 위치를 line38 -> line51로 수정하는 것이다. - line 44~46 : 플래그 레지스터도 rip 레지스터처럼 직접 접근 불가능해서 우회한다.
- line 49 : 50번째 줄에서
call do_iret
을 할건데 do_iret함수에 인자가 필요하다. 거기에 rcx에 저장된 값을 인자로 쓰기위한 부분(rdi는 1st argument를 저장하는 레지스터여서) - line 50 : do_iret함수로 rip위치를 옮긴다 -> 실행 흐름을 옮긴다.
위에서 메모리에 현재 상태를 저장하는 부분은 끝났다. 이제 메모리에서 레지스터로 데이터를 올리는 do_iret함수를 보자.
void do_iret(struct intr_frame *tf)
{
__asm __volatile(
// next_tf 주소를 rsp로 설정
"movq %0, %%rsp\n"
// 물리 레지스터에 next_tf의 gp레지스터의 값들 전부 올린다.
"movq 0(%%rsp),%%r15\n"
"movq 8(%%rsp),%%r14\n"
"movq 16(%%rsp),%%r13\n"
"movq 24(%%rsp),%%r12\n"
"movq 32(%%rsp),%%r11\n"
"movq 40(%%rsp),%%r10\n"
"movq 48(%%rsp),%%r9\n"
"movq 56(%%rsp),%%r8\n"
"movq 64(%%rsp),%%rsi\n"
"movq 72(%%rsp),%%rdi\n"
"movq 80(%%rsp),%%rbp\n"
"movq 88(%%rsp),%%rdx\n"
"movq 96(%%rsp),%%rcx\n"
"movq 104(%%rsp),%%rbx\n"
"movq 112(%%rsp),%%rax\n"
//"addq $120,%%rsp\n"
"movw 8(%%rsp),%%ds\n"
"movw (%%rsp),%%es\n"
"addq $152, %%rsp\n"
"iretq"
: : "g"((uint64_t)tf) : "memory");
}
- 상당히 별게 없다.
- 인자로 받은 메모리 위치에서 차례대로 레지스터값을 올리는 과정이다.
- 특이한 점이라면 rsp를 tf의 위치로 설정해서, 이전 rsp에 들어있던
call do_iret
의 위치는 사라진다는 점이다. 하지만 코드의 목적이 스레드를 바꾸는 것이지 끝내고 다시 돌아가거나 하는 것이 아니기 때문에 아무런 문제가 없다. - 또한 es 이후의 메모리 값들은 어떻게 레지스터로 올라는지 궁금할 수 있는데 iretq에서 다 해준다.
이번 주차에서 프로젝트를 진행하면서 정리를 너무 자유분방하게 해서, 공부한 내용을 하나로 엮는데 어려움이 있었다. 다음 주차는 TIL까진 아니더라도 적당히 하루의 끝에 정리를 해보도록 하자.
'SW 정글 일지' 카테고리의 다른 글
정글 개발일지 week10 핀토스의 마무리 (0) | 2024.05.28 |
---|---|
정글 개발일지week09 (1) | 2024.05.21 |
정글 개발일지 week08 (0) | 2024.05.14 |
정글 개발일지 week02 (0) | 2024.03.31 |
정글 에세이 : 과거와 앞으로의 정글 (1) | 2024.03.16 |