인터럽트 프레임 및 do_iret 함수
인터럽트 프레임이란 무엇인지, 그리고 do_iret 에서는 어떤 일을 하는 건지 이제서야 조금 알 것 같아 별도로 글을 작성한다.
struct intr_frame
do_iret을 이해하려면 인터럽트 프레임 구조체의 크기와, 인터럽트 프레임 구조체 내의 gp_registers 구조체, 그리고 나머지 멤버변수들의 크기를 알고있어야 한다.
데이터 크기
각각의 사이즈를 모두 출력해보았다. (뭘 굳이 찍어봐야 아나 싶은 것들도 있지만 난 아직도 데이터 크기가 어렵다 😭 )
printf( "인터럽트 프레임 사이즈: %d \\n", sizeof(struct intr_frame));
printf( "gp_registers 사이즈: %d \\n", sizeof(struct gp_registers));
printf( "uint16_t 사이즈: %d \\n", sizeof(uint16_t));
printf( "uint32_t 사이즈: %d \\n", sizeof(uint32_t));
printf( "uintptr_t 사이즈: %d \\n", sizeof(uintptr_t));
이렇게 찍으면 아래와 같이 출력된다. 이 정보들을 잘 기억하고 있자.
인터럽트 프레임 사이즈: 192
gp_registers 사이즈: 120
uint16_t 사이즈: 2
uint32_t 사이즈: 4
uintptr_t 사이즈: 8
intr_frame 구조체 정의
인터럽트 프레임 구조체는 아래와 같이 정의되어있다. 위에서 확인한 바와 같이 이 구조체는 192 바이트 크기를 가지고 있고, 첫번째 멤버변수는 gp_registers 라는 구조체는 120 바이트 크기를 가진다.
struct intr_frame {
/* Pushed by intr_entry in intr-stubs.S.
These are the interrupted task's saved registers. */
struct gp_registers R; // 범용 레지스터 - 120 바이트
uint16_t es;
uint16_t __pad1;
uint32_t __pad2;
uint16_t ds; // 세그먼트 관리
uint16_t __pad3;
uint32_t __pad4;
/* Pushed by intrNN_stub in intr-stubs.S. */
uint64_t vec_no; /* Interrupt vector number. 인터럽트 종류 */
/* Sometimes pushed by the CPU,
otherwise for consistency pushed as 0 by intrNN_stub.
The CPU puts it just under `eip', but we move it here. */
uint64_t error_code;
/* Pushed by the CPU.
These are the interrupted task's saved registers. */
uintptr_t rip; // pc
uint16_t cs; // 세그먼트 관리
uint16_t __pad5;
uint32_t __pad6;
uint64_t eflags;// cpu 상태를 나타내는 정보
uintptr_t rsp; // 스택 포인터
uint16_t ss;
uint16_t __pad7;
uint32_t __pad8;
} __attribute__((packed));
gp_registers 는 아래와 같이 정의된다.
/* Interrupt stack frame. */
struct gp_registers {
uint64_t r15;
uint64_t r14;
uint64_t r13;
uint64_t r12;
uint64_t r11;
uint64_t r10;
uint64_t r9;
uint64_t r8;
uint64_t rsi;
uint64_t rdi;
uint64_t rbp;
uint64_t rdx;
uint64_t rcx;
uint64_t rbx;
uint64_t rax;
} __attribute__((packed));
자리에 리버싱 책을 겸비한 우리 반 신 교수님이 인터럽트 프레임 구조체의 모습을 그려놓은 게 있어서 허락을 받고 가져와봤다.
do_iret 함수
do_iret 함수는 원래 스레드간 문맥 교환시에 호출되는 함수다. schedule 함수를 보면 readylist에서 다음 실행할 스레드를 찾아 이 함수를 thread_launch 함수의 인자로 넣어 호출한다.
thread_launch(next); /* schedule 함수 일부 */
그리고 thread_launch()에서는 현재 실행중인 스레드의 모든 정보를 현재 스레드의 인터럽트 프레임 구조체 tf에 담고, 인자로 들어온 next 즉, 새로 실행할 스레드의 인터럽트 프레임 구조체를 인자로 담아 do_iret을 호출한다. 그러면 do_iret은 인자로 들어온 인터럽트 프레임내의 정보를 CPU로 복원시키는 일을 한다. 이러한 방식으로 문맥 전환이 발생한다.
🔎 thread_launch() 함수 내의 주석을 보면 아래와 같다. 현재 실행 중인 스레드의 실행 문맥을 intr_frame 안에 저장한 후, next의 스레드의 tf 구조체를 인자로 담아 do_iret을 호출함으로서 새로운 thread(= 인자로 받은 next 스레드)로 전환한다.
/* The main switching logic.
* We first restore the whole execution context into the intr_frame
* and then switching to the next thread by calling do_iret.
* Note that, we SHOULD NOT use any stack from here
* until switching is done. */
이렇게 원래 do_iret은 thread_launch 내에서 호출되어 스레드 간 문맥 교환에 사용되는 함수이다. 그런데 이를 유저프로세스를 호출할 때도 사용할 수 있다. (이 이야기는 별도로 따로 하는 것으로! )
이제 진짜 do_iret으로 넘어가보면 do_iret 함수는 아래와 같이 정의되어있다. (프로젝트 1때는 이 내용이 뭘 의미하는지 자세히 알아볼 엄두도 안나서 그냥 넘어갔는데 프로젝트2를 하다보니 do_iret에 대한 분명한 이해하 필요하다는 것을 깨닫.. )
인터럽트 프레임 구조체를 인자로 받는 do_iret 함수가 하는 일은
/* Use iretq to launch the thread */
void do_iret(struct intr_frame *tf)
{
__asm __volatile(
"movq %0, %%rsp\\n"
"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 $32, %%rsp\\n"
"iretq"
:
: "g"((uint64_t)tf)
: "memory");
}
처음이자 마지막으로 한 줄씩 뜯어보자 ^^:
__asm __volatile( )
깃북에도 나와있지만 이는 최적화 장벽의 역할을 한다. 컴파일러가 최적화를 수행할 때 이 부분은 건드리지 말라고 명시하는 역할이다.
"movq %0, %%rsp\\n"
인자로 들어온 *tf의 주소를 CPU의 rsp에 저장한다. (이게 이상하다고 생각되면 똑똑한 사람… ^0^ 이에 대한 내용은 밑에서 다시 다룬다)
"movq 0(%%rsp),%%r15\\n" /* rsp위치의 값을 레지스터 r15에 저장 */
"movq 8(%%rsp),%%r14\\n" /* rsp + 8 의 위치의 값을 레지스터 r14에 저장 */
"movq 16(%%rsp),%%r13\\n" /* rsp + 16 의 위치의 값을 레지스터 r13에 저장 */
"movq 24(%%rsp),%%r12\\n" /* rsp + 24 의 위치의 값을 레지스터 r12에 저장 */
"movq 32(%%rsp),%%r11\\n" /* rsp + 32 의 위치의 값을 레지스터 r11에 저장 */
"movq 40(%%rsp),%%r10\\n" /* rsp + 40 의 위치의 값을 레지스터 r10에 저장 */
"movq 48(%%rsp),%%r9\\n" /* rsp + 48 의 위치의 값을 레지스터 r9에 저장 */
"movq 56(%%rsp),%%r8\\n" /* rsp + 56 의 위치의 값을 레지스터 r8에 저장 */
"movq 64(%%rsp),%%rsi\\n" /* rsp + 64 의 위치의 값을 레지스터 rsi에 저장 */
"movq 72(%%rsp),%%rdi\\n" /* rsp + 72 의 위치의 값을 레지스터 rdi에 저장 */
"movq 80(%%rsp),%%rbp\\n" /* rsp + 80 의 위치의 값을 레지스터 rbp에 저장 */
"movq 88(%%rsp),%%rdx\\n" /* rsp + 88 의 위치의 값을 레지스터 rdx에 저장 */
"movq 96(%%rsp),%%rcx\\n" /* rsp + 96 의 위치의 값을 레지스터 rcx에 저장 */
"movq 104(%%rsp),%%rbx\\n" /* rsp + 104 의 위치의 값을 레지스터 rbx에 저장 */
"movq 112(%%rsp),%%rax\\n" /* rsp + 112 의 위치의 값을 레지스터 rax에 저장 */
인터럽트 프레임의 gp_Register 구조체 (120바이트 크기)를 CPU에 복원하는 부분이다. rsp를 8씩 내려가면서 각 정보를 순서대로 CPU 범용 레지스터에 복원한다. 위의 그림을 보면서 코드를 보면 더 쉽게 이해가 된다.
아직까지 rsp는 여전히 tf 구조체의 시작점을 가리키고 있다. 이제 rsp의 위치를 바꿀 것이다.
"addq $120,%%rsp\\n"
rsp에 120(gp_registers의 크기)을 더해준다. 이제 아래의 인스트럭션을 수행하면서 rsp는 gp_registers 다음 부분을 가리키게 된다. 기존 rsp의 위치와 현재 rsp의 위치를 그림에 표시해 보았다.
"movw 8(%%rsp),%%ds\\n"
rsp + 8 위치의 값을 레지스터 ds에 저장한다.
"movw (%%rsp),%%es\\n"
rsp 위치의 값을 레지스터 es에 저장한다.
"addq $32, %%rsp\\n"
rsp 의 값을 32만큼 증가시킨다. 이제 rsp는 rip를 가리키고 있다. (아래 그림 참조)
현 시점에서 아직 인터럽트 프레임에서 CPU로 복원되지 않은 정보는 rip, cs, eflags, rsp, ss 이다.
"iretq"
이 정보들을 CPU 레지스터에 복원해주는 것은 iretq 인스트럭션이 실행한다. iretq 인스트럭션은 인터럽트 프레임의 rip 값을 복원함으로서 기존에 수행하던 스레드의 다음 실행 명령을 실행하는 역할인데, 이 rip가 유저프로세스의 rip 값을 갖고 있는 경우에는 CPU는 유저프로세스의 다음 실행 명령을 실행하게 된다.
유저모드에서 커널 모드로 넘어와서 do_iret을 실행하고 있는 경우에 이 인터럽트 프레임 구조체 안에 있는 rsp는 유저스택의 rsp 이기 때문에 이 rsp가 다시 CPU로 복원이 되면서 커널모드→ 유저모드로 복귀하게 된다.
: "g"((uint64_t)tf)
여기서 이 g는 인자를 의미한다. 즉 0번째 인자로(uint64_t)tf 를 넣는다는 것이다.
사실 do_iret의 첫 인스트럭션 "movq %0, %%rsp\\n" 을 tf 주소를 rsp로 옮긴다고 얼렁뚱땅 넘어갔었는데, 어떻게 %0이 tf가 되냐!! 라고 한다면 그 이유는 바로 위의 코드에서 0번째 인자로 tf를 지정해줬기 때문이다.
끝.
'TIL' 카테고리의 다른 글
video태그의 playsInline 속성 - 모바일 기기 전체화면 재생을 방지하기 위한 속성 (0) | 2023.08.28 |
---|---|
[JS] 자바스크립트에서 함수 정의 - 함수 정의 이전에 함수 호출하기 (0) | 2023.04.27 |
[OS] Pintos - system call 호출 및 흐름 (0) | 2022.11.25 |
[자료구조] Red Black Tree (레드 블랙 트리) | 레드 블랙 트리란, 경계 노드, black-height(흑색높이), Left-Rotate(좌회전), Right-Rotate(우회전), C언어 구현 (2) | 2022.10.24 |
백준 11049. (PyPy3 1등 먹은 풀이!) 행렬체인곱셈 | Python 다이나믹 프로그래밍 (0) | 2022.10.19 |
댓글