pintos를 시작한지 벌써 2주 반이나 지나버렸습니다.
1주차때도 힘들었지만 이번 2주차는 더더욱 힘들었던 주차였던것같습니다.
원래 전 기수분들은 pintos 1주차 thread주간을 1주, 2주차 UserProgramming을 1주 반을 사용하셨다고 하는데(추석), 이번에는 기간이 바뀌어 버려 pintos 2주차를 일주일이라는 짧은기간안에 구현하여야 했습니다.
비록 힘들었던 주차였지만, 많은것을 배워갈 수 있었던 주차였던것같습니다.
(많이 힘들어요..)
말씀 드렸듯, Tistory로의 이동 전에는 간략한 공부내용만을 다루겠습니다.
process_exec()함수에 있는 "유저프로그램"을 위한 인자 세팅.
- 일반적인 함수 호출 규악의 중요한 포인트 요약
1. 유저-레벨 application은 %rdi, %rsi, %rdx, %rcx, %r8, %r9 시퀀스들을 전달하기위해 정수 레지스터 사용.
2. 호출자는 다음 인스트럭션의 주소(리턴 어드레스를 스택에 푸시하고, 피호출자의 첫번째 instruction으로 점프. CALL이라는 인스트럭션 하나가 이 두가지를 모두 수행.
3. 피호출자 실행
4. 피호출자가 return값을 가지고있다면 return값은 레지스터 RAX에 저장됨.
* 5. 피호출자는 x86-64 인스트럭션인 RET를 사용하여 스택에 받았던 return 어드레스를 pop하고 그 주소가 가리키는곳으로 점프함으로써 리턴됨.
ex) 세 개의 정수 인자를 받는 함수 f()가 있다고 생각해보면,
어떤 도식은 위의 3번항목에 있는 피호출자가 실행되는 시점에, 스택 프레임과 레지스터 상태가 어떤식으로 되어있는지에 대한 예시를 보여줌. f()가 f(1, 2, 3)으로 호출되었다고 가정.
초기화된 스택의 주소는 임의의 숫자로 치면 다음과 같음.
+----------------+
stack pointer --> 0x4747fe70 | return address |
+----------------+
RDI: 0x0000000000000001 | RSI: 0x0000000000000002 | RDX: 0x0000000000000003
유저프로그램을 위한 pintos C 라이브러리는 start()함수를 유저프로그램의 시작 포인트로 지정. _start()는 lib/user/entry.c에 있음. => 여기서 _start()는 main()함수를 감싸고있는 함수. main()은 리턴되면서 exit()을 호출하게 됨.
커널은 유저 프로그램이 실행되는 것을 허가하기 전에, 레지스터에 올라가 있는 초기함수를 위한 인자를 반드시 넣어줘야 함. 이 인자들은 일반적인 호출 규악과 동일한 방식으로 전달됨.
/bin/ls -l foo bar와 같은 명령이 주어졌을 때 인자들을 어떻게 다뤄야 하는지.
1. 명령을 단어들로 쪼갬. /bin/ls, l, foo, bar 이렇게
이 단어들을 스택의 맨 처음 부분에 놓음(순서는 상관이 없음.) 왜냐하면 포인터에 의해 참조될 예정이기 때문.
각 문자열의 주소 + 경계조건을 위한 NULL Pointer를 스택에 오른쪽 -> 왼쪽 순서로 푸시함. (이들은 argv의 원소가 됨.) NULL Pointer 경계는 argv[argc]가 NULL Pointer라는 사실을 보장해줌.
그리고 이 순서는 argv[0]이 가장 낮은 가상주소를 가진다는 사실을 보장해 줌. 또한 word크기에 정렬된 접근이 정렬되지 않은 접근보다 빠르므로, 최고의 성능을 위해서는 스택에 첫 push가 발생하기 전에 스택포인터를 8의 배수로 반올림하여야 함.
%rsi가 argv주소 (argv[0]의 주소)를 가리키게 하고, %rdi를 argc로 설정.
마지막으로 가짜 return address를 push. entry함수는 절대 return되지 않겠지만, 해당 스택프레임은 다른 스택프레임들과 같은 구조를 가져야 함.
(대략적으로 이러한 구조를 가져야 함.)
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(), close()와 같은 명령어들을 마주치게되는데, 일반적으로 '시스템 콜'이라고 불리웁니다. 이러한 시스템콜은 무엇일까요?
운영체제의 커널이 제공하는 서비스에 대해, 응용 프로그램의 요청에 따라 커널에 접근하기 위한 인터페이스
즉, 일반 사용자 프로그램은 물리주소를 직접 핸들링할 수 없고, cpu 권한수준도 사용자 공간에 한정되어있는데, file 읽고 쓰는 filesystem 및 memory 할당은 실제 물리 주소에 접근하여야하므로, 일반 사용자 단에서는 바로 핸들링 할 수 없으니, 커널을 호출하는 과정입니다.
그렇다면 pintos에서는 어떻게 시스템 콜이 이루어질까요?
x86 아키텍처에서는 'syscall'이라는 instructionㅇㄹ 이용하여 system-call을 할 수 있습니다. syscall을 살펴보면 권한수준을 가장 높은 0으로 만들어주는 명령이라고 적혀있습니다. 이 때 레벨 0은 모든 리소스에 접근이 가능한 커널모드를 의미합니다.
1. 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;
}
이 코드는 pintos의 /lib/user/syscall.c 파일의 코드중 한 부분입니다.
여기서의 어셈블리코드는 num을 rax레지스터에, arg1~6을 각각 rdi ~ r9까지의 register에 복제합니다. 이후 syscall명령을 통해 syscall을 호출합니다.
2. syscall 호출
syscall_init을 통해 다음과 같은 코드가 실행이 됩니다.
void syscall_init(void)
{
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라는 file과 연결이되어, write를 하게됩니다. 이 파일을 뜯어보면 syscall_handler와 연결이되어있습니다.
3. syscall_handle
2번과정의 어셈블리어를 통해 syscall_handler가 call되므로, syscall_handler함수로 이동하게됩니다.
여기에서의 코드들을 구현하는것이 이번 pintos 주차의 주제였습니다.
pintos 내부 코드에 대한 자세한 내용은 말씀드렸듯 tistory 이사 후에 다시 말씀드리도록 하겠습니다..
pintos를 시작한지 2주반이 지나갔습니다.
정말 시간이 어떻게 지나갔는지 모를정도로 빠르게 지나갔는데,
앞만 보고 직진을 하였기 때문에 다시 왔던길을 되돌아 보면 제가 무엇을 배웠는지가 모호해지는것같아 더욱더 메모의 중요성을 절실히 느끼는 주차가 되기도 하였습니다..
하지만 pintos주간이 워낙 말도 안되게 바쁜터라..
블로그를 꾸준히 작성을 하겠습니다! 라고는 확답을 못드리겠네요..
장난입니다. 정말 최선을 다해 간단하게라도 생존신고는 하겠습니다.
다음주에 뵙겠습니다.
긴 글 읽어주셔서 감사합니다.