오늘 하루에 집중하자
  • [CS] PintOS Project 2 - User Program(2) - Argument Passing
    2022년 12월 24일 21시 55분 16초에 업로드 된 글입니다.
    작성자: nickhealthy

     

    목표: User Program에서 argument(인자) 를 설정하도록 process_exec() 함수를 수정해라

     

    x86-64 Calling Convention(함수 호출 규약)


    이 섹션에서는 64bit x86-64 에서 일반적으로 함수를 호출할 때 convention의 중요한 포인트를 요약한다.
    For more detail, you can refer System V AMD64 ABI.

     

    잠깐, KAIST gitbook을 공부하기 전에 함수 호출 규약(Calling Convention) 개념을 정리하고 가자.

    함수 호출 규약을 이해하기 위해서는 stack 과 stack frame에 대한 정확한 이해가 있어야 한다. 각각의 thread 마다 stack이 생성되고, 각각의 함수마다 stack frame이 존재한다.

     

    각각의 함수마다 stack frame이 생성되며, 해당 함수마다 관련된 데이터가 push, pop 될 것이다. 정상적인 처리를 위해서 각 함수마다 대화하는 방식이 같아야 한다.(한 사람은 한국어, 한 사람은 영어로 얘기한다면 대화가 안 될 것이다.)

    이렇게 stack frame을 사용하는 함수들간의 대화 방식(stack에 데이터를 write, read 하는 방식)을 함수 호출 규약(Calling Convention)이라고 한다.

     

    함수 호출 규약은 아래 4가지의 값을 기준으로 그 종류가 나뉘어진다.

    • Parameter 전달 방법 - stack frame을 사용해서 parameter 전달, 레지스터를 사용해서 parameter 전달
    • Parameter 전달 순서 - 함수명(parameter1, parameter2, parameter3, ...) 에서 어떤 parameter부터 전달할 것인가?
    • 함수 리턴 값 전달 방법 - 함수 리턴 값을 어디에 저장하고, 돌려줄 것인가?
    • 함수 호출 간 사용했던 stack frame을 정리하는 방법 - 함수 사용이 끝난 후 사용했던 stack frame 공간을 누가 정리할 것인가?

    그 중 C 진영에서는 __cdecl 를 사용한다. 아래는 C언어에서 만든 함수 호출 규약인 __cdecl에 대한 설명이다.

     cdecl에서 callee의 스택프레임을 caller가 정리 해야만 했던 이유

      - c언어에는 가변인자 함수(파라메터 개수 제한 x, ex-printf( ) ) 있다.

      - ex). Caller(main 함수) printf( "a=%d, b=%d, c=%d, d=%d", n1,n2,n3,n4 )           printf("a=%d, b=%d", n1,n2)

     

    Callee( ex - printf 함수 ) 의 Parameter가 가변적인 함수의 경우, callee 에서 스택 프레임 정리를 일관되게 처리할 수 있는 정형화된 routine을 만들기 어렵다(파라메터의 값이 가변적이기 때문). 만약 이것을 callee에서 모두 처리하려면, callee의 Parameter가 몇 개인지 체크하고, 그에 따라 stack frame을 정리하는 복잡한 절차를 거쳐야 한다. 하지만 caller는 callee의 Parameter가 몇 개인지 callee를 호출하는 순간, 이미 알고 있기 때문에 작업이 더 수월하다. 요약하자면 아래와 같다.

    • C언어에서는 caller가 stack frame을 정리한다.
    • callee가 stack frame을 정리하는 것이 caller가 정리하는 것보다 빠르지만, C언어에서는 가변인자 함수를 지원하기 위해 caller가 stack frame을 정리한다.
     

     

    이제 다시 KAIST gitbook 으로 돌아가서 정리해보자.

    (중요!) Calling convention은 아래와 같은 작업을 수행한다.

    • User-level application에서 인자를 전달하기 위해 다음과 같은 정수 레지스터를 사용한다.(아래 레지스터에 저장해서 넘긴다.)
      • %rdi, %rsi, %rdx, %rcx, %r8, %r9 
    • caller는 다음 instruction(return address)을 stack에 넣어주고, callee의 첫 인스트럭션으로 점프한다.
    • 이어서 callee가 실행된다.
    • callee가 반환 값을 갖는 경우, %rax 레지스터에 값을 넣는다.
    • callee는 return address를 stack에서 pop하면서 그 위치로 점프한다. x86-64 인스트럭션에서는 RET가 이를 수행한다.

    f() 함수가 f(1,2,3) 으로 3개의 정수형 인자를 받는 경우를 생각해보자.

    <아래의 다이어그램은 callee가 바라보는 stack frame  register state 의 예시>

    *참고: 초기 stack의 주소는 임의적이다.

                                 +----------------+
    stack pointer --> 0x4747fe70 | return address |
                                 +----------------+
    RDI: 0x0000000000000001 | RSI: 0x0000000000000002 | RDX: 0x0000000000000003

     

    Program Startup Details(프로그램 시작 전 디테일)

    User Program의 진입점은 lib/user/entry.c의 _start() 함수부터이다. 이 함수는 main()을 실행하고 main()이 return 하면 exit()를 호출한다.

    • argc - command line에서 파일 이름을 제외하고, 공백 기준으로 입력된 스트링의 개수(인자의 개수)
    • argv - ["/bin/ls\0', '-l\0', 'foo\0', 'bar\0'] + 센티넬(\0)
    • 주의할 것은, 각 스트링마다 1바이트씩 사이즈를 추가해줘야 한다.('\0'을 통해 스트링이 끝났다는 것을 C 표준이 알 수 있다.) 마찬가지 이유로 argv[argc] 위치에 sentinel을 넣어줘야 한다.(아래 참고)
    void
    _start (int argc, char *argv[]) {
        exit (main (argc, argv));
    }

    커널은 User Program 실행을 허용하기 전, inital function에 대한 인자를 레지스터(register)에 넣어줘야 한다. 이때 일반적인 Calling Convention과 동일한 방식으로 인자가 전달된다.

     

    </bin/ls -l foo bar 가 전달될 때 예시>

    1. 커맨드를 단어로 분리한다. ex). '/bin/ls', '-l', 'foo', 'bar'
    2. 단어들을 최상위 스택에 배치한다. 이때 포인터로 참조되기 때문에 순서는 상관없다.
    3. 각 문자열의 주소 + null pointer sentinel을 스택에 PUSH한다. 이때 오른쪽->왼쪽 순서로 PUSH 한다.
      1. '/bin/ls', '-l', 'foo', 'bar' 는 argv의 elements(요소)이다.
      2. null pointer sentinel은 'argv[argc]'가 null pointer가 되야한다. 이는 C 표준 요구사항이다.
      3. argv[0]이(프로그램) 가장 낮은 가상 주소에 있도록 한다.
      4. word로 정렬되어 있는 경우가 정렬되지 않은 경우보다 접근하는 것이 더 빠르기 때문에 stack에 first push 전, stack 포인터를 8의 배수가 되도록 조정한다.
    4. %rdi를 argc로 설정하고, %rsi가 argv(argv[0]의 주소)로 가리키고, 
    5. 마지막으로 fake return address를 넣어준다. entry 함수(진입 함수)는 반환되지 않으나 다른 stack frame과 동일한 구조를 만들어주기 위해 넣는다.

     

    아래 테이블은 user program이 실행되기 직전 stack 상태와 관련 레지스터의 상태를 보여준다. 스택은 아래로 증가한다.

     

     

    이 예시는 stack 포인터가 0x4747ffb8 로 초기화된다. 위에 표시된대로 사용자 코드는 USER_STACK에서 시작하고, 이것은 include/threads/vaddr.h에 정의되어 있다.

    RDI는 argc, RSI는 argv[0]을 가리키고 있다.

     

    Implement the argument passing(인수 전달 구현)

    현재 process_exec() 함수는 arguments를 새 프로세스로 전달하는 것을 지원하지 않는다. process_execute()를  확장하여 단순히 프로그램 파일 이름을 인수로 받아들이는 대신 공백으로 단어를 나누도록 이 기능을 구현하라.

    첫 번째 단어는 프로그램 이름이고, 두 번째 단어는 첫 번째 인수이다. 즉, process_exec("grep foo bar") 는 두 인수 foo와 bar를 전달하면서 grep 명령어를 실행해야 한다.

     

    command line 내에서 여러 공백은 단일 공백과 동일하다. 또한 command line에서 인자의 길이를 제한할 수 있다.
    예를 들어, 인수를 single page(4KB)에 적합한 인수로 제한할 수 있다.(PintOS 유틸리티가 커널에 전달할 수 있는 명령줄 인수는 관련이 없는 128 바이트의 제한이 있다.)

     

    원하는 방식으로 argument strings을 파싱(parse)할 수 있다. include/lib/string.h 로 프로토타입을 만들고 include/lib/string.c에 상세한 주석으로 구현된 strok_r() 함수를 살펴보시오. man 페이지를 보면 자세한 내용을 알 수 있다.(프롬프트에서 man strok_r 실행)

     

     


     

    Ref

    https://blog.naver.com/PostView.naver?blogId=tjdghkgkdl&logNo=10117639381

    https://casys-kaist.github.io/pintos-kaist/project2/argument_passing.html

    댓글