project2 user program 번역

site

User Programs

이제 Pintos와 협력하고 인프라 및 스레드 패키지에 익숙해지셨으므로 이제 user program을 실행할 수 있는 시스템 부분에 대한 작업을 시작해야 합니다. 기본 코드는 이미 user program 로드 및 실행을 지원하지만 I/O 또는 상호 작용은 불가능합니다. 이 프로젝트에서는 프로그램이 시스템 호출을 통해 OS와 상호 작용할 수 있도록 합니다.

이 할당에 대한 사용자 prog 디렉토리에서 작업할 것이지만, Pintos의 거의 모든 다른 부분과도 상호 작용할 것입니다. 우리는 아래에 관련 부분에 대해 설명하겠습니다.

프로젝트 1 제출서 위에 프로젝트 2를 작성하거나 새로 시작할 수 있습니다. 이 할당에는 프로젝트 1의 코드가 필요하지 않습니다. “경보 시계” 기능은 프로젝트 3과 4에서 유용할 수 있지만 엄격히 요구되지는 않습니다.

다시 돌아가서 테스트 실행 방법을 다시 읽는 것이 유용할 수 있습니다(섹션 1.2.1 테스트 참조).

1 background

지금까지 Pintos에서 실행한 모든 코드는 운영 체제 커널의 일부입니다. 예를 들어, 마지막 할당의 모든 테스트 코드가 커널의 일부로 실행되어 시스템의 권한 있는 부분에 대한 전체 액세스 권한을 갖습니다. 운영 체제 위에서 user program을 실행하기 시작하면 이는 더 이상 사실이 아닙니다. 이 프로젝트는 결과를 다룬다.

한 번에 두 개 이상의 프로세스를 실행할 수 있습니다. 각 프로세스에는 하나의 스레드가 있습니다(다중 스레드 프로세스는 지원되지 않음). user program들은 그들이 전체 기계를 가지고 있다고 착각하고 쓰여진다. 즉, 한 번에 여러 프로세스를 로드하고 실행할 때 이러한 착각을 유지하기 위해 메모리, 스케줄링 및 기타 상태를 올바르게 관리해야 합니다.

이전 프로젝트에서는 테스트 코드를 커널에 직접 컴파일했기 때문에 커널 내에서 특정 함수 인터페이스를 요구해야 했다. 지금부터 user program을 실행하여 운영 체제를 테스트하겠습니다. 이것은 당신에게 훨씬 더 큰 자유를 줍니다. user program 인터페이스가 여기에 설명된 사양을 충족하는지 확인해야 하지만 이러한 제약 조건을 고려한다면 원하는 대로 커널 코드를 재구성하거나 다시 작성할 수 있습니다.

1.1 Source Files

프로그래밍에 대한 개요를 얻는 가장 쉬운 방법은 작업할 각 부분을 간단히 검토하는 것입니다. user program에서 적은 수의 파일을 찾을 수 있지만 대부분의 작업은 다음과 같습니다.

process.c process.h ELF 바이너리를 로드하고 프로세스를 시작합니다.

pagedir.c pagedir.h 80x86 하드웨어 페이지 테이블을 위한 간단한 관리자입니다. 이 프로젝트에 대해 이 코드를 수정하고 싶지는 않겠지만 일부 기능을 호출하는 것이 좋습니다. 자세한 내용은 섹션 4.1.2.3 페이지 표를 참조하십시오.

syscall.c syscall.h 사용자 프로세스가 커널 기능에 액세스하려고 할 때마다 시스템 호출을 호출합니다. 이것은 스켈레톤 시스템 호출 핸들러입니다. 현재는 메시지를 인쇄하고 사용자 프로세스를 종료하기만 합니다. 이 프로젝트의 파트 2에서는 시스템 호출에 필요한 다른 모든 작업을 수행할 코드를 추가합니다.

exception.c exception.h 사용자 프로세스가 권한이 있거나 금지된 작업을 수행할 때, 커널에 “예외” 또는 “fault.”(3) 이러한 파일은 예외를 처리합니다. 현재 모든 예외는 메시지를 인쇄하고 프로세스를 종료하기만 하면 됩니다. 프로젝트 2의 일부 솔루션에는 이 파일의 page_fault()를 수정해야 합니다.

gdt.c gdt.h 80x86은 분할된 아키텍처이다. GDT(Global Descriptor Table)는 사용 중인 세그먼트를 설명하는 테이블입니다. 이 파일은 GDT를 설정합니다. 프로젝트에 대해 이 파일을 수정할 필요가 없습니다. GDT의 작동 방식에 관심이 있으면 코드를 읽을 수 있습니다.

tss.c tss.h TSS(Task-State Segment)는 80x86 아키텍처 작업 상태 세그먼트(Task-State Segment) 핀토스는 리눅스처럼 사용자 프로세스가 인터럽트 핸들러에 들어갈 때 스택을 전환하는 데에만 TSS를 사용한다. 프로젝트에 대해 이러한 파일을 수정할 필요가 없습니다. TSS의 작동 방식에 관심이 있으면 코드를 읽을 수 있습니다.

1.2 Using the File System

user program이 파일 시스템에서 로드되고 많은 시스템 호출이 파일 시스템을 처리해야 하므로 이 프로젝트의 파일 시스템 코드와 인터페이스해야 합니다. 그러나, 이 프로젝트의 초점은 파일 시스템이 아니라서, 우리는 filesys 디렉토리에 간단하지만 완전한 파일 시스템을 제공했습니다. filesys.h and file.h 인터페이스를 검토하여 파일 시스템 사용 방법 및 특히 많은 제한 사항을 이해하고자 합니다.

이 프로젝트에는 파일 시스템 코드를 수정할 필요가 없으므로 수정하지 않는 것이 좋습니다. 파일 시스템에서 작업하는 것은 이 프로젝트의 초점을 흐트러뜨릴 가능성이 높습니다.

이제 파일 시스템 루틴을 올바르게 사용하면 파일 시스템 구현을 개선할 수 있는 프로젝트 4의 수명이 훨씬 더 쉬워집니다. 그 전까지는 다음과 같은 제한을 허용해야 합니다.

  • 내부 동기화가 없습니다. 동시 액세스는 서로 간섭합니다. 동기화를 사용하여 한 번에 하나의 프로세스만 파일 시스템 코드를 실행해야 합니다.
  • 파일 크기는 생성 시 고정됩니다. 루트 디렉터리는 파일로 표시되므로 만들 수 있는 파일 수도 제한됩니다.
  • 파일 데이터는 단일 익스텐트로 할당됩니다. 즉, 단일 파일의 데이터는 디스크의 섹터 범위를 연속적으로 차지해야 합니다. 따라서 시간이 지남에 따라 파일 시스템이 사용됨에 따라 외부 조각화가 심각한 문제가 될 수 있습니다.
  • 하위 디렉터리가 없습니다.
  • 파일 이름은 14자로 제한됩니다.
  • 시스템 충돌 중간 작업은 자동으로 복구할 수 없는 방식으로 디스크를 손상시킬 수 있습니다. 어쨌든 파일 시스템 복구 도구가 없습니다.

한 가지 중요한 기능이 있습니다.

  • filesys_remove()에 대한 유닉스 계열 의미론이 구현된다. 즉, 파일을 제거할 때 파일이 열려 있으면 해당 블록의 할당이 취소되지 않으며 마지막으로 파일을 닫을 때까지 열린 스레드에서 액세스할 수 있습니다. 자세한 내용은 열린 파일 제거를 참조하십시오.

파일 시스템 파티션으로 모의 실행 디스크를 만들 수 있어야 합니다. pintos-mkdisk 프로그램은 이 기능을 제공한다. 사용자 prog/build 디렉토리에서 pintos-mkdisk filesys.dsk --filesys-size=2를 실행합니다. 이 명령은 2MB Pintos 파일 시스템 파티션을 포함하는 filesys.dsk라는 모의 실행 디스크를 생성합니다. 그런 다음 커널의 명령줄에서 -f -q를 전달하여 파일 시스템 파티션을 포맷합니다: pintos -f -q. -f 옵션을 사용하면 파일 시스템이 포맷되고, -q 옵션을 사용하면 포맷이 완료되는 즉시 Pintos가 종료됩니다.

시뮬레이션된 파일 시스템 안팎으로 파일을 복사하는 방법이 필요합니다. pintos -p(“put”) 및 -g (“get”) 옵션이 이를 수행합니다. 파일을 Pintos 파일 시스템에 복사하려면 pintos -p 파일 – -q 명령을 사용합니다. -p는 시뮬레이션된 커널이 아니라 pintos 스크립트용이기 때문에 –가 필요합니다. Pintos 파일 시스템에 새 이름으로 복사하려면 -new name: pintos -p file -new name – -q를 추가합니다. VM에서 파일을 복사하는 명령은 비슷하지만 -g for -p를 대체합니다.

부수적으로, 이러한 명령들은 커널의 명령행에 특수 명령 추출과 추가를 전달하고 특수 시뮬레이션된 “스크래치” 파티션으로 복사하는 방식으로 동작한다. 매우 궁금한 경우 pintos 스크립트와 filesys/fsutil.c를 살펴보고 구현 세부 정보를 확인할 수 있습니다.

다음은 파일 시스템 파티션으로 디스크를 만들고, 파일 시스템을 포맷하고, 에코 프로그램을 새 디스크에 복사한 다음, 에코를 실행하여 인수 x를 전달하는 방법에 대한 요약입니다. (인수 전달은 구현하기 전에는 작동하지 않습니다.) 예제에서 이미 예를 작성했으며 현재 디렉터리는 사용자 prog/build라고 가정합니다.

1
2
3
4
pintos-mkdisk filesys.dsk --filesys-size=2
pintos -f -q
pintos -p ../../examples/echo -a echo -- -q
pintos -q run 'echo x'

세 가지 최종 단계를 실제로 단일 명령으로 결합할 수 있습니다.

1
2
pintos-mkdisk filesys.dsk --filesys-size=2
pintos -p ../../examples/echo -a echo -- -f -q run 'echo x'

나중에 사용하거나 검사할 수 있도록 파일 시스템 디스크를 보관하지 않으려면 네 단계를 모두 단일 명령으로 결합할 수도 있습니다. –filesys-size=n 옵션은 pintos 실행 기간 동안만 약 nMB 크기의 임시 파일 시스템 파티션을 생성합니다. Pintos 자동 테스트 제품군은 다음과 같은 구문을 광범위하게 사용합니다.

pintos --filesys-size=2 -p ../../examples/echo -a echo -- -f -q run 'echo x'

rm 파일 커널 동작(예: pintos -q rm file)을 사용하여 Pintos 파일 시스템에서 파일을 삭제할 수 있습니다. 또한 ls는 파일 시스템의 파일을 나열하고 cat 파일은 파일의 내용을 디스플레이에 인쇄합니다.

1.3 How User Programs Work

Pintos는 메모리에 들어맞고 당신이 구현한 시스템 호출만을 사용하는 한 정상적인 C 프로그램을 실행할 수 있다. 특히 이 프로젝트에 필요한 시스템 호출 중 메모리 할당을 허용하는 호출이 없기 때문에 malloc()를 구현할 수 없습니다. 또한 커널이 스레드를 전환할 때 프로세서의 부동 소수점 단위를 저장하고 복원하지 않기 때문에 Pintos는 부동 소수점 연산을 사용하는 프로그램을 실행할 수 없습니다.

src/examples 디렉토리에는 몇 가지 샘플 user program이 포함되어 있습니다. 이 디렉토리의 Makefile은 제공된 예를 컴파일하며, 사용자가 직접 프로그램을 컴파일할 수도 있습니다. 일부 예제 프로그램은 프로젝트 3 또는 4가 구현된 후에만 작동합니다.

Pintos는 userprog/process.c에 제공된 로더로 ELF 실행 파일을 로드할 수 있습니다. ELF는 리눅스, 솔라리스 및 기타 많은 운영 체제에서 객체 파일, 공유 라이브러리 및 실행 파일에 사용되는 파일 형식이다. 실제로 80x86 ELF 실행 파일을 출력하는 컴파일러와 링커를 사용하여 핀토스를 위한 프로그램을 만들 수 있다. (우리는 제대로 작동할 수 있는 컴파일러와 링크 장치를 제공했습니다.)

테스트 프로그램을 시뮬레이션된 파일 시스템에 복사할 때까지 Pintos는 유용한 작업을 수행할 수 없다는 것을 즉시 깨달아야 합니다. 다양한 프로그램을 파일 시스템에 복사하기 전에는 흥미로운 작업을 수행할 수 없습니다. 새 참조 파일 시스템 디스크를 생성하여 유용한 상태 이상으로 filesys.dsk를 폐기할 때마다 이 디스크를 복사하려고 할 수 있습니다. 이러한 작업은 디버깅 중에 가끔 발생할 수 있습니다.

1.4 Virtual Memory Layout

핀토스의 가상 메모리는 사용자 가상 메모리와 커널 가상 메모리로 나뉜다. 사용자 가상 메모리의 범위는 가상 주소 0에서 PYS_BASE까지이며, threads/vaddr.h에 정의되어 있으며 기본값은 0xc0000000(3GB)입니다. 커널 가상 메모리는 최대 4GB의 PYS_BASE에서 나머지 가상 주소 공간을 차지합니다.

사용자 가상 메모리는 프로세스당입니다. 커널이 한 프로세스에서 다른 프로세스로 전환될 때 프로세서의 페이지 디렉토리 기본 레지스터를 변경하여 사용자 가상 주소 공간도 전환합니다(userprog/pageir.c의 pagedir_activate() 참조). 구조 스레드는 프로세스의 페이지 테이블에 대한 포인터를 포함합니다.

커널 가상 메모리가 글로벌합니다. 실행 중인 사용자 프로세스 또는 커널 스레드에 관계없이 항상 동일한 방식으로 매핑됩니다. 핀토스(Pintos)에서 커널 가상 메모리는 PIRSE_BASE에서 시작하여 물리적 메모리로 일대일로 매핑된다. 즉, 가상 주소 PHYS_BASE는 물리적 주소 0에 액세스하고, 가상 주소 PYSI_BASE + 0x1234는 물리적 주소 0x1234에 액세스하는 등 시스템의 물리적 메모리 크기까지 액세스합니다.

user program은 자체 사용자 가상 메모리에만 액세스할 수 있습니다. 커널 가상 메모리에 액세스하려고 하면 페이지 장애가 발생하고 사용자 prog/exception.c의 page_fault()에서 처리되며 프로세스가 종료됩니다. 커널 스레드는 커널 가상 메모리와 사용자 프로세스가 실행 중인 경우 실행 중인 프로세스의 사용자 가상 메모리에 모두 액세스할 수 있습니다. 그러나 커널에서도 매핑되지 않은 사용자 가상 주소에서 메모리에 액세스하려고 하면 페이지 장애가 발생합니다.

1.4.1Typical Memory Layout

개념적으로 각 프로세스는 원하는 대로 사용자 가상 메모리를 자유롭게 배치할 수 있습니다. 실제로 사용자 가상 메모리는 다음과 같이 배치됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
   PHYS_BASE +----------------------------------+
             |            user stack            |
             |                 |                |
             |                 |                |
             |                 V                |
             |          grows downward          |
             |                                  |
             |                                  |
             |                                  |
             |                                  |
             |           grows upward           |
             |                 ^                |
             |                 |                |
             |                 |                |
             +----------------------------------+
             | uninitialized data segment (BSS) |
             +----------------------------------+
             |     initialized data segment     |
             +----------------------------------+
             |           code segment           |
  0x08048000 +----------------------------------+
             |                                  |
             |                                  |
             |                                  |
             |                                  |
             |                                  |
           0 +----------------------------------+

이 프로젝트에서는 사용자 스택의 크기가 고정되어 있지만 프로젝트 3에서는 크기가 커질 수 있습니다. 기존에는 초기화되지 않은 데이터 세그먼트의 크기를 시스템 호출로 조정할 수 있었지만, 이를 구현할 필요는 없습니다.

Pintos의 코드 세그먼트는 주소 공간 하단에서 약 128MB의 사용자 가상 주소 0x08084000에서 시작합니다. 이 값은 [SysV-i386]에 지정되었으며 깊은 의미는 없습니다.

링크는 다양한 프로그램 세그먼트의 이름과 위치를 알려주는 “링커 스크립트”에 의해 지시된 대로 메모리에 있는 user program의 레이아웃을 설정합니다. 링커 스크립트에 대한 자세한 내용은 정보를 통해 액세스할 수 있는 링커 매뉴얼의 “스크립트” 장을 참조하십시오.

특정 실행 파일의 레이아웃을 보려면 -p 옵션을 사용하여 objdump(80x86) 또는 i386-elf-objdump(SPARC)를 실행하십시오.

1.5 Accessing User Memory

시스템 호출의 일부로서 커널은 종종 user program에 의해 제공되는 포인터를 통해 메모리에 액세스해야 한다. 사용자가 null 포인터, 매핑되지 않은 가상 메모리에 대한 포인터 또는 커널 가상 주소 공간(PHY_BASE 위)에 대한 포인터를 전달할 수 있기 때문에 커널은 매우 주의해야 합니다. 이러한 모든 유형의 잘못된 포인터는 위반 프로세스를 종료하고 리소스를 해제하여 커널이나 실행 중인 다른 프로세스에 해를 주지 않고 거부되어야 합니다.

이를 올바르게 수행하기 위한 합리적인 방법에는 적어도 두 가지가 있습니다. 첫 번째 방법은 사용자가 제공한 포인터의 유효성을 확인한 다음 참조를 취소하는 것입니다. 이 경로를 선택하면 userprog/pageir.c 및 threads/vaddr.h의 기능을 볼 수 있습니다. 이것은 사용자 메모리 액세스를 처리하는 가장 간단한 방법입니다.

두 번째 방법은 사용자 포인터가 PHYS_BASE 아래만 가리키는지 확인한 다음 참조를 취소하는 것입니다. 사용자 포인터가 잘못되면 사용자 prog/exception.c에서 page_fault()에 대한 코드를 수정하여 처리할 수 있는 “page default”가 발생합니다. 이 기술은 일반적으로 프로세서의 MMU를 이용하기 때문에 더 빠르기 때문에 실제 커널(리눅스 포함)에서 사용되는 경향이 있다.

두 경우 모두 리소스를 “유출”하지 않도록 해야 합니다. 예를 들어 시스템 호출이 malloc()로 잠금을 획득했거나 메모리를 할당했다고 가정합니다. 나중에 잘못된 사용자 포인터가 발견되더라도 잠금을 해제하거나 메모리 페이지를 해제해야 합니다. 사용자 포인터의 참조를 취소하기 전에 사용자 포인터를 확인하도록 선택하는 경우 이 방법은 간단해야 합니다. 메모리 액세스에서 오류 코드를 반환할 방법이 없기 때문에 잘못된 포인터가 페이지 장애를 일으킬 경우 처리하기가 더 어렵습니다. 따라서, 후자의 기술을 사용해 보고자 하는 분들을 위해, 우리는 약간의 유용한 코드를 제공할 것입니다.

/* Reads a byte at user virtual address UADDR.
   UADDR must be below PHYS_BASE.
   Returns the byte value if successful, -1 if a segfault
   occurred. */
static int
get_user (const uint8_t *uaddr)
{
  int result;
  asm ("movl $1f, %0; movzbl %1, %0; 1:"
       : "=&a" (result) : "m" (*uaddr));
  return result;
}
 
/* Writes BYTE to user address UDST.
   UDST must be below PHYS_BASE.
   Returns true if successful, false if a segfault occurred. */
static bool
put_user (uint8_t *udst, uint8_t byte)
{
  int error_code;
  asm ("movl $1f, %0; movb %b2, %1; 1:"
       : "=&a" (error_code), "=m" (*udst) : "q" (byte));
  return error_code != -1;
}

각 기능은 사용자 주소가 이미 PHYS_BASE보다 낮은 것으로 확인되었다고 가정합니다. 또한 커널의 페이지 폴트가 0xffffffffff로 설정되고 이전 값이 eip로 복사되도록 page_fault()를 수정한 것으로 가정합니다.

2. Suggested Order of Implementation

먼저 병렬로 발생할 수 있는 다음을 구현할 것을 제안합니다.

  • 인수 전달(3.3.3 인수 전달 참조). 모든 user program은 인수 전달이 구현될 때까지 page fault를 즉시 호출합니다.

    지금은 단순히 변화를 원할 수도 있다.

    *esp = PHYS_BASE;

    에서

    *esp = PHYS_BASE - 12;

    setup_stack()에서 이름은 (null)로 인쇄되지만 인수를 검사하지 않는 테스트 프로그램에는 사용할 수 있습니다. 인수 전달을 구현할 때까지 명령줄 인수를 전달하지 않고 프로그램만 실행해야 합니다. 프로그램에 인수를 전달하려고 하면 프로그램 이름에 해당 인수가 포함되며, 이는 실패할 수 있습니다.

  • 사용자 메모리 액세스(section 3.1.5 사용자 메모리 액세스 참조) 모든 시스템 호출은 사용자 메모리를 읽어야 합니다. 사용자 메모리에 쓸 필요가 있는 시스템 호출이 적게 있습니다.
  • 시스템 호출 인프라(section 3.3.4 시스템 호출 참조). 사용자 스택에서 시스템 호출 번호를 읽고 이를 기반으로 처리기로 보낼 수 있을 만큼 충분한 코드를 구현합니다.
  • 시스템 종료 호출입니다. 일반적인 방식으로 끝나는 모든 user program은 종료됩니다. main() 호출에서 돌아오는 프로그램도 간접적으로 종료됩니다(lib/user/entry.c의 _start() 참조).
  • 쓰기 시스템에서 시스템 콘솔인 fd1 에 쓰기를 요청합니다. 당사의 모든 테스트 프로그램은 콘솔에 기록하므로(사용자 프로세스 버전의 printf()는 이러한 방식으로 구현됨) 쓰기가 가능할 때까지 모두 오작동합니다.
  • 지금은 process_wait()를 무한 루프(영구 대기)로 변경합니다. 제공된 구현이 즉시 반환되므로 프로세스가 실제로 실행되기 전에 Pintos의 전원이 꺼집니다. 결국 올바른 구현을 제공해야 합니다.
  • 위의 사항이 구현된 후에는 사용자 프로세스가 최소한으로 작동해야 합니다. 적어도 콘솔에 쓰고 올바르게 종료할 수 있습니다. 그런 다음 구현을 세분화하여 일부 테스트가 통과되기 시작할 수 있습니다.

3. Requirements

3.2 Process Termination Messages

사용자 프로세스가 종료될 때마다, 종료 또는 다른 이유로 인해 프로세스 이름 및 종료 코드를 printf(“%s: exit(%d)\n”, …“)로 인쇄하는 것처럼 포맷합니다. 인쇄된 이름은 명령줄 인수를 제외하고 process_execute()에 전달된 전체 이름이어야 합니다. 사용자 프로세스가 아닌 커널 스레드가 종료되거나 시스템 중지 호출이 호출될 때 이러한 메시지를 인쇄하지 마십시오. 프로세스가 로드되지 않을 경우 메시지는 선택 사항입니다.

이 외에도 Pintos가 이미 인쇄하지 않은 메시지는 인쇄하지 마십시오. 디버깅하는 동안 추가 메시지가 유용할 수 있지만, 정지 스크립트가 혼동되어 점수가 낮아집니다.

3.3 Argument Passing

현재 process_execute()는 새 프로세스에 인수를 전달하는 것을 지원하지 않습니다. process_execute()를 확장하여 이 기능을 구현하여 단순히 프로그램 파일 이름을 인수로 사용하는 대신 공백에서 단어로 구분한다. 첫 번째 단어는 프로그램 이름, 두 번째 단어는 첫 번째 인수 등입니다. 즉, process_execute(“grep foo bar”)는 두 개의 인수 foo와 bar를 통과하여 gref를 실행해야 한다.

명령줄 내에서 여러 공백은 단일 공백과 같으므로 process_execute(“grep foo bar”)는 원래 예제와 동일합니다. 명령줄 인수의 길이에 적절한 제한을 둘 수 있습니다. 예를 들어, 인수를 단일 페이지에 적합한 인수(4kB)로 제한할 수 있습니다. (pintos 유틸리티가 커널에 전달할 수 있는 최대 128바이트 명령줄 인수에 제한을 두지 마십시오.)

원하는 대로 인수 문자열을 구문 분석할 수 있습니다. 길을 잃은 경우, lib/string.h로 프로토타입화되고 lib/string.c에 철저한 설명으로 구현된 strtok_r()를 살펴보십시오. 자세한 내용은 man 페이지를 참조하십시오( 프롬프트에서 man strtok_r 실행해라).

스택을 설정해야 하는 정확한 방법은 섹션 3.5.1 프로그램 시작 세부 정보를 참조하십시오.

3.4 System Calls

userprog/syscall.c에서 system call handler를 구현합니다. 기본 구현은 프로세스를 종료함으로써 “handles” 시스템 호출을 제공한다. 시스템 호출 번호, 시스템 호출 인수를 검색하고 적절한 작업을 수행해야 합니다.

다음 시스템 호출을 구현합니다. 나열된 프로토타입은 lib/user/syscall.h를 포함하는 user program에서 볼 수 있는 프로토타입입니다(이 헤더와 lib/user의 다른 모든 프로토타입은 user program 전용입니다). 각 시스템 호출에 대한 시스템 호출 번호는 lib/syscall-nr.h에 정의되어 있습니다:

  • System Call: void halt (void)

    shutdown_power_off()(스레드/init.h로 선언됨)를 호출하여 Pintos를 종료합니다. 이 기능은 거의 사용되지 않아야 합니다. 교착 상태 등에 대한 일부 정보가 손실되기 때문입니다.

  • System Call: void exit (int status)

    현재 user program을 종료하고 커널에 상태를 반환합니다. 프로세스의 상위 항목이 해당 프로세스를 기다릴 경우(아래 참조), 이 상태는 반환됩니다. 일반적으로 상태가 0이면 성공이고 0이 아닌 값은 오류를 나타냅니다.

  • System Call: pid_t exec (const char *cmd_line)

    이름이 cmd_line에 지정된 실행 파일을 실행하여 지정된 인수를 전달하고 새 프로세스의 프로그램 ID(pid)를 반환합니다. 어떤 이유로든 프로그램을 로드하거나 실행할 수 없는 경우 유효한 피드가 아닌 pid-1을 반환해야 합니다. 따라서 하위 프로세스가 실행 파일을 성공적으로 로드했는지 여부를 알 때까지 상위 프로세스는 실행에서 돌아올 수 없습니다. 이 작업을 수행하려면 적절한 동기화를 사용해야 합니다.

  • System Call: int wait (pid_t pid)

    하위 프로세스 pid를 기다렸다가 하위의 종료 상태를 검색합니다.

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

    다음 조건 중 하나라도 참일 경우 wait가 실패하고 -1이 즉시 반환되어야 합니다.

    • pid는 호출 프로세스의 직접 하위 항목을 나타내지 않습니다. pid는 호출 프로세스가 성공적인 실행 호출에서 반환 값으로 pid를 수신한 경우에만 호출 프로세스의 직접 자식입니다.

      자녀는 유전되지 않는다는 점에 유의하라. A가 B를 낳고 B가 C를 낳으면 B가 죽더라도 A는 C를 기다릴 수 없다. 프로세스 A에 의한 대기 호출(C)은 실패해야 합니다. 마찬가지로, 고아 프로세스는 상위 프로세스가 종료되기 전에 종료되는 경우 새 상위 프로세스에는 할당되지 않습니다.

    • 대기 호출 프로세스가 이미 pid에서 대기 호출되었습니다. 즉, 프로세스는 주어진 아이를 최대 한 번에 기다릴 수 있습니다.

    Processes 어떤 수의 아이들을 낳고, 어떤 순서로든 그들을 기다리고, 심지어 그들의 아이들의 일부 또는 전부를 기다리지 않고 퇴장시킬 수도 있다. 설계가 대기할 수 있는 모든 방법을 고려해야 합니다. 구조 스레드를 포함한 프로세스의 모든 리소스는 부모가 대기하든 말든 간에, 그리고 하위 리소스가 부모 이전 또는 이후에 종료되든 상관없이 해제되어야 합니다.

    초기 프로세스가 종료될 때까지 Pintos가 종료되지 않도록 해야 합니다. 제공된 Pintos 코드는 main()에서 process_wait() (userprog/process.c) (threads/init.c)로 호출하여 이를 시도한다. 기능 상단의 코멘트에 따라 process_wait()를 구현한 다음 process_wait()의 관점에서 wait system call을 구현하는 것이 좋습니다.

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

  • System Call: bool create (const char *file, unsigned initial_size)

    파일 초기 initial_size 바이트 크기라는 새 파일을 만듭니다. 성공하면 true를 반환하고 그렇지 않으면 false를 반환합니다. 새 파일을 생성해도 파일이 열리지 않습니다. 새 파일을 여는 작업은 별도의 작업으로 시스템 호출을 열어야 합니다.

  • System Call: bool remove (const char *file)

    파일이라는 파일을 삭제합니다. 성공하면 true를 반환하고 그렇지 않으면 false를 반환합니다. 열린 파일인지 닫힌 파일인지 여부에 관계없이 파일을 제거할 수 있으며 열려 있는 파일을 제거해도 파일이 닫히지 않습니다. 자세한 내용은 열린 파일 제거를 참조하십시오.

  • System Call: int open (const char *file)

    파일이라는 파일을 엽니다. 파일을 열 수 없는 경우 “파일 지시자”(fd) 또는 -1이라는 음수가 아닌 정수 핸들을 반환합니다. 0과 1로 번호가 매겨진 파일 지시자는 콘솔용으로 예약되어 있습니다. fd 0(STDIN_FILENO)은 표준 입력, fd 1(STDOUT_FILENO)은 표준 출력입니다. 열려 있는 시스템 호출은 아래 설명된 대로만 시스템 호출 인수로 유효한 이러한 파일 지시자 중 하나를 반환하지 않습니다.

    각 프로세스에는 독립적인 파일 지시자 집합이 있습니다. 파일 지시자는 하위 프로세스에서 상속되지 않습니다.

    단일 프로세스 또는 다른 프로세스에 따라 두 번 이상 파일을 열면 각 파일이 열려 새 파일 지시자가 반환됩니다. 단일 파일에 대한 서로 다른 파일 지시자는 닫아야 하는 개별 호출에서 독립적으로 닫히고 파일 위치를 공유하지 않습니다.

  • System Call: int filesize (int fd)

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

  • System Call: int read (int fd, void *buffer, unsigned size)

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

  • System Call: int write (int fd, const void *buffer, unsigned size)

    버퍼에서 열린 파일 fd에 크기 바이트를 씁니다. 실제로 쓴 바이트 수를 반환합니다. 일부 바이트를 쓸 수 없는 경우 크기보다 작을 수 있습니다. 파일 끝을 지나 쓰면 일반적으로 파일이 확장되지만 기본 파일 시스템에 의해 파일 확장자가 구현되지 않습니다. 예상되는 동작은 파일의 끝 부분까지 가능한 한 많은 바이트를 쓰고 실제 기록된 숫자를 반환하는 것이며, 바이트를 전혀 쓸 수 없는 경우 0을 반환하는 것이다.

    Fd 1은 콘솔에 씁니다. 적어도 크기가 수백 바이트를 초과하지 않는 한, 콘솔에 쓸 코드는 하나의 putbuff() 호출에 모든 버퍼를 기록해야 합니다. (더 큰 버퍼를 분리하는 것이 합리적입니다.) 그렇지 않으면, 다른 프로세스에 의한 텍스트 출력 라인이 콘솔에 인터리브되어 인간 독자와 우리의 정지 스크립트 모두를 혼란스럽게 할 수 있다.

  • System Call: void seek (int fd, unsigned position)

    열려 있는 파일 fd에서 읽거나 쓸 다음 바이트를 파일 시작부터 바이트로 나타내는 위치로 변경합니다. (따라서 0의 위치는 파일의 시작입니다.) 파일의 현재 끝을 지나는 탐색은 오류가 아닙니다. 이후 읽기는 파일의 끝을 나타내는 0바이트를 가져옵니다. 이후 쓰기는 파일을 확장하여 기록되지 않은 공백을 0으로 채웁니다. (단, Pintos 파일의 경우 프로젝트 4가 완료될 때까지 길이가 고정되어 있으므로 파일의 끝을 지나 쓰면 오류가 발생합니다.) 이러한 의미론은 파일 시스템에서 구현되며 시스템 호출 구현에 특별한 노력을 요구하지 않는다.

  • System Call: unsigned tell (int fd)

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

  • System Call: void close (int fd)

    파일 지시자 fd를 닫습니다. 프로세스를 종료하거나 종료하면 열려 있는 모든 파일 지시자가 암시적으로 닫힙니다(각 프로세스에 대해 이 함수를 호출하는 경우).

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

시스템 호출을 구현하려면 사용자 가상 주소 공간에서 데이터를 읽고 쓰는 방법을 제공해야 합니다. 시스템 호출 번호가 사용자의 가상 주소 공간에 있는 사용자 스택에 있기 때문에 시스템 호출 번호를 얻기 전에 이 기능이 필요합니다. 이것은 좀 까다로울 수 있습니다. 사용자가 잘못된 포인터, 커널 메모리에 대한 포인터, 또는 이러한 영역 중 하나에서 부분적으로 블록을 제공하는 경우 어떻게 해야 합니까? 사용자 프로세스를 종료하여 이러한 사례를 처리해야 합니다. 다른 시스템 호출 기능을 구현하기 전에 이 코드를 작성하고 테스트하는 것이 좋습니다. 자세한 내용은 section 3.1.5 사용자 메모리 액세스를 참조하십시오.

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

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

이 부분을 다 쓰면, 그리고 영원히 핀토스는 방탄이 되어야 해. user program이 할 수 있는 일은 결코 OS가 손상되거나, 패닉이 발생하거나, 어설션에 실패하거나, 또는 그 외의 오작동을 일으키지 않아야 합니다. 이 점을 강조하는 것이 중요합니다. 우리의 테스트는 여러분의 시스템 호출을 여러 가지 방법으로 깨려고 할 것입니다. 구석구석 사건을 다 생각해보고 처리해야 해요. user program이 OS를 중지시킬 수 있는 유일한 방법은 시스템 중지 호출을 호출하는 것입니다.

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

시스템 호출의 작동 방식에 대한 자세한 내용은 section 3.5.2 시스템 호출 상세 내역을 참조하십시오.

3.5 Denying Writes to Executables

실행 파일로 사용 중인 파일에 대한 쓰기를 거부하는 코드를 추가합니다. 많은 OS는 프로세스가 디스크에서 변경 중인 코드를 실행하려고 시도했을 때 예측할 수 없는 결과 때문에 이러한 작업을 수행합니다. 이는 프로젝트 3에서 가상 메모리가 구현되면 특히 중요하지만, 지금도 문제가 되지 않습니다.

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

4 FAQ

1
2
3
4
5
6
7
 threads/thread.c     |   13 
 threads/thread.h     |   26 +
 userprog/exception.c |    8 
 userprog/process.c   |  247 ++++++++++++++--
 userprog/syscall.c   |  468 ++++++++++++++++++++++++++++++-
 userprog/syscall.h   |    1 
 6 files changed, 725 insertions(+), 38 deletions(-)
  • pintos -p 파일 – -q를 실행하면 커널이 항상 패닉 상태가 됩니다.

    파일 시스템(핀토스 -f)을 포맷했습니까?

    파일 이름이 너무 깁니까? 파일 시스템은 파일 이름을 14자로 제한합니다. pintos -p ..../.../examples/echo -- -q와 같은 명령이 한계를 초과합니다. 파일을 대신 echo라는 이름 아래에 놓으려면 pintos -p ../../examples/echo -a echo ---q를 사용하십시오.

    파일 시스템이 가득 찼습니까?

    파일 시스템에 이미 16개의 파일이 포함되어 있습니까? 기본 Pintos 파일 시스템에는 16개의 파일 제한이 있습니다.

    파일 시스템이 너무 단편화되어 있어서 파일에 대한 연속 공간이 충분하지 않을 수 있습니다.

  • pintos -p…/file –을 실행하면 파일이 복사되지 않습니다.

    파일은 기본적으로 참조하는 이름으로 작성되므로, 이 경우 복사된 파일의 이름은 “../file”입니다. 대신 pintos -p ../file -a file --을 실행할 수 있습니다.

    파일 시스템의 파일을 pintos -q ls로 나열할 수 있습니다.

  • 모든 user program이 page default로 인해 중단됩니다.

    인수 전달을 구현하지 않았거나 올바르게 수행하지 않은 경우 이 문제가 발생합니다. user program을 위한 기본 C 라이브러리는 스택에서 argc와 argv를 읽으려고 시도한다. 스택이 제대로 설정되지 않은 경우 page default가 발생합니다.

  • 모든 user program이 시스템 호출과 함께 사라집니다!

    다른 것을 보기 전에 먼저 시스템 호출을 구현해야 합니다. 모든 합리적인 프로그램은 적어도 하나의 system call (exit())을 시도하며 대부분의 프로그램은 그 이상을 수행합니다. 특히 printf()는 write system call을 호출합니다. 기본 시스템 호출 처리기는 “system call!”을 인쇄하고 프로그램을 종료합니다. 그 때까지 hex_dump()를 사용하여 인수 전달이 올바르게 구현되었음을 확인할 수 있습니다(section 3.5.1 프로그램 시작 세부 정보 참조).

  • user program을 disassemble 방법은 무엇입니까?

    objdump (80x86) 또는 i386-elf-objdump (SPARC) 유틸리티는 전체 user program이나 객체 파일을 분해할 수 있다. objdump -d 파일로 호출합니다. GDB 분해 명령을 사용하여 개별 기능을 분해할 수 있습니다(섹션 E.5 GDB 참조).

  • 왜 많은 C가 Pintos 프로그램에서 작동하지 않는 파일을 포함하고 있는가? 핀토스 프로그램에서 libfoo를 사용할 수 있습니까?

    우리가 제공하는 C 라이브러리는 매우 제한적입니다. 실제 운영 체제의 C 라이브러리에서 예상되는 많은 기능을 포함하지 않는다. C 라이브러리는 I/O 및 메모리 할당에 대한 시스템 호출을 수행해야 하므로 운영 체제(및 아키텍처)를 위해 특별히 구축되어야 합니다. (물론 모든 기능이 그렇지는 않지만 일반적으로 라이브러리는 단위로 컴파일됩니다.)

    원하는 라이브러리가 핀토스가 구현하지 않는 C 라이브러리의 일부를 사용할 가능성이 높습니다. 핀토스 밑에서 작동하려면 적어도 약간의 포트링 노력이 필요할 것이다. 특히 Pintos user program C 라이브러리는 malloc() 구현이 없다.

  • 새 user program을 컴파일하려면 어떻게 해야 합니까?

    src/examples/Make file을 수정한 다음 make를 실행합니다.

  • 디버거에서 user program을 실행할 수 있습니까?

    네, 몇 가지 제한사항이 있습니다. 섹션 E.5 GDB를 참조하십시오.

  • tid_t와 pid_t의 차이점은 무엇입니까?

    tid_t는 커널 스레드를 식별하며, 커널 스레드는 process_execute()로 생성된 경우(thread_create()로 생성된 경우) 사용자 프로세스를 실행할 수 있다. 커널에서만 사용되는 데이터 유형입니다.

    pid_t는 사용자 프로세스를 식별합니다. 사용자 프로세스와 실행 및 대기 시스템 호출의 커널에서 사용됩니다.

    tid_t 및 pid_t에 적합한 유형을 선택할 수 있습니다. 기본적으로 둘 다 int입니다. 두 개의 값이 동일한 프로세스를 식별하도록 일대일 매핑을 만들 수 있으며, 더 복잡한 매핑을 사용할 수 있습니다. 당신 마음대로 하세요.

4.1 Argument Passing FAQ

  • 커널 가상 메모리에서 스택의 상단이 있지 않습니까?

    스택의 맨 위는 PHYS_BASE(일반적으로 0xc00000)이며 커널 가상 메모리가 시작되는 곳이기도 하다. 그러나 프로세서가 스택에서 데이터를 누르기 전에 스택 포인터가 줄어듭니다. 따라서 스택에 푸시된 첫 번째(4바이트) 값은 주소 0xbffffc가 됩니다.

  • PHYS_BASE가 고정되어 있습니까?

    아니요. 단순히 컴파일을 통해 0x80000000 ~ 0xf0000000의 배수인 PHYS_BASE 값을 지원할 수 있어야 합니다.

4.2 System Calls FAQ

  • 파일 지시자를 가져오려면 struct thread *만 캐스팅해도 됩니까?
  • struct thread *을 pid_t에 던져도 됩니까?

    이러한 설계 결정은 본인이 직접 내려야 합니다. 대부분의 운영 체제는 파일 지시자(또는 pids)와 커널 데이터 구조의 주소를 구별한다. 당신은 그들이 자살하기 전에 왜 그렇게 하는지 생각해 보는 것이 좋을지도 모른다.

  • 프로세스당 열려 있는 파일의 최대 수를 설정할 수 있습니까?

    임의적인 제한을 두지 않는 것이 좋다. 필요한 경우 프로세스당 128개의 열린 파일을 제한할 수 있습니다.

  • 열려 있는 파일이 제거되면 어떻게 됩니까?

    파일에 대한 표준 Unix 의미론을 구현해야 합니다. 즉, 파일을 제거하면 해당 파일에 대한 파일 지시자가 있는 프로세스가 해당 지시자를 계속 사용할 수 있습니다. 이것은 그들이 파일에서 읽고 쓸 수 있다는 것을 의미합니다. 파일에 이름이 없으며 다른 프로세스에서는 파일을 열 수 없지만 파일을 참조하는 모든 파일 지시자가 닫히거나 컴퓨터가 종료될 때까지 파일이 계속 존재합니다.

  • 4kB 이상의 스택 공간이 필요한 user program을 실행하려면 어떻게 해야 합니까?

    스택 설정 코드를 수정하여 각 프로세스에 대해 둘 이상의 스택 공간을 할당할 수 있습니다. 다음 프로젝트에서는 더 나은 솔루션을 구현할 것입니다.

  • 로드 도중 중간에 실패하면 어떻게 됩니까?

    하위 프로세스가 어떤 이유로든 로드되지 않을 경우 Exec은 -1을 반환해야 합니다. 여기에는 로드가 프로세스 도중 일부에 실패하는 경우(예: 멀티룸 테스트에서 메모리가 부족한 경우)도 포함됩니다. 따라서 로드 성공 여부를 확인할 때까지 상위 프로세스는 실행 시스템 호출에서 돌아올 수 없습니다. 아이는 경주 조건 없이 정보가 전달되도록 하기 위해 세마포어(section A.3.2 세마포어 참조)와 같은 적절한 동기화를 사용하여 이 정보를 부모에게 전달해야 한다.

5 80x86 Calling Convention

이 절에서는 Unix의 32비트 80x86 구현에서 일반 함수 호출에 사용되는 규칙의 중요한 점을 요약한다. 간결하게 하기 위해 몇 가지 세부 사항이 누락되었다. 모든 세부 정보를 원하는 경우 [SystemV-i386]을 참조하십시오.

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

  1. 호출자는 일반적으로 PUSH 어셈블리 언어 명령을 사용하여 스택에서 각 함수의 인수를 하나씩 푸시합니다. 인수는 오른쪽에서 왼쪽 순서로 푸시됩니다.

    스택은 아래로 늘어납니다. 각 푸시는 스택 포인터를 줄인 다음 C 식 *--sp = value값과 같이 현재 가리키는 위치에 저장됩니다.

  2. 호출자는 스택에서 다음 명령(반품 주소)의 주소를 누르고 호출자의 첫 번째 명령으로 이동합니다. 단일 80x86 명령인 CALL은 두 가지를 모두 수행합니다.
  3. 호출자가 실행됩니다. 제어권을 잡으면 스택 포인터가 반환 주소를 가리키고, 첫 번째 인수는 그 바로 위에 있고, 두 번째 인수는 첫 번째 인수의 바로 위에 있는 등입니다.
  4. 콜리가 반환 값을 가지고 있는 경우, 이를 레지스터 EAX에 저장합니다.
  5. 호출자는 80x86 RET 명령을 사용하여 반환 주소를 스택에서 호출하고 지정한 위치로 점프하여 반환합니다.
  6. 호출자가 스택에서 인수를 제거합니다.

세 인수의 인수를 사용하는 함수 f()를 고려하십시오. 이 다이어그램은 f()가 f(1, 2, 3)로 호출된다고 가정할 때 위의 3단계의 시작 부분에서 볼 수 있는 샘플 스택 프레임을 보여줍니다. 초기 스택 주소는 임의입니다.

1
2
3
4
5
6
7
                             +----------------+
                  0xbffffe7c |        3       |
                  0xbffffe78 |        2       |
                  0xbffffe74 |        1       |
stack pointer --> 0xbffffe70 | return address |
                             +----------------+

5.1 Program Startup Details

user program용 Pintos C 라이브러리는 lib/user/entry.c의 _start()를 user program의 진입점으로 지정합니다. 이 함수는 main()이 반환되는 경우 exit()를 호출하는 main() 주위의 wrapper 입니다.

1
2
3
4
5
void
_start (int argc, char *argv[]) 
{
  exit (main (argc, argv));
}

커널은 user program이 실행을 시작하기 전에 스택에 초기 함수에 대한 인수를 넣어야 한다. 인수는 일반 호출 규약과 동일한 방식으로 전달됩니다(섹션 3.580x86 호출 규약 참조).

다음 예제 명령의 인수를 처리하는 방법을 고려하십시오. “/bin/ls -l foo bar”. 먼저 명령어를 “/bin/ls”, “-l”, “foo”, “bar”로 나눕니다. 스택 맨 위에 단어를 배치합니다. 순서는 중요하지 않다. 왜냐하면 그것들은 포인터를 통해 참조될 것이기 때문이다.

그런 다음 각 문자열의 주소와 null 포인터 센티넬을 스택에 오른쪽에서 왼쪽으로 누릅니다. 이것들은 argv의 요소들이다. null 포인터 센티넬은 argv[argc]가 C 표준에 필요한 null 포인터임을 보장합니다. argv[0]가 가장 낮은 가상 주소에 있도록 합니다. 워드 정렬 액세스는 정렬되지 않은 액세스보다 빠르기 때문에 최상의 성능을 위해 스택 포인터를 처음 누르기 전에 4의 배수까지 내립니다.

그런 다음 argv(argv[0]의 주소)와 argc를 순서대로 누릅니다. 마지막으로, 가짜 “return address”를 푸시한다: 입력 함수는 결코 돌아오지 않지만, 스택 프레임은 다른 것과 동일한 구조를 가져야 한다.

아래 표는 PHYS_BASE가 0xc0000000이라고 가정할 때 user program을 시작하기 직전에 스택과 관련 레지스터의 상태를 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Address	    Name	        Data	    Type
0xbffffffc	argv[3][...]	bar\0	    char[4]
0xbffffff8	argv[2][...]	foo\0	    char[4]
0xbffffff5	argv[1][...]	-l\0	    char[3]
0xbfffffed	argv[0][...]	/bin/ls\0	char[8]
0xbfffffec	word-align	    0	        uint8_t
0xbfffffe8	argv[4]	        0	        char *
0xbfffffe4	argv[3]	        0xbffffffc	char *
0xbfffffe0	argv[2]	        0xbffffff8	char *
0xbfffffdc	argv[1]	        0xbffffff5	char *
0xbfffffd8	argv[0]	        0xbfffffed	char *
0xbfffffd4	argv	        0xbfffffd8	char **
0xbfffffd0	argc	        4	        int
0xbfffffcc	return address	0	        void (*) ()

이 예에서는 스택 포인터가 0xbfffffcc로 초기화됩니다.

위에 표시된 것처럼 코드는 가상 주소 바로 아래 페이지의 사용자 가상 주소 공간 맨 위에서 스택을 시작해야 합니다(threads/vaddr.h에 정의됨).

<stdio.h>에서 선언된 비표준 hex_dump() 함수를 찾을 수 있습니다., 인수 전달 코드를 디버깅하는 데 유용합니다. 위의 예에서 확인할 수 있는 내용은 다음과 같습니다.

1
2
3
4
bfffffc0                                      00 00 00 00 |            ....|
bfffffd0  04 00 00 00 d8 ff ff bf-ed ff ff bf f5 ff ff bf |................|
bfffffe0  f8 ff ff bf fc ff ff bf-00 00 00 00 00 2f 62 69 |............./bi|
bffffff0  6e 2f 6c 73 00 2d 6c 00-66 6f 6f 00 62 61 72 00 |n/ls.-l.foo.bar.|

5.2 System Call Details

첫 번째 프로젝트는 이미 운영 체제가 user program에서 제어권을 되찾을 수 있는 한 가지 방법, 즉 타이머와 I/O 장치의 인터럽트를 다뤘습니다. 이러한 인터럽트는 CPU 외부의 엔티티에 의해 발생하므로 “external” 인터럽트입니다(section A.4.3 외부 인터럽트 처리 참조).

운영 체제는 프로그램 코드에서 발생하는 이벤트인 소프트웨어 예외도 처리합니다(section A.4.2 내부 인터럽트 처리 참조). 이 오류는 page fault 또는 0으로 나눗셈과 같은 오류일 수 있습니다. 예외는 user program이 운영 체제에서 서비스(“system calls”)를 요청할 수 있는 수단이기도 합니다.

80x86 아키텍처에서 명령어는 시스템 호출에 가장 일반적으로 사용되는 수단이다. 이 지침은 다른 소프트웨어 예외와 동일한 방식으로 처리됩니다. 핀토스에서는 user program이 $0x30부터 호출하여 시스템 호출을 한다. 시스템 호출 번호와 추가 인수는 인터럽트를 호출하기 전에 정상적인 방식으로 스택에 푸시될 것으로 예상된다(section 3.580x86 호출 규약 참조).

따라서, system call handler syscall_handler()가 제어권을 얻었을 때, 시스템 호출 번호는 호출자의 스택 포인터의 32비트 워드에 있고, 첫 번째 인수는 다음 상위 주소의 32비트 워드에 있다. 호출자의 스택 포인터는 syscall_handler()에 전달된 structure int_frame의 esp 멤버로 액세스할 수 있습니다. (struct int_frame은 커널 스택에 있습니다.)

함수 반환 값에 대한 80x86 규칙은 이 값을 EAX 레지스터에 배치하는 것이다. 값을 반환하는 시스템 호출은 struct int_frame의 eax 멤버를 수정하여 수행할 수 있습니다.

시스템 호출을 구현하기 위해 대량의 반복 코드를 작성하지 않도록 해야 합니다. 각 시스템 호출 인수는 정수든 포인터든 관계없이 스택에서 4바이트를 차지합니다. 이 기능을 사용하면 각 시스템 호출의 인수를 스택에서 검색하는 데 거의 동일한 코드를 작성하지 않도록 할 수 있습니다.

Posted 2021-03-02