일단 인자파싱을 해야 시스템콜이 가능해질겁니다요
Why?
유저 프로그램은 기본적으로 이렇게 실행됩니다
int main(int argc, char *argv[]) {
// 예시: 시스템 콜 호출
write(1, argv[1], strlen(argv[1]));
exit(0);
}
그러나!
- 만약 인자 파싱이 실패하거나,
- argv[i]가 잘못된 주소를 가리키거나,
- 스택에 복사가 안 되어 있거나,
→ 그 상태에서 시스템 콜을 호출하게 되면?
→ 유저가 넘긴 포인터를 따라가다 page fault 발생
→ 잘못된 메모리 접근 → 커널 패닉 or 프로세스 종료
저도 정말 많은 페이지폴트와 커널 패닉을 겪고 울고싶었습니다
아무튼간에
인자 파싱은 시스템콜의 전제조건이라고 볼 수 있습니다
- 유저 프로그램이 syscall을 올바르게 호출하려면,
- argv[]에 인자가 제대로 들어 있어야 하고,
- 그 인자들이 가리키는 주소가 유저 주소 공간에 존재해야 하며,
- 스택이 올바르게 세팅되어 있어야 한다.
즉, parse_args() → load() → 스택 구성
이게 실패하면 syscall이 아니라 프로그램 시작도 못 한다고 볼 수 있죠
그러니까 지금 할게 뭐냐
- PintOS에서 run 'prog arg1 arg2'처럼 명령어를 실행했을 때,
- 유저 프로그램이 어떻게 main(int argc, char* argv[])로 인자를 받는가?
- 이를 위한 전체 흐름은 다음 3개의 핵심 함수로 이뤄져 있습니다:
- process_exec()
- load()
- parse_args()
- 명령어 문자열 "prog arg1 arg2"는 커널 모드에서 파싱되어서
- 유저 스택에 인자 문자열, 포인터 배열, argc, fake return address까지 직접 구성됩니다
- 아무튼간 최종적으로 유저 모드로 진입하면서 main(argc, argv) 호출이 가능해진다는 것....
그럼 일단 process_exec부터 봅시다
우선 이 함수는 유저프로그램 실행의 시작점입니다
현재 커널 스레드를 유저 프로그램으로 전환합니다
즉, ELF 실행파일을 로드하고 초기 유저 스택 구성 후 진입합니다
/* 현재 실행 컨텍스트를 f_name으로 전환합니다.
* 실패 시 -1을 반환합니다. */
int process_exec(void *f_name)
{
char *file_name = f_name;
char cp_file_name[MAX_BUF];
memcpy(cp_file_name, file_name, strlen(file_name) + 1);
bool success;
/* intr_frame을 thread 구조체 안의 것을 사용할 수 없습니다.
* 이는 현재 스레드가 재스케줄될 때,
* 그 실행 정보를 해당 멤버에 저장하기 때문입니다. */
struct intr_frame _if;
_if.ds = _if.es = _if.ss = SEL_UDSEG;
_if.cs = SEL_UCSEG;
_if.eflags = FLAG_IF | FLAG_MBS;
/* 유저 스택 페이지 할당 */
// setup_stack(&_if);
/* 현재 컨텍스트를 제거합니다. */
process_cleanup();
// memset(&_if, 0, sizeof _if);
/* 그리고 이진 파일을 로드합니다. */
ASSERT(cp_file_name != NULL);
success = load(cp_file_name, &_if);
/* 로드 실패 시 종료합니다. */
palloc_free_page(file_name);
if (!success)
return -1;
// hex_dump(_if.rsp, _if.rsp, USER_STACK - (uint64_t)_if.rsp, true);
/* 프로세스를 전환합니다. */
do_iret(&_if);
NOT_REACHED();
}
- 새로운 실행 파일 경로와 인자들을 담은 문자열 f_name을 받아서
- 현재 실행 중인 커널 스레드의 실행 컨텍스트를 새로운 유저 프로세스로 교체하는 함수
복사본 만들기
char *file_name = f_name;
char cp_file_name[MAX_BUF];
memcpy(cp_file_name, file_name, strlen(file_name) + 1);
- f_name은 커맨드라인 전체 문자열 (예: "args-multiple foo bar").
- load()에서 이걸 파싱할 거니까, 원본을 보호하려고 복사본 cp_file_name을 만들어 사용하는 것임
유저 모드용 인터럽트 프레임 초기화
struct intr_frame _if;
_if.ds = _if.es = _if.ss = SEL_UDSEG;
_if.cs = SEL_UCSEG;
_if.eflags = FLAG_IF | FLAG_MBS;
- 유저 프로그램의 초기 상태를 담는 구조체 _if를 준비
- 세그먼트 레지스터 (ds, es, ss, cs)와 플래그를 유저 모드로 셋팅
기존 프로세스 메모리 / 리소스 제거
process_cleanup();
- 현재 스레드가 기존에 실행하던 유저 프로그램을 완전히 제거 (page table, open file 등 정리)
프로그램 로드
ASSERT(cp_file_name != NULL);
success = load(cp_file_name, &_if);
- load()는 cp_file_name을 파싱해서 ELF 실행파일을 로드하고, 유저 스택도 구성하고, 인자도 셋팅함
- _if에 진입점과 초기 스택 포인터도 설정해줌
로드 실패시 종료
palloc_free_page(file_name);
if (!success)
return -1;
유저 모드 진입
do_iret(&_if);
NOT_REACHED();
- do_iret()은 커널에서 유저 모드로 진입하는 실제 작업을 함 (iretq를 내부적으로 실행함)
- 이후는 절대 도달하면 안 되므로 NOT_REACHED() (여기 도달하면 버그)
parse_args
공백으로 분리된 문자열을 파싱해서,
각 토큰(단어)의 포인터를 argv[] 배열에 저장하는 함수
/* parse_args - 공백(" ")으로 구분된 문자열을 파싱하여 argv[] 배열에 각 인자의 포인터를 저장합니다.
*
* 입력:
* - target: 전체 명령어 문자열 (예: "prog a b c")
* - argv: 파싱된 각 인자의 시작 주소를 저장할 포인터 배열
*
* 출력:
* - argc: 파싱된 인자의 개수
*
* 주의:
* - 이 함수는 문자열을 직접 수정합니다. (strtok_r이 내부적으로 '\0' 삽입)
* - 따라서 원본 문자열을 보존하려면 복사본을 만들어야 합니다.
*/
static int parse_args(char *target, char *argv[])
{
int argc = 0; // 인자 개수를 셀 변수
char *token; // 현재 파싱 중인 토큰 (단어)
char *save_ptr; // strtok_r이 내부 상태를 기억하기 위한 포인터
/* 첫 번째 토큰부터 시작하여 반복적으로 분리해냄
* strtok_r은 첫 호출 시 target을 넣고, 이후에는 NULL을 넣어 이어서 처리함
* 공백을 기준으로 문자열을 분리하여 token에 저장
*/
for (token = strtok_r(target, " ", &save_ptr);
token != NULL;
token = strtok_r(NULL, " ", &save_ptr))
{
/* 파싱된 토큰의 시작 주소를 argv 배열에 저장
* 복사하지 않고 포인터만 저장하므로 매우 효율적임
*/
argv[argc++] = token;
}
/* 마지막에는 C 관례에 따라 NULL 포인터를 추가해 종료 표시
* 이는 exec 시 argv[argc] = NULL을 요구하는 표준에 맞춤
*/
argv[argc] = NULL;
/* 최종적으로 파싱된 인자 개수를 반환 */
return argc;
}
이 함수를 거치면 예를 들어
| Index | argv[i] | 실제 메모리 내 문자열 |
| 0 | argv[0] → “prog” | ‘p’‘r’‘o’‘g’’\0’ |
| 1 | argv[1] → “a” | ‘a’’\0’ |
| 2 | argv[2] → “b” | ‘b’’\0’ |
| 3 | argv[3] → “c” | ‘c’’\0’ |
| 4 | argv[4] = NULL |
raw 문자열은 파괴되어 "prog\0a\0b\0c\0" 형태가 됨
이렇게 파싱이 됩니다
주의할 점은
- strtok_r()는 입력 문자열 자체를 파괴합니다
- → 따라서 f_name을 바로 쓰지 말고, memcpy()로 복사해 사용해야 합니다
- 하지만 저희는 strtok_r을 썼슴다
- argv[]는 포인터 배열입니다
- → 문자열 내용을 복사하지 않기 때문에, 진짜 스택 위에 문자열을 올려야 할 땐 별도 복사가 필요합니다
- 어디에서? load에서!~~
하지만

과제 설명서에 이렇게나 친절하게 나와있어서 저희는 strtok_r을 사용했답니다
load 함수
load함수는 사용자 프로그램을 메모리에 적재하고, 사용자 스택을 직접 구성하는 핵심 함수랍니다
크게 4단계로 나뉘는데
- 인자파싱(parse_args())
- ELF 실행 파일 업로드
- 사용자 스택 할당
- 사용자 스택 구성
으로 나뉩니다
결론적으론 load()는 ELF 바이너리를 메모리에 올리고, main(argc, argv)가 제대로 동작하도록 인자 배열을 스택에 직접 구성하는 함수
인자 파싱부터 스택 구성까지 모든 걸 커널이 수동으로 해주는 구조임
암튼 load함수를 보면
/* FILE_NAME에서 현재 스레드로 ELF 실행 파일을 로드합니다.
* 실행 진입점은 *RIP에, 초기 스택 포인터는 *RSP에 저장됩니다.
* 성공 시 true, 실패 시 false를 반환합니다. */
static bool
load(const char *file_name, struct intr_frame *if_)
{
struct thread *t = thread_current();
struct ELF ehdr;
struct file *file = NULL;
off_t file_ofs;
bool success = false;
int i;
char *argv[MAX_ARGS];
int argc = parse_args(file_name, argv);
uint64_t rsp_arr[argc];
/* 페이지 디렉터리를 할당하고 활성화합니다. */
t->pml4 = pml4_create();
if (t->pml4 == NULL)
goto done;
process_activate(thread_current());
/* 실행 파일을 엽니다. */
file = filesys_open(file_name);
if (file == NULL)
{
printf("load: %s: open failed\n", file_name);
goto done;
}
/* 실행 헤더를 읽고 검증합니다. */
if (file_read(file, &ehdr, sizeof ehdr) != sizeof ehdr || memcmp(ehdr.e_ident, "\177ELF\2\1\1", 7) || ehdr.e_type != 2 || ehdr.e_machine != 0x3E // amd64
|| ehdr.e_version != 1 || ehdr.e_phentsize != sizeof(struct Phdr) || ehdr.e_phnum > 1024)
{
printf("load: %s: error loading executable\n", file_name);
goto done;
}
/* 프로그램 헤더들을 읽습니다. */
file_ofs = ehdr.e_phoff;
for (i = 0; i < ehdr.e_phnum; i++)
{
struct Phdr phdr;
#ifdef WSL
// WSL 전용 코드
off_t phdr_ofs = ehdr.e_phoff + i * sizeof(struct Phdr);
file_seek(file, phdr_ofs);
if (file_read(file, &phdr, sizeof phdr) != sizeof phdr)
goto done;
#else
// docker(기본) 전용 코드
if (file_ofs < 0 || file_ofs > file_length(file))
goto done;
file_seek(file, file_ofs);
#endif
if (file_read(file, &phdr, sizeof phdr) != sizeof phdr)
goto done;
file_ofs += sizeof phdr;
switch (phdr.p_type)
{
case PT_NULL:
case PT_NOTE:
case PT_PHDR:
case PT_STACK:
default:
/* 이 segment는 무시합니다. */
break;
case PT_DYNAMIC:
case PT_INTERP:
case PT_SHLIB:
goto done;
case PT_LOAD:
if (validate_segment(&phdr, file))
{
bool writable = (phdr.p_flags & PF_W) != 0;
uint64_t file_page = phdr.p_offset & ~PGMASK;
uint64_t mem_page = phdr.p_vaddr & ~PGMASK;
uint64_t page_offset = phdr.p_vaddr & PGMASK;
uint32_t read_bytes, zero_bytes;
if (phdr.p_filesz > 0)
{
/* Normal segment.
* Read initial part from disk and zero the rest. */
read_bytes = page_offset + phdr.p_filesz;
zero_bytes = (ROUND_UP(page_offset + phdr.p_memsz, PGSIZE) - read_bytes);
}
else
{
/* Entirely zero.
* Don't read anything from disk. */
read_bytes = 0;
zero_bytes = ROUND_UP(page_offset + phdr.p_memsz, PGSIZE);
}
if (!load_segment(file, file_page, (void *)mem_page,
read_bytes, zero_bytes, writable))
goto done;
}
else
goto done;
break;
}
}
/* 스택을 설정합니다. */
if (!setup_stack(if_))
goto done;
/* 시작 주소를 설정합니다. */
if_->rip = ehdr.e_entry;
/* TODO: 여기에 코드를 작성하세요.
* TODO: 인자 전달을 구현하세요 (project2/argument_passing.html 참고). */
for (int i = argc - 1; i >= 0; i--)
{
if_->rsp -= strlen(argv[i]) + 1;
rsp_arr[i] = if_->rsp;
memcpy((void *)if_->rsp, argv[i], strlen(argv[i]) + 1);
}
while (if_->rsp % 8 != 0)
{
if_->rsp--; // 주소값을 1 내리고
*(uint8_t *)if_->rsp = 0; // 데이터에 0 삽입 => 8바이트 저장
}
if_->rsp -= 8; // NULL 문자열을 위한 주소 공간, 64비트니까 8바이트 확보
memset(if_->rsp, 0, sizeof(char **));
for (int i = argc - 1; i >= 0; i--)
{
if_->rsp -= 8; // 8바이트만큼 rsp감소
memcpy(if_->rsp, &rsp_arr[i], sizeof(char **));
}
if_->rsp -= 8;
memset(if_->rsp, 0, sizeof(void *));
if_->R.rdi = argc;
if_->R.rsi = if_->rsp + 8;
success = true;
done:
/* load의 성공 여부와 상관없이 여기로 도달합니다. */
file_close(file);
return success;
}
파싱과 메모리 구조 준비
struct thread *t = thread_current();
...
int argc = parse_args(file_name, argv);
uint64_t rsp_arr[argc];
- 현재 스레드를 불러오고
- file_name 문자열을 공백 기준으로 잘라 argv[]에 인자 주소 저장 (parse_args)
- 예: "args-multiple foo bar" → argv[0]="args-multiple", argv[1]="foo", ...
- rsp_arr[i]는 이후 유저 스택에 각 문자열이 복사된 위치를 저장하는 배열
페이지 디렉토리 생성 및 활성화
t->pml4 = pml4_create();
if (t->pml4 == NULL)
goto done;
process_activate(thread_current());
- 새 주소 공간(Page Table)을 생성하여 이 스레드에 연결
- process_activate()는 해당 페이지 테이블을 현재 프로세스에 적용함
ELF 파일 열기
file = filesys_open(file_name);
if (file == NULL)
{
printf("load: %s: open failed\\n", file_name);
goto done;
}
- 실행 파일을 파일 시스템에서 염
- 실패하면 load() 중단하고 false 반환
ELF헤더 읽고 검증
if (file_read(file, &ehdr, sizeof ehdr) != sizeof ehdr || ... )
{
printf("load: %s: error loading executable\\n", file_name);
goto done;
}
- ELF 헤더를 읽고, 형식이 올바른 ELF 64비트 실행파일인지 확인함
프로그램 헤더(세그먼트 테이블)읽기
for (i = 0; i < ehdr.e_phnum; i++) {
...
if (phdr.p_type == PT_LOAD) {
...
load_segment(...);
}
}
- ELF 파일에는 여러 개의 “세그먼트”(LOAD 가능한 영역)가 정의돼 있음
- PT_LOAD 타입만 메모리에 로딩하고, 읽을 바이트 수, 0으로 채울 바이트 수 계산 후 load_segment() 호출
유저 스택 설정
if (!setup_stack(if_))
goto done;
- 유저 주소 공간의 스택 페이지를 맨 위에 할당하고, rsp를 초기화
진입점(rip)설정
if_->rip = ehdr.e_entry;
- ELF 헤더에 정의된 e_entry 값을 rip에 저장 → 유저 모드에서 실행 시작할 주소

과제 설명서에 이런식으로 나와있을텐데 스택구성을 해줘야만 사용자 프로그램이 정상적으로 실행됩니다
아마 이과정에서 수많은 커널패닉과 페이지폴트를 겪으시겠조
- 문자열은 역순으로 스택에 복사
- 포인터 배열도 역순으로 스택에 삽입
- 항상 8바이트 정렬을 맞춰야 함
- argv[argc] = NULL 포인터 필요
- 가장 마지막에 fake return address = 0 삽입
- rdi = argc, rsi = argv 포인터 배열 시작 주소
인자 문자열들을 유저 스택에 복사
for (int i = argc - 1; i >= 0; i--) {
if_->rsp -= strlen(argv[i]) + 1;
rsp_arr[i] = if_->rsp;
memcpy((void *)if_->rsp, argv[i], strlen(argv[i]) + 1);
}
- 스택에 문자열을 역순으로 복사
- 각 문자열의 주소를 rsp_arr[i]에 저장해놓음
스택 정렬(8byte 정렬-패딩넣기)
while (if_->rsp % 8 != 0) {
if_->rsp--;
*(uint8_t *)if_->rsp = 0;
}
- x86-64 ABI에 따라 8바이트 정렬해야 함
argv배열 만들기
if_->rsp -= 8; // NULL 포인터
memset(if_->rsp, 0, sizeof(char **));
for (int i = argc - 1; i >= 0; i--) {
if_->rsp -= 8;
memcpy(if_->rsp, &rsp_arr[i], sizeof(char **));
}
- argv[argc] = NULL 추가
- 각 argv[i] 주소들을 스택에 역순으로 저장 (포인터 배열 생성)
fake return address 삽입
if_->rsp -= 8;
memset(if_->rsp, 0, sizeof(void *));
- 유저 main() 함수 호출 후 돌아갈 주소지만, PintOS에선 사용 안 하므로 0
레지스터에 인자 전달(rdi, rsi)
if_->R.rdi = argc; // 첫 번째 인자
if_->R.rsi = if_->rsp + 8; // argv[0]의 주소 (rsp 바로 위에 있음)
- x86-64 리눅스 ABI에 맞춰 인자 전달
- rdi ← argc, rsi ← argv 배열 시작 주소
성공 표시
success = true;
정리 및 반환
done:
file_close(file);
return success;
아무튼간 정리하자면
1. process_exec(f_name)
└─ 유저 프로세스를 시작하기 위한 진입점
└─ 문자열 (ex: "prog a b c")을 load()에 전달
2. load(file_name, &if_)
├─ parse_args(): 공백 단위로 파싱 → argv[] 배열 구성
├─ ELF 실행파일을 열어서 segment들을 메모리에 로딩
├─ setup_stack(): 사용자 스택 할당
└─ 인자들 스택에 아래 순서대로 넣기
① 인자 문자열 (ex: "a\0", "b\0" 등)
② 문자열 주소들 (argv[i])
③ argv 배열 주소
④ argc
⑤ fake return address
3. load() 성공 시
└─ if_.rip ← 실행 시작 주소
└─ if_.rsp ← 스택 포인터
└─ if_.rdi ← argc, if_.rsi ← argv
→ 이 정보로 유저 모드 진입 가능
4. do_iret(&if_)
└─ 유저 모드 전환 + main(argc, argv) 시작
이런 흐름인거고
이 과제는 왜하는 것인가 라고 한다면
“커널이 process_exec()을 호출하면
그 안에서 load()가 인자 파싱하고,
스택에 맞게 인자들을 복사한 뒤
레지스터 세팅을 하고 유저 모드로 진입하는 과정”
이라 할 수 있고!
왜 스택에 인자를 직접 넣어줘야 하는가! 라고 한다면
pintos는 일반적인 운영체제와 달리, 사용자 프로그램의 실행환경을 커널이 수동으로 구성해줘야 합니다
즉, main(int argc, char* argv[])이 호출되기 위해서는
- 문자열 복사 ("prog", "a", "b"…)
- 포인터 배열 (argv[i])
- 정렬 패딩
- NULL 포인터, argc, return address
까지 모두 커널이 직접 유저 스택에 push해줘야 한다~~!
이 작업들은 load()함수 내부에서 이루어지며,
최종적으로는 유저 스택의 구조는 C프로그램의 main함수가 기대하는 형태와 일치하게 된다는 겁니다잉
인자파싱에서 기억할 건
- parse_args: 문자열 → 인자 배열로 분리 (포인터만 저장)
- load: ELF 파일 로드 + 스택 구성 + 레지스터 설정
- 유저모드 진입: do_iret()을 통해 실행
그리고 테스트를 통과하려면 process_wait에 무한루프를 걸어줘야한다고 하더라구요
참고하시고 그 뒤 수정본으로 포스팅 될 것 같네요
다음은 이제 시스템콜로 돌아오겠어요
'크래프톤정글 > Pintos' 카테고리의 다른 글
| User Program _System Call(exit, write, open, close, create, remove) (0) | 2025.05.21 |
|---|---|
| User Program _System Call(filesize, read, seek, tell) (0) | 2025.05.19 |
| User Program _ main부터 (0) | 2025.05.17 |
| PintOS_Priority_Scheduling_part_3_donation (1) | 2025.05.13 |
| 지금까지의 개념 정리_Alram Clock & Priority Scheduling (0) | 2025.05.12 |