

process_create_initd[구현 1-2] process_create_initd에서 쓰레드 만들 때, 첫 인자만 보내야 함
thread_create의 file_name 매개변수로는 echo x y z가 전달됨echo x y z은 fn_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'으로 수정)하기 때문char형 배열)을 전달해야 함file_name의 내용을 strlcpy로 buffer에 복사하고, buffer를 strtok_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만 들어가게 파싱해야 함file_name을 bufferA에 복사하고, 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로 알아서 잘 나눈 뒤, 사용자 스택에 전달해야 함// 0단계. 변수 선언
uint64_t argc = 0; // 인자의 수
char bufferB[128]; // 버퍼
strlcpy(bufferB, file_name, 128);
char* argv[30]; // 스택 내 인자의 주소 저장
argc에 인자 수, argv내 스택 내 푸시한 각 인자의 주소를 저장file_name을 bufferB에 복사// 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으로 빠짐if_->rsp를 파싱할 데이터의 크기만큼 낮춤. (문자열은 널문자 1바이트도 고려해야함)memcpy를 이용해 token을 if_->rsp를 시작으로 데이터의 크기만큼 복사.argv 배열에 현재 스택 포인터 주소 저장argc도 갱신해야 함argv 맨 뒤에 NULL 주소도 포함 (인자를 푸시할 땐 안 들어가나, 포인터를 푸시할 땐 들어가야 함)// 2단계. 패딩 바이트를 추가한다 (사실 이미 초기화할때 0이라 스택 포인터만 낮추면 됨)
int padding = if_->rsp % 8;
if_->rsp -= padding;
setup_stack에서 이미 메모리공간 할당할 때 바이트를 모두 0으로 채움// 3단계. 문자열이 저장된 주소를 역순으로 푸시한다
for (int i = argc; i >= 0; i--){
if_->rsp -= 8;
memcpy((void *)if_->rsp, &argv[i], 8);
}
argv에 저장한 주소를 차례로 푸시argv 맨 뒤에 있는 널 주소부터 푸시argv[i]를 memcpy하면, 주소가 가리키는 문자열을 푸시해 버림.&argv[i]를 memcpy해야 정상적으로 주소를 푸시함에 유의.// 4단계. 널 주소를 푸시한다 (사실 이미 초기화할때 0이라 스택 포인터만 낮추면 됨)
if_->rsp -= 8;
// 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 (현재 스택포인터)| 스택 주소 | 저장 데이터 | 크기 | 16진수 |
|---|---|---|---|
0x4747fff2 | 문자열 args-multiple\0 | 14B | 61 72 67 73 2d 6d-75 6c 74 69 70 6c 65 00 |
0x4747ffed | 문자열 some\0 | 5B | 73 6f 6d 65 00 |
0x4747ffe3 | 문자열 arguments\0 | 10B | 61 72 67 75 6d 65 6e 74 73 00 |
0x4747ffdf | 문자열 for\0 | 4B | 66 6f 72 00 |
0x4747ffda | 문자열 you!\0 | 5B | 79 6f 75 21 00 |
0x4747ffd8 | 패딩 0 | 2B | 00 00 |
0x4747ffd0 | 널주소 0x00000000 | 8B | 00 00 00 00 00 00 00 00 |
0x4747ffc8 | you!의 주소 0x4747ffda | 8B | da ff 47 47 00 00 00 00 |
0x4747ffc0 | for의 주소 0x4747ffdf | 8B | df ff 47 47 00 00 00 00 |
0x4747ffb8 | arguments의 주소 0x4747ffe3 | 8B | e3 ff 47 47 00 00 00 00 |
0x4747ffb0 | some의 주소 0x4747ffed | 8B | ed ff 47 47 00 00 00 00 |
0x4747ffa8 | args-multiple의 주소 0x4747fff2 | 8B | f2 ff 47 47 00 00 00 00 |
0x4747ffa0 | 널 주소 0 | 8B | 00 00 00 00 00 00 00 00 |
SS: 스택 세그먼트 (스택메모리가 저장된 영역 가리킴)RSP: 스택 포인터 (지역변수 및 함수 호출 위해 필요)EFLAGS: 상태 플래그 (조건 등)CS: 코드 세그먼트 (실행할 코드가 저장된 메모리영역 가리킴)RIP: 사용자 코드의 프로그램 카운터 (인터럽트 직전 명령의 다음 주소)RAX, RBX 등)SS, RSP 푸시됨. 같은 모드 간 전환의 경우, 이 둘은 푸시되지 않음.int N 등 명령어가 실행되면, (1) CPU가 자동으로 아래 레지스터 값을 푸시SS->RSP->EFLAGS->CS->RIP 값을 순서대로 커널 스택에 푸시RSP)를 커널 스택 위치로 이동하고, 인터럽트 핸들러의 주소로 점프RAX, RBX) 값은 (2) 핸들러의 어셈블리어 코드 내에서 푸시 (intr-stubs.S)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) 핸들러의 어셈블리어 코드 내에서 팝이 이루어짐RIP->CS->RFLAGS->RSP->SS), 이후 (2)iretq 명령어를 통해 팝이 이루어짐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_code랑 vec_no, es, ds 등 일부 레지스터를 추가로 푸시하는 코드가 있긴 한데, 일단 생략함)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 코드는 앞서 살펴본 인터럽트 핸들러 어셈블리어 코드와 거의 비슷한데...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");
}