디버깅 과정 몇 개를 소개하ㅏ겠다
구현한 코드
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 사용에 도전했다.
(이번에도 윤호씨.. 감사해요)
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);
}

패스 (⸝⸝⸝ᵒ̴̶̷̥́ ᵕ ก̀⸝⸝⸝)ෆ.............
'정글 > Pintos' 카테고리의 다른 글
| [pintos] 파일 디스크립터 간단한 설명 (0) | 2025.05.27 |
|---|---|
| [pintos] 프로세스 - PID / fork로 자식 스레드가 생성되는 과정 (0) | 2025.05.26 |
| [pintos] 2주차 - 키워드 정리 (0) | 2025.05.24 |
| [pintos] 1주차 - Priority Scheduling 구현 과정 (1) | 2025.05.20 |
| [pintos] 1주차 - Priority Scheduling 문서 해석 / 코드 해석 (1) | 2025.05.20 |