[PintOS] Project 2 : Argument Passing
Setup
아무 코드도 수정하지 않고 처음 빌드 시에, thread 부분에서의 커널 패닉 방지를 위한 코드를 다음과 같이 추가해주었다. 준비된 스레드가 없을 때 쓸데없이 idle thread로 context switch 되는 것을 막아 잘못된 스케줄링이 이루어지지 않도록 하는 코드이다.
void
thread_yield (void) {
struct thread *curr = thread_current ();
enum intr_level old_level;
ASSERT (!intr_context ()); // Do Not yield in interrupt context
/* Project 2 : args 에러 방어 코드 추가 */
if(list_empty(&ready_list))
return;
old_level = intr_disable (); // Disable interrupt to protect critical section
if (curr != idle_thread)
// list_push_back (&ready_list, &curr->elem);
list_insert_ordered(&ready_list, &curr->elem, cmp_priority, NULL);
do_schedule (THREAD_READY);
intr_set_level (old_level);
}
void
test_max_priority(void){
/* Project 2 : args 에러 방어 코드 추가 */
if(list_empty(&ready_list))
return;
struct thread *t = list_entry(list_front(&ready_list), struct thread, elem);
if(thread_current()->priority < t->priority){
if(intr_context())
intr_yield_on_return();
else
thread_yield();
}
}
아무것도 손대지 않고 다음과 같이 테스트가 실행되면 성공이다.
경로 : pintos-kaist/userprog/build
- userprog 경로에서 make 실행 후 하위 디렉토리 build 에서 다음과 같이 명령어 수행
pintos --fs-disk=10 -p tests/userprog/args-single:args-single -- -q -f run 'args-single onearg'
구현 목표
- 프로세스 생성 시 명령줄 인자를 적절히 파싱하고
- 사용자 스택에 x86-64 calling convention에 맞추어 적재하고
- 사용자 프로그램이 인자를 사용할 수 있도록 하는 기능을 process_exec()에 구현한다
함수 호출 흐름
Pintos 프로젝트를 진행하면서, "pintos run 'echo hi'" 명령이 어떻게 실제 사용자 프로그램을지 알아보자!
- 프로그램 실행
- 사용자는 pintos run 명령을 통해 프로그램을 실행
- 이 명령은 내부적으로 pintos 스크립트를 통해 QEMU를 실행하고 커널 이미지를 로드
- 커널 부팅 및 초기화
- main() 함수가 진입점
- 커맨드라인 인자를 파싱하고 적절한 액션을 결정
- 유저 프로그램 실행 준비
- run_task() : 인자로 받은 프로그램을 실행하기 위해 새로운 유저 프로세스를 실행한다
- process_create_initd() : 사용자 프로세스를 위한 최초 스레드 생성
- thread_create 에서 “echo”라는 유저 프로세스를 initd()로 실행
- 유저 코드 진입
- initd()에서 유저 프로세스를 진입
- process_exec() 을 통해 유저 프로그램 ELF를 메모리에 로드하고 스택 설정
- do_iret() 으로 커널 모드 → 유저 모드로 전환
- 유저 프로그램 실행
- ELF의 entry_point로 지정된 _start() 함수에서 main()이 호출된다
- 시스템 콜 처리
- 유저 프로그램이 write, exit 등의 시스템 콜을 호출하면 인터럽트를 통해 커널의 syscall_handler로 진입
- 프로그램 종료
- exit 시스템 콜을 통해 상태값을 반환하고, 커널은 자원을 해제, 스레드 종료
- halt를 호출하여 power_off 되거나, 부모 프로세스가 자식 종료 상태를 수거하며 종료
PintOS와 일반 운영 체제와의 차이점
PintOS는 “프로세스 = 스레드” 인 단일 스레드 환경이다. 프로세스 간 주소 공간 분리는 존재하지만, 그 안에서의 멀티 스레딩은 구현되어있지 않다.
반면에 일반 운영체제는 다중 프로세스 + 다중 스레드 기반이다. 커널은 여러 CPU 코어에서 여러 스레드를 동시에 실행할 수 있도록 스케줄링한다. 이 구조적 차이를 바탕으로 process_exec()
이나 syscall_handler()
같은 부분을 구현할 때도, Pintos는 “단순하게 단일 유저 실행 흐름”에 집중하면 된다는 것을 기억해야 한다! 우리가 CSAPP에서 배운 내용과 실제 구현의 차이점도 바로 이 부분에서 나타난다.
구현
참조 라이브러리 함수
strtok_r()
: 문자열을 토큰으로 분리한다. PintOS에서는 lib/string.c에 구현되어 있다.memcpy(void *dest, const void *src, size_t n)
: src 메모리 영역에서 n 바이트만큼 복사해서 dest 메모리 영역에 저장한다. 바이트 단위 복사.memset(void *ptr, int value, size_t num)
: 메모리 주소 ptr부터 num 바이트를 value 값 (정수 0~255)을 바이트 단위로 채운다.
수정 함수
process_exec()
- 사용자 프로그램 실행 시 인자(argument)를 스택에 적절히 적재하기 위한 준비 단계이다.
strtok_r
을 사용하여 명령어 문자열을 분리한다.- 입력 문자열(
file_name
)을 공백 기준으로 잘라서argv
배열에 하나씩 저장한다. - 예 : echo x, y, z → echo\0, x\0, y\0, z\0 이렇게 분리한다
- 입력 문자열(
load
함수가 호출되며 ELF 헤더를 읽고setup_stack
으로rsp
의 초기 위치를 확보한다- 스택 페이지가 만들어지면,
argument_stack
함수를 호출하여 실제 데이터를 스택에 올린다.
int
process_exec (void *f_name) {
char *file_name = f_name;
bool success;
/* We cannot use the intr_frame in the thread structure.
* This is because when current thread rescheduled,
* it stores the execution information to the member. */
struct intr_frame _if;
_if.ds = _if.es = _if.ss = SEL_UDSEG;
_if.cs = SEL_UCSEG;
_if.eflags = FLAG_IF | FLAG_MBS;
/* We first kill the current context */
process_cleanup ();
/* Project 2 : args */
char *token, *save_ptr;
char *argv[MAX_ARGC]; // 128 byte
int argc = 0;
/* 문자열을 파싱하여 argv 포인터 배열로 저장한다 */
for (token = strtok_r(file_name, " ", &save_ptr); token != NULL;
token = strtok_r(NULL, " ", &save_ptr))
{
argv[argc++] = token;
}
/* And then load the binary */
success = load (file_name, &_if);
/* Project 2 : args */
argument_stack(argv, argc, &_if);
/* If load failed, quit. */
palloc_free_page (file_name);
if (!success)
return -1;
hex_dump(_if.rsp, _if.rsp, USER_STACK - _if.rsp, true);
/* Start switched process. */
do_iret (&_if);
NOT_REACHED ();
}
추가 함수
argument_stack()
- 스택 메모리에 데이터를 적재하는 함수이다.
- argv[] 에 저장된 문자열 인자들을 스택 메모리에 복사하고, 각 인자의 주소를 스택에 저장하고, 레지스터를 설정해 사용자 프로그램이 인자를 사용할 수 있게 한다.
- 다음과 같은 순서로 스택에 푸시되어야 한다.
(가장 높은 주소)
+---------------------+
| 'bar\0' | ← argv[3]
+---------------------+
| 'foo\0' | ← argv[2]
+---------------------+
| '-l\0' | ← argv[1]
+---------------------+
| '/bin/ls\0' | ← argv[0]
+---------------------+
| Word alignment (0) |
+---------------------+
| NULL (argv[4]) |
+---------------------+
| argv[3] 포인터 |
+---------------------+
| argv[2] 포인터 |
+---------------------+
| argv[1] 포인터 |
+---------------------+
| argv[0] 포인터 | ← RSI (%rsi)
+---------------------+
| Fake return address | ← RSP (%rsp)
+---------------------+
(가장 낮은 주소)
- 문자열 데이터를 역순으로 복사한다
- 스택은 높은 주소 → 낮은 주소로 자라기 때문에 for 문으로 역순회하여 복사한다
- 각 문자열의 시작 주소를
arg_addr[]
에 저장하고, 나중에 이 포인터를 4번에서 스택에 넣는다. - 여기서
strlen(argv[i])
에서 +1 을 하는 이유는 널 종료문자 (\0
) 길이를 포함하기 위함이다
- 스택을 8 바이트로 정렬한다
- NULL 포인터를 삽입한다
- C의
argv[]
배열은 마지막 원소가 NULL이어야 한다
- C의
- 각 문자열의 포인터를 역순으로 저장한다
argv[0]
,argv[1]
…argv[argc]
까지 포인터들을 저장한다
rdi
는argc
,rsi
는argv
의 시작 주소를 담은 레지스터를 설정한다- 프로그램이
main()
함수를 마치고ret
명령어를 실행할때 돌아올 가짜 반환 주소를 설정한다. 실제로는 돌아올 곳이 없기 때문에 0을 넣는다.
void
argument_stack (char **argv, int argc, struct intr_frame *if_){
char *arg_addr[MAX_ARGC];
int arg_len;
/* 1. 문자열 데이터를 스택에 역순으로 push */
for(int i= argc - 1 ; i >= 0 ;i--){
arg_len = strlen(argv[i]) + 1;
if_->rsp -= arg_len;
memcpy(if_->rsp, argv[i], arg_len); // 문자열 복사 (destination, source, size)
arg_addr[i] = if_->rsp; // 해당 문자열의 시작 주소 저장
}
/* 2. word alignment by 8
* 하위 3 비트를 0으로 &연산하여 스택 포인터를 8의 배수로 맞춘다 */
while (if_->rsp % 8 != 0){
if_->rsp--;
}
/* 3. NULL 포인터 삽입 : argv[argc] = NULL */
if_->rsp -= sizeof(char *); // 공간 확보
*(char **)if_->rsp = NULL; // 실제 NULL 값을 저장 (sentinel)
/* 4. argv[i] 포인터들을 역순으로 스택에 Push */
for(int i = argc - 1; i >= 0; i--){
if_->rsp -= sizeof(char *);
*(char **)if_->rsp = arg_addr[i];
}
/* 5. 레지스터 설정: rsi = argv 주소, rdi = argc 값 */
if_->R.rsi = if_->rsp;
if_->R.rdi = argc;
/* 6. Fake return address 삽입 (0으로 설정) */
if_->rsp -= sizeof(void *);
*(void **)if_->rsp = 0;
}
디버깅 (hex_dump)
process_exec 내부에 hex_dump를 삽입한다.
hex_dump(_if.rsp, _if.rsp, USER_STACK - _if.rsp, true);
- 메모리의 특정 영열을 바이트 단위로 출력하여 값이 어떻게 저장되어있는지 확인하기 위한 디버깅 함수이다.
- 첫 번째
_if.rsp
: 출력할 주소의 시작 위치. 스택 포인터가 현재 가리키는 위치 - 두 번째
_if.rsp
: 해당 메모리 주소를 출력할 때 기준 주소 USER_STACK - _if.rsp
: 출력할 크기- 여기서 KERN_BASE(최상단 주소)로 넣었으나, 너무 큰 영역의 데이터를 모두 출력하려 해서 커널 패닉이 오거나 제대로 작동하지 않았다. 디버깅을 위해 봐야할 곳의 위치를 적절히 설정하는 것이 중요하다.
- true : ASCII 값 출력 여부.
- 이 함수는 argument_stack 이 호출된 뒤에, 즉 스택에 문자열과 포인터가 모두 적재된 뒤에 호출되어야 한다.
- 결국 우리가 원하던 대로 스택이 구성되었는지를 확인용 함수이기 때문이다.
args-single 테스트 케이스
system call! 이 출력되면 성공 🎉
'IT 성장기 (교육이수) > 크래프톤정글 (2025.03-07)' 카테고리의 다른 글
[PintOS] Project 3 : Virtual Memory 이해 (2) | 2025.06.04 |
---|---|
[CS] Jungle OS 특강 정리 (0) | 2025.06.02 |
[PintOS] Project 1 : Priority Donation 구현 (0) | 2025.05.30 |
[PintOS] GDB Debugging Tool 사용 방법 (1) | 2025.05.29 |
[PintOS] File Descriptor 와 System Call 의 이해 (0) | 2025.05.23 |