세리프 따라잡기

WEEK10 - PintOS Project2 - WIL 본문

SW사관학교 정글

WEEK10 - PintOS Project2 - WIL

맑은 고딕 2022. 6. 2. 10:53

새롭게 알게 된 것이나

공유하면 좋을 것 같은 정보 적어둬라...

그래서 wil 발표에서 써먹게 ㅇ-ㅇ

 

아래는 내가 정리한 wil에 쓸 내용~~


이번 PintOS Project2CS적인 부분은?

 

Table of Contents

 

1. Argument Passing

2. User Memory Access

3. System Calls

4. Process Termination Message

5. Deny Write on Executables

 

= kernel vs user, 둘의 차이

= register에 대해

= 어셈블리어에 대해

= system call에 대해

= page + 페이징 기법 + 페이지 테이블 + 4단계 페이징 기법

= 논리주소와 가상주소의 1:1 매핑의 과정

 

 

 

#내가 참고했던 사이트 정리글

 

아래는 내가 작성한 WIL 😎~


 

1. 파일 관련 시스템 콜에 lock을 왜 쓰는가?

Q. 예를 들어 두 프로세스가 동시에 파일을 생성하려고 한다면?

-1. 한 프로세스가 a파일이 파일 시스템에서 없다는 걸 확인하고 생성하려 시도함

-2. context swithcing 발생하는데

-3. 다른 프로세스도 a파일이 없는 걸 확인하고 또 생성을 하려함

-4. 또 context swithcing 발생

-5. 그렇게 또 파일이 생성되어 2개가 만들어진다면 어떻게 되는가?

 

위의 예시는 두 작업자가 하나의 자원에 동시 접근(write/read) 하는 상황을 말하는데,

비유하자면, 자동차가 한 대만 다닐 수 있는 병목 구간에 두 자동차가 같이 들어가려고 하는 상황과 같다.

올바른 절차
: 공유자원을 가져온 A의 작업 시작
A의 작업이 반영된 공유자원 [A 작업 끝]
→ A의 작업이 반영된 공유자원을 가져온 B의 작업 시작
A의 작업이 반영된 B의 작업이 반영된 공유자원 [B 작업 끝]

예시의 상황이 발생한 절차
: 공유자원을 가져온 A의 작업 시작
→ (interrupt / context switching 발생)
→ 공유자원을 가져온 B의 작업 시작
A의 작업이 반영된 공유자원 [A 작업 끝]
B의 작업이 반영된 공유자원 [B 작업 끝]

= 이런 현상은 순서와 공유 자원 때문에 일어나는데, 이를 방지하기 위해서는 동기화를 해주어야 한다.

 

둘 이상의 작업자가 공유자원에 접근하게 되는 환경에서, 연산이 섞이는 일이 없도록 원자적 연산을 보장하거나, 동기화 기법을 써야한다.

= 즉, 공유자원에 대한 동기화를 위하여 lock을 사용한다~

 

아래는 lock 코드가 사용된 syscall의 write 함수!

int write (int fd, const void *buffer, unsigned size) {
	check_address(buffer);
	lock_acquire(&filesys_lock); //lock 요청

	int ret;
	struct file *file_obj = get_file_from_fd_table(fd);
	
	if (file_obj == NULL) {
		lock_release(&filesys_lock);
		return -1;
	}

	/* STDOUT */
	if (fd == STDOUT) {
		putbuf(buffer, size);		/* to print buffer strings on the display*/
		ret = size;
	}
	/* STDIN */
	else if (fd == STDIN) {
		ret = -1;
	}
	else {
		ret = file_write(file_obj, buffer, size);
	}

	lock_release(&filesys_lock); //lock 해제

	return ret;
}

 

 

2. ELF 파일이 뭐야?

ELF(Executable and Linkable Format)

: 리눅스에서 실행 가능(Executable)하고 링크 가능(Linkable)한 파일(file)의 format을 elf라고 한다.

: 유닉스 계열 시스템들의 표준 바이너리 파일 형식

= 즉, 실행 가능한 바이너리 또는 오브젝트 파일 등의 형식을 규정한 것.

:윈도우 시스템으로 말하자면, PE파일 형식이 ELF파일 형식
= 결국 링커를 거쳐서 나온 실행 파일이다~ 라고 보면 된다.

 

오프젝트 파일은 3가지 종류

- 재배치 가능한 파일(relocatable file)

  : 코드와 데이터를 다른 오브젝트 파일과 링킹될  있도록 

- 실행 파일(executable file)

  : 코드와 데이터를 타겟 운영체제에서 실행시킬  있도록 

- 공유 오브젝트 파일(shared object file)

  : 재할당 가능한 데이터를 정적 혹은 동적으로 다른 공유 오브젝트들과 공유할  있도록 

 

ELF파일 = ELF헤더 + 프로그램 헤더 테이블 + 섹션 헤더 테이블

1. ELF 헤더 : 파일의 구성을 나타내는 로드맵과 같은 역할을 하며, 첫 부분을 차지

2. 섹션 : 링킹을 위한 object 파일의 정보를 다량으로 가지고 있으며, 이에 해당하는 것으로는 명령, 데이터, 심볼 테이블, 재배치 정보 등이 들어간다.

3. 프로그램 헤더 테이블(옵션) : 시스템에 어떻게 프로세스 이미지를 만들지를 지시

    프로세스의 이미지를 만들기 위해서 사용되는 파일은 반드시 프로그램 헤더 테이블을 가져야하며, 재배치 가능  파일의 경우엔 가지지 않아도 된다.

4. 섹션 헤더 테이블 : 파일의 섹션들에 대해서 알려주는 정보를 포함

    모든 섹션은 이 테이블에 하나의 엔트리(entry)를 가져야 한다.

    각각의 엔트리는 섹션 이름이나, 섹션의 크기와 같은 정보를 제공해 준다. 

    만약 파일이 링킹하는 동안 사용된다면, 반드시 섹션 헤더 테이블을 가져야 하며, 다른 object파일은 섹션 헤더 테이블을 가지고 있지 않을 수도 있다. 

    이에 해당하는 정보들은 리눅스의 경우 ~/include/linux/elf.h에서 찾을 수 있다고..!

 

/* We load ELF binaries.  The following definitions are taken
 * from the ELF specification, [ELF1], more-or-less verbatim.
 = ELF 바이너리를 로드합니다. 다음 정의는 거의 그대로 ELF 사양 [ELF1]에서 가져온 것입니다. */

/* ELF types.  See [ELF1] 1-2. */
#define EI_NIDENT 16

#define PT_NULL    0            /* Ignore. */
#define PT_LOAD    1            /* Loadable segment. */
#define PT_DYNAMIC 2            /* Dynamic linking info. */
#define PT_INTERP  3            /* Name of dynamic loader. */
#define PT_NOTE    4            /* Auxiliary info. */
#define PT_SHLIB   5            /* Reserved. */
#define PT_PHDR    6            /* Program header table. */
#define PT_STACK   0x6474e551   /* Stack segment. */

#define PF_X 1          /* Executable. */
#define PF_W 2          /* Writable. */
#define PF_R 4          /* Readable. */

/* Executable header.  See [ELF1] 1-4 to 1-8.
 * This appears at the very beginning of an ELF binary. */
struct ELF64_hdr {
	unsigned char e_ident[EI_NIDENT];
	uint16_t e_type;
	uint16_t e_machine;
	uint32_t e_version;
	uint64_t e_entry;
	uint64_t e_phoff;
	uint64_t e_shoff;
	uint32_t e_flags;
	uint16_t e_ehsize;
	uint16_t e_phentsize;
	uint16_t e_phnum;
	uint16_t e_shentsize;
	uint16_t e_shnum;
	uint16_t e_shstrndx;
};

struct ELF64_PHDR {
	uint32_t p_type;
	uint32_t p_flags;
	uint64_t p_offset;
	uint64_t p_vaddr;
	uint64_t p_paddr;
	uint64_t p_filesz;
	uint64_t p_memsz;
	uint64_t p_align;
};

→ 그대로 elf 표준 형식을 따서 만든 것이라 다음 표를 참고하면 된다! [차례대로 올림]

 

세그먼트 타입:

elf 헤더 구조:

프로그램 헤더:

 

3. do_iret() 함수에 대해

/* Use iretq to launch the thread
= iretq를 사용하여 스레드를 시작합니다.*/
void
do_iret (struct intr_frame *tf) {
	__asm __volatile(
			"movq %0, %%rsp\n"
			"movq 0(%%rsp),%%r15\n"
			"movq 8(%%rsp),%%r14\n"
			"movq 16(%%rsp),%%r13\n"
			"movq 24(%%rsp),%%r12\n"
			"movq 32(%%rsp),%%r11\n"
			"movq 40(%%rsp),%%r10\n"
			"movq 48(%%rsp),%%r9\n"
			"movq 56(%%rsp),%%r8\n"
			"movq 64(%%rsp),%%rsi\n"
			"movq 72(%%rsp),%%rdi\n"
			"movq 80(%%rsp),%%rbp\n"
			"movq 88(%%rsp),%%rdx\n"
			"movq 96(%%rsp),%%rcx\n"
			"movq 104(%%rsp),%%rbx\n"
			"movq 112(%%rsp),%%rax\n"
			"addq $120,%%rsp\n"
			"movw 8(%%rsp),%%ds\n"
			"movw (%%rsp),%%es\n"
			"addq $32, %%rsp\n"
			"iretq"
			: : "g" ((uint64_t) tf) : "memory");
}

어떤 함수 내에서 호출 된 do_iret() 함수가 '_if 구조체' 안의 값으로 레지스터 값을 수정하고, 스레드를 시작함.

do_iret 함수 내의 어셈블리어 iretq는 Interrupt return (64-bit operand size)인데, 말하자면 kernel mode → user mode로 돌아올 때 사용 [ iretq로 유저 영역에 복귀해서 프로세스 시작! ]

= do_iret() 함수를 이용하면 새로운 프로세스로 context switching 된다~

 

+

mov는 앞에 것을 뒤에 복사하라는 뜻이다. mov instruction은 접미사에 따라서 movb, movw, movl, movq의 네 개의 유형으로 구분 가능한데, 여기서 쓰인 movq와 movw의 의미는 각각 다음 표와 같다.

C 선언 Intel 데이터 타입 어셈블리어 접미사 사이즈 (byte)
char byte b 1
short word w 2
int double word l 4
long quad word q 8
char * quad word q 8
float single precision s 4
double double precision l 8

 

add는 앞과 뒤를 더하라는 뜻이다. 뒤에 붙은 q의 의미는 비트 수를 나타낸다. 예를 들어 add의 addl은 32비트, addq는 64비트 이런 식이다.

 

+

__asm__ __volatile(: : :"memory") ← 이 함수는 뭘까?

= gcc compiler에게 코드를 최적화를 하지 말고, c 혹은 asseble로 구현(코딩)된 순서대로 컴파일하여 instruction을 만들라는 것.

 

4. file_deny_write()를 사용하는 이유는?

1번의 syscall에, 왜 lock을 쓰는가와 비슷한 느낌이라고 보면 된다고 생각한다.

 

예를 들어 fd = 5인 파일을 읽고 있다고 하면, 읽는 중에 아직 읽지 않은 다른 파일이 변경되는 일이 발생하면 안 되기 때문에, 현재 load된 파일은 write를 할 수 없게 처리한다.

static bool load (const char *file_name, struct intr_frame *if_) {
...
	file_deny_write(file);
...
}

→ 즉, load 후에 처리해주고 마찬가지로 close를 하게 되면, 다시 풀어주면 된다.

/* Closes FILE. */
void
file_close (struct file *file) {
	if (file != NULL) {
		file_allow_write (file); //여기서 다시 풀어줌
		inode_close (file->inode);
		free (file);
	}
}

 
Comments