Project2. User Programming - Argument Passing

김민호·2025년 11월 25일

pintos

목록 보기
5/9

process_exec() 함수가 현재는 프로그램 이름만 받도록 구현되어 있다. 이를 확장하여 명령줄 인자를 처리할 수 있도로 구현해야한다.

1. 목표

  • process_exec()이 실행되는 로직
  • process_exec()가 넘겨 받는 문자열 인자들을 parshing
  • 사용자 프로그램의 스택에 인자를 설정
  • 사용자 프로그램이 실행되기 직전에 argc, argv 인자를 레지스터에 설정

구현된 기존 로직 살펴보기

process_exec()

현재 프로그램에 덮어씌우는 방식

  • fork() + exec() -> 쉘
  • fork() -> 크롬 탭, 웹 서버 요청
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; // 인터럽트 플래그 On | 필수

	/* We first kill the current context */
	process_cleanup (); //현재 실행 중인 프로세스를 종료하고 자원 정리

	/* And then load the binary */
	success = load (file_name, &_if);

	/* If load failed, quit. */
	palloc_free_page (file_name);
	if (!success)
		return -1;

	/* Start switched process. */
	do_iret (&_if);
	NOT_REACHED ();
}

load()

나중에 구현할 때 file_name -> cmd_line으로 변경 후 파싱 진행

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;

	// 새로운 빈 페이지 테이블 생성
	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;
	}

	// ELF 헤더 검증
	if (file_read (file, &ehdr, sizeof ehdr) != sizeof ehdr
			|| memcmp (ehdr.e_ident, "\177ELF\2\1\1", 7) // ELF 매직넘버 검사
			|| ehdr.e_type != 2 // Executable Check
			|| ehdr.e_machine != 0x3E // amd64 -> x86-64
			|| ehdr.e_version != 1 // ELF version check
			// ELF Program Header Entry size, Program Hdr
			|| ehdr.e_phentsize != sizeof (struct Phdr) 
			|| ehdr.e_phnum > 1024) { // head count check
		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; // 프로그램 헤더 하나를 담을 변수
		
		if (file_ofs < 0 || file_ofs > file_length (file))
			goto done;
			
		file_seek (file, file_ofs); // file의 위치를 file_ofs 위치로 이동
		
		if (file_read (file, &phdr, sizeof phdr) != sizeof phdr)
			goto done;
			
		file_ofs += sizeof phdr; // 다음 헤더 시작 위치를 가리키도록 변경
		
		// 세그먼트 타입 확인: 각 세크먼트 타입은 프로그램 헤더 항목에 있다. 1:1 mapping
		// 세그먼트 타입을 확인하고, 어떻게 처리할지 결정하는 필터 역할	
		switch (phdr.p_type) {
			case PT_NULL: // 비어있거나 사용X
			case PT_NOTE: // 주석이나 정보
			case PT_PHDR: // 이미 읽었으므로 로드X
			case PT_STACK: // setup_stack() 함수로 직접 스택을 만들어야함.
			default:
				/* Ignore this segment. */
				break;
				
			case PT_DYNAMIC: // 다음 3가지는 동적 링킹과 관련된 세그먼트.
			case PT_INTERP:  // pinto는 동적 링킹 지원X
			case PT_SHLIB:
				goto done;
				
			case PT_LOAD: // 실제로 메모리에 로드해야 한다.
				if (validate_segment (&phdr, file)) { // 세그먼트 유효한지 검사
					bool writable = (phdr.p_flags & PF_W) != 0;
					// 페이지 크기를 4kb로 내림
					uint64_t file_page = phdr.p_offset & ~PGMASK;
					// 가상 메모리 주소
					uint64_t mem_page = phdr.p_vaddr & ~PGMASK;
					// 가상 주소가 페이지의 시작에서부터 얼마나 떨어져 있는지 계산(offset)
					uint64_t page_offset = phdr.p_vaddr & PGMASK;
					
					// .bss 세그먼트를 처리하기 위해 존재한다.
					uint32_t read_bytes, zero_bytes;
					if (phdr.p_filesz > 0) { // .bss + .data
						/* Normal segment.
						* Read initial part from disk and zero the rest. */
						read_bytes = page_offset + phdr.p_filesz;
						// 0으로 채워할 바이트 수. 4kb로 올림. .bss 처리
						zero_bytes = (ROUND_UP (page_offset + phdr.p_memsz, 
						PGSIZE) - read_bytes); 
					} else { .bss
						/* 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;
		}
	}

	/* Set up stack. */
	// 스택 공간 할당 + 스택 포인터 설정
	if (!setup_stack (if_))
		goto done;

	/* Start address. */
	if_->rip = ehdr.e_entry;

	success = true;

done:
	/* We arrive here whether the load is successful or not. */
	file_close (file);
	return success;
}

2. 핵심 아이디어

사용자로부터 입력받은 command_line (예: "ls -l foo")을 쪼개서 커널이 이해할 수 있는 데이터 형태로 가공하는 단계입니다.

1. 문자열 복사

가장 먼저 고려해야 할 점은 "왜 원본 문자열을 그대로 쓰면 안 되는가?" 입니다. 여기에는 방어적 프로그래밍과 메모리 보호라는 두 가지 중요한 이유가 있습니다.

  • 이유 1: strtok_r의 동작 방식
    • strtok_r 함수는 구분자(공백 등)를 찾으면 해당 위치의 문자를 \0 (NULL)으로 덮어씌워 문자열을 잘라냅니다. 즉, 원본 데이터를 직접 수정하는 파괴적인 함수입니다.
  • 이유 2: 원본 데이터의 메모리 위치
    • process_exec에 넘어온 f_name 포인터가 가리키는 곳이 수정 불가능한 영역(Read-Only Data Segment, 리터럴 문자열)일 수 있습니다. 이곳을 수정하려 시도하면 Page Fault가 발생하여 커널 전체가 멈출(Panic) 위험이 있습니다.

결론: palloc_get_page(0) 등을 사용해 커널 메모리에 새로운 페이지를 할당받고, strlcpy로 문자열을 안전하게 복사한 뒤 작업을 수행해야 합니다.


2. 변수 및 배열 설계

파싱한 데이터를 담아둘 자료구조를 설계합니다. C언어의 표준 main 함수 인자를 생각하면 이해가 쉽습니다.

  • argc (Argument Count)
    • 인자의 개수를 세는 정수 변수입니다.
  • argv (Argument Vector)
    • 인자들의 문자열 주소(포인터)를 저장할 배열입니다.
    • 크기: 핀토스 과제 기준으로는 보통 인자 개수에 제한을 둡니다. 넉넉하게 char *argv[64] (혹은 128) 정도로 선언하면 충분합니다.
    • argv[0]에는 항상 프로그램의 이름(예: ls)이 들어갑니다.

3. 문자열 분리 및 스택 삽입

복사한 커맨드 라인을 파싱 한 후, x86-64 Calling Convention에 맞춰 스택을 빈틈없이 채워야 합니다. 스택은 높은 주소에서 낮은 주소로(High Address → Low Address) 자란다는 점을 항상 기억해야 합니다.

전체 과정은 크게 4단계로 나뉩니다.

1. 인자 데이터(String Data) 삽입

  • 가장 먼저 파싱 된 문자열 데이터 자체를 스택에 넣습니다.
  • strtok_r로 분리된 토큰(문자열)을 하나씩 스택에 복사합니다 (memcpy).
  • 이때, 나중에 포인터 배열(argv)을 만들 때 사용하기 위해, 문자열이 저장된 스택 주소(rsp)를 별도 배열(argv_list)에 기록해 두어야 합니다.

2. Word Align (8바이트 정렬)

  • 성능 최적화와 규약을 위해 스택 포인터를 8의 배수로 맞춰야 합니다.
  • 문자열 데이터를 다 넣은 직후의 rsp가 8로 나누어떨어지지 않는다면, 8의 배수가 될 때까지 스택 포인터를 내리고 남는 공간을 0 (NULL)으로 채웁니다.

3. 인자 주소 배열(String Pointers) 삽입

  • 이제 앞서 기록해 둔 문자열의 주소들을 스택에 넣을 차례입니다. 주의할 점은 역순 삽입입니다.
  • Sentinel (argv[argc]): C 표준에 따라 인자 배열의 끝은 NULL이어야 합니다. 가장 먼저 0을 넣습니다.
  • Pointers (argv[argc-1] ... argv[0]): 기록해 둔 주소들을 거꾸로 스택에 넣습니다. 그래야 스택의 위쪽(낮은 주소)에서 볼 때 argv[0], argv[1] 순서대로 정렬됩니다.

4. 가짜 반환 주소 (Fake Return Address) & 레지스터 설정

  • Fake Return Address: main 함수도 누군가에게 호출된 것처럼 보이게 하기 위해, 반환 주소 공간(void pointer 크기)을 확보하고 0으로 채웁니다.
  • 레지스터 설정: 이제 사용자 프로그램이 시작될 때 인자를 알 수 있도록 CPU 레지스터를 세팅합니다.
  • RDI: 인자의 개수 (argc)
  • RSI: 인자 배열의 시작 주소 (argv 즉, argv[0]을 가리키는 스택 주소)

3. 구현

위 다이어그램의 구조(x86-64 Calling Convention)를 코드로 구현합니다. 이 작업은 load() 내부에서 수행됩니다.

  • Parsing & Data Push: 문자열을 파싱하여 스택에 문자열 데이터를 밀어 넣습니다.
  • Alignment: 8바이트 단위로 스택 포인터를 정렬합니다.
  • Pointer Push: 문자열들의 주소(argv)를 스택에 넣습니다.
  • Register: 프로그램 시작 시 인자를 받을 수 있도록 레지스터를 세팅합니다.

수정된 load() :

static bool load (const char *cmd_line, 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 *file_name;
	char *copy_cmd_line;
	char *token;
	int count = 0;
	char *save_ptr;
	char *address[64];

	copy_cmd_line = palloc_get_page (0);
	if (copy_cmd_line == NULL)
		goto done; 
	strlcpy (copy_cmd_line, cmd_line, PGSIZE);

	token = strtok_r(copy_cmd_line, " ", &save_ptr); 
	file_name = token;

	if (file_name == NULL) {
		goto done;
	}

	/* Allocate and activate page directory. */
	t->pml4 = pml4_create ();
	if (t->pml4 == NULL)
		goto done;
	process_activate (thread_current ());

	/* Open executable file. */
	file = filesys_open (file_name);
	if (file == NULL) {
		printf ("load: %s: open failed\n", file_name);
		goto done;
	}

	// rox
	file_deny_write(file);

	/* Read and verify executable header. */
	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;
	}

	/* Read program headers. */
	file_ofs = ehdr.e_phoff;
	for (i = 0; i < ehdr.e_phnum; i++) {
		struct Phdr phdr;

		if (file_ofs < 0 || file_ofs > file_length (file))
			goto done;
		file_seek (file, file_ofs);

		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:
				/* Ignore this 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;
		}
	}

	/* Set up stack. */
	if (!setup_stack (if_))
		goto done;

	/* Start address. */
	if_->rip = ehdr.e_entry;

	while (token != NULL && count < 64) {
		if_->rsp -= (strlen(token) + 1);
		memcpy(if_->rsp, token, strlen(token) + 1); 
	
		address[count++] = if_->rsp;
		token = strtok_r(NULL, " ", &save_ptr);
	}

	// 배열의 마지막 NULL 넣기.
	address[count] = NULL;
	
	//8바이트 정렬하기
	if_->rsp = if_->rsp & ~0x7;

	// 스택에 배열 넣기 
	for (int i = count; i >= 0; i--) {
		if_->rsp -= sizeof(address[i]);
		*((char **)(if_->rsp)) = address[i];
	}

	// 레지스터 설정하기
	if_->R.rdi = count;
	if_->R.rsi = if_->rsp;

	if_->rsp -= sizeof(char *);
	*((char **)(if_->rsp)) = 0;

	success = true;

done:
	if (copy_cmd_line != NULL) {
		palloc_free_page(copy_cmd_line);
	}

	/* We arrive here whether the load is successful or not. */
	if (success) {
		t->hold_file = file;
	} else {
		file_close (file); // 무조건 닫으면 안된다. file의 쓰기 금지 락을 유지해야하기 때문이다.
	}

	return success;
}
profile
개발자를 꿈꾸고 있어요

0개의 댓글