
인수 전달 구현하기
우선 x86-64 시스템의 함수 호출 관례(System V AMD64 ABI)를 다시 살펴보자.
- 사용자 레벨의 응용프로그램은 시퀀스를 전달하기 위해 정수 레지스터들을 사용한다:
%rdi,%rsi,%rdx,%rcx,%r8,%r9 - 호출자는 스택에 다음 인스트럭션의 주소(리턴 주소)를 push하고 피호출자의 첫 번째 인스트럭션으로 점프한다:
CALL - 피호출자가 실행된다.
- 피호출자가 리턴값을 가지면
%rax레지스터에 저장한다. - 피호출자는 스택에서 리턴 주소를 pop하고 특정된 주소로 점프함으로써 리턴한다:
RET
위의 ABI에 의하면, 정수/포인터 인수 1~6은 RDI, RSI, RDX, RCX, R8, R9에 저장되고, 6개를 초과하게 되면 나머지는 스택에 저장된다. 리턴값의 경우 최대 64비트까지는 RAX에 저장되고, 최대 128비트까지는 RAX와 RDX에 저장된다.
3개의 정수 인수를 가지는 함수 f()를 생각해보자. 아래 그림은 f()가 f(1, 2, 3)과 같이 호출되었다고 가정했을 때, (위의 3번째 단계에서) 피호출자에서 보이는 샘플 스택 프레임과 레지스터 상태를 보여준다(초기 스택 주소는 임의로 정했다).

RDI:0x0000000000000001RSI:0x0000000000000002RDX: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 = 0x4747ffe8로 align
- 마지막 문자열까지 복사하고 나면
- 즉, 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][…] → … 순이다.
"bar"문자열 복사 →argv[3][…](0x4747fffc)"foo"문자열 복사 →argv[2][…](0x4747fff8)"-l"문자열 복사 →argv[1][…](0x4747fff5)"/bin/ls"문자열 복사 →argv[0][…](0x4747ffed)- word-align → 5바이트 padding → (
0x4747ffe8) - NULL →
argv[4] = 0→ (0x4747ffe0) argv[3]=0x4747fffc→ (0x4747ffd8)argv[2]=0x4747fff8→ (0x4747ffd0)argv[1]=0x4747fff5→ (0x4747ffc8)argv[0]=0x4747ffed→ (0x4747ffc0)- 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 |