[운체] 오늘의 삽질 - 0719

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

Argument Passing 구현 완료

process_create_initd

[구현 1-2] process_create_initd에서 쓰레드 만들 때, 첫 인자만 보내야 함

  • 현재 위 함수 내 thread_createfile_name 매개변수로는 echo x y z가 전달됨
  • 원본 echo x y zfn_copy에 백업한 뒤, thread_create의 쓰레드 이름으로는 echo만 전달해야 함.
tid_t process_create_initd (const char *file_name){
    // 앞뒤 코드는 생략했음에 유의

    // file_name을 버퍼에 복사
    char buffer[128];
    strlcpy(buffer, file_name, 128);

    char *save_ptr, *file_token;
	// [구현 1-2] 현재 file_name "echo x y z" 꼴이라면
    //  file_name은 "echo"만 전달해야 함
	file_token = strtok_r(buffer, " ", &save_ptr);	// 첫 번째 argument만 저장됨
    tid = thread_create (file_token, PRI_DEFAULT, initd, fn_copy);
}
  • strtok_r엔 문자열 상수나, char *형 매개변수를 그대로 전달할 수 없음
    • strtok_r은 내부적으로 파싱할 문자열의 내용을 직접 수정 (공백을 '\0'으로 수정)하기 때문
  • 수정 가능한 버퍼 (e.g., char형 배열)을 전달해야 함
  • 따라서 file_name의 내용을 strlcpybuffer에 복사하고, bufferstrtok_r에 대입
  • 그러면 첫 인자에 해당하는 file_token을 얻을 수 있고, 이를 thread_create에 대입

load

[구현 1-4] load에서 파일을 오픈할 때, 첫 인자만 보내야 함

static bool
load (const char *file_name, struct intr_frame *if_) {
    // 앞뒤 코드 생략

	/* Open executable file. */
	// [구현 1-4: file_name 맨 앞 argument만 parse해야 함]
	char *file_token, *dummy_ptr;
	char bufferA[128];   // 버퍼
	strlcpy(bufferA, file_name, 128);
	file_token = strtok_r(bufferA, " ", &dummy_ptr);	// 첫 번째 argument만 저장됨
    file = filesys_open (file_token);
}
  • 앞서 놓쳤던 부분인데, load 함수 내 filesys_open에도 첫 번째 argument만 들어가게 파싱해야 함
  • 방법은 [구현 1-2]와 크게 다르지 않음
  • file_namebufferA에 복사하고, buffer_A로 파싱하고, 파싱한 file_token으로 파일 열기

[구현 1-3] load에서 스택 초기화가 완료된 이후, argument passing하기

/* TODO: Your code goes here.
	 * TODO: Implement argument passing (see project2/argument_passing.html). */
// [구현 1-3] 인자 스택에 넣기 (스택주소는 if_->rsp)
// 문자열 자체는 정순, 주소는 역순으로 넣는 식으로 구현해보자
  • 현재 file_name 'echo x y z'echo, x, y, z로 알아서 잘 나눈 뒤, 사용자 스택에 전달해야 함
  • 이게 제일 난관이였는데, 차례차례 해 봅시다

argument passing 과정

// 0단계. 변수 선언
uint64_t argc = 0;		// 인자의 수
char bufferB[128];   // 버퍼
strlcpy(bufferB, file_name, 128);
char* argv[30];	// 스택 내 인자의 주소 저장
  • 일단 필요한 변수부터 설정
    • argc에 인자 수, argv내 스택 내 푸시한 각 인자의 주소를 저장
    • file_namebufferB에 복사
// 1단계. 문자열 정순으로 푸시한다
char *token, *save_ptr;
for (token = strtok_r(bufferB, " ", &save_ptr); token != NULL; token = strtok_r(NULL, " ", &save_ptr)){
    if_->rsp -= strlen(token) + 1;  // 널 문자 1바이트 포함
    memcpy((void *)if_->rsp, token, strlen(token) + 1);
    argv[argc] = (char*)if_->rsp;
    argc += 1;

}
argv[argc] = NULL;	// 마지막 널주소
  • 문자열은 어떤 순서대로 푸시하든 상관없음
    • 이후 푸시할 문자열을 가리키는 포인터만, 역순으로 잘 푸시하면 됨
  • strtok_r로 파싱 반복 -> 각 데이터는 token으로 빠짐
  • 스택에 데이터를 푸시할 때 공통적으로 할 일
      1. 스택포인터 if_->rsp를 파싱할 데이터의 크기만큼 낮춤. (문자열은 널문자 1바이트도 고려해야함)
      1. memcpy를 이용해 tokenif_->rsp를 시작으로 데이터의 크기만큼 복사.
  • 푸시한 이후 argv 배열에 현재 스택 포인터 주소 저장
    • 현재 푸시한 문자열의 주소를 저장하게 됨
  • argc도 갱신해야 함
  • 이후 argv 맨 뒤에 NULL 주소도 포함 (인자를 푸시할 땐 안 들어가나, 포인터를 푸시할 땐 들어가야 함)
// 2단계. 패딩 바이트를 추가한다 (사실 이미 초기화할때 0이라 스택 포인터만 낮추면 됨)
int padding = if_->rsp % 8;
if_->rsp -= padding;
  • setup_stack에서 이미 메모리공간 할당할 때 바이트를 모두 0으로 채움
  • 따라서 우린 값을 바꿔줄 필요는 없고, 8의 배수에 맞게 스택 포인터값만 낮춰 주면 됨
// 3단계. 문자열이 저장된 주소를 역순으로 푸시한다
for (int i = argc; i >= 0; i--){
  if_->rsp -= 8;
  memcpy((void *)if_->rsp, &argv[i], 8);
}
  • 앞선 argv에 저장한 주소를 차례로 푸시
    • argv 맨 뒤에 있는 널 주소부터 푸시
    • 주소는 무조건 8바이트임에 유의
    • argv[i]memcpy하면, 주소가 가리키는 문자열을 푸시해 버림.
    • &argv[i]memcpy해야 정상적으로 주소를 푸시함에 유의.
// 4단계. 널 주소를 푸시한다 (사실 이미 초기화할때 0이라 스택 포인터만 낮추면 됨)
if_->rsp -= 8;
  • 널 주소를 푸시해야 하는데, 역시 이미 0바이트로 채워져 있으니 스택 포인터만 낮추면 됨
// 5단계. 레지스터를 갱신한다
if_->R.rdi = argc;
if_->R.rsi = if_->rsp + 8;
  • rdi 레지스터는 argc(인자의 수)로, rsi 레지스터는 첫번째 인자의 포인터가 푸시된 주소로 설정

hexdump 디버깅

int
process_exec (void *f_name) {
    // 앞뒤 코드 생략
	/* And then load the binary */
	success = load (file_name, &_if);
	hex_dump(_if.rsp, _if.rsp, USER_STACK - _if.rsp, true);
}
  • load 함수 뒤에 hex_dump 함수를 추가하면, 우리가 스택에 푸시한 데이터를 16진수 형태로 확인할 수 있음
# /workspaces/pintos_kj9_SSS/pintos/userprog/build 폴더
pintos --fs-disk=10 -p tests/userprog/args-multiple:args-multiple -- -q -f run 'args-multiple some arguments for you!'
  • 위 커맨드로 run 'args-multiple some arguments for you!'를 실행했을 때, 인자 패싱이 잘 되는지 출력 결과로 확인 가능
000000004747ffa0  00 00 00 00 00 00 00 00-f2 ff 47 47 00 00 00 00 |..........GG....|
000000004747ffb0  ed ff 47 47 00 00 00 00-e3 ff 47 47 00 00 00 00 |..GG......GG....|
000000004747ffc0  df ff 47 47 00 00 00 00-da ff 47 47 00 00 00 00 |..GG......GG....|
000000004747ffd0  00 00 00 00 00 00 00 00-00 00 79 6f 75 21 00 66 |..........you!.f|
000000004747ffe0  6f 72 00 61 72 67 75 6d-65 6e 74 73 00 73 6f 6d |or.arguments.som|
000000004747fff0  65 00 61 72 67 73 2d 6d-75 6c 74 69 70 6c 65 00 |e.args-multiple.|
  • 주소 범위: 0x4747ffff -> 0x4747ffa0 (현재 스택포인터)
  • 1바이트 = 16진수 2자리 임에 유의할 것
  • 주소는 낮은 주소부터 낮은 바이트가 저장되어, 역순으로 보임에 유의할 것 (리틀 엔디안 방식)
    • 문자열은 리틀 엔디안이여도 정순으로 저장됨
스택 주소저장 데이터크기16진수
0x4747fff2문자열 args-multiple\014B61 72 67 73 2d 6d-75 6c 74 69 70 6c 65 00
0x4747ffed문자열 some\05B73 6f 6d 65 00
0x4747ffe3문자열 arguments\010B61 72 67 75 6d 65 6e 74 73 00
0x4747ffdf문자열 for\04B66 6f 72 00
0x4747ffda문자열 you!\05B79 6f 75 21 00
0x4747ffd8패딩 02B00 00
0x4747ffd0널주소 0x000000008B00 00 00 00 00 00 00 00
0x4747ffc8you!의 주소 0x4747ffda8Bda ff 47 47 00 00 00 00
0x4747ffc0for의 주소 0x4747ffdf8Bdf ff 47 47 00 00 00 00
0x4747ffb8arguments의 주소 0x4747ffe38Be3 ff 47 47 00 00 00 00
0x4747ffb0some의 주소 0x4747ffed8Bed ff 47 47 00 00 00 00
0x4747ffa8args-multiple의 주소 0x4747fff28Bf2 ff 47 47 00 00 00 00
0x4747ffa0널 주소 08B00 00 00 00 00 00 00 00

커널 스택

  • 시스템 콜을 비롯한 인터럽트가 발생하면, CPU는 호출한 프로세스의 레지스터 정보를 커널 스택에 푸시
    • SS: 스택 세그먼트 (스택메모리가 저장된 영역 가리킴)
    • RSP: 스택 포인터 (지역변수 및 함수 호출 위해 필요)
    • EFLAGS: 상태 플래그 (조건 등)
    • CS: 코드 세그먼트 (실행할 코드가 저장된 메모리영역 가리킴)
    • RIP: 사용자 코드의 프로그램 카운터 (인터럽트 직전 명령의 다음 주소)
    • 범용 레지스터 (RAX, RBX 등)
  • cf. 사용자->커널 모드로 전환되는 인터럽트에서만 SS, RSP 푸시됨. 같은 모드 간 전환의 경우, 이 둘은 푸시되지 않음.
  • 이후 인터럽트핸들러의 실행이 끝나면, 커널 스택에 저장된 정보를 이용해 정확히 사용자 프로그램이 중단됐던 시점으로 복귀

인터럽트 프레임

인터럽트 발생 시

  • 이를 위해, 인터럽트 발생 시 레지스터 정보를 각 프로세스/쓰레드의 커널 스택에 푸시
  • int N 등 명령어가 실행되면, (1) CPU가 자동으로 아래 레지스터 값을 푸시
    • 현재 사용자 모드의 SS->RSP->EFLAGS->CS->RIP 값을 순서대로 커널 스택에 푸시
    • 이후 현재 스택 포인터(RSP)를 커널 스택 위치로 이동하고, 인터럽트 핸들러의 주소로 점프
  • 이후 범용 레지스터(RAX, RBX) 값은 (2) 핸들러의 어셈블리어 코드 내에서 푸시 (intr-stubs.S)
  • (1), (2)에서 스택에 푸시되는 레지스터 정보는 인터럽트 프레임이라 불림 (struct intr_frame 자료형으로 관리)
### 커널 스택의 구조
## 하단 (먼저 푸시/나중에 팝)
CPU가 푸시       ↖ 가장 먼저 푸시되고 (int N), 가장 나중에 pop (iretq)
│ ss
│ rsp
│ eflags
│ cs
│ rip
└─────────────
인터럽트 핸들러가 푸시 (intr-stubs.S) ↖ 나중에 푸시되고, 먼저 pop
│ ds, es
│ r15, r14, ..., rax
└─────────────
## 상단 (나중에 푸시/먼저 팝)

인터럽트로부터 복귀 시

  • 커널 스택에 저장된 인터럽트 프레임 값들을 팝해 상태를 복원
    • 인터럽트 핸들러가 푸시했던 값(RAX, RBX)은, (1) 핸들러의 어셈블리어 코드 내에서 팝이 이루어짐
    • CPU가 푸시했던 값은(RIP->CS->RFLAGS->RSP->SS), 이후 (2)iretq 명령어를 통해 팝이 이루어짐
  • 최종적으로 CPU는 RIP가 가리키는 주소로 점프

핀토스 동작 원리

  • 핀토스의 struct intr_frame tf 자료형으로 인터럽트 프레임을 저장, 관리
    • 커널 스택에 저장되는 정보를 모두 확인 가능
    • 범용 레지스터는 별도의 struct gp_registers R 멤버로 관리
    • 아래 코드상 상단에 위치한 값은 나중에 푸시되고 먼저 팝됨
    • 아래 코드상 하단에 위치한 값은 먼저 푸시되고 나중에 팝됨
struct intr_frame {
	// 인터럽트 핸들러가 푸시/팝함. 나중에 푸시되고, 먼저 팝됨 (intr-stubs.S). */
	struct gp_registers R;
	uint16_t es;
	uint16_t __pad1;
	uint32_t __pad2;
	uint16_t ds;
	// 본 글에선 vec_no, error_code 설명은 생략함.
	uint16_t __pad3;
	uint32_t __pad4;
	uint64_t vec_no;
	uint64_t error_code;	// 얘 푸시는 특이하게 CPU가 할 수도 있고, 인터럽트 핸들러가 할 수도 있음.

	/* 여기서부터 CPU가 푸시/팝함. 제일 먼저 푸시되고(int N), 제일 나중에 팝됨(iretq). */
	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));

/* 범용 레지스터들, rax -> r15 순으로 푸시되고 그 역순으로 팝됨. */
struct gp_registers {
	uint64_t r15;
	uint64_t r14;
	uint64_t r13;
	uint64_t r12;
	uint64_t r11;
	uint64_t r10;
	uint64_t r9;
	uint64_t r8;
	uint64_t rsi;
	uint64_t rdi;
	uint64_t rbp;
	uint64_t rdx;
	uint64_t rcx;
	uint64_t rbx;
	uint64_t rax;
} __attribute__((packed));
  • 아래 코드는 시스템 콜이 발생할 때 핀토스에서 돌아가는 코드
    • 정확히는 int N 호출 시 CPU가 먼저 값들을 푸시 -> 스택 포인터 이동하는 과정이 먼저 이루어지고, 하단 코드가 실행됨
    • (도중에 error_codevec_no, es, ds 등 일부 레지스터를 추가로 푸시하는 코드가 있긴 한데, 일단 생략함)
  • 우선 과정은
    • (1) 인터럽트핸들러가 범용 레지스터 등 값을 푸시
    • (2) 인터럽트핸들러 함수 호출
    • (3) 함수 종료 후, 인트럽트핸들러가 스택에서 값을 팝한 뒤, iretq 실행
// 인터럽트핸들러 동작 과정
.func intr_entry
intr_entry:
	// (1) 인터럽트핸들러가 범용 레지스터 등 값을 푸시
    // 대충 rax, rbx 등 범용 레지스터 푸시하는 내용..
	subq $16,%rsp			// 푸시는 기본적으로 rsp를 낮추고, 해당 위치에 데이터를 복사하는 식으로 이루어짐.
	movw %ds,8(%rsp)
	movw %es,0(%rsp)
	subq $120,%rsp
	movq %rax,112(%rsp)
	movq %rbx,104(%rsp)
	movq %rcx,96(%rsp)
	movq %rdx,88(%rsp)
	movq %rbp,80(%rsp)
	movq %rdi,72(%rsp)
	movq %rsi,64(%rsp)
	movq %r8,56(%rsp)
	movq %r9,48(%rsp)
	movq %r10,40(%rsp)
	movq %r11,32(%rsp)
	movq %r12,24(%rsp)
	movq %r13,16(%rsp)
	movq %r14,8(%rsp)
	movq %r15,0(%rsp)

    // 생략

    // (2) 인터럽트핸들러 함수 호출
    // 현재 스택 포인터의 주소를, intr_handler 함수 매개변수로 전달.
    // syscall_handler의 매개변수 f에 스택 포인터 주소가 들어간다고 생각하면 됨.
	movq %rsp,%rdi
	call intr_handler

	// (3) 핸들러함수 종료 후, 인트럽트핸들러가 스택에서 값을 팝한 뒤, `iretq` 실행
    // 이후 intr_handler 에서 복귀 시, 스택에 저장된 레지스터 값들을 팝하며 복귀.
    movq 0(%rsp), %r15
	movq 8(%rsp), %r14
	movq 16(%rsp), %r13
	movq 24(%rsp), %r12
	movq 32(%rsp), %r11
	movq 40(%rsp), %r10
	movq 48(%rsp), %r9
	movq 56(%rsp), %r8
	movq 64(%rsp), %rsi
	movq 72(%rsp), %rdi
	movq 80(%rsp), %rbp
	movq 88(%rsp), %rdx
	movq 96(%rsp), %rcx
	movq 104(%rsp), %rbx
	movq 112(%rsp), %rax
	addq $120, %rsp
	movw 8(%rsp), %ds
	movw (%rsp), %es
	addq $32, %rsp

    // 여기서 팝 못한 건 iretq에서 해 줌.
	iretq
.endfunc
  • 참고로 intr_handler는 실제 인터럽트 핸들러의 동작을 정의한 함수임
  • 우리가 곧 구현해야 하는 syscall_handler (userprog/syscall.c)은, 인터럽트 프레임이 모두 쌓인 커널 스택의 주소를 인자로 받음
  • 우리는 해당 매개변수를 통해 멤버 값을 꺼내올 수 있음. f->rsi, f->rdi와 같이...
/* The main system call interface */
void
syscall_handler (struct intr_frame *f UNUSED) {
	// TODO: Your implementation goes here.
	printf ("system call!\n");
	thread_exit ();
}

do_iret은 어떨까

  • 우리가 프로그램을 실행하는 과정에서 do_iret이 호출된다는 사실을 기억하실 거임.
  • do_iret 코드는 앞서 살펴본 인터럽트 핸들러 어셈블리어 코드와 거의 비슷한데...
    • 대신 앞선 코드 중 (3) 레지스터 값들을 복원하는 부분부터만 실행됨.
  • 그야 기존 프로그램에서 커널로 전환한 게 아니라, 아예 실행도 안 된 프로그램을 실행했으니, 커널 스택에 푸시를 할 수 있는 게 없음.
  • 대신 우리가 커널코드에서 따로 struct intr_frame을 만들어 주고 그걸 매개변수로 보내면, 거기서 값들을 복원하는 식임.
/* Use iretq to launch the thread */
void
do_iret (struct intr_frame *tf) {
	__asm __volatile(
			"movq %0, %%rsp\n"
			"movq 0(%%rsp),%%r15\n"
			"movq 8(%%rsp),%%r14\n"
			"movq 16(%%rsp),%%r13\n"
			"movq 24(%%rsp),%%r12\n"
			"movq 32(%%rsp),%%r11\n"
			"movq 40(%%rsp),%%r10\n"
			"movq 48(%%rsp),%%r9\n"
			"movq 56(%%rsp),%%r8\n"
			"movq 64(%%rsp),%%rsi\n"
			"movq 72(%%rsp),%%rdi\n"
			"movq 80(%%rsp),%%rbp\n"
			"movq 88(%%rsp),%%rdx\n"
			"movq 96(%%rsp),%%rcx\n"
			"movq 104(%%rsp),%%rbx\n"
			"movq 112(%%rsp),%%rax\n"
			"addq $120,%%rsp\n"
			"movw 8(%%rsp),%%ds\n"
			"movw (%%rsp),%%es\n"
			"addq $32, %%rsp\n"
			"iretq"
			: : "g" ((uint64_t) tf) : "memory");
}
profile
뭔가 만드는 걸 좋아하는 개발자 지망생입니다. 프로야구단 LG 트윈스를 응원하고 있습니다.

0개의 댓글