[Jungle] Week9. Pintos Project 2. Argument Passing

somi·2024년 5월 24일
0

[Krafton Jungle]

목록 보기
58/68

Project 2. User Prgrograms

과제를 하기 전에 가장 중요한 것 과제에 대한 큰 틀을 먼저 이해하기! 과제를 찬찬히 이해해보고 이 과제가 왜 중요하고, 왜 해야하는지 생각해보자.

GOAL: 사용자 프로그램을 실행할 수 있는 환경 구축하기
-> OS가 커널에서 벗어나서 실제 사용자 프로그램을 다룰 수 있도록
-> System Call 인터페이스 구현하기

그렇다면 우리는 왜 시스템 콜을 구현해야 하는가?
: 사용자 프로그램은 os의 기능을 사용하기 위해 시스템 콜을 호출하고 이를 통해서 파일의 입출력, 프로세스 관리, 메모리 할당 등의 다양한 작업을 수행하기 때문에

=> 사용자 프로그램을 실행할 수 있는 환경 구축하기 !
: OS가 커널 코드 실행에서 벗어나서 실제 사용자 프로
그램을 다룰 수 있게끔


To run a program

  • 실행 파일을 디스크에서 읽어오기
    : 운영체제는 디스크에서 실행 파일을 읽어와야 한다. Filesystem을 사용해서 파일을 찾아 읽어들인다. -> 파일 시스템은 파일의 위치를 추적하고 파일 내용을 메모리로 업로드한다.
    => 파일 시스템 issue 처리가 필요함 (예: 파일 권한 없거나 파일 손상 등)

  • 프로그램 실행을 위한 메모리 할당
    : 메모리는 프로그램 코드, 데이터, 스택, 힙 등에 사용
    => virtual memory allocation
    : 가상 메모리는 실제 물리적 메모리 주소와 독립적으로 프로그램에 큰 주소 공간 제공

  • 프로그램에 매개 변수 전달
    : 프로그램이 실행될 때 명령줄 인자와 같은 매개변수를 전달해야 함
    => set up user stack
    => 프로그램이 실행될 때 사용할 스택 설정 - 함수 호출, 지역 변수 저장, 매개 변수 전달 등에 사용

  • context switch to the user program
    : 운영체제는 커널 모드에서 사용자 모드로 전환해서 프로그램 실행
    -> OS는 프로그램이 종료될 때까지 대기함. 종료되면 이를 처리하고 할당된 자원을 반납함


Stack grows downward
: 스택은 메모리의 높은 주소에서 낮은 주소로 확장된다.
함수 호출 시마다 스택 프레임이 추가되며 함수의 지역변수, 반환주소, 인자 등을 포함함


1. Passing the arguments and creating a thread

  • command line에서 명령어 실행하기

  • 현재 Pintos 에서는 프로그램과 인자를 구분하지 못하는 구조다.
    예를 들면 $ls -a를 pintos에서는 하나의 프로그램명으로 인식한다.

=> 프로그램 이름과 인자를 구분해서 스택에 저장하고 인자를 프로그램에 전달해야 한다.


현재 pintos에서 사용자 프로그램을 실행하는 함수인process_create_initd()는 인자로 전달된 명령어 문자열을 받아 새로운 프로세스를 생성하고 실행함.

tid_t
process_create_initd (const char *file_name) {
	char *fn_copy;
	tid_t tid;

	/* Make a copy of FILE_NAME.
	 * Otherwise there's a race between the caller and load(). */
	fn_copy = palloc_get_page (0);
	if (fn_copy == NULL)
		return TID_ERROR;
	strlcpy (fn_copy, file_name, PGSIZE);

	/* Create a new thread to execute FILE_NAME. */
	/*tid : 쓰레드의 id -> 시스템에서 각 쓰레드를 고유하게 식별하는 값*/
	tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
	if (tid == TID_ERROR)
		palloc_free_page (fn_copy);
	return tid;
}

filename이 명령어 문자열이고, 프로그램의 이름과 인자 모두 포함된 하나의 문자열로 처리되고 있다.

사용자 프로그램을 실행할 때 호출되는process_exec() 도 마찬가지로 file_name 변수를 통해 전달된 문자열을 그대로 load() 함수에 전달하고 있음.

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

GOAL: 프로그램 이름과 인자를 구분해서 user stasck에 저장하고, 인자를 프로그램에 전달하기!


user program이 실행될 때의 흐름 이해가 중요하다.

  • init.c 파일 안의 main()함수
    : 핀토스가 부팅되면 main() 함수 먼저 처리됨

  • run_actions(argv)

static void
run_actions (char **argv) {
	/* An action. */
	struct action {
		char *name;                       /* Action name. */
		int argc;                         /* # of args, including action name. */
		void (*function) (char **argv);   /* Function to execute action. */
	};

	/* Table of supported actions. */
	static const struct action actions[] = {
		{"run", 2, run_task}, /*action: run, argc: 2, function to execute: run_task*/

: main()함수는 run_actions(argv)함수 호출하여 argv 기반으로 동작 수행 ->
: command line에서 run action을 준 경우 run_task함수가 실행

  • 안의 run_task 함수
static const struct action actions[] = {
		{"run", 2, run_task}, /*action: run, argc: 2, function to execute: run_task*/

: run 동작이 요청되면 run_task함수가 실행됨.
-> process_create_initd() 함수를 호출하여 실제 사용자 프로그램 실행

  • process_create_initd()
    : 사용자 프로그램을 실행하기 위한 초기 설정, initd 함수 호출
/* A thread function that launches first user process. */
static void
initd (void *f_name) {
#ifdef VM
	supplemental_page_table_init (&thread_current ()->spt);
#endif

	process_init ();

	if (process_exec (f_name) < 0)
		PANIC("Fail to launch initd\n");
	NOT_REACHED ();
}
  • initd 함수는 process_exec() 함수 호출하여 사용자 프로그램 실행
    : 사용자 프로그램의 이름과 인자를 받아 처리
	success = load (file_name, &_if);
  • 사용자 프로그램 로딩을 위해load() 함수 호출
    : 사용자 프로그램 실행 파일을 메모리에 로드하고 Command line 인자들을 user stack에 저장하는 작업 수행 -> 인자들이 사용자 프로그램에 전달될 수 있게 된다!

ELF(Executable and Linkable Format) Binary

: os에서 실행 가능한 파일 형식 중 하나
→ 실행 파일, 오브젝트 파일, 공유 라이브러리, 코어 덤프 파일을 포함한 다양한 바이너리 파일을 표현하는데 사용

  1. ELF header : 파일의 시작 부분, 파일의 구조와 구성요소에 대한 기본 정보 → 파일 타입, 머신 타입, 엔트리 포인트 주소

  2. 프로그램 헤더 테이블: 실행 파일이나 공유 라이브러리의 경우, 이 헤더는 운영체제에 프로그램 실행을 위해 필요한 정보를 제공함. → 메모리 매핑을 위한 세그먼트 정보가 포함됨

  3. 섹션 헤더 테이블: 오브젝트 파일의 경우, 이 헤더는 링커에게 필요한 정보 제공 → 코드, 데이터, 심볼 테이블 등의 섹션 정보

  4. 섹션: 실제 코드와 데이터가 포함된 부분

    .text : 실행 코드
    .data : 초기화된 데이터
    .bss: 초기화되지 않은 데이터
    .rodata: 읽기 전용 데이터
    .symtab : 심볼 테이블
    .strtab : 문자열 테이블

운영체제는 elf 형식의 실행 파일을 읽고, 필요한 메모리 영역에 적재하며, 프로그램을 실행할 준비를 해야 한다.


구현하기

process_create_intd

tid_t
process_create_initd (const char *file_name) {
	char *fn_copy;
	tid_t tid;

	/* Make a copy of FILE_NAME.
	 * Otherwise there's a race between the caller and load(). */
	fn_copy = palloc_get_page (0); //file_name의 복사본 저장 -> 페이지 크기의 메모리 할당
	
	if (fn_copy == NULL) 	//메모리 할당에 실패하면 
		return TID_ERROR;
	strlcpy (fn_copy, file_name, PGSIZE); //fn_copy에 file_name 복사

	/*Argument Passing
	file_name을 받아와서 null 기준으로 문자열 파싱

	strtok_r(): 지정된 문자를 기준으로 문자열을 자름*/
	char *parsing;
	strtok_r(file_name, " ", &parsing);

	/* Create a new thread to execute FILE_NAME. */
	/*
	tid : 쓰레드의 id -> 시스템에서 각 쓰레드를 고유하게 식별하는 값
		initd: 새로 생성된 쓰레드가 실행할 함수, 전달될 인자: fn_copy
	*/
	tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
	if (tid == TID_ERROR)
		palloc_free_page (fn_copy);
	return tid;
}
char *parsing;
strtok_r(file_name, " ", &parsing);

: strtok_r 함수를 사용하여 file_name을 공백을 기준으로 파싱한다. 첫 번째 토큰 file_name을 남기고 나머지 문자열을 parsing에 저장한다.


process_exec

int
process_exec (void *f_name) {
	char *file_name = f_name;
	bool success;

	/*Project2. Argument Parsing*/
	char file_name_copy[128]; //원본을 복사할 배열의 크기는 최대 128 byte
	memcpy(file_name_copy, file_name, strlen(file_name) + 1); //원본 문자열을 memcpy로 복사하고 1을 더해줘야 함(\0)

	/* 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_copy, &_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 ();
}
char file_name_copy[128]; //원본을 복사할 배열의 크기는 최대 128 byte
	memcpy(file_name_copy, file_name, strlen(file_name) + 1); //원본 문자열을 memcpy로 복사하고 1을 더해줘야 함(\0)

수정 가능한 복사본 file_name_copy를 만든다.
file_name을 file_name_copy 배열에 복사하는데, 문자열의 문자와 마지막 null 종료 문자까지 포함해서 +1을 해서 복사한다.

그리고 elf 실행 파일을 메모리에 로드하고, 프로세스 실행한다. load가 성공적으로 끝났다면,
do_iret(&_if) 를 호출하여 인터럽트 프레임에 저장된 새로운 프로세스의 상태로 cpu의 레지스터 값을 복원한다. 새로운 프로세스로의 context switch!

생긴 의문

  • palloc_free_page(file_name_copy)가 아니라 palloc_free_page(file_name)?

    file_name_copy 배열은 함수가 종료될 때 자동으로 스택에서 해제. 스택 메모리는 함수 호출 시 할당되고, 해당 함수가 리턴되면 자동으로 해제되는 특성이 있다

    따라서 file_name_copy에 대해 palloc_free_page()를 호출할 필요가 없다.


load()

char *token; //현재 토큰을 저장할 포인터
	char *save_ptr;  //strtok_r 함수가 내부적으로 사용하는 상태 정보를 저장할 포인터
	char *argv[128]; // 
	uint64_t argc = 0; //토큰 개수 

	for (token = strtok_r(file_name, " ", &save_ptr); token != NULL; token = strtok_r(NULL, " ", &save_ptr)) {
		argv[argc++] = token; // argv에 parsing된 현재 토큰 저장
		
     
 ...
 
 	argument_stack(argv, argc, if_);
}

파일 이름에서 인자를 추출해서 argv 배열에 저장한다.
argv에 추후 프로그램에 전달될 인자들이 담긴다.
마찬가지로 strtok_r() 함수를 사용하여 공백 문자를 기준으로 인자를 분리해준다.
이때 argc는 파싱된 총 토큰의 개수가 된다.

argument_stack()

argv 배열에 저장된 인자들을 스택에 역순으로 삽입,
이후 각 인자의 주소를 스택에 삽입, 마지막으로 리턴 주소 설정

argv: 인자 문자열 배열, argc: 인자의 개수, if_: interrupt frame 구조체

void argument_stack(char **argv, int argc, struct intr_frame *if_){

	char *arg_address[128]; /*각 인자의 주소를 저장할 배열*/

	//스택은 높은 주소에서 낮은 주소로 쌓이기 때문에 역순으로 삽입
	//argc -1 부터 0까지 역순으로 반복
	for (int i = argc - 1 ; i >= 0 ; i--) {
		int argv_len = strlen(argv[i]); //현재 인자의 길이(스택에 복사할 떄 필요한 공간을 확보하기 위함) 

		if_->rsp = if_->rsp - (argv_len + 1); 
		//현재 스택 포인터 = 스택 포인터를 [ 인자의 길이 + 1(NULL 문자)] 만큼 감소시킴

		memcpy(if_->rsp, argv[i], argv_len + 1 ); //인자를 스택에 복사 
		
		arg_address[i] = if_->rsp; //복사된 인자의 시작 주소를 arg_address[i]에 저장
	}

	//8 byte Padding
	//스택 포인터가 8바이트로 정렬되지 않은 경우 while문으로 계속 패딩 
	while(if_->rsp % 8 != 0) {

		if_->rsp--; //스택 포인터 1 byte 감소 -> 스택의 높은 주소에서 낮은 주소로 이동하는 것
		
		*(uint8_t *)if_->rsp = 0; //현재 스택 포인터가 가리키는 위치에 0 저장
	}

	
	//주소값 Insert
	//if_->rsp: stack pointer, 현재 스택의 최상단 

	// argc부터 1씩 감소시키며 0까지 반복
	for (int i = argc; i >=0; i--){

		if_->rsp = if_->rsp - 8; //각 반복마다 스택 포인터를 8바이트 감소시킴
		 
		if (i==argc) {
			memset(if_->rsp, 0, sizeof(char **)); 
			//sizeof(char **): 포인터의 크기 
			//i==argc일 때 => NULL 포인터로 채움(끝 표시)
		}

		//실제 인자값의 주소 복사
		else {
			//memcpy 사용하여 arg_address[i]의 주소를 if_->rsp위치에 복사 
			memcpy(if_->rsp, &arg_address[i], sizeof(char **)); //실제 인자의 주소를 스택에 복사 - 8바이트를 복사
			/* sizeof(char *)와 같은 결과 but  이중 포인터를 달*/
		}
	}

	//모든 인자가 스택에 설정된 후에 스택 포인터를 다시 8바이트 감소시키고
	//fake return address 를 스택에 저장
	if_->rsp = if_->rsp - 8; 
	memset(if_->rsp, 0, sizeof(void *));


	if_->R.rdi = argc;
	if_->R.rsi = if_->rsp + 8; 
}


사용자 스택에 command line의 인자들을 8 byte(64 bit니까) 정렬에 맞추어서 insert, 그리고 인자의 주소들을 스택에 insert, fake return address를 마지막으로 스택에 insert하는 함수다.

그리고 인자로는 argc와 argv[0]의 주소, 인자 배열의 시작 주소를 넘겨준다.

헷갈리 부분
memcpy(if_->rsp, &arg_address[i], sizeof(char **))

&arg_address[i]char ** 타입 ⇒ arg_address 배열의 i번째 요소의 주소를 복사하는 것

  • arg_address[i]는 char * 타입
  • &arg_address[i]는 char ** 타입
    둘 다 포인터이기 때문에 size는 8바이트로 동일하긴 함.

uint8_t

포인터 캐스팅: 특정 메모리 주소를 다른 타입의 포인터로 변환하는 과정

void *ptr = some_address;  // 어떤 메모리 주소를 가리키는 void 포인터
uint8_t *byte_ptr = (uint8_t *)ptr;  // 이 주소를 uint8_t 타입의 포인터로 캐스팅

void 포인터는 어떤 타입의 데이터도 가리킬 수 있는 일반적인 포인터 하지만 직접 메모리 접근은 불가능하다.
void * 포인터는 타입이 없기 때문에 이를 통해 메모리에 접근하려면 특정 타입으로 캐스팅해야 함

(uint8_t *)ptr는 ptr을 uint8_t 타입의 포인터로 변환

이렇게 하면 해당 메모리 주소를 uint8_t 타입의 데이터로 접근할 수 있게 된다.
uint8_t *byte_ptr는 1바이트 크기의 부호 없는 정수를 가리키는 포인터. 이 포인터를 사용하면 메모리 주소를 1바이트 단위로 접근할 수 있다.


이렇게 하고,

process_wait()

함수를 수정해주어야 한다.
현재 핀토스에서는 사용자 프로세스를 생성하고 나서 프로세스 종료를 대기할 때, 자식 프로세스가 종료될 때까지 대기하는 기능이 구현되어 있지 않다.
따라서 임시로 코드를 넣어준다.

int
process_wait (tid_t child_tid UNUSED) {
	/* XXX: Hint) The pintos exit if process_wait (initd), we recommend you
	 * XXX:       to add infinite loop here before
	 * XXX:       implementing the process_wait. */
	for (int i = 0; i < 100000000; i++)
    {
	}
	return -1;
}

그러면 process_exec에 넣어준 hex_dump 코드를 통해 현재 사용자 스택의 상태를 확인할 수 있게 된다!


참고

memset 함수

: 특정 메모리 영역을 주어진 값으로 채우는 데 사용

void *memset(void *s, int c, size_t n);

void *s : 값을 설정할 메모리의 시작 주소
int c : 메모리에 설정할 값. 이 값은 unsigned char로 변환되어 사용된다.
size_t n : 설정할 바이트의 수.

memset(buffer, 0, 10);buffer의 시작부터 10바이트를 0으로 채우는 것 -> 이는 주로 메모리를 초기화할 때 사용된다.


memcpy 함수

: 하나의 메모리 영역에서 다른 메모리 영역으로 데이터 복사하는데 사용

void *memcpy(void *dest, const void *src, size_t n);

void *dest : 데이터를 복사할 대상 메모리 영역의 시작 주소.

const void *src : 복사할 데이터의 원본 메모리 영역의 시작 주소

size_t n : 복사할 바이트의 수

memcpy의 경우, memcpy(dest, src, 10);src에서 dest로 10바이트를 복사. 이는 데이터를 한 위치에서 다른 위치로 이동시키거나 복제할 때 사용된다

  • memset: 메모리 영역을 특정 값으로 초기화하는데 사용
  • memcpy: 메모리 영역에서 다른 메모리 영역으로 데이터를 복사하는데 사용

profile
📝 It's been waiting for you

0개의 댓글