운영체제 기본/보호

관리자 (토론 | 기여)님의 2026년 10월 31일 (토) 07:45 판 (메모리 접근에서 #PF·#GP, IDT 진입과 커널의 예외 처리까지 연결)

목차 · 이전: 추상화 · 다음: 장치 제어와 입출력

약속을 지키지 않는 프로그램

다음 C 코드는 유효한 객체를 가리키지 않는 포인터로 값을 쓰려는 버그다.

*(int *)NULL = 1;

NULL에 int *라는 형식을 붙여도 값을 저장할 공간이 생기지는 않는다. C에서는 널 포인터 역참조의 동작을 정의하지 않는다. 따라서 이 코드가 반드시 특정 기계 명령을 실행하거나 정해진 오류로 종료한다고 보장할 수는 없다.[1]

실제 쓰기 명령이 실행되는 경우, 일반적인 메모리 보호 환경에서는 대상의 매핑과 쓰기 권한을 검사한다. 유효한 매핑이나 권한이 없으면 접근이 차단된다. 포인터 값을 만들어 냈다고 다른 프로세스의 메모리에 접근할 권한까지 얻는 것은 아니다.[2]

반대로 프로그램들이 같은 실제 메모리를 자유롭게 건드릴 수 있다면, 잘못된 포인터 쓰기가 다른 프로그램의 데이터나 운영체제의 관리 정보에 도달할 수 있다. NULL에 쓰는 이 한 줄이 반드시 다른 프로세스를 덮어쓴다는 뜻은 아니다. 잘못된 주소에 대한 쓰기를 누가, 어디서 막는가가 보호의 문제다. 올바른 코드를 작성하라는 약속만으로는 실수나 악의적인 접근을 차단할 수 없다.

보호는 프로그램이 접근할 수 있는 자원과 허용된 연산에 경계를 두고 이를 강제하는 일이다. 파일이나 주소 공간이라는 추상화를 제공하더라도 프로그램이 그 아래의 저장장치나 다른 주소 공간에 직접 접근할 수 있다면 경계는 무너진다. 따라서 자원을 관리하는 운영체제에는 요청을 거부할 수단과, 프로그램이 그 수단을 우회하지 못하게 하는 구조가 함께 필요하다.

보호 규칙 자체도 보호해야 한다. 프로그램이 자신의 접근 권한을 기록한 메모리를 마음대로 수정할 수 있다면 권한 검사는 의미가 없어진다. 규칙을 집행하는 코드와 상태를 보호 대상과 분리해야 한다. [3]

실행 권한, 주소, 자원의 경계

하드웨어의 권한 모드를 사용하는 운영체제에서는 일반 프로그램을 제한된 권한으로 실행하고, 메모리 매핑이나 장치 제어처럼 시스템 전체에 영향을 주는 동작은 커널이 담당한다. 사용자 모드에서도 산술 연산과 허용된 메모리 접근은 CPU가 직접 실행한다. 모든 명령을 커널이 대신 실행하면 보호를 위해 지나치게 큰 비용을 치르게 된다.

주소 공간은 접근 가능한 메모리의 범위를 나눈다. MMU(Memory Management Unit, 메모리 관리 장치)는 CPU가 사용하는 가상 주소를 물리 주소로 변환하고, 그 주소에 대한 읽기·쓰기·실행이 허용되는지 검사하는 하드웨어다. 운영체제가 주소의 연결 관계와 접근 권한을 설정하면 MMU가 메모리 접근에 그 규칙을 적용한다. 운영체제가 모든 메모리 접근을 소프트웨어로 하나씩 감시하는 방식은 아니다. 프로그램이 다른 주소를 계산했다고 해서 해당 메모리에 접근할 권한까지 생기지는 않는다. 허용되지 않은 접근을 발견하면 CPU는 아래에서 설명하는 예외 처리 경로로 제어를 넘긴다. 주소 변환 정보를 저장하는 페이지 테이블과 변환 결과를 캐시하는 TLB는 가상 메모리에서 다룬다.

자원별 권한은 별도로 판단한다. 사용자 모드라는 이유만으로 모든 파일 접근이 금지되는 것은 아니며, 어떤 파일을 읽을 수 있다고 해서 수정도 허용되는 것은 아니다. CPU 권한 모드는 어떤 종류의 동작을 직접 실행할 수 있는지, 주소 공간은 어떤 메모리에 접근할 수 있는지, 자원 권한은 어떤 대상에 어떤 연산을 허용할지를 나눈다. [4]

보호 위반과 예외

CPU가 명령어를 실행하다가 별도의 처리가 필요한 조건을 발견하여 정해진 처리 코드로 제어를 넘기는 일을 예외(exception)라고 한다. 여기서 다루는 메모리 접근 예외는 해당 명령어의 실행에 따라 동기적으로 발생한다. 장치가 현재 명령어와 무관하게 보내는 하드웨어 인터럽트와 구별되는 점이다.[5]

x86에서 메모리 접근에 필요한 페이지 매핑이 없거나 페이지 접근 권한을 위반하면 페이지 폴트(page fault, #PF)가 발생한다. 주소의 형태 자체가 x86-64의 규칙에 맞지 않는 경우처럼 일반 보호 예외(general-protection exception, #GP)가 발생하는 경우도 있다. 즉, 잘못된 메모리 접근을 모두 #PF라고 부르는 것은 아니다.[5]

CPU는 #PF의 벡터 14 또는 #GP의 벡터 13으로 인터럽트 서술자 테이블(Interrupt Descriptor Table, IDT)의 게이트를 선택하고, 그 항목이 가리키는 커널 진입 코드로 실행을 넘긴다. 커널의 예외 처리 코드가 원인과 복구 가능성을 판단한다. 허용되지 않은 사용자 프로그램의 접근이라면 운영체제는 오류를 알리고, 처리되지 않은 오류는 보통 프로세스 종료로 이어진다. CPU가 직접 프로그램을 죽이는 것은 아니다.[5]

반면 #PF가 항상 잘못된 프로그램을 뜻하지는 않는다. 유효한 주소에 필요한 페이지를 아직 준비하지 않았을 뿐이라면 운영체제가 이를 마련하고 실행을 재개할 수 있다.[6] 이처럼 복구할 수 있는 경우는 가상 메모리의 페이지 폴트 처리에서 다룬다. IDT를 통한 진입과 복귀의 구체적인 과정은 장치 제어와 입출력의 IDT 절에서 이어서 설명한다.

경계를 넘는 요청도 검증해야 한다

격리된 프로그램도 파일을 읽고 다른 프로그램과 통신해야 한다. 시스템 호출은 프로그램이 정해진 진입점을 통해 커널에 작업을 요청하는 경로다. 하드웨어가 제어를 커널로 넘기면 커널 코드가 요청을 처리하고 결과를 돌려준다. 진입·복귀 과정은 장치 제어와 입출력을 참조한다. 호출자는 요청할 작업과 인자를 고를 수 있지만 커널에서 실행할 임의의 명령을 지정할 수는 없다.

예를 들어 데이터를 읽어 달라는 요청에는 대상, 결과를 저장할 주소, 최대 길이가 들어간다. 커널은 그 대상을 읽을 권한이 있는지와 결과를 돌려줄 메모리 범위가 유효한지 확인해야 한다. 주소와 길이를 합산할 때 정수 범위를 넘는 경우도 고려해야 한다. 호출자가 넘긴 숫자나 포인터를 신뢰하면 커널이 자신의 권한으로 금지된 접근을 대신 수행할 수 있다. [7]

검사 시점과 사용 시점 사이의 변화도 문제다. 다른 스레드가 인자가 담긴 메모리를 바꿀 수 있다면, 검사한 내용과 실제 처리한 내용이 달라질 수 있다. 필요한 값을 커널의 관리 영역에 복사하거나, 대상의 수명을 유지하고 동기화하는 등 처리 과정 내내 가정이 유지되도록 만들어야 한다.

격리와 협력의 비용

경계를 넘을 때는 실행 상태를 보존하고 복원하며 인자와 권한을 확인하는 비용이 든다. 데이터를 복사해야 할 수도 있다. 다만 커널에 진입했다고 반드시 다른 스레드로 문맥 교환하는 것은 아니다. 요청을 바로 끝내면 같은 스레드가 돌아오고, 기다려야 한다면 다른 스레드에 CPU를 넘길 수 있다.

이미지 처리 기능을 별도 프로세스에 두면 그 기능의 잘못된 메모리 쓰기가 편집기의 메모리를 직접 훼손하는 일을 막을 수 있다. 대신 큰 이미지와 결과를 주고받는 통신 비용이 생긴다. 공유 메모리로 복사를 줄이면 같은 영역에 대한 동기화와 변경 권한을 관리해야 한다. 경계를 나누는 단위와 데이터를 교환하는 방식은 함께 설계해야 한다.

최소 권한 원칙은 각 구성 요소에 필요한 자원과 연산만 허용하여 오류나 침해의 피해 범위를 줄인다. 그러나 허용된 출력 파일에 잘못된 결과를 쓰는 것까지 막아 주지는 않는다. 보호는 프로그램의 정확성, 공유 데이터의 동시성, 장애 후 복구를 모두 보장하는 말이 아니다. 어떤 실패를 경계 안에 가두고 어떤 협력을 허용할지 정하는 설계다.

참고 문헌

  1. ↑ C11 위원회 초안 N1570, 6.5.3.2 문단 4와 각주 102.
  2. ↑ Linux 커널 문서: Page Tables, 주소 변환과 페이지 접근 권한.
  3. ↑ Jerome H. Saltzer·Michael D. Schroeder, The Protection of Information in Computer Systems (1975), §I.
  4. ↑ xv6: Operating system organization.
  5. ↑ 5.0 5.1 5.2 Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, 2016년 9월, 6.1~6.3절, 6.12절 및 6.15절의 General-Protection Exception과 Page-Fault Exception.
  6. ↑ Linux 커널 문서, Page Tables, Page Faults.
  7. ↑ xv6: Traps and system calls.

목차 · 이전: 추상화 · 다음: 장치 제어와 입출력