[크래프톤정글/WIL] 8주차 회고: Pintos Project 2 User Programs와 메모리 보호, 인간적 친밀함을 넘어 기술적 협업의 밀도를 향해

코딩이 어려운 사람·2026년 10월 22일

WIL

목록 보기
8/8

소주제

  1. 8주차 학습 개요와 기술 스택: Pintos Project 2 User Programs와 유저 메모리 보안 아키텍처
  2. 내가 직면했던 현실과 한계: 인간적인 친밀함과 업무적 엔지니어링 협업의 불균형
  3. 핵심 기술 개념과 코드 분석: x86-64 System V ABI 스택 프레임(Stack Setup) 6단계 파이프라인
  4. 유저 포인터 3단계 보안 검증(check_address)과 4KB 페이지 경계 검사(check_buffer)
  5. 협업과 소통의 성찰: '친한 팀'을 넘어 '성과를 내는 프로페셔널 엔지니어링 팀'의 기준
  6. 실행 가능한 액션 플랜: 스몰톡과 기술 싱크업의 분리, PR 기반 비동기 코드 리뷰 체계화
  7. 8주차 핵심 엔지니어링 Q&A 3선

1. 8주차 학습 개요와 기술 스택: Pintos Project 2 User Programs와 유저 메모리 보안 아키텍처

8주차는 커널이 유저 프로세스를 안전하게 격리 실행하고 서비스를 제공하는 Pintos Project 2: User Programs를 수행한 주차였습니다. 명령행 문자열을 파싱(strtok_r)하여 x86-64 호출 규약에 맞게 유저 스택을 초기화하는 Argument Passing, CPU 특권 전이(syscall / sysret), 커널 모드 패닉과 메모리 침범을 차단하는 3단계 유저 포인터 검증, 그리고 멀티스레드 동시성 제어를 위한 filesys_lock 상호 배제를 완성했습니다.

[x86-64 시스템 콜 진입 및 유저 포인터 보안 검증 파이프라인]

[Ring 3: 유저 프로세스]
  mov $SYS_WRITE, %rax  ; 시스템 콜 번호
  mov $1, %rdi          ; fd
  mov $buf, %rsi        ; 유저 버퍼 포인터
  syscall               ; 하드웨어 특권 전이
  │
  ▼
[Ring 0: 커널 syscall_entry]
  ├─ intr_frame에 유저 레지스터 저장
  ├─ syscall_handler() 호출
  │      │
  │      ▼ [유저 포인터 보안 검증]
  │      ├─ 1. ptr == NULL 검사
  │      ├─ 2. !is_user_vaddr(ptr) (KERN_BASE 침범 차단)
  │      ├─ 3. pml4_get_page() (물리 페이지 미매핑 차단)
  │      └─ 4. check_buffer() (4KB 페이지 경계 연속 검증)
  │
  └─ f->R.rax = result; 대입 후 sysret 복귀

2. 내가 직면했던 현실과 한계: 인간적인 친밀함과 업무적 엔지니어링 협업의 불균형

"인간적인 대화는 많이 했지만, 업무적인 협업은 많이 못했던 한주였습니다."

정글 과정이 중반을 넘어서며 팀원들과의 인간적인 유대감과 라포(Rapport)는 최고조에 달했습니다. 함께 식사하고 일상을 공유하며 팀 분위기는 더할 나위 없이 따뜻하고 편안했습니다.

하지만 역설적이게도 이 '인간적인 친밀함'이 '업무적인 엔지니어링 협업'의 밀도를 떨어뜨리는 결과를 낳았습니다. 핀토스 Project 2는 시스템 콜 인자 검증, 프로세스 생명주기(fork/wait), 파일 디스크립터 테이블 설계 등 고도의 아키텍처적 합의와 냉철한 코드 리뷰가 요구되는 프로젝트였습니다. 그러나 서로 너무 편해진 탓에 날카로운 기술 토론이나 깐깐한 코드 비판을 주저하게 되었고, 잡담으로 흘러가는 회의가 늘어났습니다. "좋은 사람들과 즐거운 시간을 보낸 것"과 "엔지니어로서 팀의 기술적 성과를 극대화한 것" 사이의 괴리를 실감하며 많은 반성을 남긴 한 주였습니다.


3. 핵심 기술 개념과 코드 분석: x86-64 System V ABI 스택 프레임(Stack Setup) 6단계 파이프라인

유저 프로그램이 실행될 때 명령행 인자를 올바르게 참조할 수 있도록 0x47480000(USER_STACK)부터 시작하여 스택 프레임을 아래로 구축했습니다.

[x86-64 유저 스택 프레임 메모리 레이아웃]

높은 주소 (USER_STACK: 0x47480000)
+-------------------------------------------------------+
| 인자 문자열 데이터 (Right to Left: "world\0", "echo\0") |
+-------------------------------------------------------+
| 16바이트 스택 정렬 제로 패딩 (Zero Padding: 0~15 바이트)| (%rsp % 16 == 0)
+-------------------------------------------------------+
| argv[argc] = 0x0000000000000000 (8바이트 NULL 센티넬)  |
+-------------------------------------------------------+
| argv[i] 주소 포인터 배열 (argv[2], argv[1], argv[0])    |
+-------------------------------------------------------+ ◄── argv 시작 주소 (%rsi)
| 가짜 반환 주소 (Fake Return Address: 8바이트 0x0)       |
+-------------------------------------------------------+ ◄── 최종 유저 스택 포인터 (%rsp)
낮은 주소
  • 16바이트 정렬의 필수성: x86-64 ABI는 함수 진입 시 스택 포인터가 16바이트 경계에 정렬되어 있을 것을 강제합니다. 가변 길이 문자열들을 먼저 푸시한 후, 스택 포인터의 하위 4비트를 검사하여 제로 패딩을 삽입함으로써 정렬 규약을 무결하게 충족했습니다.

4. 유저 포인터 3단계 보안 검증(check_address)과 4KB 페이지 경계 검사(check_buffer)

커널 모드는 Ring 0 슈퍼바이저 권한을 가지므로, 유저가 전달한 잘못된 포인터를 무단 역참조하면 커널 전체가 크래시되는 Denial of Service나 커널 메모리가 유출되는 Confused Deputy 보안 취약점이 발생합니다.

/* 1. 단일 주소 3단계 검증 */
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); /* 위반 시 프로세스 즉각 강제 종료 */
    }
}

/* 2. 4KB 페이지 경계를 가로지르는 멀티페이지 버퍼 검사 */
void check_buffer(const void *buffer, size_t size, bool to_write) {
    if (buffer == NULL || !is_user_vaddr(buffer) || !is_user_vaddr(buffer + size)) {
        exit(-1);
    }
    char *ptr = (char *)pg_round_down(buffer);
    char *end = (char *)pg_round_down(buffer + size - 1);
    while (ptr <= end) {
        check_address(ptr);
        ptr += PGSIZE;
    }
}
  • 버퍼 경계 초과 방어: 버퍼의 시작점(0x1FFF)이 유효하더라도 버퍼의 끝점(0x2100)이 매핑되지 않은 다음 페이지에 걸쳐 있을 때 발생하는 커널 페이지 폴트를 원천 차단했습니다.
  • SYS_READ vs SYS_WRITE의 권한 규약: SYS_WRITE는 커널이 유저 버퍼를 읽으므로 읽기 가능(Readable) 권한을, SYS_READ는 커널이 유저 버퍼에 쓰므로 반드시 쓰기 가능(Writable) 권한을 검증해야 한다는 하드웨어적 접근 방향성을 명확히 정립했습니다.

5. 협업과 소통의 성찰: '친한 팀'을 넘어 '성과를 내는 프로페셔널 엔지니어링 팀'의 기준

8주차의 경험은 '진짜 프로페셔널한 팀워크'가 무엇인지에 대해 깊은 성찰을 던져주었습니다. "인간적인 친밀함은 협업의 훌륭한 윤활유이지만, 그 자체가 팀의 목표가 되어서는 안 된다."

친하고 편안한 분위기는 소통의 장벽을 낮추어 주지만, 자칫 긴장감을 잃고 기술적 타협을 정당화하는 안식처가 되어서는 안 됩니다. 훌륭한 엔지니어링 팀은 인간적인 신뢰를 바탕으로 하되, 설계와 코드 앞에서는 가장 엄격하고 투명하게 서로의 로직을 비판하고 검증할 수 있어야 합니다. 친밀함을 넘어 기술적 협업의 밀도를 높이는 팀 문화를 설계해야 한다는 소중한 기준을 세웠습니다.


6. 실행 가능한 액션 플랜: 스몰톡과 기술 싱크업의 분리, PR 기반 비동기 코드 리뷰 체계화

  1. 회의 시간의 구조적 분리: 팀 미팅 시작 10분은 자유로운 스몰톡으로 아이스브레이킹을 진행하되, 본 회의에 들어가면 명확한 타임키퍼를 두고 기술적 아젠다(설계 합의, 버그 트래킹)에만 철저히 집중하기.
  2. PR 기반 비동기 코드 리뷰 강제: 모든 기능 브랜치(feat/*)는 최소 1명 이상의 승인(LGTM)이 없으면 develop 브랜치에 병합할 수 없도록 브랜치 보호 규칙 설정하기.
  3. 코드 리뷰 체크리스트 작성: 메모리 누수 여부, 에러 처리 시 락 해제 여부, 경계 조건 검증 등 기술적 검증 포인트를 표준 체크리스트로 문서화하여 감정적 피드백이 아닌 기술적 팩트 중심 리뷰 정착시키기.

7. 8주차 핵심 엔지니어링 Q&A 3선

Q1. 시스템 콜 핸들러에서 반환값을 기록할 때 일반 return 대신 f->R.rax에 직접 대입해야 하는 하드웨어적 이유는?

답변: syscall 명령어를 통해 유저 모드에서 커널 모드로 진입할 때, syscall_entry 어셈블리 루틴은 유저 프로세스의 실행 상태(모든 범용 레지스터)를 커널 스택 상의 struct intr_frame에 푸시하여 보존합니다. 커널 내부의 C 함수인 syscall_handler가 일반 return 문을 사용하면 핸들러 자체의 반환값으로만 사용될 뿐 유저 레지스터 보존 영역에는 반영되지 않습니다. 핸들러 종료 후 어셈블리 복귀 루틴이 커널 스택의 intr_frame 레지스터들을 다시 CPU 레지스터로 복원(pop)하여 sysret을 수행하므로, 스택 상에 보존된 유저의 %rax 복원 위치인 f->R.rax에 명시적으로 값을 써넣어야만 유저 프로세스로 복귀했을 때 시스템 콜의 반환값을 올바르게 수신할 수 있습니다.

Q2. SYS_READ 호출 시 유저 버퍼에 대해 읽기 권한이 아닌 쓰기(Writable) 권한을 검증해야 하는 정확한 이유는?

답변: SYS_READ 시스템 콜의 물리적 동작은 커널이 디스크 파일이나 콘솔 장치로부터 바이트 스트림을 인출하여 유저가 지정한 메모리 주소(buffer)에 저장하는 작업입니다. 즉, 커널이 유저 메모리 공간에 직접 쓰기(Write) 연산을 수행합니다. 만약 유저 프로그램이 코드 세그먼트(.text)나 읽기 전용 상수 영역(R-X)의 주소를 SYS_READ의 버퍼로 전달했을 때 쓰기 가능 여부를 사전에 검증하지 않으면, 커널이 쓰기 시도를 하는 순간 CPU 하드웨어 예외인 페이지 폴트(#PF)가 발생하여 커널이 비정상 종료됩니다. 따라서 버퍼 공간이 쓰기 가능한 메모리인지 반드시 사전 검증해야 합니다.

Q3. 핀토스 시스템 콜 핸들러에서 파일 I/O 수행 시 filesys_lock을 획득해야 하는 운영체제 설계상의 이유는?

답변: 핀토스의 기본 파일 시스템 코드는 스레드 안전성(Thread-safety)이 확보되어 있지 않습니다. 여러 프로세스가 동시에 file_read()나 file_write()를 호출할 경우 내부의 Inode 메타데이터, 파일 오프셋 포인터, 디스크 블록 할당 테이블(Free Map)을 보호하는 동기화 메커니즘이 없어 경쟁 상태(Race Condition)가 발생합니다. 이로 인해 동일 디스크 블록에 데이터가 중복 기록되거나 파일 시스템 메타데이터가 파손되는 치명적 손상이 일어납니다. 따라서 모든 파일 시스템 API 접근을 전역 filesys_lock으로 상호 배제하여 한 번에 단 하나의 스레드만 파일 시스템 연산을 수행하도록 보장해야 합니다.


크래프톤 정글 8주차 Pintos User Programs 스택 셋업 및 메모리 검증 아키텍처 다이어그램

profile
그사세가 아닌 내세상으로 만들기

0개의 댓글