본문으로 이동
  • 현재 모든 문서는 작성 중입니다. 보시는 데 불편함이 있더라도 양해 부탁드립니다.

운영체제 기본/보호

~ldmsys
관리자 (토론 | 기여)님의 2026년 11월 1일 (일) 01:49 판 (도입의 보호 규칙 중복 문단 제거, 인용을 보호 설정 설명으로 이동)

프로그램 오류와 보호의 필요성

메모리 보호가 없는 산업용 기기에서 계산기 프로그램과 GPIO 핀에 PWM 모터를 물려 제어를 하는 모터 제어 프로그램이 함께 실행된다고 생각해 보자. 기본적으로 계산기 프로그램은 계산 결과를 자신의 버퍼에 저장하는 역할을 하므로 모터를 제어할 이유가 하나도 없다. 그런데 계산기 프로그램이 취약하여 결과를 저장할 주소가 공격자에 의해 악의적으로 변조되어 버퍼의 주소가 아니라 GPIO 주소가 들어가면 무슨 일이 일어날까?

#define GPIO_OUT1_REG 0x3FF44010

// Usage: add(1, 2, &buffer)
void add(uint32_t a, uint32_t b, volatile uint32_t *result)
{
    *result = a + b;
}
정상 작동 시                   악의적으로 작동하는 경우
add(a, b, &buffer)             add(a, b, (volatile uint32_t *)GPIO_OUT1_REG)
        |                              |
        +---- 같은 *result = a + b ----+
        |                              |
  계산기의 결과 버퍼              PWM 제어 레지스터
        |                              |
   계산 결과 보관                 모터 구동 신호 변경?
                                       |
                                 예기치 않은 기계 동작??

계산값이 모터 제어값으로 쓰이면 기계가 예기치 못한 방식으로 움직이고, 경우에 따라 사람이 다치는 결과를 초래할 수도 있다. 정작 문제를 일으킨 계산기 프로그램은 모터 제어와는 원래 아무 관련이 없었는데도 말이다. 이런 문제를 막으려면 프로그램이 접근할 수 있는 영역을 제한해야 한다. 이것이 보호가 필요한 이유다.

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

커널(Kernel)은 CPU·메모리·장치의 사용을 관리하고 프로그램의 요청을 처리하는 운영체제의 핵심 코드다. 커널 코드와 그 관리 정보도 메모리에 있으므로 일반 프로그램이 마음대로 수정하지 못하게 해야 한다.

일반 프로그램의 명령은 CPU에서 직접 실행된다. 운영체제가 그 사이에 끼어들어 명령 하나, 메모리 접근 하나마다 소프트웨어로 권한을 검사하면 실행 비용이 지나치게 커진다. 따라서 운영체제가 허용 범위와 권한을 설정하고, CPU가 실행 중 그 규칙을 검사하도록 하드웨어의 도움을 받는다. 정상적인 연산은 직접 실행하고, 허용되지 않거나 추가 처리가 필요한 동작에서만 커널로 제어를 넘긴다.[1]

CPU의 권한 모드 (Privilege Mode)는 일반 프로그램과 커널이 실행할 수 있는 명령 및 접근 가능한 제어 상태를 구분한다. x86-64에서는 보통 사용자 프로그램을 현재 특권 수준(Current Privilege Level, CPL) 3, 커널을 CPL 0에서 실행한다. RISC-V RV64의 일반적인 운영체제 구성에서는 사용자 프로그램이 U-mode, 커널이 S-mode를 사용한다. 이름과 단계는 다르지만, 사용자 코드가 시스템 전체의 규칙을 임의로 바꾸지 못하게 한다는 목적은 같다.

메모리 접근에서는 MMU (Memory Management Unit, 메모리 관리 장치)가 운영체제를 돕는다. MMU는 가상 주소를 물리 주소로 바꾸고 접근 권한을 검사하는 하드웨어다. 어느 주소를 연결하고 읽기·쓰기·실행을 어디까지 허용할지는 운영체제가 주소 연결표인 페이지 테이블에 설정한다. MMU는 그 설정을 메모리 접근마다 적용한다. 정책과 설정은 운영체제가 맡고, 반복되는 주소 변환과 검사는 하드웨어가 맡는 분업이다. 프로그램이 다른 주소를 계산해도 허용되지 않은 접근까지 가능해지는 것은 아니다.[2] 페이지 테이블과 변환 결과를 캐시하는 TLB는 가상 메모리에서 이어서 설명한다.

검사에 쓰이는 설정도 보호해야 한다. 사용자 프로그램이 페이지 테이블을 바꾸거나 다른 페이지 테이블을 선택할 수 있다면 MMU가 적용하는 규칙 자체를 고칠 수 있기 때문이다. 페이지 테이블을 담은 메모리의 보호와, 주소 변환을 제어하는 레지스터의 접근 제한이 함께 필요하다.[3]

특권 명령과 장치 접근

특권 명령 (Privileged Instruction)은 CPU의 현재 권한에 따라 실행이 제한되는 명령이다. 사용자 프로그램이 CPU를 멈추거나 예외 처리와 메모리 보호에 필요한 제어 상태를 마음대로 바꾸면 다른 프로그램과 커널도 영향을 받는다. CPU는 이를 실행하기 전에 필요한 권한이 있는지 검사한다.

  • x86-64: CPU를 정지 상태로 만드는 HLT를 CPL 3에서 실행하면 일반 보호 예외 #GP가 발생한다. 명령 자체가 존재하지 않는 경우의 잘못된 명령 예외 #UD와 구별된다.[4]
  • RISC-V RV64: U-mode 프로그램이 mstatus처럼 더 높은 권한을 요구하는 제어·상태 레지스터 (Control and Status Register, CSR)에 접근하면 불법 명령 예외 (Illegal Instruction Exception)가 발생한다. 모든 CSR이 커널 전용인 것은 아니며, 레지스터마다 필요한 권한이 정해져 있다.[5]

장치 접근을 운영체제에 맡기는 목적은 응용 프로그램의 하드웨어 사용을 무작정 금지하는 데 있지 않다. 이 설계에서는 커널을 시스템 전체의 장치 사용을 조정하는 신뢰된 코드로 두고, 응용 프로그램의 요청을 검증하여 허용된 동작을 수행하도록 권한을 맡긴다. 커널과 드라이버는 요청한 값과 범위, 장치의 상태와 제어 순서, 다른 작업과의 충돌을 확인한다. 응용 프로그램은 정해진 요청 경로로 장치를 사용하며, 필요하면 운영체제가 지정한 일부 장치 영역에 직접 접근할 권한을 받을 수도 있다.[1]

CPU는 그 장치 조작의 의미가 안전한지 판단하지 않는다. CPU의 역할은 일반 프로그램이 운영체제의 판단을 우회하여 제한된 명령이나 장치 영역에 접근하지 못하도록 경계를 지키는 것이다. 이 분업은 커널과 드라이버가 검증을 올바르게 수행한다는 신뢰를 전제로 하므로, 그 코드의 오류까지 하드웨어 권한 검사가 막아 주지는 않는다.

장치에 직접 명령을 내리는 경로도 제한해야 한다. x86의 포트 입출력 IN·OUT은 CPU가 I/O 권한 수준(I/O Privilege Level, IOPL)과 필요한 경우 태스크 상태 세그먼트(Task State Segment, TSS)에 연결된 포트별 허용 비트맵을 검사하며, 허용되지 않은 접근은 #GP로 막는다. 따라서 이 명령들이 무조건 커널 모드에서만 실행된다고 단정하기보다, 운영체제가 설정한 I/O 권한을 CPU가 강제한다고 보는 것이 정확하다.[6]

메모리 매핑 입출력 (Memory-Mapped I/O, MMIO)은 장치 레지스터를 주소로 접근한다. 운영체제가 사용자 주소 공간에 해당 장치 영역을 매핑하지 않거나 필요한 권한을 주지 않으면 일반 프로그램은 그 경로로 장치를 제어할 수 없다. 허용된 일부 영역만 매핑해 주는 구성도 가능하다. 포트 입출력과 MMIO의 차이는 장치 제어와 입출력, 구체적인 매핑은 MMIO 장치 매핑으로 이어진다.

보호 위반과 예외

CPU가 명령어를 실행하다가 별도의 처리가 필요한 조건을 발견하여 정해진 처리 코드로 제어를 넘기는 일을 예외 (Exception)라고 한다. 허용되지 않은 메모리 접근과 특권 명령 실행은 모두 실행 중인 명령에 따라 동기적으로 발생하는 예외의 원인이 될 수 있다. 장치가 현재 명령어와 무관하게 보내는 하드웨어 인터럽트와 구별된다.[6]

x86에서 페이지 매핑이 없거나 페이지 접근 권한을 위반하면 페이지 폴트 (Page Fault, #PF)가 발생한다. 일반 데이터 접근의 주소 형태가 x86-64의 규칙에 맞지 않는 경우나 앞에서 본 HLT 권한 위반은 일반 보호 예외 (General-Protection Exception, #GP)의 원인이 된다. 보호 위반이 모두 메모리 문제인 것도, 모든 메모리 문제가 #PF인 것도 아니다.[6]

x86에서는 CPU가 #PF의 벡터 14 또는 #GP의 벡터 13으로 인터럽트 서술자 테이블 (Interrupt Descriptor Table, IDT)의 게이트를 선택하고 커널 진입 코드로 제어를 넘긴다. RV64에서는 IDT 대신 트랩 벡터와 원인 레지스터를 사용한다. 사용자 프로그램의 트랩을 S-mode 커널이 받는 구성에서는 stvec으로 진입하고 scause 등에서 원인을 확인한다. 구체적인 진입 방식이 달라도, 하드웨어가 원인을 알리고 운영체제가 처리하는 경계는 같다.[7]

운영체제: 권한 모드·페이지 테이블·I/O 권한 설정
                         |
                         v
사용자 명령 실행 → CPU/MMU가 설정된 권한 검사
                         |
             +-----------+-----------+
             |                       |
          허용된 동작             권한 위반 등
             |                       |
          직접 실행              예외 → 커널 처리

예외를 받았다고 곧바로 프로그램을 종료하지는 않는다. 유효한 주소인데 필요한 페이지가 아직 준비되지 않았거나, 쓰기 시 복사 때문에 쓰기 권한을 잠시 막아 둔 경우에는 커널이 매핑을 고치고 명령을 다시 실행할 수 있다. 자세한 복구 과정은 페이지 폴트의 처리에서 다룬다.

반대로 허용되지 않은 사용자 접근을 복구할 수 없다면 운영체제는 프로그램에 오류를 통지한다. Linux에서는 잘못된 메모리 접근에 SIGSEGV 같은 시그널 (Signal)을 보낼 수 있으며, 기본 처리에 따르면 프로세스가 종료된다. CPU가 직접 시그널을 보내거나 프로그램을 죽이는 것은 아니다. 예외를 어느 시그널로 바꿀지는 아키텍처와 운영체제의 처리 규칙에 달려 있다.[8]

시그널은 보호 위반 전용 통지가 아니다. 종료 요청이나 자식 프로세스의 상태 변화도 알릴 수 있다. 발생·전달·처리와 다른 통신 방식의 관계는 프로세스 간 통신에서 설명한다. IDT를 통한 진입과 복귀는 장치 제어와 입출력의 IDT 절로 이어진다.

시스템 콜과 요청 검증

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

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

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

격리와 협력의 비용

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

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

하드웨어가 접근 경계를 지켜도, 쓰기가 허용된 메모리에 잘못된 계산 결과를 저장하는 것까지 막지는 않는다. 공유 영역에서 두 스레드가 같은 값을 동시에 갱신하는 문제도 남는다. 하드웨어 보호는 실행과 접근의 경계를 강제하고, 그 경계 안의 계산과 동기화는 프로그램이 올바르게 구성해야 한다.

참고 문헌

  1. ↑ 1.0 1.1 MIT xv6: Operating system organization, 사용자 모드·커널 모드와 하드웨어 격리.
  2. ↑ Linux 커널 문서: Page Tables, 주소 변환과 페이지 접근 권한.
  3. ↑ Jerome H. Saltzer·Michael D. Schroeder, The Protection of Information in Computer Systems (1975), §I.
  4. ↑ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 2A, HLT, Protected Mode Exceptions.
  5. ↑ RISC-V Privileged Architecture: Control and Status Registers, CSR 접근 권한과 불법 명령 예외.
  6. ↑ 6.0 6.1 6.2 Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, 예외 처리, 특권 수준 및 I/O 보호.
  7. ↑ RISC-V Privileged Architecture: Supervisor-Level ISA, stvec·scause와 예외 처리.
  8. ↑ Linux man-pages: signal(7), signal dispositions 및 hardware exception에서 발생하는 시그널.
  9. ↑ xv6: Traps and system calls.