운영체제(구현) - 핀토스-2(Passing the argument)

연도·2024년 6월 5일
0
post-thumbnail

문자열 Passing하기

요약한 그림

pintos의 프로그램 실행 모델

구현 전 알아야 할 것

load 함수

  • User process의 페이지 테이블 생성
  • 파일을 open하고, ELF 헤더정보를 메모리로 읽어 들임.
  • 각 세그먼트의 가상주소공간 위치를 읽어 들임
  • 각 세그먼트를 파일로부터 읽어 들임.
  • 스택 생성 및 초기화

esp : 스택 포인터 주소

eip : text 세그먼트 시작 주소

함수 호출 방식(80*86)

Argument Passing 흐름

프로그램 실행과 스택을 통한 인자 전달

유저 스택에 인자 삽입

구현해야 할 것

tid_t process_create_initd()

주어진 ‘file_name’ 문자열을 사용하여 새로운 스레드 생성

  1. file_name 문자열 파싱
  2. 커맨드 라인의 첫번째 토큰을 thread_Create() 함수에 첫 인자로 전달

코드

tid_t	
process_create_initd (const char *file_name) {
	char *fn_copy;
	tid_t tid;
	
	fn_copy = palloc_get_page (0); // 페이지 크기의 메모리 블록을 할당. 0(기본)
	if (fn_copy == NULL)
		return TID_ERROR;
	strlcpy (fn_copy, file_name, PGSIZE); // 문자열을 안전하게 복사.

	
	// 문자열을 공백을 기준으로 파싱하여 첫번째 토큰 추출
	char *save_ptr;
	strtok_r(file_name, " ", &save_ptr);
	// 새 스레드 생성(첫 번째 토큰 보내기)
	tid = thread_create(file_name, PRI_DEFAULT, initd, fn_copy);

	// 오류 처리
	if(tid == TID_ERROR)
		palloc_free_page(fn_copy);

	return tid;

}

설명

  1. palloc_get_page를 이용해서 fn_copy에 페이지 크기의 메모리를 블록할당하고 그 할당한 곳에 인자로 받은 *file_name을 복사한다.
  2. strtok_r을 이용해서 인자로 받은 *file_name을 앞부분만 추출해서 thread_create에 첫번째 토큰을 보낸다.
  3. 새 스레드를 생성하지 못했을 때 palloc_free_page를 이용하여 공간을 할당한 페이지를 반환한다(fn_copy)

int process_exec(void *f_name)

저장된 실행 파일을 현재 프로세스에 로드하고 실행하는 역할

  1. file_name 문자열 파싱
  2. 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 ();

	// 변수 초기화
	int count = 0;
	char *parse[128];
	char *token;
	char *save_ptr;

	// 인자 파싱
	for (token = strtok_r(file_name, " ", &save_ptr); token != NULL;
	token = strtok_r(NULL, " ", &save_ptr))
	{
		parse[count] = token;
		count++;
	}

	/* And then load the binary */
	/* if_.esp는 스택 포인터
	바이너리 로드 -  'file_name'에 지정된 프로그램을 메모리에 적재한다.
	load - '_if'에 초기 스택 포인터와 엔트리 포인트(프로그램의 시작주소) 설정 */
	success = load (file_name, &_if); // load() : file_name의 프로그램을 메모리에 적재
	
	// 로드 실패 처리
	if (!success)
	{
		palloc_free_page(file_name);
		return -1;
	}

	// 유저 프로그램이 실행 되기 전에 argument_stack() 함수 호출하여 스택에 인자 저장.
	argument_stack(parse, count , &_if.rsp);
	
	// 디버깅 툴 - 메모리 내용을 16진수로 화면에 출력. 유저 스택에 인자를 저장 후 유저 스택 메모리 확인
	hex_dump(_if.rsp , _if.rsp , USER_STACK-(uint64_t)_if.rsp , true);
	/* Start switched process. */
	
	/* If load failed, quit. */
	palloc_free_page (file_name);

	do_iret (&_if); // 인터럽트 반환 명령을 사용하여 새로운 사용자 모드 프로그램 실행
	NOT_REACHED (); 
}

설명

  1. 변수 초기화 및 인자 파싱:
    • 주어진 문자열을 공백을 기준으로 분리하여 parse 배열에 저장합니다.
  2. 바이너리 로드:
    • file_name에 지정된 프로그램을 메모리에 적재합니다.
    • 실패 시 메모리를 해제하고 -1을 반환합니다.
  3. 인자 스택에 저장:
    • 파싱된 인자들을 스택에 저장합니다.
  4. 디버깅 (선택적):
    • 메모리 내용을 16진수로 출력하여 스택에 인자들이 올바르게 저장되었는지 확인합니다.
  5. 메모리 해제 및 새로운 사용자 모드 프로그램 실행:
    • 메모리를 해제하고, do_iret를 사용하여 새로운 사용자 모드 프로그램을 실행합니다.

argument_stack(char parse, int count, void rsp)

proces_exec() 함수에서 parsing한 프로그램 이름 and 인자를 스택에 저장

  1. 프로그램 이름 및 인자(문자열) push
  2. 프로그램 이름 및 인자 주소들 push
  3. argv (문자열을 가리키는 주소들의 배열을 가리킴) push
  4. argc (문자열의 개수 저장) push
  5. fake address(0) 저장

코드

void
argument_stack(char **parse, int count, void **rsp) // 주소를 전달 받았으니 이중 포인터
{
	char *stack_ptr = (char *)*rsp; // 현재 스택 포인터를 가리키는 포인터
    uintptr_t addr[count]; // 각 문자열의 주소 저장.

    /*
	1. 프로그램 이름 및 인자(문자열) push
    스택은 아래 방향으로 성장하므로 스택에 인자 추가시 string을 오른쪽 > 왼쪽(역방향) push
	'memcpy'를 사용하여 문자열을 스택에 복사.
	*/
	for (int i = count - 1; i >= 0; i--) {
        int len = strlen(parse[i]) + 1; // NULL 포함
        stack_ptr -= len;
        memcpy(stack_ptr, parse[i], len);
        addr[i] = (uintptr_t)stack_ptr;
    }

    /*
	2. 정렬 패딩 push
	각 문자열 push 후 (8)byte 단위로 정렬하기 위해 필요한 만큼 padding 추가.
	*/
    while ((uintptr_t)stack_ptr % 8 != 0) {
        stack_ptr--;
        *stack_ptr = 0;
    }

    /*
	3. 프로그램 이름 및 인자 주소들 push
    각 문자열의 주소를 스택에 역순으로 저장. 마지막에 NULL 포인터 추가하여 문자열 배열의 끝 표시
	*/
	for (int i = count; i >= 0; i--) {
        stack_ptr -= sizeof(uintptr_t);
        *(uintptr_t *)stack_ptr = (i == count) ? 0 : addr[i]; // 마지막은 NULL 포인터
    }

    /*
	4. argv (문자열을 가리키는 주소들의 배열을 가리킴) push
	이 주소는 프로그램이 실행될 때 인자를 참조하는데 사용
	*/ 
    uintptr_t argv = (uintptr_t)stack_ptr;
    stack_ptr -= sizeof(uintptr_t);
    *(uintptr_t *)stack_ptr = argv;

    // 5. argc (문자열의 개수 저장) push
    stack_ptr -= sizeof(int);
    *(int *)stack_ptr = count;

    /*
	6. fake address(0) 저장
	다음 인스트럭션 주소를 push 해야 하는데, 지금은 프로세스를 생성하는 거라서 반환 주소x
	*/
    stack_ptr -= sizeof(void *);
    *(void **)stack_ptr = 0;

	// 최종 스택 포인터 설정.
    *rsp = (void *)stack_ptr;
}

설명

  1. 프로그램 이름 및 인자(문자열) push:
    • 스택에 프로그램 이름과 인자 문자열을 역순으로 복사하여 저장합니다.
    • memcpy를 사용하여 parse[i] 값을 stack_ptr에 복사합니다.
    • 스택은 LIFO 형식이므로, 반복문을 뒤에서부터 돌아서 문자열을 오른쪽에서 왼쪽으로(역방향) push합니다.
  2. 정렬 패딩 push:
    • 스택이 8바이트 단위로 정렬되도록 패딩을 추가합니다.
    • 현재 스택 포인터(stack_ptr)가 8로 나누어 떨어질 때까지 0을 채워 정렬합니다.
  3. 각 문자열의 시작 주소 push:
    • 각 문자열의 시작 주소를 스택에 역순으로 저장합니다.
    • 마지막에 NULL 포인터를 추가하여 문자열 배열의 끝을 표시합니다.
  4. argv (문자열을 가리키는 주소들의 배열을 가리킴) push:
    • 이전 단계에서 스택에 저장된 문자열 주소 배열의 시작 주소를 스택에 저장합니다.
    • 이 주소는 프로그램이 실행될 때 인자를 참조하는 데 사용됩니다.
  5. argc (문자열의 개수 저장) push:
    • 인자의 개수를 스택에 저장합니다.
    • 이는 프로그램이 실행될 때 argc로 사용됩니다.
  6. 가짜 리턴 주소 (0) push:
    • 가짜 리턴 주소(0)를 스택에 저장합니다.
    • 이는 현재 프로세스가 리턴할 주소가 없기 때문입니다.
  7. 최종 스택 포인터 설정:
    • 수정된 스택 포인터를 원래 포인터(rsp)에 저장하여 함수 호출자가 이를 참조할 수 있도록 합니다.

ex)

  1. 프로그램 이름 및 인자(문자열) push
스택 포인터: 0x80000000
==========================
| 문자열       | 스택 주소  |
==========================
| "arg2\0"     | 0x7FFFFFF8 |
| "arg1\0"     | 0x7FFFFFE8 |
| "program_name\0" | 0x7FFFFFD8 |
==========================
  1. 각 문자열의 시작 주소 push
스택 포인터: 0x7FFFFFD8
==========================
| 문자열 시작 주소   | 스택 주소  |
==========================
| NULL               | 0x7FFFFFD0 |
| 0x7FFFFFF8 (arg2)  | 0x7FFFFFC8 |
| 0x7FFFFFE8 (arg1)  | 0x7FFFFFC0 |
| 0x7FFFFFD8 (program_name) | 0x7FFFFFB8 |
==========================
  1. ‘argv’ (문자열을 가리키는 주소들의 배열을 가리킴) push
스택 포인터: 0x7FFFFFB8
==========================
| `argv` 주소     | 스택 주소  |
==========================
| 0x7FFFFFB8      | 0x7FFFFFB0 |
==========================
  1. ‘argc’ (문자열의 개수 저장) push
스택 포인터: 0x7FFFFFB0
==========================
| `argc` 값       | 스택 주소  |
==========================
| 3               | 0x7FFFFFA8 |
==========================
  1. 가짜 리턴 주소(0) push
스택 포인터: 0x7FFFFFA8
==========================
| 가짜 리턴 주소  | 스택 주소  |
==========================
| 0               | 0x7FFFFFA0 |
==========================
  1. 최종 스택 상태
| 주소       | 값                         |
|------------|----------------------------|
| 0x7FFFFFA0 | 0                          | (가짜 리턴 주소)
| 0x7FFFFFA8 | 3                          | (argc)
| 0x7FFFFFB0 | 0x7FFFFFB8                 | (argv 배열의 시작 주소)
| 0x7FFFFFB8 | 0x7FFFFFD8                 | (program_name의 주소)
| 0x7FFFFFC0 | 0x7FFFFFE8                 | (arg1의 주소)
| 0x7FFFFFC8 | 0x7FFFFFF8                 | (arg2의 주소)
| 0x7FFFFFD0 | 0                          | (NULL 포인터)
| 0x7FFFFFD8 | "program_name\0"           | (프로그램 이름)
| 0x7FFFFFE8 | "arg1\0"                   | (첫 번째 인자)
| 0x7FFFFFF8 | "arg2\0"                   | (두 번째 인자)
| 0x80000000 |                            | (초기 스택 포인터)

이 그림은 각 단계에서 스택이 어떻게 변경되는지 보여줍니다. 프로그램이 실행될 때, 스택에서 이 데이터를 참조하여 argcargv를 통해 인자들을 올바르게 처리할 수 있습니다.

0개의 댓글