Showing

[Pintos] Project 2 : Argument Passing, 인터럽트 프레임 본문

컴퓨터 공학, 전산학/핀토스

[Pintos] Project 2 : Argument Passing, 인터럽트 프레임

RabbitCode 2023. 5. 7. 05:59

1. Argument Passing 인자 전달

 

Argument Passing 사용자가 입력하는 정보들(명령줄 인자)을 프로그램에게 전달하는 것을 말합니다.

 

예1)  "grep apple fruits.txt"와 같은 명령어가 있다면, "grep"은 실행할 프로그램 이름이며, "apple"은 검색할 단어, "fruits.txt"는 검색 대상 파일이 될 수 있습니다. 

 

예2) "gomoku start --size=15 --mode=multiplayer"와 같은 명령어가 있다고 가정할때, "gomoku" 실행할 프로그램 이름이고, "start"는 게임 시작 옵션, "--size=15"는 게임판의 크기를 15 설정, "--mode=multiplayer"는 멀티플레이 모드 등 이렇게 입력된 정보를 프로그램에서 활용하면, "gomoku"이라는 프로그램이 15x15 크기의 게임판을 만들어 멀티플레이 모드로 게임을 시작하게 만들 수 있어야 합니다. 그 첫번째 작업이 Argument Passing 인자 전달 입니다.

 

결국, 사용자 입력 정보들을 잘 전달해야 비로소 프로그램은 사용자가 기대한 대로의 작업을 수행할 것입니다.

 

핀토스 project2에서는 사용자가 명령을 입력할 때 그 명령어에 대한 인자를 받아들이고, 프로그램에 전달할 수 있도록 해야합니다.

현재 pintos는 커맨드 라인에 명령어를 작성하면, 프로그램 이름과 인자를 구분하지 못 하고 적은 명령어 전체를 하나의 프로그램으로 인식하게 구현되어 있어 프로그램과 인자를 구분하여 파싱하고 패싱할 수 있도록 하는 것이 목표

 커맨드 라인에서 프로세스 이름을 확인, 커맨드 라인을 파싱하여 인자를 확인, 그 인자를 스택에 삽입하는 것을 구현

 

 

 init.c 파일은 운영체제의 초기화를 담당하는 프로그램으로, 이 파일의 main 함수에서는 명령어를 읽어와서 적절히 처리하는 과정을 거치게 됩니다. 특히 read_command_line 함수를 호출하여 명령어를 읽어오고, 이 명령어는 argv라는 배열에 저장됩니다.

 

유저 프로세스 생성되었다면 커널은 프로세스 종료를 대기 process_wait(process_create_initd(task));

 명령어가 run이라는 단어로 시작한다면, 매핑된 run_task 함수가 호출되고, argv[1]에는 run 이후에 오는 인자가 저장됩니다.

가령 run args-single onearg 명령어를 실행하면 'args-single'이라는 프로그램을 실행하면서 'onearg'라는 인자(argument)를 전달한다는 의미에서 'args-single'은 단순히 인자 onearg(argument) 하나를 출력하는 프로그램이라고 볼 수 있습니다.

 

run_task 함수는 해당 인자를 process_create_initd 함수의 인자로 전달하여 새로운 스레드를 생성하고, 이 스레드는 생성된 후 initd 함수를 실행하게 됩니다.

2. process_exec()

 

process_exec는 이름에 걸맞게 실행 파일의 이름을 받아와서, 해당 실행 파일을 현재 스레드에 로드하고, 새로운 프로세스로 실행하는 함수입니다. process_exec는 load 함수를 호출하여 ELF 파일을 읽어들이고, 실행 파일의 진입 지점(entry point)과 초기 스택 포인터를 설정합니다. 또한 인자 전달을 위한 스택 공간(인자는 마지막 인자부터 역순으로 스택에 저장되며, 인자의 개수도 함께 스택에 저장)을 만들고, 인자배열과 그 크기를 이용해서 인터럽트 프레임의 레지스터 값을 설정하고, 실행 파일의 진입 지점(entry point)으로 이동하여 새로운 프로세스를 실행할 수 있어야 합니다.

 

 

3. load()

프로그램을 메모리에 적재 후 프로그램 시작

프로그램 실행 파일 내의 각 영역은 프로그램 실행 시 메모리에 로드, BSS는 초기화되지 않은 전역 변수나 정적 변수가 저장되는 메모리 영역

(1) 메모리 로드

프로그램 실행 파일을 로드하는 과정은 보통 운영체제가 수행합니다. 실행 파일을 읽어서 메모리에 로드하고, 실행을 위한 환경을 설정하는 등의 작업을 수행한 이후에 프로그램이 실행되면, 메모리에 올라간 코드 섹션과 데이터 섹션, BSS 섹션 등의 정보를 바탕으로 프로그램이 실행됩니다.

[메모리구조],하나의 프로세스는 유저영역에서 코드 영역, 데이터 영역, 힙 영역, 스택 영역으로 나뉘어져 있고, 유저 스택은 낮은주소로 쌓이기 때문에 커널영역을 침범하지 않음

 

프로그램 실행 파일이 메모리에 로드될 때, 하나의 프로세스는 유저영역에서 코드 영역, 데이터 영역, 힙 영역, 스택 영역으로 나뉘어져 있다. 각 영역은 프로그램 실행 중에 특정한 목적을 위해 사용됩니다.

  • 코드 섹션 (Text section)
  • 데이터 섹션 (Data section)
  • BSS 섹션 (Block Started by Symbol section)

 

코드 섹션"Text section"은 프로그램의 실행 파일 내에 프로그램 코드가 저장되는 영역을 의미합니다. 코드 섹션에 저장된 코드는 실행 파일이 메모리에 로드될  읽기 전용 메모리로 설정되며, 실행 중에는 수정할  없습니다. 따라서, 코드 섹션에 저장된 프로그램 코드는 실행 중에 변경되지 않으며, 안정적인 실행이 가능합니다.

 

 데이터 섹션과 BSS 섹션은 읽기/쓰기가 가능한 메모리로 설정되어 있으며, 변수 값의 변경이 가능합니다.

 

BSS Block Started by Symbol 약자로, 초기화되지 않은 전역 변수나 정적 변수가 할당되는 영역을 의미합니다. 프로그램의 시작 시점에 BSS 영역은 0으로 초기화되며, 이후에 프로그램이 실행되면서 BSS 영역에 필요한 값들이 할당되고 사용됩니다. BSS 영역은 프로그램이 종료될 때까지 유지되며, 이후에는 해당 메모리 공간이 해제됩니다.

 

데이터 섹션(Data section)은 초기화된 전역 변수나 정적 변수가 저장되는 메모리 영역입니다. 프로그램의 실행 파일 내에 초기값이 함께 저장되어 있으며, 프로그램 실행 시 메모리에 로드됩니다. 전역 변수와 정적 변수는 프로그램 전체에서 공유되는 변수이기 때문에, 데이터 섹션에 저장되어 다른 함수나 모듈에서도 접근이 가능합니다. 프로그램의 시작과 동시에 할당되고, 프로그램이 종료되어야 메모리에서 소멸되는데 이것이 바로 전역변수가 프로그램이 종료될 때 까지 존재하는 이유이기도 합니다.

 

<기타>

힙(Heap) 영역

- 필요에 의해 동적으로 메모리를 할당 할 때 사용

(동적할당 같은 경우는 힙 영역에 속한다)

- 낮은 주소에서 높은 주소로 쌓인다

 

스택(Stack) 영역

- 함수 호출 시 생성되는 지역 변수와 매개 변수가 저장되는 영역

- 함수 호출이 완료되면 사라짐

- 높은 주소에서 낮은 주소로 쌓인다

 

 

페이지 디렉토리 생성, 페이지 테이블 활성화, 프로그램 파일 Open의 과정을 거침
이 함수는 다음 스레드를 실행하기 위해 CPU를 설정. 모든 컨텍스트 전환에서 호출. TSS는 멀티태스킹을 구현하기 위한 자료구조

(2) process_activate

/* 현재 실행중인 스레드의 페이지 테이블을 비활성화하고,
다음 스레드의 페이지 테이블을 활성화하고 인터럽트 처리에 사용할 스레드의 커널 스택을 설정합니다. */
pml4_activate (next->pml4);
/* 인터럽트 처리에 사용할 스레드의 커널 스택을 설정합니다. */
tss_update (next);

Pintos 운영 체제에서 스레드 스케줄링에서 실행 중인 스레드를 변경할 때마다 호출됩니다. 

 

pml4_activate 함수는 현재 스레드의 페이지 매핑 계층 레지스터(PML4)를 다음 스레드의 PML4로 교체하는 역할을 합니다. 이 함수는 페이지 테이블을 전환하여, 현재 스레드의 가상 주소 공간에서 다음 스레드의 가상 주소 공간으로 컨텍스트를 전환합니다.

 

TSS 구조체는 태스크 스테이트 세그먼트(Task State Segment)를 나타내며, 인터럽트나 예외 발생 시 운영 체제가 해당 스레드의 커널 스택을 찾는 데 사용됩니다. tss_update 함수는 현재 스레드가 사용하는 TSS 구조체의 rsp0 필드를 다음 스레드가 사용하는 커널 스택 주소로 갱신하는 역할을 합니다.

이러한 작업을 통해, 다음 스레드가 실행되기 위한 준비가 완료됩니다.

✔️TSS는 멀티태스킹을 구현하기 위한 자료구조

TSS는 Task State Segment의 약자이며, x86 아키텍처에서 사용되는 자료구조입니다. TSS는 현재 실행 중인 태스크의 상태를 저장하는 역할을 하며, 태스크 전환 시에는 TSS에 저장된 정보를 기반으로 다음 태스크의 실행을 시작합니다. TSS는 GDT(Global Descriptor Table) 내의 세그먼트 디스크립터를 통해 접근되며, TSS는 프로세스나 태스크 간의 전환이 일어날 때 자동으로 갱신됩니다. 

 

 

(3) 인터럽트 프레임

 

문서 작업 중인데 갑자기 전화가 와서 통화를 한 후에, 다시 작업을 시작할  현재까지 작성한 내용이 남아있는 것이 당연하게 느껴집니다. 마찬가지로 운영체제에서는 마지막으로 실행한 부분으로 되돌아가더라도 이어서 계속 작업할 수 있도록 인터럽트 발생  실행 중인 상태를 잠시 저장하고 나중에 이어서 실행할  있도록 하는 장치가 필요합니다. 바로 인터럽트 프레임입니다.

 

인터럽트 프레임은 현재 프로세스의 상태를 보존하고, 인터럽트 처리가 끝난 후 다시 복귀하여 진행할 수 있도록 구성된 구조체입니다. 

 

인터럽트가 발생하면, 현재 프로세스가 실행 중인 코드의 상태(레지스터 값, 프로그램 카운터 등)를 인터럽트 프레임에 저장하고, 인터럽트 처리가 끝난 후 해당 인터럽트를 발생시킨 코드의 다음 명령어부터 다시 실행할 수 있도록, 인터럽트 발생 이전의 상태로 복구할 수 있어야 합니다.

 

load 함수 안 : ELF 파일의 실행 시작 지점인 ehdr.e_entry를 _if.rip 레지스터에 저장

_if 는 struct intr_frame 으로 선언된 변수입니다. 이 구조체는 인터럽트와 같은 이벤트를 처리하기 위해 사용되며, 핀토스에서는 사용자 프로그램의 실행을 위한 컨텍스트 정보를 저장하는 데에 사용됩니다. 구체적으로 ds, es, ss, cs, eflags 등의 멤버를 가지고 있습니다.

 

load 함수에서는 ELF 파일(실행 파일을 위한 바이너리 파일 포맷)에서 프로그램의 시작 주소(ehdr.e_entry)를 인터럽트 프레임의 if_->rip  레지스터에 저장하여, 해당 프로그램이 시작되는 지점으로 인터럽트 프레임을 설정합니다. 또한 ELF 파일의 구조를 검증하고, 프로그램 헤더를 읽어와 세그먼트를 메모리에 로드합니다.

 

4. argument_stack

argument_stack 함수는 자식 프로세스가 실행될 때 필요한 작업입니다.

기본 pintos는 유저 스택에 프로그램명과 인자값이 삽입되지 않기 때문에 유저 프로그램을 실행할 시 유저 스택은 비어 있는 상태

인자들을 스택에 삽입하는 기능을 구현해야함

유저 스택에 파싱된 토큰들(프로그램 이름과 인자들)을 저장하는 함수로 세가지 인자를 넘겨주어야 합니다.

void argument_stack(char **parse, int count, void **esp)

* parse : 프로그램 이름과 인자가 저장되어 있는 메모리 공간, count : 인자의 개수, esp: 스택 포인터를 가리키는 주소

실질적으로 argv 배열의 인자들을 순서대로 스택에 삽입합니다. 스택의 주소를 역순으로 계산하여 인자를 삽입하고, NULL 포인터를 스택에 삽입하여 인자의 끝을 표시합니다.

따라서, argument_stack 함수는 자식 프로세스가 실행될 , 프로세스가 필요로 하는 인자들을 스택에 삽입하는 역할을 합니다

 

위의 구현을 보면 _if.rsp 는 _if 구조체의 멤버 중 하나로, 스택 포인터(rsp)의 값을 저장합니다. 이 값은 함수가 반환되었을 때, 호출자 함수가 스택의 어디까지 pop해야 하는지를 알려주는 역할을 합니다.

마지막으로 rspp void ** 타입으로 선언된 변수입니다. 이는 _if.rsp 변수의 주소를 저장하기 위해 사용됩니다. rspp 포인터를 이용하여 _if.rsp 변수를 간접적으로 참조할 있습니다.

예를 들어 '$pintos run 'echo x'라는 커맨드 라인을 입력시, 유저 스택의 상단에는 'echo'와 'x'라는 인자가 저장되고 중단에는 각 인자들의 주소값&nbsp; argv[0],&nbsp; argv[1], *argv[2]가 아래에서부터 위로 저장. 하단에는 인자의 갯수 argc와 중단에 저장된 주소값들의 주소값 **argv가 저장되고 마지막으로 함수를 호출하는 부분의 다음 수행 명령어 주소인 return address가 저장

 

[인자들을 스택에 저장]

  1. 인자의 문자열을 문자 하나씩 스택에 저장. 인자 문자열의 길이만큼 반복문을 수행하여 문자열의 끝부터 스택에 저장합니다. 각 문자를 스택에 저장할 때는 스택 포인터(rsp)를 감소시키고, 해당 스택 위치에 1바이트 크기의 문자를 저장합니다.
  2. 인자 문자열의 저장이 끝나면, 스택 포인터(rsp)가 8의 배수가 되도록 패딩을 추가. 스택의 워드(word) 정렬 유지하기 위함
  3. 각 인자의 주소와 널포인터(null pointer)를 스택에 저장. 이는 argv 배열의 원소로 사용됨. 먼저, 널포인터를 스택에 저장한 뒤, 인자들의 주소를 스택에 저장. 인자들은 역순으로 저장
  4. 가짜 리턴 어드레스 0(fake return address)을 스택에 저장. 실제로 사용되지 않지만, 함수 호출과정 일관성을 유지하기 위함

이렇게 스택에 인자를 저장함으로써, 해당 프로세스는 실행 중에 스택을 이용하여 인자 값을 변경하거나 함수의 지역 변수와 반환 등을 저장하고 사용할 있습니다.

 

5. 요약

 

 project2에서 처리해주어야 하는 작업은 (1) 입력받은 명령어를 공백으로 구분하여 인자들을 parsing하고, (2)이를 스택에 저장하되, (3) 스택에는 인자, 8바이트 정렬을 맞추기 위한 공백, 파싱한 인자의 스택 주소, 가짜 반환 주소를 차례대로 저장해주는 것입니다. 이를 통해 사용자가 기대하는 방식으로 프로그램이 실행되게 될 것입니다.

 

6. 보충

(1) 유저영역 커널영역

유저 영역 

: 프로그램이 동작하기 위해 사용되는 메모리 공간

→ 스택 영역, 힙 영역, 데이터 영역, 코드 영역

  • 사용자 모드 : 사용자 영역의 응용 프로그램이 사용하는 모드
  • 사용자 영역의 응용프로그램이 커널 영역의 기능을 사용하기 위해서는 시스템 콜이 필요함
  • 유저 어플리케이션은 사용자 모드에서만 실행 o

 

시스템 콜: 프로세스가 하드웨어에 직접 접근해서 필요한 기능을 사용할 수 있게함

Ex) 파일을 open하거나 read 해주는 시스템 콜

 

 

커널 영역 

: 운영체제를 실행시키기 위해서 필요한 메모리 공간, 메모리에서 유저 영역을 제외한 영역

  • 커널이 위치함 
  • 커널 모드 : 운영체제가 CPU 제어권을 가지고 운영체제 코드를 실행하는 모드
  • 운영체제 코드는 커널 모드에서만 실행 o ,
  • 커널이 실행될 때는 커널모드에 진입되어 메모리 공간 어디든 접근할 수 있음

 

커널 : 운영체제의 핵심부로 컴퓨터 자원(CPU, 메모리, 파일 등)들을 관리함

→ 메모리에 상주하는 운영체제의 부분 = 커널

  • 커널은 컴퓨터 자원을 관리하며 사용자와의 상호작용을 하지 않음 

 

+ 커널 모드 유저 모드의 차이점

: 프로세스가 유저 모드에서 동작할 때는 커널 영역으로의 접근이 금지된다. 반면, 커널 모드에서 동작할 때는 모든 영역의 접근이 허용된다

 

커널 레벨 쓰레드 vs 유저 레벨 쓰레드

https://www.youtube.com/watch?v=sOt80Kw0Ols

 

(2) 유저스택 커널스택

 

이 개념을 완벽하게 알면 CPU도 만들어볼 법 할까?

스레드는 하나의 프로세스 내에서 실행되는 실행 흐름. 스레드는 프로세스 내에서 코드, 데이터, 힙 등의 자원을 공유하면서 실행. 하지만, 스레드는 각각 독립적인 스택을 가지고 있으며, 이 스택은 커널 스택과 유저 스택으로 구분.

커널 스택(Kernel stack)은 커널 모드에서 사용되는 스택 스레드가 커널 모드에서 실행되는 동안에는 커널 스택을 사용하여 중요한 정보를 저장하고 관리. 예를 들어, 인터럽트가 발생하거나 시스템 콜을 호출할 때는 커널 모드에서 실행되기 때문에, 해당 작업에서 사용되는 스택은 커널 스택. 커널 스택은 보통 운영체제에 의해 관리되며, 크기가 고정되어 있다.

유저 스택(User stack)은 유저 모드에서 실행되는 프로그램(코드)에서 사용되는 스택. 스레드가 유저 모드에서 실행되는 동안에는 유저 스택을 사용하여 함수 호출, 지역 변수 및 인자 등의 정보를 저장하고 관리. 예를 들어, C 언어로 작성된 함수가 호출되면, 해당 함수의 매개변수와 지역 변수는 유저 스택에 저장. 유저 스택은 각 스레드마다 별도로 할당되며, 크기는 스레드 생성 시에 결정.

, 커널 스택과 유저 스택은 각각 커널 모드와 유저 모드에서 사용되는 스택으로, 스레드가 실행될 각각 별도로 할당.

CPU가 커널영역의 일을 했다는 것을 표시하고 연한부분은 CPU가 커널이외의 영역, 즉 사용자 영역의 작업을 했다는 것

=CPU가 하는 일들을 링 형태로 구분해서 표현한 그림으로, 그림의 링은 안쪽에 가까울 수록 커널과 운영체제의 핵심에 가까운 일을 뜻합니다. 즉, 가장 안쪽 Ring 0는 CPU가 커널 영역에서 하는 일을 뜻합니다. CPU가 해당 영역의 작업을 수행할 때, CPU는 커널모드가 됩니다. 반대로 CPU가 Ring 0 밖의 작업을 수행할 때는 CPU가 유저모드가 됩니다.

 

CPU의 모드가 커널모드에서 사용자모드로, 사용자모드에서 커널모드로 전환하는 것을 컨텍스트 체인지(Context change)라고 합니다.

 

(3) rsp rip의 개념

RSP RIP x86 아키텍처에서 사용되는 레지스터입니다. RSP 스택 포인터(SP) 유사하며, 현재 스택의 주소를 가리킵니다. RIP 명령어 포인터(IP) 유사하며, 현재 실행 중인 명령어의 주소를 가리킵니다.

 

스택은 일반적으로 스택 포인터(SP) 레지스터를 사용하여 관리. SP 레지스터는 유저 모드에서는 유저스택의 맨 위를 가리키고, 커널 모드로 전환될 때는 커널스택의 맨 위.

커널스택 또한 SP 레지스터를 사용하여 관리됩니다.

 

함수 호출 또는 인터럽트가 발생하면 현재 실행 중인 코드의 RIP 값이 유저스택에 저장되며, 그 다음 함수 또는 인터럽트 처리를 위해 커널 모드로 전환되면서 RSP 값이 커널스택의 맨 위를 가리키게 됩니다. 이후, 커널 모드에서 해당 함수 또는 인터럽트를 처리하면서 RSP 값이 변경되며, 함수 호출이 끝나면 반환 주소(RIP)가 스택에서 팝되어 이전 실행 지점으로 돌아갑니다.

 

(4) 커널 스택과 공유 자원

커널 스택은 각 쓰레드마다 별도로 할당됩니다. 이는 각 쓰레드가 커널 모드에서 실행될 때, 해당 쓰레드의 커널 스택이 사용됨을 의미합니다.

커널 스택은 공유 자원을 가집니다. 커널 스택에 저장되는 함수 호출 정보나 지역 변수 등은 쓰레드가 실행될 때마다 달라질 있지만, 커널 스택 자체는 메모리 공간을 공유합니다. 이는 커널 스택이 쓰레드마다 크기가 동일하게 할당되며, 미리 정해진 크기만큼 메모리를 사용한다는 것을 의미합니다.

 

스택 프레임(Stack Frame)이라는 개념이 사용됩니다. 스택 프레임은 함수 호출 저장되는 함수 호출 정보와 지역 변수 등을 담는 논리적인 단위입니다. 쓰레드의 스택 프레임은 해당 쓰레드의 실행 흐름에 따라 독립적으로 관리됩니다. 하지만 커널 스택 전체에서는 쓰레드의 스택 프레임이 겹치지 않도록 관리됩니다.

 

쓰레드는 자신만의 커널 스택을 가지지만, 커널 스택은 메모리 공간을 공유하며, 스택 프레임 단위로 각각의 쓰레드가 독립적으로 관리됩니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

참고 문헌