ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Jungle] Pintos_Project2_User Programs: gitbook 해설
    Pintos 2023. 6. 4. 01:20

    Argument Passing

    인수 전달

    process_exec()에서 사용자 프로그램에 대한 인수를 설정합니다.

    x86-64 호출 규칙
    이 섹션에서는 64비트 x86-64 구현의 유닉스에서 일반 함수 호출에 사용되는 규칙의 중요한 사항을 요약합니다. 간결성을 위해 일부 세부 사항은 생략했습니다. 자세한 내용은 System V AMD64 ABI를 참조하십시오.

    호출 규칙은 다음과 같이 작동합니다:

    사용자 수준 응용 프로그램은 %rdi, %rsi, %rdx, %rcx, %r8 및 %r9 시퀀스를 전달하기 위해 정수 레지스터로 사용합니다.
    호출자는 스택에서 다음 명령어의 주소(반환 주소)를 푸시하고 호출자의 첫 번째 명령어로 점프합니다. 단일 x86-64 명령어인 CALL은 이 두 가지를 모두 수행합니다.
    호출자가 실행됩니다.
    호출자가 반환값을 가지고 있으면 레지스터 RAX에 저장합니다.
    호출자는 스택에서 반환 주소를 꺼내고 x86-64 RET 명령어를 사용하여 지정한 위치로 점프하여 반환합니다.
    세 개의 int 인수를 받는 함수 f()를 생각해 봅시다. 이 다이어그램은 f()가 f(1, 2, 3)으로 호출된다고 가정할 때 위 3단계의 시작 부분에서 호출자가 볼 수 있는 샘플 스택 프레임과 레지스터 상태를 보여줍니다. 초기 스택 주소는 임의입니다:

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

     

    프로그램 시작 세부 정보

    사용자 프로그램용 핀토스 C 라이브러리는 lib/user/entry.c에 있는 _start()를 사용자 프로그램의 시작 지점으로 지정합니다. 이 함수는 main()을 감싸는 래퍼로, main()이 반환되면 exit()를 호출합니다:

    void
    _start (int argc, char *argv[]) {
        exit (main (argc, argv));
    }

    커널은 사용자 프로그램이 실행을 시작하기 전에 초기 함수에 대한 인수를 레지스터에 넣어야 합니다. 인자는 일반적인 호출 규칙과 동일한 방식으로 전달됩니다.

    다음 예제 명령의 인수를 처리하는 방법을 고려하십시오: /bin/ls -l foo bar.

    1, 명령을 단어로 나눕니다: /bin/ls, -l, foo, bar.

    2. 스택의 맨 위에 단어를 배치합니다. 포인터를 통해 참조되므로 순서는 중요하지 않습니다.

    3. 각 문자열의 주소와 널 포인터 센티널을 오른쪽에서 왼쪽 순서로 스택에 밀어 넣습니다. 이것이 argv의 요소입니다. 널 포인터 센티널은 C 표준에서 요구하는 대로 argv[argc]가 널 포인터인지 확인합니다. 이 순서는 argv[0]이 가장 낮은 가상 주소에 있도록 보장합니다. 단어 정렬 액세스는 정렬되지 않은 액세스보다 빠르므로 최상의 성능을 얻으려면 첫 번째 푸시 전에 스택 포인터를 8의 배수로 반올림하세요.

    4. %rsi를 argv(argv[0]의 주소)로 가리키고 %rdi를 argc로 설정합니다.

    5. 마지막으로 가짜 "반환 주소"를 푸시합니다. 입력 함수는 절대 반환하지 않지만 스택 프레임은 다른 함수와 동일한 구조를 가져야 합니다.

    아래 표는 사용자 프로그램이 시작되기 직전의 스택 상태와 관련 레지스터를 보여줍니다. 스택이 아래로 내려가는 것을 확인할 수 있습니다.

    이 예제에서 스택 포인터는 0x4747ffb8로 초기화됩니다. 위와 같이 코드가 include/threads/vaddr.h에 정의된 USER_STACK에서 스택을 시작해야 합니다.

    <stdio.h>에 선언된 비표준 hex_dump() 함수는 인자 전달 코드를 디버깅하는 데 유용할 수 있습니다.

     

    인수 전달을 구현합니다.
    현재 process_exec()은 새 프로세스에 인자 전달을 지원하지 않습니다. 이 기능을 구현하려면 process_exec()을 확장하여 프로그램 파일 이름을 단순히 인자로 받는 대신 공백으로 단어로 나누도록 하세요. 첫 번째 단어는 프로그램 이름, 두 번째 단어는 첫 번째 인수가 되는 식으로 말이죠. 즉, process_exec("grep foo bar")는 두 개의 인자 foo와 bar를 전달하는 grep을 실행해야 합니다.

    명령줄 내에서 여러 개의 공백은 하나의 공백에 해당하므로 process_exec("grep foo bar")는 원래 예제와 동일합니다. 명령줄 인수의 길이에 합리적인 제한을 둘 수 있습니다. 예를 들어 인수를 한 페이지(4KB)에 들어갈 수 있는 길이로 제한할 수 있습니다. (핀토스 유틸리티가 커널에 전달할 수 있는 명령줄 인수는 128바이트로 제한되어 있습니다).

    인자 문자열은 원하는 방식으로 구문 분석할 수 있습니다. 길을 잃었다면 include/lib/string.h에 프로토타입이 있고 lib/string.c에 자세한 주석과 함께 구현된 strtok_r()을 참조하세요. 자세한 내용은 man 페이지(프롬프트에서 man strtok_r 실행)에서 확인할 수 있습니다.

     


    User Memory

    사용자 메모리 액세스 구현

    시스템 호출을 구현하려면 사용자 가상 주소 공간에서 데이터를 읽고 쓸 수 있는 방법을 제공해야 합니다. 인수를 받을 때는 이 기능이 필요하지 않습니다. 그러나 시스템 호출의 인수로 제공된 포인터에서 데이터를 읽을 때는 이 기능을 통해 프록시해야 합니다. 사용자가 잘못된 포인터를 제공하거나 커널 메모리에 대한 포인터를 제공하거나 해당 영역 중 하나에 부분적으로 블록을 제공하면 어떻게 될까요? 이러한 경우 사용자 프로세스를 종료하여 처리해야 합니다.

     


     

    System Calls

    시스템 호출
    시스템 호출 인프라를 구현합니다.

    userprog/syscall.c에서 시스템 호출 핸들러를 구현합니다. 우리가 제공하는 스켈레톤 구현은 프로세스를 종료하여 시스템 호출을 "처리"합니다. 시스템 호출 번호를 검색한 다음 시스템 호출 인수를 검색하고 적절한 작업을 수행해야 합니다.

     

    시스템 호출 세부 정보

    첫 번째 프로젝트에서는 이미 운영 체제가 사용자 프로그램으로부터 제어권을 되찾을 수 있는 한 가지 방법, 즉 타이머와 I/O 디바이스의 인터럽트를 다루었습니다. 이러한 인터럽트는 CPU 외부의 엔티티에 의해 발생하기 때문에 "외부" 인터럽트입니다.

    운영 체제는 프로그램 코드에서 발생하는 이벤트인 소프트웨어 예외도 처리합니다. 예외는 페이지 오류나 0으로 나눗셈과 같은 오류일 수 있습니다. 예외는 사용자 프로그램이 운영 체제에 서비스("시스템 호출")를 요청할 수 있는 수단이기도 합니다.

    기존 x86 아키텍처에서는 시스템 호출이 다른 소프트웨어 예외와 동일하게 처리되었습니다. 그러나 x86-64에서는 제조업체가 시스템 호출을 위한 특수 명령어인 syscall을 도입했습니다. 이를 통해 시스템 호출 핸들러를 빠르게 호출할 수 있습니다.

    오늘날 syscall 명령은 x86-64에서 시스템 호출을 호출하는 데 가장 일반적으로 사용되는 수단입니다. 핀토에서 사용자 프로그램은 시스템 호출을 위해 syscall을 호출합니다. 시스템 호출 번호와 추가 인수는 두 가지를 제외하고는 시스템 호출 명령을 호출하기 전에 일반적인 방식으로 레지스터에 설정되어야 합니다:

    - %rax is the system call number.

    - The fourth arguments is %r10, not %rcx.

     

    따라서 시스템 호출 핸들러 syscall_handler()가 제어권을 얻으면 시스템 호출 번호는 rax에 있고 인수는 %rdi, %rsi, %rdx, %r10, %r8, %r9의 순서로 전달됩니다.

    호출자의 레지스터는 전달된 struct intr_frame에 액세스할 수 있습니다. (struct intr_frame은 커널 스택에 있습니다.)

    함수 반환 값에 대한 x86-64 규칙은 RAX 레지스터에 값을 배치하는 것입니다. 값을 반환하는 시스템 호출은 struct intr_frame의 rax 멤버를 수정하여 값을 반환할 수 있습니다.

     

    다음 시스템 호출을 구현합니다.

    나열된 프로토타입은 include/lib/user/syscall.h를 포함하는 사용자 프로그램에서 볼 수 있는 프로토타입입니다(이 헤더와 include/lib/user의 다른 모든 헤더는 사용자 프로그램에서만 사용할 수 있습니다). 각 시스템 호출에 대한 시스템 호출 번호는 include/lib/syscall-nr.h에 정의되어 있습니다:


    void halt (void);

    power_off()를 호출하여 pintos를 종료합니다(src/include/threads/init.h에 선언됨). 이 함수는 교착 상태 상황 등에 대한 일부 정보를 잃게 되므로 거의 사용하지 않는 것이 좋습니다.


    void exit (int status);

    현재 사용자 프로그램을 종료하여 커널에 status를 반환합니다. 프로세스의 부모가 wait(기다리는 경우)(아래 참조) 반환되는 상태가 이 상태입니다. 일반적으로 status 0은 성공을 나타내고 0이 아닌 값은 오류를 나타냅니다.


    pid_t fork (const char *thread_name);

    현재 프로세스의 복제본인 새 프로세스를 THREAD_NAME이라는 이름으로 생성합니다. 호출 저장 레지스터인 %RBX, %RSP, %RBP, %R12 - %R15를 제외한 레지스터의 값은 복제할 필요가 없습니다. 자식 프로세스의 pid를 반환해야 하며, 그렇지 않으면 유효한 pid가 아닐 수 있습니다. 자식 프로세스에서 반환 값은 0이어야 합니다. 자식 프로세스에는 파일 기술자 및 가상 메모리 공간을 포함한 중복된 리소스가 있어야 합니다. 부모 프로세스는 자식 프로세스가 성공적으로 복제되었는지 여부를 알기 전까지는 포크에서 반환해서는 안 됩니다. 즉, 자식 프로세스가 리소스를 복제하는 데 실패하면 부모의 포크 () 호출은 TID_ERROR를 반환해야 합니다.


    템플릿은 threads/mmu.c의 pml4_for_each()를 사용하여 해당 페이지 테이블 구조를 포함한 전체 사용자 메모리 공간을 복사하지만, 전달된 pte_for_each_func의 누락된 부분을 채워야 합니다(가상 주소 참조).


    int exec (const char *cmd_line);

    주어진 인수를 전달하여 현재 프로세스를 cmd_line에 지정된 이름의 실행 파일로 변경합니다. 성공하면 절대 반환되지 않습니다. 그렇지 않으면 어떤 이유로든 프로그램을 로드하거나 실행할 수 없는 경우 프로세스가 종료 상태 -1로 종료됩니다. 이 함수는 실행을 호출한 스레드(exec)의 이름을 변경하지 않습니다. 파일 기술자는 실행 호출(exec) 내내 열려 있는 상태로 유지된다는 점에 유의하세요.


    int wait (pid_t pid);

    자식 프로세스 pid를 기다렸다가 자식의 종료 상태를 검색합니다. pid가 아직 살아있다면 종료될 때까지 기다립니다. 그런 다음 pid가 종료하기 위해 전달한 상태를 반환합니다. pid가 exit()를 호출하지 않았지만 커널에 의해 종료된 경우(예: 예외로 인해 종료된 경우) wait(pid)는 -1을 반환해야 합니다. 부모 프로세스가 wait를 호출할 때 이미 종료된 자식 프로세스를 기다리는 것은 완전히 합법적이지만, 커널은 여전히 부모가 자식의 종료 상태를 검색하거나 자식이 커널에 의해 종료되었음을 알 수 있도록 허용해야 합니다.

    wait는 다음 조건 중 하나라도 참이면 실패하고 즉시 -1을 반환해야 합니다:

    - pid는 호출 프로세스의 직접 자식을 참조하지 않습니다. 호출 프로세스가 포크에 대한 성공적인 호출에서 반환 값으로 pid를 받은 경우에만 호출 프로세스의 직접 자식이 됩니다. 자식은 상속되지 않습니다. A가 자식 B를 생성하고 B가 자식 프로세스 C를 생성하는 경우, B가 죽었더라도 A는 C를 기다릴 수 없습니다. 프로세스 A의 wait(C) 호출은 실패해야 합니다. 마찬가지로 고아가 된 프로세스는 부모 프로세스가 먼저 종료되면 새 부모에 할당되지 않습니다.

    - 기다림을 호출하는 프로세스는 이미 pid에서 기다림을 호출했습니다. 즉, 프로세스는 최대 한 번만 특정 자식을 기다릴 수 있습니다.

     

     

    프로세스는 자식을 얼마든지 생성할 수 있고, 어떤 순서로든 자식을 기다릴 수 있으며, 심지어 일부 또는 모든 자식을 기다리지 않고 종료할 수도 있습니다. 설계는 대기가 발생할 수 있는 모든 방법을 고려해야 합니다. 구조체 스레드를 포함한 프로세스의 모든 리소스는 부모가 기다리든 기다리지 않든, 자식이 부모보다 먼저 종료하든 나중에 종료하든 관계없이 해제되어야 합니다.

    초기 프로세스가 종료될 때까지 Pintos가 종료되지 않도록 해야 합니다.

    제공된 핀토스 코드는 main()(threads/init.c)에서 process_wait()(userprog/process.c)를 호출하여 이 작업을 시도합니다. 함수 상단의 주석에 따라 process_wait()를 구현한 다음 process_wait()의 관점에서 대기 시스템 호출을 구현하는 것이 좋습니다.

    이 시스템 호출을 구현하려면 다른 어떤 것보다 훨씬 더 많은 작업이 필요합니다.


    bool create (const char *file, unsigned initial_size);

    초기 initial_size 바이트 크기의 file이라는 새 파일을 생성합니다. 성공하면 참(True)을 반환하고, 그렇지 않으면 거짓(False)을 반환합니다. 새 파일을 생성한다고 해서 파일이 열리지는 않습니다. 새 파일을 열려면(open) 시스템 호출이 필요한 별도의 작업입니다.


    bool remove (const char *file);

    file이라는 파일을 삭제합니다. 성공하면 참을 반환하고, 그렇지 않으면 거짓을 반환합니다. 파일은 열려 있는지 여부에 관계없이 제거할 수 있으며 열려 있는 파일을 제거해도 닫히지 않습니다. 자세한 내용은 자주 묻는 질문에서 열려 있는 파일 제거하기를 참조하세요.


    int open (const char *file);

    file이라는 파일을 엽니다. "파일 기술자"(fd)라는 음수가 아닌 정수 핸들을 반환하거나 파일을 열 수 없는 경우 -1을 반환합니다. 0과 1로 번호가 매겨진 파일 기술자는 콘솔용으로 예약되어 있습니다. fd 0(STDIN_FILENO)은 표준 입력, fd 1(STDOUT_FILENO)은 표준 출력입니다. 열린 시스템 호출은 이러한 파일 기술자 중 어느 것도 반환하지 않으며, 아래에 명시적으로 설명된 경우에만 시스템 호출 인수로 유효합니다. 각 프로세스에는 독립적인 파일 기술자 집합이 있습니다. 파일 기술자는 자식 프로세스에 의해 상속됩니다. 단일 프로세스에서든 다른 프로세스에서든 단일 파일을 두 번 이상 열면 열 때마다 새 파일 기술자가 반환됩니다. 단일 파일에 대한 서로 다른 파일 기술자는 별도의 닫기 호출을 통해 독립적으로 닫히며 파일 위치를 공유하지 않습니다. 추가 작업을 수행하려면 0부터 시작하는 정수를 반환하는 Linux 스키마를 따라야 합니다.


    int filesize (int fd);

    fd로 열린 파일의 크기(바이트)를 반환합니다.


    int read (int fd, void *buffer, unsigned size);

    fd로 열린 파일에서 버퍼로 크기 바이트를 읽습니다. 실제로 읽은 바이트 수(파일 끝에서 0), 파일을 읽을 수 없는 경우(파일 끝이 아닌 다른 조건으로 인해) -1을 반환합니다. fd 0은 input_getc()를 사용하여 키보드에서 읽습니다.


    int write (int fd, const void *buffer, unsigned size);

    버퍼에서 열린 파일 fd에 크기 바이트를 씁니다. 실제로 쓰여진 바이트 수를 반환하며, 일부 바이트가 쓰여지지 않은 경우 크기보다 작을 수 있습니다. 파일 끝을 지나서 쓰면 일반적으로 파일이 확장되지만 기본 파일 시스템에서는 파일 확장이 구현되지 않습니다. 예상되는 동작은 파일 끝 부분까지 가능한 한 많은 바이트를 쓰고 실제로 쓰여진 수를 반환하거나, 바이트가 전혀 쓰여지지 않은 경우 0을 반환하는 것입니다. 콘솔에 쓰는 코드는 적어도 크기가 수백 바이트보다 크지 않은 한 putbuf() 호출 한 번으로 버퍼를 모두 써야 합니다(큰 버퍼는 분할하는 것이 합리적입니다). 그렇지 않으면 서로 다른 프로세스에서 출력된 텍스트 줄이 콘솔에 끼어들어 사람이 읽는 사람과 채점 스크립트 모두를 혼동할 수 있습니다.


    void seek (int fd, unsigned position);

    열린 파일 fd에서 읽거나 쓸 다음 바이트를 파일 시작부터 바이트 단위로 표시되는 위치로 변경합니다(따라서 위치가 0이면 파일의 시작입니다). 파일의 현재 끝을 지나서 찾는 것은 오류가 아닙니다. 나중에 읽으면 파일 끝을 나타내는 0바이트를 얻습니다. 나중에 쓰기는 파일을 확장하여 기록되지 않은 간격을 0으로 채웁니다. (단, 핀토스에서는 프로젝트 4가 완료될 때까지 파일 길이가 고정되어 있으므로 파일 끝을 지나서 쓰면 오류가 반환됩니다.) 이러한 시맨틱은 파일 시스템에서 구현되며 시스템 호출 구현에 특별한 노력이 필요하지 않습니다.


    unsigned tell (int fd);

    열린 파일 fd에서 읽거나 쓸 다음 바이트의 위치를 파일 시작부터 바이트 단위로 반환합니다.


    void close (int fd);

    파일 기술자 fd를 닫습니다. 프로세스를 종료하거나 종료하면 마치 이 함수를 각각 호출하는 것처럼 열려 있는 모든 파일 기술자가 암시적으로 닫힙니다.


     

    이 파일은 다른 시스템 호출을 정의합니다. 지금은 무시하세요. 프로젝트 3에서 일부는 구현하고 나머지는 프로젝트 4에서 구현할 것이므로 확장성을 염두에 두고 시스템을 설계해야 합니다.

    여러 사용자 프로세스가 한 번에 호출할 수 있도록 시스템 호출을 동기화해야 합니다. 특히, 여러 스레드에서 한 번에 filesys 디렉터리에 제공된 파일 시스템 코드를 호출하는 것은 안전하지 않습니다. 시스템 호출 구현은 파일 시스템 코드를 중요한 섹션으로 취급해야 합니다. process_exec()도 파일에 액세스한다는 사실을 잊지 마세요. 현재로서는 filesys 디렉터리의 코드를 수정하지 않는 것이 좋습니다.

    사용자 프로세스가 C 프로그램에서 각 시스템 호출을 호출할 수 있는 방법을 제공하기 위해 lib/user/syscall.c에 각 시스템 호출에 대한 사용자 수준 함수를 제공했습니다. 각각은 약간의 인라인 어셈블리 코드를 사용하여 시스템 호출을 호출하고 (적절한 경우) 시스템 호출의 반환값을 반환합니다.

    이 부분이 끝나면, 그리고 앞으로도 핀토는 방탄이 될 것입니다. 사용자 프로그램이 할 수 있는 어떤 작업도 OS의 충돌, 패닉, 어설션 실패 또는 기타 오작동을 유발해서는 안 됩니다. 이 점을 강조하는 것이 중요합니다. 테스트는 여러 가지 방법으로 시스템 호출을 중단하려고 시도할 것입니다. 모든 코너 케이스를 생각하고 처리해야 합니다. 사용자 프로그램이 OS를 중단시킬 수 있는 유일한 방법은 중단 시스템 호출을 호출하는 것입니다.

    시스템 호출에 잘못된 인수가 전달된 경우 허용되는 옵션에는 오류 값 반환(값을 반환하는 호출의 경우), 정의되지 않은 값 반환 또는 프로세스 종료가 포함됩니다.

     


    Process Termination Messages

    프로세스 종료 메시지
    프로세스 종료 메시지 인쇄

    사용자 프로세스가 종료 호출 또는 기타 이유로 인해 종료될 때마다 프로세스의 이름과 종료 코드를 다음과 같은 형식으로 인쇄합니다.

    printf ("%s: exit(%d)\n", ...);

    인쇄되는 이름은 fork()에 전달된 전체 이름이어야 합니다. 사용자 프로세스가 아닌 커널 스레드가 종료되거나 중단 시스템 호출이 호출될 때는 이러한 메시지를 인쇄하지 마세요. 프로세스를 로드하지 못했을 때 이 메시지는 선택 사항입니다.

    이 메시지 외에 제공된 핀토가 이미 인쇄하지 않는 다른 메시지는 인쇄(print)하지 마세요. 디버깅 중에 추가 메시지가 유용할 수 있지만 채점 스크립트를 혼동하여 점수가 낮아질 수 있습니다.

     


    Denying Writes to Executables

    실행 파일에 쓰기 거부
    실행 파일에 대한 쓰기를 거부합니다.

     

    실행 파일로 사용 중인 파일에 대한 쓰기를 거부하는 코드를 추가하세요. 프로세스가 디스크에서 변경 중인 코드를 실행하려고 할 경우 예측할 수 없는 결과가 발생할 수 있기 때문에 많은 OS에서 이 기능을 사용합니다. 이는 프로젝트 3에서 가상 메모리를 구현한 후에 특히 중요하지만, 지금이라도 이 기능을 추가하는 것이 좋습니다.

    열린 파일에 대한 쓰기를 방지하기 위해 file_deny_write()를 사용할 수 있습니다. 파일에 대해 file_allow_write()를 호출하면 다시 사용할 수 있습니다(다른 오프너에서 파일 쓰기를 거부하지 않는 한). 파일을 닫으면 쓰기 기능도 다시 활성화됩니다. 따라서 프로세스의 실행 파일에 대한 쓰기를 거부하려면 프로세스가 계속 실행되는 동안 파일을 열어 두어야 합니다.

Designed by Tistory.