본문 바로가기
개발일지

pintos - USER PROGRAMS (1)

by Peter.JH 2023. 12. 5.
728x90

Argument Passing

목표: 커맨드 라인 파싱 기능 구현

과제 설명:

- pintos는 프로그램과 인자를 구분하지 못하는 구조

  예: $ls –a /* pintos는 ‘ls -a’를 하나의 프로그램명으로 인식

- 프로그램 이름과 인자를 구분하여 스택에 저장, 인자를 응용 프로그램에 전 달하는 기능 구현

 

전체 흐름중에 내가 수정해야할 함수의 역할을 파악하는것이 중요하다. 

 

break point를 걸어 어떻게 넘어오는지 확인하자

 

 

커널은 사용자 프로그램이 실행을 시작하기 전에 초기 함수에 대한 인수를 레지스터에 넣어야 한다. 인수는 일반적인 호출 규칙과 동일한 방식으로 전달된다고 한다.

 

*일반적인 호출 규칙

https://en.wikipedia.org/wiki/X86_calling_conventions#System_V_AMD64_ABI

 

현재 process_exec() 함수를 보자

/* Switch the current execution context to the f_name.
 * Returns -1 on fail. */
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 ();

	/* 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 ();
}

 

`process_exec()` 함수는 현재 실행 컨텍스트를 주어진 함수 이름 `f_name`으로 전환하는 역할을 하고있다. 

더보기

이 함수는 intr_frame  `_if`를 설정하여 새로운 프로세스를 시작하는데 필요한 정보를 담습니다. 이 프레임은 사용자 데이터 세그먼트와 코드 세그먼트 셀렉터, 인터럽트 플래그, 멀티프로세싱 플래그 등을 설정합니다.

함수는 먼저 `process_cleanup()`을 호출하여 현재 실행 컨텍스트를 종료합니다. 이는 현재 프로세스의 모든 리소스를 해제하고, 프로세스를 종료하는 역할을 합니다.

그 후, `load()` 함수를 호출하여 새로운 실행 파일을 로드합니다. `_if` 인터럽트 프레임을 인자로 전달하여, 실행 파일의 엔트리 포인트와 초기 스택 포인터를 설정합니다.

만약 로드가 실패하면, 함수는 -1을 반환하고 종료합니다.

그렇지 않으면, `do_iret()` 함수를 호출하여 새로운 프로세스를 시작합니다. 이 함수는 인터럽트 프레임을 복원하여, 새로운 프로세스의 실행 상태를 설정합니다.

마지막으로, `NOT_REACHED()` 매크로는 이 위치를 실행 코드가 도달하지 않아야 함을 나타냅니다. 이는 `do_iret()` 함수가 리턴하지 않기 때문에 필요합니다. 이 매크로는 보통 프로그램의 오류를 검출하는데 사용됩니다.

 

 

* intr_frame 

struct intr_frame {
	/* Pushed by intr_entry in intr-stubs.S.
	   These are the interrupted task's saved registers. */
	struct gp_registers R;
	uint16_t es;
	uint16_t __pad1;
	uint32_t __pad2;
	uint16_t ds;
	uint16_t __pad3;
	uint32_t __pad4;
	/* Pushed by intrNN_stub in intr-stubs.S. */
	uint64_t vec_no; /* Interrupt vector number. */
/* Sometimes pushed by the CPU,
   otherwise for consistency pushed as 0 by intrNN_stub.
   The CPU puts it just under `eip', but we move it here. */
	uint64_t error_code;
/* Pushed by the CPU.
   These are the interrupted task's saved registers. */
	uintptr_t rip;
	uint16_t cs;
	uint16_t __pad5;
	uint32_t __pad6;
	uint64_t eflags;
	uintptr_t rsp;
	uint16_t ss;
	uint16_t __pad7;
	uint32_t __pad8;
} __attribute__((packed));

 

`intr_frame` 구조체는 인터럽트 발생 시, CPU에 의해 스택에 푸시되는 레지스터들의 상태를 저장한다. 이 구조체는 인터럽트 핸들러가 호출될 때 현재 레지스터의 상태를 백업하고, 인터럽트 처리가 끝난 후에 원래의 상태를 복원하는 데 사용된다.

더보기

`struct gp_registers R`: 일반 목적 레지스터를 저장

`uint16_t es`, `uint16_t ds`: ES와 DS 세그먼트 레지스터를 저장. 

`uint16_t es`, `uint16_t ds :

x86 아키텍처의 세그먼트 레지스터, 세그먼트 레지스터는 세그먼트 기반 메모리 관리에서 중요한 역할을 한다. 세그먼트는 메모리의 연속된 블록을 나타내며, 각 세그먼트는 세그먼트 레지스터를 통해 참조된다.


es: Extra Segment 레지스터로, 특정 연산에서 사용할 메모리 세그먼트
ds: Data Segment 레지스터로, 일반적으로 데이터 세그먼트. 프로그램의 데이터를 저장하는 데 사용

 

`uint64_t vec_no`: 인터럽트 벡터 번호를 저장합니다. 이는 인터럽트를 구분하는 데 사용

`uint64_t error_code`: CPU에 의해 푸시되기도 하며, 그렇지 않은 경우 인터럽트 스텁에 의해 0으로 푸시됩니다. CPU는 이를 `eip` 바로 아래에 둡니다.

`uintptr_t rip`: 인터럽트가 발생한 지점의 명령 포인터를 저장

`uint16_t cs`: 코드 세그먼트 레지스터를 저장

`uint64_t eflags`: EFLAGS 레지스터를 저장. 이는 CPU의 상태를 나타내는 플래그들을 포함하고 있음

`uintptr_t rsp`: 스택 포인터를 저장

`uint16_t ss`: 스택 세그먼트 레지스터를 저장

`__padN` 필드들은 패딩을 위한 것으로, 구조체의 정렬을 위해 사용됩니다. `__attribute__((packed))`는 구조체를 메모리에 패킹하여 추가적인 공간을 사용하지 않도록 합니다.

 

인터럽트 프레임은 인터럽트 요청이 들어왔을 때 실행중인 컨텍스트(프로세스의 상태)를 커널스택에 저장하기 위한 구조체입니다. 

 

 

1. argument passing

현재 process_exec()은 새 프로세스에 인수를 전달하는 기능을 지원하지 않는다. 따라서 process_exec()안에 argument passing을 만들지 load안에 만들지 고민했다. 

 

`process_exec` 함수와 `load` 함수는 프로그램을 로드하고 실행하는 과정에서 다른 역할을 수행한다.

 

1. `process_exec` 함수는 프로그램을 실행하기 위해 필요한 준비 과정을 수행한다. 이는 프로그램의 파일을 열거나, 프로세스를 생성하거나, 인자를 파싱하는 등의 작업을 포함한다.

장점: 인자 파싱이 프로그램 실행의 전반적인 준비 과정의 일부로 이해되고, 이 과정이 완료된 후에 `load` 함수를 호출하여 프로그램을 로드할 수 있다. 즉, `process_exec` 함수가 프로그램 실행의 전체 과정을 관리하게 된다.

단점: `process_exec` 함수가 너무 많은 역할을 수행하게 되어 복잡해질 수 있다. 또한, 인자 파싱이 실패하면 프로그램 실행 전체가 실패하게 되는데, 이는 프로그램 로드와 별개의 문제이므로 분리하는 것이 좋다고 판단했다. 

 

2. `load` 함수는 프로그램의 파일을 메모리에 로드하는 역할을 수행한다. 이는 파일을 읽어서 메모리에 적절한 위치에 복사하는 등의 작업을 포함합니다.

장점: 프로그램을 로드하는 과정과 인자를 파싱하는 과정이 밀접하게 연관되어 있다는 점을 명확하게 표현할 수 있다는 점, 직관적이다.

단점: `load` 함수가 프로그램 로드 외의 역할을 수행하게 되어 복잡해질 수 있다. 

 

load함수가 이미 많이 길어 보기 힘들어 질 수 있지만 load와 argument passing이 관련된 작업이니 load함수안에서 하는게 맞다고 판단하였다. 

 

	/* for Argument Passing */
	char *argv[128];
	char *token, *save_ptr;
	int argc = 0;
	token = strtok_r(file_name, " ", &save_ptr);
	argv[argc] = token;
	argc += 1;
	
	for (token = strtok_r (NULL, " ", &save_ptr); token != NULL; token = strtok_r (NULL, " ", &save_ptr)) {
		argv[argc] = token;
		argc += 1;
	}

 

*strtok_r 함수는 주어진 문자열을 특정 구분자(delimiters)를 기준으로 토큰으로 나누는 함수

 

2. argument stack 

이제 프로그램의 이름과 인자를 구분했으니 스택에 저장해야한다. 

 

void 
argument_stack (char **argv, int argc, struct intr_frame *if_) {
	char *arg_address[128];

	/* save arg[0] ~ arg[3] */
	for (int i = argc-1; i > -1; i--) {
		int argv_len = strlen(argv[i]);

		if_->rsp = if_->rsp - (argv_len + 1);
		memcpy(if_->rsp, argv[i], argv_len + 1);
		arg_address[i] = if_->rsp;
	}

	/* for padding */
	while (if_->rsp % 8 != 0) {
		if_->rsp = if_->rsp - 1;
		*(uint8_t *)if_->rsp = 0;
	}

	/* insert address, include Sentinel */
	for (int i = argc; i > -1; i--) {
		if_->rsp = if_->rsp - 8;
		if (i == argc) {
			memset(if_->rsp, 0, sizeof(char **));
		}
		else {
			memcpy(if_->rsp, &arg_address[i], sizeof(char **));
		}
	}

	if_->R.rsi = if_->rsp;
	if_->R.rdi = argc;

	/* save fake address(0) */
	if_->rsp = if_->rsp - 8;
	memset(if_->rsp, 0, sizeof(void *));
}

 

프로그램 실행에 필요한 인자들을 스택에 저장하는 함수를 만들었다. 이 함수는 사용자 프로그램의 인자를 스택에 저장하고, 그 인자들의 주소를 스택에 저장한다. (`char **argv`는 인자 배열을 가리키는 포인터, `int argc`는 인자의 개수, 그리고 `struct intr_frame *if_`는 인터럽트 프레임을 가리키는 포인터)

 

*함수설명

1. 인자들을 스택에 저장: 각 인자는 스택에 역순으로 저장되며, 각 인자의 주소는 `arg_address` 배열에 저장

2. 패딩 추가하기: x86-64 아키텍처에서는 데이터가 8바이트 단위로 정렬되어야 하므로(quadword경계), 필요에 따라 0으로 패딩을 추가

3. 인자들의 주소를 스택에 저장: 인자들의 주소는 역순으로 스택에 저장, 마지막에는 NULL을 추가하여 인자 리스트의 끝을 나타낸다.

4. `if_->R.rsi`와 `if_->R.rdi` 설정

5. 가짜 주소 저장하기: 가짜 주소(0)를 스택에 추가.

이전 프레임의 리턴 주소, 사용자 프로그램은 이전 프레임이 없으므로 0을 넣었다.

 

그렇다면 언제 이 함수를 사용할까? 프로그램이 실행되기 시작할 때 스택에 필요한 인자들이 이미 준비되어 있어야 하기에 프로그램의 실행 시작 지점을 설정한 후에 프로그램의 인자를 스택에 올려야한다. 

 

load함수안 

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

 

밑에 추가하면 된다.

728x90

'개발일지' 카테고리의 다른 글

pintos (부끄러운)실수 기록  (0) 2023.12.14
Git - Divergent branches  (0) 2023.12.09
pintos - USER PROGRAMS (intro)  (0) 2023.12.04
pintos - THREADS(1)  (0) 2023.11.28
웹서버만들기(1)  (4) 2023.11.17