운영체제에서 사용자가 셸(Shell)을 통해 프로그램을 실행할 때 전달하는 문자열(예: /bin/echo hello world)은 단일 텍스트 스트림 형태로 커널에 전달됩니다. 커널은 이 문자열을 프로그램 실행 바이너리 파일명과 개별 인자(Argument)들로 분해하여 유저 프로세스의 초기 가상 메모리 공간에 재배치해야 합니다.
[핀토스 명령행 인자 전달 제어 흐름도]
$ /bin/echo hello world
│
▼
process_create_initd(file_name)
├─ 1. 원본 명령행 문자열 복사본 생성 (페이지 할당: palloc_get_page)
├─ 2. 첫 번째 토큰 분리: "/bin/echo" (스레드 생성용 이름 추출)
└─ 3. initd 메인 스레드 생성 (thread_create)
│
▼ (initd 스레드 컨텍스트)
initd(void *f_name)
└─ process_exec(f_name)
└─ load(f_name, &intr_frame)
├─ 1. ELF 바이너리 디스크 로드 및 가상 메모리 매핑
├─ 2. 명령행 인자 토큰화 (strtok_r 파싱 -> argv[], argc)
├─ 3. 유저 스택 프레임 구성 (argument_stack / setup_stack)
└─ 4. 유저 프로세스 진입 (do_iret -> Ring 3 전환)
process_create_initd()와 load()의 역할 분리process_create_initd(const char *file_name):thread_create()의 인자로 전달합니다.load(const char *file_name, struct intr_frame *if_):if_->rsp)를 이동시키며 스택 프레임을 구축합니다.명령행 문자열을 공백 문자로 파싱할 때 표준 C 라이브러리 함수인 strtok() 대신 재진입(Reentrant)이 가능한 strtok_r()를 사용해야 합니다. 멀티스레드 커널 환경에서 strtok()은 내부 정적(Static) 포인터를 공유하므로 스레드 간 문맥 교환 발생 시 파싱 상태가 오염되는 데이터 레이스가 발생합니다.
[strtok_r()을 통한 명령행 토큰화 및 메모리 상태 전이]
초기 상태:
/ b i n / e c h o h e l l o w o r l d \0
▲ ▲ ▲ ▲
0 9 15 21
1회 호출: token = strtok_r(command, " ", &save_ptr);
/ b i n / e c h o \0 h e l l o w o r l d \0
▲ ▲ ▲
token │ save_ptr
공백이 '\0'로 치환됨 (argv[0] 확정)
2회 호출: token = strtok_r(NULL, " ", &save_ptr);
/ b i n / e c h o \0 h e l l o \0 w o r l d \0
▲ ▲ ▲
token │ save_ptr (argv[1] 확정)
3회 호출: token = strtok_r(NULL, " ", &save_ptr);
/ b i n / e c h o \0 h e l l o \0 w o r l d \0
▲ ▲
token save_ptr (argv[2] 확정)
4회 호출: token = strtok_r(NULL, " ", &save_ptr); -> 반환값: NULL (토큰화 종료, argc = 3)
'\0')와 NULL 포인터의 물리적 차이'\0' (NUL 문자, ASCII Code 0x00):strtok_r()은 구분자(공백) 위치의 바이트를 '\0'로 덮어써서 원본 문자열을 여러 개의 독립된 문자열 조각으로 물리 분할합니다.NULL (널 포인터, (void *)0):argv[argc]의 끝을 나타내는 센티넬(Sentinel) 값이나 함수 포인터 실패 반환값으로 사용됩니다.argv)과 인자 개수(argc) 바인딩 규약char *argv[128];
int argc = 0;
char *token, *save_ptr;
for (token = strtok_r(command_copy, " ", &save_ptr); token != NULL;
token = strtok_r(NULL, " ", &save_ptr)) {
argv[argc] = token;
argc++;
}
argv[argc] = NULL; /* POSIX 표준: argv 배열의 끝은 NULL 포인터로 종단 */
argc는 전체 인자의 총 개수(바이너리 실행 파일명 포함)입니다.0부터 argc - 1까지이며, argv[argc]는 반드시 NULL이어야 합니다. 따라서 argv 배열의 크기는 최소 argc + 1 이상이어야 합니다.핀토스 커널 메모리 할당자 palloc_get_page()는 4KB(4096 바이트) 단위의 물리 페이지를 반환합니다. 인자 전달 시 커널 스택(한 페이지 4KB)이 오버플로우되는 것을 방지하기 위해 명령행 문자열 복사본은 별도 페이지에 할당해야 합니다.
PGSIZE vs sizeof(PGSIZE) 매크로 연산 오류PGSIZE는 <round.h> 및 <vaddr.h>에 정의된 상수로, 물리 페이지 크기인 4096 (0x1000) 정수 리터럴을 의미합니다.
sizeof(PGSIZE)는 컴파일러에 의해 정수 리터럴 4096의 자료형 크기로 치환됩니다. C 표준에서 4096 리터럴은 int 또는 unsigned int형이므로 sizeof(int) = 4바이트가 계산됩니다.
치명적 오류 시나리오:
/* 잘못된 코드: 4바이트만 복사되어 버퍼 오버런 및 데이터 손실 발생 */
strlcpy(dest, src, sizeof(PGSIZE));
/* 올바른 코드: 4096바이트 한 페이지 전체 크기 지정 */
strlcpy(dest, src, PGSIZE);
sizeof 연산자char buf[512];에 대해 sizeof(buf)를 호출하면 배열의 전체 바이트 크기인 512가 반환됩니다.char *ptr;에 대해 sizeof(ptr)를 호출하면 포인터 변수 자체의 크기인 8바이트(x86-64 기준)가 반환됩니다. 포인터에 sizeof를 적용하여 버퍼 크기를 산출하는 행위는 메모리 오염의 주원인이 됩니다.done)char *fn_copy = palloc_get_page(PAL_ZERO);
if (fn_copy == NULL) {
return TID_ERROR; /* 물리 메모리 고갈에 따른 안전한 종료 */
}
strlcpy(fn_copy, file_name, PGSIZE);
tid_t tid = thread_create(file_name, PRI_DEFAULT, initd, fn_copy);
if (tid == TID_ERROR) {
palloc_free_page(fn_copy); /* 스레드 생성 실패 시 할당받은 페이지 메모리 누수 방지 */
}
프로그램 로딩 완료 후, 유저 프로세스가 최초로 실행(main(int argc, char **argv))될 때 인자들을 메모리에서 읽어올 수 있도록 System V AMD64 ABI 규약에 맞추어 유저 스택(0x47480000)에 데이터를 배치해야 합니다.
[x86-64 유저 스택 프레임(Stack Frame) 상세 메모리 구조]
높은 주소 (USER_STACK: 0x47480000)
+-------------------------------------------------------+
| 인자 문자열 2: "world\0" (6 바이트) |
+-------------------------------------------------------+
| 인자 문자열 1: "hello\0" (6 바이트) |
+-------------------------------------------------------+
| 인자 문자열 0: "/bin/echo\0" (10 바이트) |
+-------------------------------------------------------+ ◄── 문자열 끝 주소
| 스택 정렬 패딩 (Zero Padding: 0~15 바이트) | (%rsp % 16 == 0 정렬)
+-------------------------------------------------------+
| argv[3] = 0x0000000000000000 (8 바이트 NULL 센티넬) |
+-------------------------------------------------------+
| argv[2] = 0x4747FFF2 (8 바이트: "world" 가리킴) |
+-------------------------------------------------------+
| argv[1] = 0x4747FFEC (8 바이트: "hello" 가리킴) |
+-------------------------------------------------------+
| argv[0] = 0x4747FFE2 (8 바이트: "/bin/echo" 가리킴) |
+-------------------------------------------------------+ ◄── argv 시작 주소 (%rsi)
| 가짜 반환 주소 (Fake Return Address: 8 바이트 0x0) |
+-------------------------------------------------------+ ◄── 최종 유저 스택 포인터 (%rsp)
낮은 주소
argv[argc - 1]부터 argv[0] 순서로 문자열 길이(strlen + 1)만큼 스택 포인터(%rsp)를 하향 이동하며 실제 문자 데이터를 복사합니다.uint64_t arg_addr[])에 기록합니다.#GP 발생).0x00 바이트를 채워 %rsp를 8의 배수 혹은 16의 배수로 내림(Round Down)합니다.argv[argc] NULL 종단 포인터 푸시:0x0000000000000000 (8바이트) 값을 스택에 푸시합니다.argv[i] 주소 포인터 역순 푸시:argv[argc - 1]부터 argv[0]까지 8바이트씩 스택에 푸시합니다.main() 함수가 반환될 경우를 대비하여 스택 최하단에 8바이트 크기의 0x0을 푸시합니다. (유저 프로그램 종료 규약).%rdi: 인자 개수 argc 저장.%rsi: argv 배열의 시작 주소(스택 상에서 argv[0] 포인터가 위치한 주소) 저장.사용자 프로세스는 권한이 제한된 Ring 3(User Mode)에서 실행되므로, 파일 입출력이나 프로세스 생성 등 커널 자원을 요구할 때 syscall 기계어 명령어를 통해 Ring 0(Kernel Mode)으로 특권을 전이합니다.
[x86-64 syscall / sysret 하드웨어 특권 전이 파이프라인]
[Ring 3: 유저 프로세스]
mov $SYS_WRITE, %rax ; 시스템 콜 번호 (9)
mov $1, %rdi ; 인자 1 (fd = STDOUT)
mov $buffer, %rsi ; 인자 2 (버퍼 시작 주소)
mov $12, %rdx ; 인자 3 (출력 크기)
syscall ; 하드웨어 특권 전이 실행!
│
├─ 1. CPU 하드웨어가 복귀 주소를 %rcx에 백업 (%rcx = %rip)
├─ 2. CPU 하드웨어가 플래그를 %r11에 백업 (%r11 = %rflags)
├─ 3. MSR(IA32_LSTAR)에 등록된 커널 syscall_entry 주소로 점프
├─ 4. CPL 0 (Ring 0) 전환 및 커널 스택으로 전환
▼
[Ring 0: 커널 공간 - syscall_entry in threads/intr-stubs.S]
├─ 5. intr_frame 구조체 형태로 모든 유저 레지스터를 커널 스택에 푸시 (Context Save)
├─ 6. syscall_handler(struct intr_frame *f) C 함수 호출
│ │
│ ▼
[syscall_handler() in userprog/syscall.c]
├─ 7. 시스템 콜 번호 판별: switch (f->R.rax)
├─ 8. 인자 레지스터 역참조: %rdi, %rsi, %rdx, %r10, %r8, %r9
├─ 9. 유저 포인터 보안 검증 및 작업 수행
└─ 10. 반환값 기록: f->R.rax = result; ([중요] 핵심)
│
▼
[syscall_entry 복귀 루틴]
├─ 11. 커널 스택의 intr_frame 레지스터 복원 (Context Restore)
└─ 12. sysret 기계어 명령어 실행!
├─ %rcx 값을 %rip에 로드하여 유저 코드로 복귀
├─ %r11 값을 %rflags로 복구
└─ CPL 3 (Ring 3) 환원 -> 유저 프로세스 계속 실행
f->R.rax 반환값의 실체%rax 레지스터를 통해 반환값을 넘기므로 return result;를 사용합니다.intr_frame) 컨텍스트 내부에서 동작합니다. 핸들러 함수가 종료된 후 syscall_entry 어셈블리 복귀 코드가 스택에 저장되어 있던 레지스터 값들을 CPU 물리 레지스터로 복원(pop)하므로, 반드시 f->R.rax = result;와 같이 구조체 필드에 반환값을 기록해야 합니다. 일반 return을 수행하면 복구 과정에서 해당 값이 스택의 이전 값으로 덮어씌워져 유저 프로그램이 반환값을 수신하지 못합니다.| 시스템 콜 | 번호 (%rax) | 인자 1 (%rdi) | 인자 2 (%rsi) | 인자 3 (%rdx) | 반환값 (%rax) |
|---|---|---|---|---|---|
SYS_HALT | 0 | 없음 | 없음 | 없음 | 반환 없음 (시스템 종료) |
SYS_EXIT | 1 | int status | 없음 | 없음 | 반환 없음 (프로세스 종료) |
SYS_FORK | 2 | const char *thread_name | 없음 | 없음 | 자식 프로세스 pid_t |
SYS_EXEC | 3 | const char *cmd_line | 없음 | 없음 | 성공 시 미반환, 실패 시 -1 |
SYS_WAIT | 4 | pid_t pid | 없음 | 없음 | 자식의 종료 상태코드 |
SYS_CREATE | 5 | const char *file | unsigned initial_size | 없음 | 성공 여부 (bool) |
SYS_REMOVE | 6 | const char *file | 없음 | 없음 | 성공 여부 (bool) |
SYS_OPEN | 7 | const char *file | 없음 | 없음 | 파일 디스크립터 (fd) |
SYS_FILESIZE | 8 | int fd | 없음 | 없음 | 파일 크기 (int) |
SYS_READ | 9 | int fd | void *buffer | unsigned size | 실제 읽은 바이트 수 |
SYS_WRITE | 10 | int fd | const void *buffer | unsigned size | 실제 쓴 바이트 수 |
SYS_SEEK | 11 | int fd | unsigned position | 없음 | 없음 |
SYS_TELL | 12 | int fd | 없음 | 없음 | 현재 파일 오프셋 |
SYS_CLOSE | 13 | int fd | 없음 | 없음 | 없음 |
커널은 최고 특권 레벨(Ring 0)로 동작하므로, 유저가 전달한 잘못된 포인터를 무단 역참조하면 커널 패닉(Kernel Crash)이 발생하거나, 커널 내부 메모리가 유저에 의해 변조되는 Confused Deputy 보안 취약점에 노출됩니다. 따라서 모든 유저 포인터는 역참조 이전에 커널 수준에서 완벽히 검증되어야 합니다.
[유저 포인터 3단계 보안 검증 파이프라인]
유저 포인터 (void *vaddr)
│
├─ [1단계: NULL 검사]
│ if (vaddr == NULL) -> 즉시 비정상 종료 (exit(-1))
│
├─ [2단계: 가상 주소 경계 검사]
│ if (!is_user_vaddr(vaddr)) -> 즉시 비정상 종료 (exit(-1))
│ (KERN_BASE 0x8004000000 이상의 커널 메모리 주소 침범 차단)
│
└─ [3단계: 물리 페이지 매핑 검사]
if (pml4_get_page(thread_current()->pml4, vaddr) == NULL)
-> 즉시 비정상 종료 (exit(-1))
(페이지 테이블에 매핑되지 않은 세그멘테이션 폴트 주소 차단)
check_address)void check_address(const void *addr) {
struct thread *curr = thread_current();
if (addr == NULL || !is_user_vaddr(addr) || pml4_get_page(curr->pml4, addr) == NULL) {
exit(-1); /* 오류 코드 -1과 함께 프로세스 즉각 종료 */
}
}
check_buffer)유저가 전달하는 I/O 버퍼는 수 바이트에서 수십 킬로바이트에 달할 수 있으며, 여러 4KB 물리 페이지 경계에 걸쳐 존재할 수 있습니다. 버퍼의 시작 주소가 유효하더라도 버퍼 중간이나 끝부분이 매핑되지 않은 메모리에 걸쳐 있다면 커널이 I/O 도중 페이지 폴트를 유발하게 됩니다.
[4KB 페이지 경계를 가로지르는 버퍼 검증 구조]
페이지 0x1000 (매핑 완료) 페이지 0x2000 (미매핑 상태!)
+-------------------------------+-------------------------------+
| | [ Buffer Start | Buffer End ] | |
+-------------------------------+-------------------------------+
▲ ▲
0x1F00 0x2100 (size = 512B)
(유효 주소) (유효하지 않은 주소: 차단 필수!)
void check_buffer(const void *buffer, size_t size, bool to_write) {
/* 1. 포인터 오버플로우 방어: buffer + size가 주소 공간 상한선을 넘는지 검사 */
if (buffer == NULL || !is_user_vaddr(buffer) || !is_user_vaddr(buffer + size)) {
exit(-1);
}
/* 2. 버퍼가 걸쳐 있는 모든 4KB 페이지의 시작 주소를 순회 검증 */
char *ptr = (char *)pg_round_down(buffer);
char *end = (char *)pg_round_down(buffer + size - 1);
while (ptr <= end) {
check_address(ptr);
/* 3. 쓰기 작업(SYS_READ 등)인 경우 페이지가 쓰기 가능한지(Writable) 검증 */
if (to_write) {
/* 가상 메모리 관리(Project 3) 확장 시 pml4 쓰기 플래그 확인 */
}
ptr += PGSIZE;
}
}
파일 입출력 시스템 콜에서 가장 빈번하게 발생하는 개념적 오류는 시스템 콜의 이름(READ/WRITE)과 메모리 버퍼에 요구되는 하드웨어 접근 권한(Access Permission)의 방향을 혼동하는 것입니다.
[SYS_READ vs SYS_WRITE의 데이터 흐름과 버퍼 권한 비교]
1. SYS_WRITE(fd, buffer, size): "데이터를 파일/콘솔로 출력"
[유저 버퍼] ──────────(커널이 데이터를 읽음)──────────► [출력 장치 / 파일]
요구 권한: 버퍼 메모리는 반드시 "읽기 가능(Readable)"해야 함!
(코드 세그먼트와 같은 읽기 전용 상수 메모리도 출력 가능)
2. SYS_READ(fd, buffer, size): "데이터를 파일/키보드에서 입력받음"
[입력 장치 / 파일] ──(커널이 데이터를 써넣음)─────────► [유저 버퍼]
요구 권한: 버퍼 메모리는 반드시 "쓰기 가능(Writable)"해야 함! [중요]
(읽기 전용 페이지(.text)에 read()를 시도하면 커널이 쓰기 예외를 유발하므로 차단 필수)
| 비교 항목 | SYS_WRITE | SYS_READ |
|---|---|---|
| 시스템 콜 목적 | 유저 메모리의 데이터를 파일 또는 화면에 기록 | 파일 또는 키보드 입력을 유저 메모리에 적재 |
| 커널의 버퍼 접근 동작 | 커널이 유저 버퍼를 읽음 (Read) | 커널이 유저 버퍼에 씀 (Write) |
| 필수 유저 버퍼 권한 | 읽기 가능 (Readable) | 쓰기 가능 (Writable) |
| 표준 I/O 스트림 | fd == 1 (STDOUT_FILENO): putbuf() 호출 | fd == 0 (STDIN_FILENO): input_getc() 호출 |
| 파일 디스크립터 처리 | fd >= 2: file_write(file, buffer, size) | fd >= 2: file_read(file, buffer, size) |
핀토스의 기본 파일 시스템(filesys/)은 다중 스레드 환경에서 디스크 블록 할당이나 디렉터리 탐색 시 동시 접근을 제어하는 락 메커니즘을 내장하고 있지 않습니다. 여러 프로세스가 동시에 파일을 생성, 읽기, 쓰기를 수행하면 Inode 구조체 파손 및 디스크 블록 충돌이 발생합니다.
filesys_lock) 설계모든 파일 시스템 관련 시스템 콜 처리는 동기화 락을 통해 임계 구역(Critical Section)을 상호 배제(Mutual Exclusion)해야 합니다.
struct lock filesys_lock;
void syscall_init(void) {
lock_init(&filesys_lock);
/* 시스템 콜 인터럽트 등록 */
}
파일 I/O 도중 유효하지 않은 버퍼 주소 접근 등으로 exit(-1)을 호출할 때 획득했던 filesys_lock을 해제하지 않고 프로세스가 종료되면, 시스템 전체 스레드가 영구적인 데드락에 빠집니다.
/* SYS_READ 구현 예시: 안전한 락 해제 패턴 */
case SYS_READ: {
int fd = f->R.rdi;
void *buffer = (void *)f->R.rsi;
unsigned size = f->R.rdx;
/* 1. 파일 시스템 락을 잡기 전 유저 버퍼 쓰기 권한을 먼저 완벽히 검증 */
check_buffer(buffer, size, true);
if (fd == 0) {
/* 키보드 입력 처리 */
uint8_t *buf = (uint8_t *)buffer;
for (unsigned i = 0; i < size; i++) {
buf[i] = input_getc();
}
f->R.rax = size;
} else if (fd >= 2) {
/* 파일 입력 처리 */
struct file *file = get_file_from_fd(fd);
if (file == NULL) {
f->R.rax = -1;
break;
}
lock_acquire(&filesys_lock);
off_t bytes = file_read(file, buffer, size);
lock_release(&filesys_lock); /* 작업 완료 즉시 락 해제 */
f->R.rax = bytes;
} else {
f->R.rax = -1;
}
break;
}
개발 환경(VS Code, Docker, WSL, Linux Host) 간의 아키텍처 차이로 인해 발생하는 진단 불일치 현상을 정확히 파악해야 불필요한 코드 수정을 방지할 수 있습니다.
-msoft-float, 매크로 정의(#define PGSIZE)를 인식하지 못하여 무해한 코드에도 적색 밑줄(Error Squiggle)을 표시할 수 있습니다.kernel.bin 바이너리를 생성하는 유일한 판정 주체입니다. 코딩 시 에디터 밑줄에 의존하지 않고, 반드시 컨테이너/터미널 환경에서 make 컴파일러 출력 로그를 기준으로 오류를 판별해야 합니다.Clock skew detected 경고의 물리적 원인과 해결발생 원인: Windows 호스트 OS와 Docker/WSL 가상 머신 간의 시스템 시계(RTC)가 동기화되지 않아, 소스코드의 최종 수정 타임스탬프(mtime)가 가상 머신의 현재 시각보다 미래의 시각으로 기록된 경우 GNU Make가 발생시키는 경고입니다.
영향: Make가 파일 간의 수정 선후 관계를 판별하지 못해 증분 빌드가 실패하거나 이전 캐시가 유지됩니다.
해결 방안:
# 1. 터미널 환경에서 모든 파일의 타임스탬프를 현재 시각으로 일괄 동기화
find . -type f -exec touch {} +
# 2. 클린 리빌드 수행
cd userprog/build
make clean && make
답변: SYS_READ 시스템 콜의 물리적 동작은 커널이 디스크 파일이나 콘솔 장치로부터 바이트 스트림을 인출하여 유저가 지정한 메모리 주소(buffer)에 저장하는 작업입니다. 즉, 커널이 유저 메모리 공간에 직접 쓰기(Write) 연산을 수행합니다. 만약 유저 프로그램이 코드 세그먼트(.text)나 읽기 전용 상수 영역(R-X)의 주소를 SYS_READ의 버퍼로 전달했을 때 쓰기 가능 여부를 사전에 검증하지 않으면, 커널이 쓰기 시도를 하는 순간 CPU 하드웨어 예외인 페이지 폴트(#PF)가 발생하여 커널이 비정상 종료됩니다. 따라서 버퍼 공간이 쓰기 가능한 메모리인지 반드시 사전 검증해야 합니다.
답변: syscall 명령어를 통해 유저 모드에서 커널 모드로 진입할 때, syscall_entry 어셈블리 루틴은 유저 프로세스의 실행 상태(모든 범용 레지스터 값)를 커널 스택 상의 struct intr_frame에 푸시하여 보존합니다. 커널 내부의 C 함수인 syscall_handler가 일반 return 문을 사용하면 핸들러 자체의 반환값으로만 사용될 뿐 유저 레지스터 보존 영역에는 영향을 주지 못합니다. 핸들러 반환 후 어셈블리 복귀 루틴이 커널 스택의 intr_frame 레지스터들을 다시 CPU 레지스터로 복원(pop)하여 sysret을 수행하므로, 스택 상에 보존된 유저의 %rax 복원 위치인 f->R.rax에 명시적으로 값을 써넣어야만 유저 프로세스로 복귀했을 때 시스템 콜의 반환값을 올바르게 수신할 수 있습니다.
답변: 인자 문자열은 가변 길이(char[]) 데이터이므로 문자열들을 모두 스택에 푸시한 시점의 스택 포인터(%rsp)는 임의의 바이트 주소에 위치하게 됩니다. x86-64 System V ABI 규약은 함수 진입 및 포인터 참조 시 메모리 버스 정렬을 위해 스택 포인터가 16바이트 경계에 맞추어질 것을 요구합니다. 따라서 가변 데이터 저장이 완료된 직후 0~15바이트의 제로 패딩을 삽입하여 스택 주소를 정렬합니다. 그 위에 푸시되는 argv 배열의 요소들은 고정 크기(8바이트 포인터)이므로 정렬 상태가 유지되며, 프로세스 진입점인 main(int argc, char **argv) 함수가 첫 번째 인자(argv[0])부터 8바이트 단위로 고속 캐시 정렬 참조를 수행할 수 있게 됩니다.
답변: 가상 메모리는 4KB 고정 크기 페이지 단위로 물리 프레임에 매핑됩니다. 유저가 전달한 버퍼의 크기가 수 바이트에서 수십 킬로바이트일 경우, 버퍼의 시작 주소는 매핑된 유효 페이지(예: 0x1FFF)에 위치하지만 버퍼의 나머지 데이터 영역은 매핑되지 않은 다음 페이지(예: 0x2000)에 걸쳐 존재할 수 있습니다. 시작 주소만 검사하고 파일 I/O를 수행하면 커널이 버퍼의 중간 데이터를 읽거나 쓰는 도중 하드웨어 페이지 폴트가 발생하여 커널 패닉이 일어납니다. 따라서 버퍼가 점유하는 시작 페이지부터 끝 페이지까지의 모든 4KB 경계 주소를 순회하며 물리 매핑 및 접근 권한을 전수 검증해야 합니다.
답변: 핀토스의 기본 파일 시스템 코드는 스레드 안전성(Thread-safety)이 확보되어 있지 않습니다. 여러 프로세스가 동시에 file_read()나 file_write()를 호출할 경우 내부의 Inode 메타데이터, 파일 오프셋 포인터, 디스크 블록 할당 테이블(Free Map)을 보호하는 동기화 메커니즘이 없어 경쟁 상태(Race Condition)가 발생합니다. 이로 인해 동일 디스크 블록에 데이터가 중복 기록되거나 파일 시스템 메타데이터가 파손되는 치명적 손상이 일어납니다. 따라서 모든 파일 시스템 API 접근을 전역 filesys_lock으로 상호 배제하여 한 번에 단 하나의 스레드만 파일 시스템 연산을 수행하도록 보장해야 합니다.
