[운체] 오늘의 삽질 - 0718

방법이있지·2025년 7월 18일
post-thumbnail

Argument Passing

  • 어제 글 말미에서 언급했듯이 이런 문제들을 해결해야 함
  • [구현 1-1] 무한루프 땜빵... 이건 쉽고

  • [구현 1-2] process_create_initd에서 쓰레드 만들 때, 쓰레드 이름 수정해야 함

    • 현재 위 함수 내 thread_createfile_name 매개변수로는 echo x y z가 전달됨
    • 원본 echo x y zfn_copy에 백업한 뒤, thread_create의 쓰레드 이름으로는 echo만 전달해야 함.
  • [구현 1-3] load에서 스택 초기화가 완료된 이후, argument passing하기

    • 즉 현재 'echo x y z'echo, x, y, z로 알아서 잘 나눈 뒤, 사용자 스택에 전달해야 함
  • 그러면 도대체 (1) 문자열을 어떻게 파싱하고

  • (2) 파싱한 각 정보를 어디에 전달해야 하는지.... 를 공부해야 함.

strtok_r

  • Python의 .split() 이랑 비슷
// 경로: lib/string.c
char *strtok_r(char *s, const char *delimiters, char **save_ptr)
  • 문자열 sdelimiters로 나눔.
  • 처음 호출 시 s에 분리할 문자열을 전달. 이후 호출 시 NULL을 전달.
    • 이후엔 save_ptr을 이용해 내부적으로 문자열을 기억
#include <stdio.h>
#include <string.h>

int main(void){
    char s[] = "사랑한다 나의 LG여 영원하라 무적 LG여";
    char *token, *save_ptr;
    for (token = strtok_r(s, " ", &save_ptr); token != NULL;
    token = strtok_r(NULL, " ", &save_ptr)){
        printf("'%s' ", token);
    };
}

// [출력결과]
// '사랑한다' '나의' 'LG여' '영원하라' '무적' 'LG여'

[구현 1-2] process_create_initd

  • 현재 전달받은 file_name'echo x y z'
  • echo만 잘라내서 thread_create할 때 쓰레드명으로 전달할 것
  • 기존 file_name은 미리 fn_copy에 복사해 뒀으니 괜찮음.
    • 새로운 쓰레드에서 initd 함수의 매개변수로 fn_copy가 전달됨
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);

    // [구현 1-2] 현재 file_name은 "echo x y z"
    //  file_name은 "echo"만 전달해야 함
	// "echo"를 이름으로 쓰레드 만들기.. strtok으로 나중에 구현해야 함

	tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
	if (tid == TID_ERROR)
		palloc_free_page (fn_copy);
	return tid;
}

쓰레드의 사용자 영역 / 커널 영역

  • 한 프로세스의 가상 주소 공간은 아래와 같이 구성됨
# 위->아래로 높은 주소 -> 낮은 주소
[ 커널 영역 (Kernel Space) ]     ← OS가 관리하는 공간 (사용자 코드에서는 접근 불가)
├── PCB/TCB, 커널 내부 자료구조 등
├── 커널 스택 2 (쓰레드 2)
├── 커널 스택 1 (쓰레드 1) - 높은 주소에서 낮은 주소로 성장
├── 커널 코드/데이터

[ 사용자 영역 (User Space) ]     ← 프로세스 입장에서 "내가 접근 가능한 공간"
├── 쓰레드 2
│   └── 사용자 스택 2 (개별) - 높은 주소에서 낮은 주소로 성장
├── 쓰레드 1
│   └── 사용자 스택 1 (개별)
├── 힙 영역 (공유)          - 낮은 주ㅡ소에서 높은 주소로 성장
├── 데이터 영역 (공유)
├── 코드 영역 (공유)
  • 쓰레드는 한 프로세스 내 독립적 실행 흐름의 단위
  • 쓰레드별로 사용자 스택, 커널 스택이 별개로 존재한다는 점에 유의할 것
    • 사용자 스택엔, 각 쓰레드의 호출 함수, 복귀 주소, 지역 변수, 매개변수 등 정보를 둠
    • 커널 스택엔, 인터럽트 처리 중 기존 레지스터 정보 (후술할 인터럽트 프레임) 등 정보를 둠

사용자 프로그램 <-> 커널 프로그램 간 전환

  • 인터럽트 프레임
    • 인터럽트가 발생했을 때, 이후 정확히 복귀할 수 있도록 CPU가 현재 실행 중인 쓰레드의 상태를 저장하는 자료구조
    • 커널 스택에 푸시함. (사용자 스택이 아님.)
    • 핀토스에선 struct intr_frame 구조체가 인터럽트 프레임 역할을 함
      • 현재 레지스터 값(특히 스택 포인터 rsp, 프로그램 카운터 rip) 등을 멤버로 저장
  • int N 명령어로 사용자 프로그램 -> OS(커널)로 진입
    • integer가 아니라 interrupt입니다... 이것도 시스템 콜이니까 일종의 인터럽트. N은 인터럽트 번호.
    • CPU는 현재 쓰레드 메모리의 커널 스택으로 rsp(스택포인터 레지스터) 이동.
    • 이후 struct intr_frame 형태로, 사용자 프로그램의 레지스터 상태를 커널스택에 푸시.
  • iret 명령어로 OS(커널) -> 사용자 프로그램으로 복귀
    • iret 실행 전 struct intr_frame에 저장된 레지스터 상태를, 커널스택에서 팝해 복원
    • 이후 iret 명령어는 복원된 레지스터 상태를 기반으로, 사용자 프로그램으로 점프

process_exec

  • 핀토스에서는 후술할 process_exec를 통해, OS -> 사용자 프로그램으로의 전환이 이루어짐
    • 대신 기존에 돌아가던 사용자 프로그램이 없으므로, 스택의 intr_frame엔 저장된 게 있을 리가 없음. 따라서 우리가 입력받은 argument를 통해 알아서 인터럽트 프레임을 초기화해야 함.
    • 예를 들어, 인터럽트 프레임의 RIP(프로그램 카운터)를 실행할 사용자 코드의 시작 주소로, RSP(스택 포인터)를 사용자 스택 주소로 설정해야 함.
    • 이 과정에서 사용자 스택을 초기화하고, 보낼 argument를 사용자스택에 올려두어야 함.
  • 새로운 쓰레드에서 process_exec()을 통해 f_name을 실행
    • 여기서 f_name"echo x y z"와 같은 형태
/* 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;

	// (1) 새로 실행할 사용자 프로그램의 인터럽트 프레임을 선언해 준다.
    // 초기값 몇개를 할당해 주는데 이건 이해할 필요 없을 듯.
	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 ();

	// (2) 프로그램을 로딩하기.
    // _if는 load 함수에 넘겨져, 인터럽트 프레임 및 사용자 스택 초기화에 사용된다.
    // load에서 사용자 스택에 인자를 전달하는 과정도 이루어진다.
	success = load (file_name, &_if);

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

	// (3) 사용자 프로그램으로 점프할 때, _if에 저장된 정보를 사용한다.
	do_iret (&_if);
	NOT_REACHED ();
}

[구현 1-3] load

  • 일단 잡다한 일들이 많이 발생하는데, 생략
    • 페이지 테이블 만들고 (pml4_create ();), 파일 열고 (filesys_open();)ELF 헤더 읽음 (file_read (file, &ehdr, sizeof ehdr)).
    • setup_stack이란 함수가 안에서 실행되며 사용자스택이 초기화됨.
    • if_->rsp = USER_STACK;로, 사용자 스택의 top 주소 저장됨 (0x47480000임)
    • if_->rip = ehdr.e_entry;로, 실행할 사용자 프로그램의 시작주소 저장됨.
  • 이 과정까지는 다 해 주는데... 이때 argument passing을 implement해야 함.
    • filesys_open에는 "echo x y z"'echo만 들어가야 할 것 같은데, 확실한진 모르겠다.
    • "echo", "x", "y", "z"setup_stack 이후, 따로 사용자 스택에 푸시해야 함
static bool
load (const char *file_name, struct intr_frame *if_) {
	struct thread *t = thread_current ();
	// 생략

	/* Open executable file. -> 기존 file_name에서 맨 앞 명령어만 파싱 (echo 등)*/
	file = filesys_open (file_name);

    // 생략

	/* Set up stack. */
    // setup_stack 내 `if_ -> rsp = USER_STACK` 코드 있음
	if (!setup_stack (if_))
		goto done;

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

	// 여기서 스택에 푸시하면서 ARGUMENT PASSING해야 함.
    // 현재 스택 top 주소인 if_->rsp부터 시작

    // 생략
}

setup_stack

  • 구현해야 하는 줄 알았는데 이미 있네
  • 커널과 사용자프로그램은 서로 다른 가상 주소공간을 사용
  • palloc_get_page을 통해, 커널은 4KB(1페이지) 크기의 메모리를 할당
    • 반환값은, 커널이 호출했으므로 커널 주소. 그런데 사용자 스택으로 써먹으려면, 사용자 가상 주소 공간에서도 이 페이지에 접근 가능해야 함.
    • 이걸 가능하게 하는 게 아래 install_page 함수.
  • install_page(user_addr, kpage, writable)는 사용자 주소 공간의 user_addr 주소에, kpage가 가리키는 물리 페이지를 매핑
    • 아래 코드에선, USER_STACK - PGSIZE부터 1페이지만큼의 영역이, kpage에 해당되는 물리 페이지에 연결됨
  • 암튼 이제부턴 이 1페이지 크기의 공간을 이용해, 값을 푸시하거나, 스택을 아래 방향으로 확장할 수 있음.
static bool
setup_stack (struct intr_frame *if_) {
	uint8_t *kpage;
	bool success = false;

	// 실제 물리 프레임을 OS 물리 메모리 풀에서 할당
	// 이 물리 프레임에 대응하는 커널 가상 주소를 반환
	// kpage: 커널 공간에서, 할당된 물리 페이지에 접근할 수 있는 가상주소
	kpage = palloc_get_page (PAL_USER | PAL_ZERO);	// 사용자 페이지 할당, 모든 바이트는 0으로 초기화. 이때 한 페이지는 4KB.


	if (kpage != NULL) {
		// 사용자의 가상 주소 공간에 페이지 테이블을 설정
		// 사용자 가상 주소 (USER_STACK - PGSIZE(4KB))에, 커널이 가진 물리 페이지 (kpage)를 매핑
		// true: 사용자 process가 페이지에 쓰기 가능
		success = install_page (((uint8_t *) USER_STACK) - PGSIZE, kpage, true);
		if (success)
			if_->rsp = USER_STACK;
		else
			palloc_free_page (kpage);
	}
	return success;
}

핀토스에서 argument passing하는 법

  • e.g., "/bin/ls -l foo bar"

  • (1) strtok_r로, "/bin/ls", -l, foo, bar로 파싱

  • (2) 스택 top에다 각 문자열을 푸시 (보통 역순으로)

    • 푸시는 * 연산자로 직접 주소에 값을 할당하면 될 듯함.

    • e.g., if_ -> rsp(현재 스택포인터 top)의 초기값은 0x47480000일 때

      • 스택은 높은 주소 -> 낮은 주소로 자람에 유의할 것.

      • cf. 각 포인터의 주소는 8의 배수여야 하므로, 패딩도 넣어야 할 수 있음.

        주소이름데이터자료형크기
        0x4747fffc*argv[3]"bar\0"char[4]4바이트
        0x4747fff8*argv[2]"foo\0"char[4]4바이트
        0x4747fff5*argv[1]"-l\0"char[3]3바이트
        0x4747ffed*argv[0]"/bin/ls\0"char[8]8바이트
        0x4747ffe88바이트 정렬용0uint8_t[]5바이트
  • (3) 푸시한 각 문자열의 주소를 푸시. 이때 argv 배열 맨 끝의 널 포인터를 먼저 푸시해야 함.

    • 포인터는 8바이트임에 유의할 것.

    • e.g., 위 예제에서 계속

      주소이름데이터자료형크기
      0x4747ffe0argv[4]0 (널포인터)char *8바이트
      0x4747ffd8argv[3]0x4747fffcchar *8바이트
      0x4747ffd0argv[2]"0x4747fff8"char *8바이트
      0x4747ffc8argv[1]"0x4747fff5"char *8바이트
      0x4747ffc0argv[0]"0x4747ffed"char *8바이트
  • (4) fake return address를 푸시 (0)

    • e.g., 위 예제에서 계속

      주소이름데이터자료형크기
      0x4747ffb8리턴 주소0void *8바이트
    • 이후 스택 포인터 (if_-> rsp)는 fake return address의 위치인 0x4747ffb8로 설정됨

  • (5) 레지스터 %rsiargv(즉 argv[0]의 주소)로, %rdiargc(여기선 4)로 설정

    • e.g., 위 예제에서 계속
    • %rdi4, %rsiargv[0]의 주소인 0x4747ffc0으로 저장.
    • if_ -> R -> rsi, if_ -> R -> rdi를 수정하면 될 듯함.

do_iret

  • 초기화된 struct intr_frame *tf 구조체에서 레지스터 값들을 복원
  • iretq 명령어로 CPU 상태를 전환해, 사용자 프로그램을 시작
/* Use iretq to launch the thread */
void
do_iret (struct intr_frame *tf) {
    // __volatile은 컴파일러의 최적화를 막음
	__asm __volatile(
			"movq %0, %%rsp\n"          // 커널스택 포인터(rsp)를 tf가 가리키는 주소로 설정

            // tf의 멤버들을 레지스터로 복원하는 과정
			"movq 0(%%rsp),%%r15\n"     // tf에 저장된 값을 꺼내, 일반 레지스터들에 복원
			"movq 8(%%rsp),%%r14\n"
			// 생략

            // 사용자모드로 전환하며, 사용자 프로그램 시작
			"iretq"
			: : "g" ((uint64_t) tf) : "memory");
}
profile
뭔가 만드는 걸 좋아하는 개발자 지망생입니다. 프로야구단 LG 트윈스를 응원하고 있습니다.

0개의 댓글