Krafton Jungle/5. PintOS

[PintOS 2주차] Day 3

munsik22 2025. 5. 17. 11:34

형이 집에 안 와서 슬픈 문식이

인수 전달 구현하기

우선 x86-64 시스템의 함수 호출 관례(System V AMD64 ABI)를 다시 살펴보자.

  1. 사용자 레벨의 응용프로그램은 시퀀스를 전달하기 위해 정수 레지스터들을 사용한다:
    %rdi, %rsi, %rdx, %rcx, %r8, %r9
  2. 호출자는 스택에 다음 인스트럭션의 주소(리턴 주소)를 push하고 피호출자의 첫 번째 인스트럭션으로 점프한다: CALL
  3. 피호출자가 실행된다.
  4. 피호출자가 리턴값을 가지면 %rax 레지스터에 저장한다.
  5. 피호출자는 스택에서 리턴 주소를 pop하고 특정된 주소로 점프함으로써 리턴한다: RET

위의 ABI에 의하면, 정수/포인터 인수 1~6은 RDI, RSI, RDX, RCX, R8, R9에 저장되고, 6개를 초과하게 되면 나머지는 스택에 저장된다. 리턴값의 경우 최대 64비트까지는 RAX에 저장되고, 최대 128비트까지는 RAXRDX에 저장된다.

 

3개의 정수 인수를 가지는 함수 f()를 생각해보자. 아래 그림은 f()f(1, 2, 3)과 같이 호출되었다고 가정했을 때, (위의 3번째 단계에서) 피호출자에서 보이는 샘플 스택 프레임과 레지스터 상태를 보여준다(초기 스택 주소는 임의로 정했다).

  • RDI: 0x0000000000000001
  • RSI: 0x0000000000000002
  • RDX: 0x0000000000000003

아래 테이블은 사용자 프로그램 시작 직전의 스택과 관련된 레지스터의 상태를 보여준다. 스택이 아래 방향으로 성장한다는 것을 기억하자.

  • RDI : 4 인수의 개수(argc)
  • RSI : 0x4747FFC0 인수 배열(argv)의 시작 주소

여기서 스택 포인터는 0x4747FFB8로 초기화되어 있다. 위에서 나타난 대로, 작성한 코드는 0x4748000으로 정의된 USER_STACK에서 시작해야 한다.

주소는 위에서 아래로 (높은 주소 → 낮은 주소)
스택은 위에서 아래로 자라기 때문에 실제 메모리는 아래로 push됨
Address Name Data Type 설명
0x4747fffc argv[3][...] 'bar\0' char[4 문자열 "bar"을 스택에 복사
0x4747fff8 argv[2][...] 'foo\0' char[4] 문자열 "foo" 복사
0x4747fff5 argv[1][...] '-l\0' char[3] 문자열 "-l" 복사
0x4747ffed argv[0][...] '/bin/ls\0' char[8] 프로그램 이름 복사
0x4747ffe8 word-align 0 uint8_t[] 정렬 맞추기 위해 5바이트 패딩 삽입
0x4747ffe0 argv[4] 0 char * NULL, argv 종료 표시
0x4747ffd8 argv[3] 0x4747fffc char * bar의 주소
0x4747ffd0 argv[2] 0x4747fff8 char * foo의 주소
0x4747ffc8 argv[1] 0x4747fff5 char * -l의 주소
0x4747ffc0 argv[0] 0x4747ffed char * /bin/ls의 주소
0x4747ffb8 return address 0 void (*)() fake return address (사용되지 않음)

이 구조는 C에서의 main(int argc, char **argv)을 만족시키기 위한 스택 구조다.

int main(int argc, char **argv)

이 조건을 맞추기 위해:

  • 문자열 데이터를 먼저 복사 (char[])
  • 그 주소들을 다시 push해서 argv[] 배열 구성 (char *[])
  • 마지막에 argv[argc] = NULL
  • 마지막에 fake return address
Stack (rsp →)
  0x4747fffc  "bar\0"
  0x4747fff8  "foo\0"
  0x4747fff5  "-l\0"
  0x4747ffed  "/bin/ls\0"
  0x4747ffe8  padding (word-align)
  0x4747ffe0  argv[4] = NULL
  0x4747ffd8  argv[3] = "bar"
  0x4747ffd0  argv[2] = "foo"
  0x4747ffc8  argv[1] = "-l"
  0x4747ffc0  argv[0] = "/bin/ls"
  0x4747ffb8  return addr = 0
구성 요소 의미
문자열 (char[]) 유저 스택에 복사되는 실제 인자 문자열
argv[i] (char*) 해당 문자열의 주소를 담는 포인터 배열
argv[argc] = NULL C 규약에 맞는 배열 종료
word-align 스택 정렬 보장 (8 byte 맞춤)
return address 유저 프로그램에서는 사용되지 않지만 관례적으로 넣음
int main(int argc, char **argv) {
    printf("argc: %d\n", argc);            // 4
    printf("argv[0]: %s\n", argv[0]);      // "/bin/ls"
    printf("argv[1]: %s\n", argv[1]);      // "-l"
    printf("argv[2]: %s\n", argv[2]);      // "foo"
    printf("argv[3]: %s\n", argv[3]);      // "bar"
    printf("argv[4]: %p\n", argv[4]);      // NULL
}
구성 요소 위치
rsp 0x4747ffb0 argc = 4
rsp+8 0x4747ffb8 return addr = 0
rsp+16 0x4747ffc0 argv[0] = "/bin/ls"
rsp+24 0x4747ffc8 argv[1] = "-l"
rsp+32 0x4747ffd0 argv[2] = "foo"
rsp+40 0x4747ffd8 argv[3] = "bar"
rsp+48 0x4747ffe0 argv[4] = NULL
💭 word-align이 필요한 이유가 뭐지?
  • CPU나 ABI 규약에서는 스택 포인터(rsp)가 정렬alignment되어 있어야 효율적이고 안정적으로 작동한다.
    • System V x86 ABI에 따르면 함수 호출 직전에는 스택 포인터가 16의 배수로 정렬되어야 함
  • 왜 정렬이 필요한가?
    • CPU가 4바이트 또는 8바이트 단위로 접근할 때, 정렬되지 않은 주소는 느리고 위험
    • 특히 구조체, 포인터, 인자 배열 등을 push할 때 정렬이 안 되면 segmentation fault 발생 가능성도 있음
  • 왜 5바이트만큼 정렬했을까?
    • 이건 예시 상황에서 argv 문자열들을 먼저 복사했더니 rsp가 5바이트 정렬 어긋났기 때문이다.
      • 마지막 문자열까지 복사하고 나면 rsp = 0x4747ffed
      • 이건 8바이트 기준 정렬이 깨짐
      • 그래서 rsp -= 5 하여 rsp = 0x4747ffe8align
    • 즉, word-align의 실제 크기는 rsp % 8 의 나머지를 맞추기 위한 여유 공간이다.
스택 조작 직후 주소 정렬 후 주소 삽입된 패딩 크기
0x4747ffed 0x4747ffe8 5바이트

hex_dump?

void hex_dump (uintptr_t ofs, const void *buf_, size_t size, bool ascii);
Dumps the SIZE bytes in BUF to the console as hex bytes arranged 16 per line.
Numeric offsets are also included, starting at OFS for the first byte in BUF.
If ASCII is true, then the corresponding ASCII characters are also rendered alongside.
  • 메모리 덤프를 16진수 형태로 출력한다.
  • 인수들이 정확히 유저 스택에 push되었는지를 확인한다.
💭 init.c에서 argv = read_command_line (); argv = parse_options (argv);
처럼 처리가 되고 process_exec()로 넘어간다.
main에서 파싱이 되었는데 또 process_exec()에서 파싱 로직을 구현해야 하는 이유가 뭐지?

 

결론부터 말하자면, main()에서 파싱한 결과는 현재 프로세스의 커널 메모리 안에만 존재하고, process_exec()은 새로운 사용자 프로세스를 만드는 것이기 때문에, 해당 유저 스택에 argv 형식으로 직접 다시 세팅해줘야 하기 때문이다.

  • process_exec()는:
    • 현재 실행 중인 thread를 제거하고,
    • 새로운 유저 프로세스를 로딩해서
    • 유저 스택 rsp에 인자 문자열을 올려야 함
  • 그런데 기존 argv[] 배열은 커널 영역의 힙/스택 안에 있으므로, 유저 스택에 직접 새로 파싱해서 옮겨야 한다.
질문 답변
왜 또 파싱해야 함? 유저 스택은 별개 공간이고, argv를 올려줘야 하기 때문
이전 파싱 결과를 그대로 못 쓰나? 못 씀. 커널 메모리 안에 있고, 유저 프로그램은 접근 불가
그럼 어디서 인자 전달 결정됨? main()에서 파싱 후, process_exec()에서 직접 재구성

 

그리고 main()에서 실행되는 parse_options()는 옵션을 확인하는데만 사용되고, 이후로는 커맨드 전체가 다른 함수로 전달되기 때문에 process_exec()에서 따로 파싱 로직을 구현해야 된다고 한다.


구현 아이디어

🔹 스택에 들어가는 순서는 argv[3][…]argv[2][…]argv[1][…]argv[0][…] → … 순이다.

  1. "bar" 문자열 복사 → argv[3][…] (0x4747fffc)
  2. "foo" 문자열 복사 → argv[2][…] (0x4747fff8)
  3. "-l" 문자열 복사 → argv[1][…] (0x4747fff5)
  4. "/bin/ls" 문자열 복사 → argv[0][…] (0x4747ffed)
  5. word-align → 5바이트 padding → (0x4747ffe8)
  6. NULL → argv[4] = 0 → (0x4747ffe0)
  7. argv[3] = 0x4747fffc → (0x4747ffd8)
  8. argv[2] = 0x4747fff8 → (0x4747ffd0)
  9. argv[1] = 0x4747fff5 → (0x4747ffc8)
  10. argv[0] = 0x4747ffed → (0x4747ffc0)
  11. fake return address → 0 → (0x4747ffb8)
"bar" → "foo" → "-l" → "/bin/ls" → word-align → NULL → argv[3] → argv[2] → argv[1] → argv[0] → return addr
  • 주소도 점점 낮아지고,
  • 나중에 argv[] 포인터 배열이 역순으로 참조할 수 있다.

🔹 word-align은 문자열들을 스택에 복사한 후, rsp가 8바이트 정렬이 되도록 맞추기 위해 padding을 추가하는 단계다.

  • rsp = USER_STACK (스택 최상단)
  • 문자열들을 push: rsp -= strlen(argv[i]) + 1
  • 문자열 복사가 끝난 시점에서 rsp는 정렬이 깨져 있을 수 있음
  • 따라서 rsp를 정렬해주기 위해 padding 추가 → 이게 word-align

테스트 진행

1️⃣ 파일 시스템 생성 (한 번만 하면 됨)

pintos-mkdisk filesys.dsk 10
  • 10MB짜리 filesys.dsk 생성됨

2️⃣ 파일 시스템 포맷

pintos -v --fs-disk=filesys.dsk -- -f -q

 

3️⃣ 유저 프로그램 실행

pintos -v -k -T 15 -m 20 --fs-disk=filesys.dsk -- -q run 'echo x'

실행 결과 갑자기 thread.c에서 kernel PANIC이 발생했다 🤯

 

backtrace를 사용해 정확히 어떤 코드에서 문제가 발생했는지 확인했다.

$ gdb kernel.o
(gdb) list *0x8004218366
0x8004218366 is in debug_panic (../../lib/kernel/debug.c:32).
27                      va_start (args, message);
28                      vprintf (message, args);
29                      printf ("\n");
30                      va_end (args);
31
32                      debug_backtrace ();
33              } else if (level == 2)
34                      printf ("Kernel PANIC recursion at %s:%d in %s().\n",
35                                      file, line, function);
36              else {
(gdb) list *0x80042071be
0x80042071be is in thread_yield (../../threads/thread.c:324).
warning: Source file is more recent than executable.
319             struct thread *curr = thread_current ();
320             enum intr_level old_level;
321
322             ASSERT (!intr_context ());
323
324             old_level = intr_disable ();
325             if (curr != idle_thread) {
326                     // list_push_back (&ready_list, &curr->elem);
327                     list_insert_ordered(&ready_list, &curr->elem, cmp_priority, NULL); // The current thread is inserted to ready_list to prioirty order.
328             }
(gdb) list *0x8004207ddb
0x8004207ddb is in do_preemption (../../threads/thread.c:703).
698             }
699             return res;
700     }
701
702     void
703     do_preemption (void) {
704             if (!intr_context() && !list_empty (&ready_list)) {
705                     struct thread *front = list_entry(list_front(&ready_list), struct thread, elem);
706                     if (thread_get_priority() < front->priority)
707                             thread_yield ();
(gdb) list *0x800420a565
0x800420a565 is in sema_up (../../threads/synch.c:119).
warning: Source file is more recent than executable.
114                     list_sort(&sema->waiters, cmp_priority, NULL);
115                     thread_unblock (list_entry (list_pop_front (&sema->waiters), struct thread, elem));
116             }
117             sema->value++;
118             do_preemption();
119             intr_set_level (old_level);
120     }
121
122     static void sema_test_helper (void *sema_);
123
(gdb) list *0x800421492d
0x800421492d is in interrupt_handler (../../devices/disk.c:526).
521                             if (c->expecting_interrupt) {
522                                     inb (reg_status (c));               /* Acknowledge interrupt. */
523                                     sema_up (&c->completion_wait);      /* Wake up waiter. */
524                             } else
525                                     printf ("%s: unexpected interrupt\n", c->name);
526                             return;
527                     }
528
529             NOT_REACHED ();
530     }
(gdb) list *0x800420935d
0x800420935d is in intr_handler (../../threads/interrupt.c:352).
347             }
348
349             /* Invoke the interrupt's handler. */
350             handler = intr_handlers[frame->vec_no];
351             if (handler != NULL)
352                     handler (frame);
353             else if (frame->vec_no == 0x27 || frame->vec_no == 0x2f) {
354                     /* There is no handler, but this interrupt can trigger
355                        spuriously due to a hardware fault or hardware race
356                        condition.  Ignore it. */
(gdb) list *0x800420977b
(gdb) list *0x80042073de
0x80042073de is in kernel_thread (../../threads/thread.c:424).
419                     asm volatile ("sti; hlt" : : : "memory");
420             }
421     }
422
423     /* Function used as the basis for a kernel thread. */
424     static void
425     kernel_thread (thread_func *function, void *aux) {
426             ASSERT (function != NULL);
427
428             intr_enable ();       /* The scheduler runs with interrupts off. */
(gdb)

 

문제의 원인은 sema_up()do_preemption()thread_yield()가 인터럽트 중에 호출된 것이었다.

  • 디스크 인터럽트가 발생 → interrupt_handler() (disk.c:526)
  • 그 안에서 sema_up() 호출 (synch.c:114)
  • sema_up()do_preemption() 호출 (synch.c:118)
  • do_preemption()thread_yield() 호출 (thread.c:707)
  • thread_yield()인터럽트 중이었기 때문에 panic 발생 (thread.c:322)

즉, 인터럽트 핸들러 내부에서 do_preemption() 또는 thread_yield()가 호출되어서 ASSERT (!intr_context ());에 걸린 것이 문제였다.

void thread_set_priority (int new_priority) {
	if (!thread_mlfqs) {
		thread_current ()->priority_ori = new_priority; // Set priority of the current thread
		// list_sort(&ready_list, cmp_priority, NULL); // Reorder the ready_list
		thread_refresh_priority();
		if (!intr_context())
			do_preemption();
	}
}

void do_preemption (void) {
	if (!intr_context() && !list_empty (&ready_list)) {
		struct thread *front = list_entry(list_front(&ready_list), struct thread, elem);
		if (thread_get_priority() < front->priority)
			thread_yield ();
	}
}

 

do_preemption()을 호출하는 함수에 !intr_context() 조건을 달아서 인터럽트 핸들러에서 thread_yield()가 호출되는 것을 방지해서 문제를 해결했다.

하지만 바로 다른 오류가 발생했다. echo라는 파일이 없기 때문이었다 🙄

'Krafton Jungle > 5. PintOS' 카테고리의 다른 글

[PintOS 2주차] Day 6  (0) 2025.05.20
[PintOS 2주차] Day 4-5  (0) 2025.05.19
[PintOS 2주차] Day 2  (0) 2025.05.16
[PintOS 2주차] Day 1  (0) 2025.05.15
[PintOS 1주차] Bonus: Advanced Scheduler  (0) 2025.05.15