정글/Pintos

[pintos] 2주차 - Argument Passing 구현 과정

nkdev 2025. 5. 24. 17:07

디버깅 과정 몇 개를 소개하ㅏ겠다 

구현한 코드

static bool
load (const char *file_name, struct intr_frame *if_) {
	struct thread *t = thread_current ();
	struct ELF ehdr;
	struct file *file = NULL;
	off_t file_ofs;
	bool success = false;
	int i;


	/* arguments set up */
	int argc = 0;
	char *argv[128];
	char **save_ptr;
	char *str;

	for(str = strtok_r(file_name, " ", save_ptr); str != NULL; str = strtok_r(file_name, " ", save_ptr)){
		argv[argc++] = str;
	}

	/* Allocate and activate page directory. */
	t->pml4 = pml4_create ();
	if (t->pml4 == NULL)
		goto done;
	process_activate (thread_current ());

	/* Open executable file. */
	file = filesys_open (file_name);
	if (file == NULL) {
		printf ("load: %s: open failed\n", file_name);
		goto done;
	}

	/* Read and verify executable header. */
	if (file_read (file, &ehdr, sizeof ehdr) != sizeof ehdr
			|| memcmp (ehdr.e_ident, "\177ELF\2\1\1", 7)
			|| ehdr.e_type != 2
			|| ehdr.e_machine != 0x3E // amd64
			|| ehdr.e_version != 1
			|| ehdr.e_phentsize != sizeof (struct Phdr)
			|| ehdr.e_phnum > 1024) {
		printf ("load: %s: error loading executable\n", file_name);
		goto done;
	}

	/* Read program headers. */
	file_ofs = ehdr.e_phoff;
	for (i = 0; i < ehdr.e_phnum; i++) {
		struct Phdr phdr;

		if (file_ofs < 0 || file_ofs > file_length (file))
			goto done;
		file_seek (file, file_ofs);

		if (file_read (file, &phdr, sizeof phdr) != sizeof phdr)
			goto done;
		file_ofs += sizeof phdr;
		switch (phdr.p_type) {
			case PT_NULL:
			case PT_NOTE:
			case PT_PHDR:
			case PT_STACK:
			default:
				/* Ignore this segment. */
				break;
			case PT_DYNAMIC:
			case PT_INTERP:
			case PT_SHLIB:
				goto done;
			case PT_LOAD:
				if (validate_segment (&phdr, file)) {
					bool writable = (phdr.p_flags & PF_W) != 0;
					uint64_t file_page = phdr.p_offset & ~PGMASK;
					uint64_t mem_page = phdr.p_vaddr & ~PGMASK;
					uint64_t page_offset = phdr.p_vaddr & PGMASK;
					uint32_t read_bytes, zero_bytes;
					if (phdr.p_filesz > 0) {
						/* Normal segment.
						 * Read initial part from disk and zero the rest. */
						read_bytes = page_offset + phdr.p_filesz;
						zero_bytes = (ROUND_UP (page_offset + phdr.p_memsz, PGSIZE)
								- read_bytes);
					} else {
						/* Entirely zero.
						 * Don't read anything from disk. */
						read_bytes = 0;
						zero_bytes = ROUND_UP (page_offset + phdr.p_memsz, PGSIZE);
					}
					if (!load_segment (file, file_page, (void *) mem_page,
								read_bytes, zero_bytes, writable))
						goto done;
				}
				else
					goto done;
				break;
		}
	}

	/* Set up stack. */
	if (!setup_stack (if_))
		goto done;

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

	/* TODO: Your code goes here.
	 * TODO: Implement argument passing (see project2/argument_passing.html). 
	file_name을 " " 기준으로 잘라서 argv[]의 0번째부터 차례로 넣기
	argv[0]은 가상 주소의 가장 낮은 곳, 각 문자열 + null pointer sentinel을 argv의 요소로 넣기
	rsi가 argv를, rdi가 argc를 가리키게 만들어야 한다.
	fake return address를 push
	 */

	
	setup_args(argv, argc, if_);

	success = true;

done:
	/* We arrive here whether the load is successful or not. */
	file_close (file);
	return success;
}

/* argv[]에 저장되어 있는 command line 인자를 뒤에서 부터 꺼내서 스택에 push하기 */
void
setup_args(char **argv, int argc, struct intr_frame *if_){

	int total_size = 0; //8bytes 정렬을 위해 저장한 모든 값들의 byte 크기를 누적하여 저장해둠
	int padding_size = 0; //8bytes 정렬 시 필요한 패딩 사이즈
	char *addr[argc]; 

	/* 스택에 args를 반대 방향으로 push */
	for(int i=0; i<argc; i++){ //argc(argv배열의 길이)만큼 반복
		int arg_len = strlen(argv[argc-i]) + 1; //"\0" 포함
		if_ -> rsp -= arg_len; //push해준 데이터의 byte 크기 만큼 스택 포인터 감소
		memcpy(if_ -> rsp, argv[argc-i], arg_len); //argv배열의 맨 뒤에 있는 값부터 차례로 스택에 push. 주의할 점 1: 사이즈를 sizeof(argv[argc-i])로 주면 안 된다. argv에 담긴 문자열 크기가 아니라 포인터 크기인 8바이트를 의미하기 때문이다. 그래서 str_len이라는 변수를 따로 만들어서 넣어준다. 주의할 점 2 : rsp를 먼저 줄이고 mempcy를 수행해야 한다. 스택 공간을 먼저 확보한 후 써야 한다.
		addr[i] = if_ -> rsp; //if_에 값을 하나 저장할 때마다 그 때의 rsp값을 addr[]에 저장
		total_size += arg_len; //정렬을 위한 사이즈 계산
	}

	/* 8bytes 정렬 */
	padding_size = 8 - (total_size % 8); //필요한 패딩 bytes 사이즈 구하기
	if_ -> rsp -= padding_size; //패딩 사이즈 만큼 스택 포인터 감소
	memset(if_ -> rsp, 0, if_ -> rsp + padding_size); //해당 사이즈 만큼 0으로 채우기
	
	/* char* 타입 크기 만큼 0으로 채우기 */
	if_ -> rsp -= sizeof(char *);
	memset(if_ -> rsp, 0, sizeof(char *)); 

	/* 맨 처음에 스택에 들어간 args들이 스택의 어디에 push되었는지에 대한 주소를 push */
	for(int i=0; i<argc; i++){
		if_ -> rsp -= sizeof(char *);
		memcpy(if_ -> rsp, addr[i], sizeof(char *)); //char* 크기의 공간에 주소를 저장
	}
	
	/* 가짜 return address push */
	if_ -> rsp -= sizeof(void *);
	memset(if_ -> rsp, 0, sizeof(void *));

}

 

이렇게 구현했는데 

 

첫 번째 문제

Kernel panic in run: PANIC at ../../lib/string.c:223 in strtok_r(): assertion `save_ptr != NULL' failed.

이런 오류가 뜨면서 args-single 테스트가 fail이 떴다.

 

	/* arguments set up */
	int argc = 0;
	char *argv[128];
	char **save_ptr;
	char *str;

	for(str = strtok_r(file_name, " ", save_ptr); str != NULL; str = strtok_r(file_name, " ", save_ptr)){
		printf("-----------str : %s----------", str);
		argv[argc++] = str;
	}

load 함수에 내가 새로 추가한 부분이다.

strtok_r()을 새로 사용한 부분은 이 부분 밖에 없으니 여기서 오류를 찾아보자. 

 

strtok_r (char *s, const char *delimiters, char **save_ptr) {

strtok_r()은 세 번째 인자로 char **save_ptr을 받는다. 즉 char* 타입 변수의 주소를 받겠다는 말이다.

 

따라서

char *save_ptr;

이렇게 선언하고

strtok_r(file_name, " ", &save_ptr)

이렇게 주소값을 넘겨줘야 한다.

 

두 번째 문제

msg()로 디버깅한 모습.. gdb는 너무 어려워서 계속 출력을 찍어보면서 해결했다.

	/* 스택에 args를 반대 방향으로 push */
	for(int i=0; i<argc; i++){ //argc(argv배열의 길이)만큼 반복
		msg("str : %s\n", argv[argc-i-1]);
		int arg_len = strlen(argv[argc-i-1]) + 1; //"\0" 포함
		if_ -> rsp -= arg_len; //push해준 데이터의 byte 크기 만큼 스택 포인터 감소
		memcpy(if_ -> rsp, argv[argc-i-1], arg_len); //argv배열의 맨 뒤에 있는 값부터 차례로 스택에 push. 주의할 점 1: 사이즈를 sizeof(argv[argc-i])로 주면 안 된다. argv에 담긴 문자열 크기가 아니라 포인터 크기인 8바이트를 의미하기 때문이다. 그래서 str_len이라는 변수를 따로 만들어서 넣어준다. 주의할 점 2 : rsp를 먼저 줄이고 mempcy를 수행해야 한다. 스택 공간을 먼저 확보한 후 써야 한다.
		msg("len = %d\n", arg_len);
		addr[i] = if_ -> rsp; //if_에 값을 하나 저장할 때마다 그 때의 rsp값을 addr[]에 저장
		msg("value : %p, byte size : %d \n", *addr[i], sizeof(addr[i]));
		total_size += arg_len; //정렬을 위한 사이즈 계산
	}

 

strlen("onearg")+1=7인데 길이가 6이라고 출력되고.. 

strlen("args-single")+1=11인데 길이가 1이라고 출력되고!!!

알고 보니 또 포인터 문제였다. 이놈의 포인터야.......

int arg_len = strlen(argv[argc-i-1]) + 1; //"\0" 포함

이 부분에서 argv[argc-i-1]값을 가져와야 하는데 &argv[argc-i-1]이렇게 가져오고 있었다.

char** argv에 저장된 요소를 가져올 때는 그냥 argv[i] 이렇게 가져오면 된당..

memcpy(if_ -> rsp, &addr[i], sizeof(char *));

참고로 여기서 addr[i]의 값을 if_->rsp위치에 memcpy할 때는 왜 앰퍼샌드를 붙였냐면..

memcpy (void *dst_, const void *src_, size_t size)

memcpy의 프로토타입을 보면 알 수 있다. 두 번째 인자로 복사할 값이 아니라 복사할 값을 가리키는 포인터를 요청했기 때문에 주소를 준 것이다. 얘 때문에 아무 생각 없이 strlen() 인자로도 &를 붙여버린 것 같다. 

 

* 주소값 저장할 때는 char타입을 사용하기

그리고 성광이가 과거의 내가 왜 addr[]를 char*타입을 저장하는 배열로 선언했는지 물어봤는데 선뜻 대답하지 못했다.

생각해보니 addr[] 안에 if_->rsp 즉 숫자로 된 주소값을 저장할 거니까 int* addr[]로 선언하는게 숫자로 이루어진 주소값이 담긴다는 사실도 바로 알 수 있고 좋지 않을까 생각이 들었다. 

 

그런데 성광이의 말로는, 주소값을 저장할 공간은 char타입으로 지정하는 게 좋다고 한다. 

char 타입은 1byte=8bit로, 시스템에서 주소 길이와 같기 때문이다.

주소만 저장할 건데 굳이 4byte의 int 타입으로 선언할 필요가 없공간 낭비일 뿐이다.

 

hex_dump 출력 결과 스택에 두 인자 push된 모습

 

근데 여전히 테스트 통과는 안 된다. 。°(° ᷄ᯅ ᷅°)°。

 

다른 Userprog 테스트들도 마찬가지로 내 Argument Passing 구현부를 사용하면 모조리 fail이 뜨고

예찬님의 Argument Passing 코드를 사용하면 pass가 떴다. 

 

일단 이 부분을 뒤로하고 System Call을 구현하다가 진짜 내 코드를 포기할 수 없고 너무 아까워서 !!!!

며칠 뒤에 다시 도전해봤다. 그리고 드디어 ㅜㅠ

패스 해버렸당 !

 

두 번째 시도에는 기필코 Argument Passing을 성공시키겠다는 의지로 gdb 사용에 도전했다.

(이번에도 윤호씨.. 감사해요)

https://nkdev.tistory.com/178

 

gdb

우리반에 몇 없는 gdb 사용자 김윤호씨의 도움에 감사를 표하며 글을 시작하겠습니다 ^ㅁ^도커환경을 실행시키고 있는 터미널 2개 켜기 현재 실행 중인 도커 목록을 확인한다.docker ps -a 그 중 접

nkdev.tistory.com

사용 방법은 여기를 참고해주시길

 

세 번째 문제

 

output이 위처럼 나와야 하는데 아래처럼 출력되어서 fail이 발생했다.

(Pintos 테스트는 출력값으로 pass/failed를 판단함)

 

file name은 내가 디버깅용으로 찍은 내용이고,

args-single: exit(0)이 출력되어야 하는데 args-single one: exit(0)이 출력되었다.

 

~~~: exit(0)에서 ~~~에 해당하는 부분은 스레드 이름을 출력하는 부분인데 

스레드 이름이 args-single이 아니라 args-single one으로 파싱되어서 생긴 문제인 것 같아서, 이 값이 어떻게 넘겨지는지 따라가봤다.

 

process_create_initd()는 thread_create()에 file_name을 넘기고

tid_t
process_create_initd (const char *file_name) {
	...
	/* Create a new thread to execute FILE_NAME. */
	tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
	if (tid == TID_ERROR)
		palloc_free_page (fn_copy);
	return tid;
}

thread_create()는 받은 name을 init_thread()로 넘겨서 이름이 "name"인 스레드를 만든다.

tid_t
thread_create (const char *name, int priority,
		thread_func *function, void *aux) {
	/* Initialize thread. */
	init_thread (t, name, priority);  
    ...
}

init_thread()를 보면 strlcpy()를 사용해서 받은 name을 t->name에 저장하는 것을 볼 수 있다.

즉 name값을 새로 생성할 스레드의 이름으로 지정한다.

static void
init_thread (struct thread *t, const char *name, int priority) {
	ASSERT (t != NULL);
	ASSERT (PRI_MIN <= priority && priority <= PRI_MAX);
	ASSERT (name != NULL);

	memset (t, 0, sizeof *t);
	t->status = THREAD_BLOCKED;
	strlcpy (t->name, name, sizeof t->name); //name을 t->name에 복사
	t->tf.rsp = (uint64_t) t + PGSIZE - sizeof (void *);
	t->priority = priority;
	t->initial_priority = priority;
	t->magic = THREAD_MAGIC;
}

 

그리고 exit() 시스템콜이 스레드 name과 status를 출력하는 이 부분에서 name이 잘못 출력되어 test failed가 뜬 것이다.

void exit(int status){
	printf("%s: exit(%d)\n", thread_current()->name, status);
	thread_exit();
}

 

file_name을 출력해보면 args-single onearg가 나오는 것을 볼 수 있다.

tid_t
process_create_initd (const char *file_name) {
	...
	printf("-------%s------\n", file_name);

	/* Create a new thread to execute FILE_NAME. */
	tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
    ...
}

args-single만 thread->name으로 넘겨야 하므로 strtok_r()로 파싱해서 넘겨주자

tid_t
process_create_initd (const char *file_name) {
	...
	/* 파싱 !!!!!!! (커맨드 라인의 첫 번째 인자를 스레드 이름으로 넘김) */
	char *save_ptr;
	char *token;
	token = strtok_r(file_name, " ", &save_ptr);

	/* Create a new thread to execute FILE_NAME. */
	tid = thread_create (token, PRI_DEFAULT, initd, fn_copy);
}

패스 (⸝⸝⸝ᵒ̴̶̷̥́ ᵕ ก̀⸝⸝⸝)ෆ.............