ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [PintOS] Project 2 - Argument Passing (2)
    Projects/Krafton_Jungle_4 2024. 3. 16. 14:24
    728x90

    이제 user_stack에 문자 배열과 문자열 개수를 전달하는 작업을 수행해야 한다.

    load()에서 setup_stack() 을 통해 인터럽트 프레임의 rsp의 위치를 USER_STACK으로 설정해 두었다. 주소는 0x47480000 이다.

    그전에 각각의 레지스터들이 어떤 역할을 하는지 정리를 해 보자

    CSAPP에서 나온 레지스터 종류

    범용 레지스터

    작은 데이터 저장 공간으로 연산처리, 연산 결과, 복귀 주소 등 작은 데이터를 기억하는 레지스터이다.

    • RAX - Accumulator
      주로 산술 연산에 사용되고, 함수의 반환 값도 이 레지스터를 통해 전달된다.
    • RBX - Base
      메모리 주소를 저장하기 위한 용도로 사용된다.
    • RCX - Counter
      반복문에서 카운터로 사용되는 역할이다.
    • RDX - Data
      다른 레지스터를 서포트하는 레지스터로 입출력 및 일부 산술 연산에 사용된다

    인덱스 레지스터

    • RSI - Source Index
      문자열이나 배열 연산에서 소스의 주소를 가리키는데 사용된다.
      혹은 데이터를 복사할 때, 복사할 데이터의 주소가 저장된다.
    • RDI - Destination Index
      문자열이나 배열 연산에서 목적지의 주소를 가리키는데 사용된다.
      혹은 데이터를 복사할 때 복사된 목적지의 주소가 저장된다.
    • R8 ~ R15
      x86-64 아키텍처에서 추가된 새로운 범용 레지스터로, 추가적인 인덱스 값으로 사용될 수 있다.
      R8D ~ R15D까지는 32비트 값으로 사용하고
      R8W ~ R15W는 16비트
      R8B ~ R15B는 8비트 값을 위해 사용된다.

    포인터 레지스터

    • RBP - Base Pointer
      함수의 스택 프레임을 가리키는 데 사용된다.
      현재 함수의 지역 변수와, 매개 변수에 접근하는 데 쓰인다.
    • RSP - Stack Pointer
      현재 스택의 최상단을 가리키는데 사용된다.
      함수 호출 시 매개 변수 전달, 지역 변수 저장, 스택 프레임 관리 등에 쓰인다.

    세그먼트 레지스터

    세그먼트란 무엇인가?

    • CS - Code Segment
      현재 실행 중인 코드의 세그먼트를 가리킨다.
    • DS - Data Segment
      데이터 세그먼트의 주소를 저장한다.
    • ES, FS, GS
      추가 데이터 세그먼트를 위한 레지스터로 특정 목적으로 사용될 수 있다.
    • SS - Stack Segment
      스택 세그먼트의 주소를 저장한다.

    명령어 포인터 레지스터

    • RIP - Instruction Pointer
      현재 실행 중인 명령어의 주소를 가리킨다.
      명령어가 실행 될 때마다 자동으로 업데이트된다.
      직접적으로 인덱스 레지스터나 포인터 레지스터로 분류되지는 않는다.

    플래그 레지스터

    • RFLAGS
      프로그램의 상태를 나타내는 플래그들을 저장한다.
      산술 연산의 결과가 0일 때 제로 플래그(ZF)가 설정되는 방식이다.

    이상으로 레지스터에 대한 설명을 마쳤는데, 우리가 사용할 것으로 예상되는 레지스터들은 다음과 같다.

    • rdi
    • rsi
    • rdx
    • rcx
    • r8
    • r9

    그 중에서도 명령어의 배열이 저장되어 있는 argv[] 변수와 명령어의 개수인 argc 변수를 포인터 레지스터로 하여금 가리키게 만들면 된다는 것이 깃북에 나와있다.

    이 부분이 이해하기 제일 어려운 것 같다.

    1. 문자열을 파싱하는 것 까지는 수행된 것 같다.
    2. 하지만 파싱된 단어들을 스택의 위에 위치시키는 것을 어떻게 해야할지를 전혀 알려주지 않는다. 감이 안잡히는 중이다.
    3. 문자열의 주소 + 널포인터를 스택에 오른쪽에서 왼쪽으로 어떻게 push해야할지 모르겠다. push를 직접 구현해야 하는 것인가?
      널 포인터의 경계는 argv[argc]가 널 포인터라는 사실을 보장해준다는 말은 또 무엇인가.
      그리고 argv[0]이 가장 낮은 주소를 가진다는 사실을 보장해준다는 말은 무엇인가.. 그냥 가장 낮은 주소이다 이렇게 표현하는게 어려웠을까...
      스택에 첫 푸시가 발생하기 전에 스택포인터를 8배수로 반올림하는 것은 일단 생각하지 말자.
    4. %rsi가 argv주소를 가리키게 하고 %rdi를 argc로 설정한다는 말도 명확히 이해하기 어렵다.
      해당 레지스터들이 하는 역할과 argv와 argc를 설정해주는 것이 잘 매치되지 않는다.
    5. 가짜 '리턴 어드레스'를 푸시한다는 것은 무엇일까.
      리턴 어드레스면 리턴어드레스지 왜 가짜를 넣어줄까..

    수만가지의 의문이 들지만 일단 대가리 박아보자.

    argv[]의 주소와 argc의 주소를 rsi포인터와 rdi 포인터가 바라보게 해주면 되는건가? 일단 시도해 보자

    음 하지만 이는 단순히 포인터를 argv 배열의 주소로 할당해주기만 하면 끝나는 것은 아닌 것 같다.
    스택은 위에서 아래로 자라나기 때문에, 주소값은 보다시피 줄어들고 있다.

    ex) 0x4747fffc -> 0x4747fff8 -> 0x4747fff5 -> 0x4747ffed ...

    그리고 중요한 말이 있다.

    만약 명령어들을 다 push하고 나면 스택 포인터(%rsp)의 주소가 표에서 제일 마지막 부분인 0x4747ffb8로 초기화(초기화에 대한 말이 잘 이해가 안되는데 그냥 그 값으로 설정된다고 이해했다)될 것이며,
    명령어가 아무것도 들어가지 않은 스택이 가리키는  즉, 스택 포인터의 위치는 USER_STACK(0x47480000)이라는 것이다.

    -> 하나씩 넣기 시작하면 8바이트(?)씩 줄어드는 것이다.

     

    이제 파싱된 argv배열의 주소를 하나씩 찍어보자.

    할 필요 없는 짓거리였다.

     

    기본 개념을 잡았다.

    1. rsp를 배열원소 개수 * 8바이트 만큼 옮겨 놓는다.
      rsp의 위치를 찍어보려고 열심히 printf("%d", if_->rsp)를 했는데 웬걸! 16진수는 %d가 아닌 %x로 보여져야 한다! 제기랄.
    2. 옮겨진 rsp의 위치에 memcopy를 활용해 배열의 원소들을 넣는다.
    3. 패딩은 그냥 안해준다. 단편화를 위한 것 같으니
    4. argv의 주소를 차례로 push한다.

    디버깅을 위해 hex_dump()를 활용하여 rsp를 찍어봐야 한다.
    -> hex_dump() 사용법은 시간 절약을 위해 찾아보았다.

    첫 번째 시도

    void push_stack(struct intr_frame *intr_f, char argv, int argc)
    {
        intr_f->rsp = intr_f->rsp - (14 * argc);
        printf("data push 이후 rsp값 %x \n\n", intr_f->rsp);
        for (argc; argc == 0; argc = argc - 1)
        {
            memcpy(intr_f->rsp, argv, 14);
        }
        intr_f->rsp = intr_f->rsp - (8 * argc);
        printf("address push 이후 rsp값 %x \n\n", intr_f->rsp);
        for (argc; argc == 0; argc = argc - 1)
        {
            memcpy(intr_f->rsp, &argv, 8);
        }
    }

     

    timeout이 발생한다. argv 배열의 크기가 너무 큰 것 같다.
    일단 배열 원소를 프린트 찍어보려고 한다.


    일단 우리는 포인터 배열을 사용해야 한다.

    load()에서 파싱을 할 때, argv 배열을 일반 배열로 선언하여 strlcpy을 사용하여 직접 값을 복사하는 방법도 있다. 하지만 그렇게 하기 위해서는 strlcpy의 첫 번째 인자인 dest에 argv[d]와 같이 동적으로 할당을 해야 하는데, 우리의 가여운 pintOS는 malloc을 지원하지 않는다. 따라서 우리는 이 문제를 char형 포인터 배열로 해결할 수 있다.

     

    우선, load()를 수정해주자

    load()

    static bool
    load(const char *file_name, struct intr_frame *if_)
    {
        struct thread *t = thread_current();
        struct ELF ehdr;
        struct file *file = NULL;
        char *token, *save_ptr;
        char *argv[128];
        int argc = 0;
        off_t file_ofs;
        bool success = false;
        int i;
        int d = 0;
    
        printf("file_name: %s \n\n", file_name);
    
        /* Allocate and activate page directory. */
        t->pml4 = pml4_create();
        if (t->pml4 == NULL)
            goto done;
        process_activate(thread_current());
    
        for (token = strtok_r(file_name, " ", &save_ptr);
             token != NULL;
             token = strtok_r(NULL, " ", &save_ptr))
        {
            argv[argc] = token;
            argc++;
        }
        file_name = argv[0];
     	
        // ....
    
        /* Set up stack. */
        if (!setup_stack(if_))
            goto done;
    
        /* Start address. */
        if_->rip = ehdr.e_entry;
        printf("초기 rsp 위치: %x \n\n", if_->rsp);
        printf("초기 rdi 값: %d \n\n", if_->R.rdi);
        /* TODO: Your code goes here.
         * TODO: Implement argument passing (see project2/argument_passing.html). */
    
        push_stack(if_, argv, argc);
        hex_dump(if_->rsp, if_->rsp, USER_STACK - (uint64_t)if_->rsp, true);
    
        success = true;
    
    done:
        /* We arrive here whether the load is successful or not. */
        file_close(file);
        return success;
    }
    • *argv[128]
      일반 배열을 포인터 배열로 선언한다.
      포인터 배열과 일반 배열의 차이는 다음과 같다.
      그림 설명
      이렇게 선언하면, 우리가 기존에 argv[d] 처럼 동적으로 할당하는 것을 피할 수 있다. 포인터로 값을 참조하기 때문이다.
      이제 드디어 파이썬처럼 배열에 값을 넣어줄 수 있다. 근데 이것도 사실 배열 포인터에 토큰의 주소를 넣어주는 것이다.
    • push_stack
      인자로 만약 *argv를 넘겨주면, argv[0]의 값만을 넘겨주게 된다.
      따라서 배열 전체를 나타내고 싶으면 argv 배열자체의 시작 주소인 argv를 인자로 넘겨주어야 한다.
      *argv는 argv를 역참조함을 의미한다. 즉, argv의 0번째 원소가 갖는 값을 넘겨주게 되는 것이다.
      argv의 0번째 원소가 갖는 값은 우리 기준에서는 'args-single'이 저장된 주소이다.
      char *argv[128]일 때, *argv 와 argv[0]은 같은 결과를 출력한다!!!!!!!!!!!!

    push_stack()

    void push_stack(struct intr_frame *intr_f, char **argv, int argc)
    {
        for (int i = argc - 1; i >= 0; i--) // 명령어 인자 데이터 넣기
        {
            size_t len = strlen(argv[i]) + 1;
            intr_f->rsp -= len;
            memcpy(intr_f->rsp, argv[i], len);
        }
        intr_f->rsp = intr_f->rsp - sizeof(uint8_t); // 스택 포인터를 8의 배수로 반올림
        memset(intr_f->rsp, 0, sizeof(uint8_t));
        intr_f->rsp = intr_f->rsp - sizeof(char *); // 스택 포인터를 8의 배수로 반올림
        memset(intr_f->rsp, 0, sizeof(char *));
        // printf("data push 이후 rsp값 %x \n\n", intr_f->rsp);
        for (int j = argc - 1; j >= 0; j--)
        {
            intr_f->rsp -= sizeof(char *) * argc;
            memcpy(intr_f->rsp, &argv[argc], sizeof(char *));
        }
        intr_f->rsp = intr_f->rsp - sizeof(void (*)()); // return할 함수가 있을 시 return address 설정
        memset(intr_f->rsp, 0, sizeof(void (*)()));
    
        intr_f->R.rsi = argv[0];
        intr_f->R.rdi = argc;
    }

     

    거의 다 끝났다 싶었다. 그러나 데이터는 잘 들어가는데 주소값이 잘 들어가지 않았음을 확인했다.

     

    주소를 나타내는 GG ... GG 부분이 보이질 않네!

    원인은 이랬다.

     

    우리가 파일 이름과 인자들을 넣고 난 다음의 rsp 위치에 push해야할 데이터들은 다음과 같다.

     

    하지만 나는 계속 argv[2] 이름의 스택 위치에 argv 배열의 주소 자체를 넣어주고 있었다.

    그래서 다음과 같이 수정하였다.

    push_stack() 2차 수정

    void push_stack(struct intr_frame *intr_f, char **argv, int argc)
    {
        char *addrv[128];
        
        for (int i = argc - 1; i >= 0; i--) // 명령어 인자 데이터 넣기
        {
            size_t len = strlen(argv[i]) + 1;
            intr_f->rsp -= len;
            memcpy((void *)intr_f->rsp, argv[i], len);
            addrv[i] = intr_f->rsp;
        }
        intr_f->rsp = intr_f->rsp - sizeof(uint8_t *); // 스택 포인터를 8의 배수로 반올림
        memset((void *)intr_f->rsp, 0, sizeof(uint8_t *));
    
        intr_f->rsp = intr_f->rsp - sizeof(char *); // 스택 포인터를 8의 배수로 반올림
        memset((void *)intr_f->rsp, 0, sizeof(char *));
        
        for (int j = argc - 1; j >= 0; j--)
        {
            intr_f->rsp -= sizeof(char *);
            memcpy((void *)intr_f->rsp, &addrv[j], sizeof(char *));
        }
    
        intr_f->rsp = intr_f->rsp - sizeof(void (*)()); // return할 함수가 있을 시 return address 설정
        memset((void *)intr_f->rsp, 0, sizeof(void (*)()));
    
        intr_f->R.rsi = argv[0];
        intr_f->R.rdi = argc;
    }

    디버깅 과정을 이야기해 보자면,

    파일 인자 데이터를 넣을 때마다 addrv 배열에 해당 주소를 저장하고, 이를 두 번째 for문에서 memcpy를 통해 동일하게 넣어주려 하였다.

    하지만 계속 kernel panic이 발생하였다.

     

    문제는 역시 포인터였다....

    일단 warning으로 나왔던 캐스팅이 안된 sizeof 함수나 memcpy의 인수들을 다 처리하였다.

    당연히 이것이 문제는 아니었을 것이고, 여러 디버깅 끝에 두 번째 for문의 addrv[j]부분이 문제임을 알았다.

     

    우리는 memcpy를 통해 복사받을 메모리를 가리키는 포인터를 첫 번째 인자로 써야 한다.

    intr_f->rsp은 그대로 가리키고 있는 위치를 잘 나타내지만 두 번쨰 인자인 addrv[j]는 그대로 쓰면 안된다.

    addrv[j]와 &addrv[j]의 차이

    • addrv 배열은 문자열의 주소를 저장하기 때문에, addr[j]는 char* 타입의 문자열 주소를 나타낸다.
      즉, addrv[j]의 값 자체는 실제 문자열 데이터가 저장된 메모리의 주소를 나타낸다.
    • &addrv[j]는 addrv[j]의 주소이다.
      우리는 이 배열의 주소에 있는 값들을 copy할 것이기 때문에, 배열 주소를 통해 접근해야하므로
      이를 memcpy에 넣어주는 것이 맞다

    addrv[j]를 통해 문자열의 주소를 직접적으로 접근하는 것은 rsp의 위치 때문에 그런 것인지는 몰라도, 정확하게 어떤 연유로 터지는 것인지는 모르겠다.

    어쨌든 우리는 스택에 위치한 인자 데이터들의 주소를 담고 있는 배열을 이용할 때, 배열의 주소를 통해 접근하여 스택에 복사하여야만 한다.

    실제 스택의 데이터에는 배열의 주소가 아닌 인자 데이터들이 위치한 주소들이 복사되지 않을까?

     

    이제 결과가 제대로 나온다.

     

Designed by Tistory.