운영체제 기본/보호: 두 판 사이의 차이
출처·문체 정리: 커널 원문은 git.kernel.org, em dash는 콜론으로 변경 |
보호 필요성: RV64 보드의 GPIO 전원 제어 예시로 MMIO 접근의 시스템 전체 영향 설명 |
||
| (같은 사용자의 중간 판 16개는 보이지 않습니다) | |||
| 1번째 줄: | 1번째 줄: | ||
[[운영체제 기본| | {{이전 다음 문서 | ||
|이전=← 이전 문서<br />[[운영체제 기본/추상화|추상화]] | |||
|목차=[[운영체제 기본#총론|총론 목차]] | |||
|다음=다음 문서 →<br />[[운영체제 기본/장치 제어와 입출력|장치 제어와 입출력]] | |||
}} | |||
== 프로그램 오류와 보호의 필요성 == | |||
RV64 CPU를 사용하는 어떤 보드에서 '''GPIO (General-Purpose Input/Output, 범용 입출력)'''의 출력선 하나가 전원 제어 회로에 연결되어 있다고 하자. 이 보드는 그 선의 값을 0으로 바꾸면 전원이 끊기도록 설계되어 있다. GPIO 제어 레지스터는 주소 공간에 배치되어 있어, 해당 주소에 값을 쓰면 RAM의 데이터가 아니라 장치의 상태가 바뀐다. 이런 방식을 '''메모리 매핑 입출력(Memory-Mapped I/O, MMIO)'''이라고 한다. GPIO를 이용한 시스템 전원 제어는 실제로도 쓰인다.<ref name="gpio-poweroff">Linux 커널 문서, [https://cdn.kernel.org/doc/html/latest/driver-api/gpio/drivers-on-gpio.html Subsystem drivers using GPIO], poweroff-gpio.</ref> | |||
프로그램이 그 레지스터에 제한 없이 접근할 수 있는 환경이라면 다음 쓰기 한 번으로 기기의 전원을 끊을 수 있다. <code>POWER_GPIO_OUTPUT</code>은 이 예의 보드에서 정한 레지스터 주소를 뜻한다. GPIO의 주소와 전원 회로 연결은 RV64 명령어 집합이 아니라 보드 설계가 정한다. | |||
<pre> | |||
#include <stdint.h> | |||
void mistaken_write(void) | |||
{ | |||
volatile uint32_t *power_gpio = | |||
(volatile uint32_t *)(uintptr_t)POWER_GPIO_OUTPUT; | |||
*power_gpio = 0; | |||
} | |||
</pre> | |||
<pre> | |||
프로그램의 메모리 쓰기 | |||
| | |||
v | |||
GPIO 제어 레지스터 → 출력선 변경 → 전원 제어 회로 → 기기 전원 차단 | |||
</pre> | |||
프로그램 입장에서는 평범한 메모리 쓰기지만, 결과는 변수 하나의 변경으로 끝나지 않는다. 저장 중이던 데이터가 손실되거나, 전원 순서와 전압을 제어하는 회로에서는 잘못된 조작이 장치 손상으로 이어질 수도 있다. 결과는 연결된 회로에 달려 있다. 장치뿐 아니라 운영체제 코드가 있는 RAM을 덮어써도 시스템 전체가 멈출 수 있다. '''프로그램의 실수가 다른 프로그램과 기기 전체의 피해로 번지지 않게 하는 것'''이 보호가 필요한 이유다. | |||
'''보호'''는 이런 접근이 대상에 도달하기 전에 프로그램의 실행·접근 경계를 강제하는 일이다. 운영체제는 일반 프로그램이 장치 레지스터나 커널 메모리를 직접 쓰지 못하게 설정하고, CPU는 실행 중 그 설정을 검사한다. 프로그램은 장치를 관리하는 커널 코드에 작업을 요청해야 한다. RISC-V에서는 페이지 단위 접근 권한과 물리 메모리 보호(PMP) 같은 하드웨어 수단으로 접근을 제한할 수 있다. 단순히 페이징을 끈다고 모든 보호가 사라지는 것은 아니다.<ref name="riscv-pmp">RISC-V, [https://docs.riscv.org/reference/isa/priv/machine.html Machine-Level ISA], Physical Memory Protection.</ref> | |||
보호 규칙 자체도 보호해야 한다. 프로그램이 자신의 접근 권한을 기록한 메모리를 마음대로 수정할 수 있다면 권한 검사는 의미가 없어진다. 규칙을 집행하는 코드와 상태를 보호 대상과 분리해야 한다.<ref name="source-1">[https://web.mit.edu/saltzer/www/publications/protection/Basic.html Jerome H. Saltzer·Michael D. Schroeder, The Protection of Information in Computer Systems (1975), §I].</ref> | |||
== | == 실행 권한, 주소, 자원의 경계 == | ||
커널(Kernel)은 CPU·메모리·장치의 사용을 관리하고 프로그램의 요청을 처리하는 운영체제의 핵심 코드다. 커널 코드와 그 관리 정보도 메모리에 있으므로 일반 프로그램이 마음대로 수정하지 못하게 해야 한다. | |||
이 차이는 시스템 | 일반 프로그램의 명령은 CPU에서 직접 실행된다. 운영체제가 그 사이에 끼어들어 명령 하나, 메모리 접근 하나마다 소프트웨어로 권한을 검사하면 실행 비용이 지나치게 커진다. 따라서 '''운영체제가 허용 범위와 권한을 설정하고, CPU가 실행 중 그 규칙을 검사하도록 하드웨어의 도움을 받는다.''' 정상적인 연산은 직접 실행하고, 허용되지 않거나 추가 처리가 필요한 동작에서만 커널로 제어를 넘긴다.<ref name="source-2">[https://mit-pdos.github.io/xv6-riscv-book/first.html MIT xv6: Operating system organization], 사용자 모드·커널 모드와 하드웨어 격리.</ref> | ||
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는 그 설정을 메모리 접근마다 적용한다. '''정책과 설정은 운영체제가 맡고, 반복되는 주소 변환과 검사는 하드웨어가 맡는 분업'''이다. 프로그램이 다른 주소를 계산해도 허용되지 않은 접근까지 가능해지는 것은 아니다.<ref name="page-protection">[https://docs.kernel.org/mm/page_tables.html Linux 커널 문서: Page Tables], 주소 변환과 페이지 접근 권한.</ref> 페이지 테이블과 변환 결과를 캐시하는 TLB는 [[운영체제 기본/가상 메모리#페이지 테이블과 주소 변환|가상 메모리]]에서 이어서 설명한다. | |||
검사에 쓰이는 설정도 보호해야 한다. 사용자 프로그램이 페이지 테이블을 바꾸거나 다른 페이지 테이블을 선택할 수 있다면 MMU가 적용하는 규칙 자체를 고칠 수 있기 때문이다. 페이지 테이블을 담은 메모리의 보호와, 주소 변환을 제어하는 레지스터의 접근 제한이 함께 필요하다. | |||
== 특권 명령과 장치 접근 == | |||
'''특권 명령 (Privileged Instruction)'''은 CPU의 현재 권한에 따라 실행이 제한되는 명령이다. 사용자 프로그램이 CPU를 멈추거나 예외 처리와 메모리 보호에 필요한 제어 상태를 마음대로 바꾸면 다른 프로그램과 커널도 영향을 받는다. CPU는 이를 실행하기 전에 필요한 권한이 있는지 검사한다. | |||
* '''x86-64''': CPU를 정지 상태로 만드는 <code>HLT</code>를 CPL 3에서 실행하면 일반 보호 예외 <code>#GP</code>가 발생한다. 명령 자체가 존재하지 않는 경우의 잘못된 명령 예외 <code>#UD</code>와 구별된다.<ref name="intel-hlt">Intel, [https://cdrdv2-public.intel.com/812383/253666-sdm-vol-2a.pdf Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 2A], HLT, Protected Mode Exceptions.</ref> | |||
* '''RISC-V RV64''': U-mode 프로그램이 <code>mstatus</code>처럼 더 높은 권한을 요구하는 제어·상태 레지스터 (Control and Status Register, CSR)에 접근하면 불법 명령 예외 (Illegal Instruction Exception)가 발생한다. 모든 CSR이 커널 전용인 것은 아니며, 레지스터마다 필요한 권한이 정해져 있다.<ref name="riscv-csr">[https://docs.riscv.org/reference/isa/priv/priv-csrs.html RISC-V Privileged Architecture: Control and Status Registers], CSR 접근 권한과 불법 명령 예외.</ref> | |||
장치에 직접 명령을 내리는 경로도 제한해야 한다. x86의 포트 입출력 <code>IN</code>·<code>OUT</code>은 CPU가 I/O 권한 수준(I/O Privilege Level, IOPL)과 필요한 경우 태스크 상태 세그먼트(Task State Segment, TSS)에 연결된 포트별 허용 비트맵을 검사하며, 허용되지 않은 접근은 <code>#GP</code>로 막는다. 따라서 이 명령들이 무조건 커널 모드에서만 실행된다고 단정하기보다, '''운영체제가 설정한 I/O 권한을 CPU가 강제한다'''고 보는 것이 정확하다.<ref name="intel-exception" /> | |||
메모리 매핑 입출력 (Memory-Mapped I/O, MMIO)은 장치 레지스터를 주소로 접근한다. 운영체제가 사용자 주소 공간에 해당 장치 영역을 매핑하지 않거나 필요한 권한을 주지 않으면 일반 프로그램은 그 경로로 장치를 제어할 수 없다. 허용된 일부 영역만 매핑해 주는 구성도 가능하다. 포트 입출력과 MMIO의 차이는 [[운영체제 기본/장치 제어와 입출력|장치 제어와 입출력]], 구체적인 매핑은 [[운영체제 기본/Linux의 메모리 매핑/MMIO 장치 매핑|MMIO 장치 매핑]]으로 이어진다. | |||
== 보호 위반과 예외 == | |||
CPU가 명령어를 실행하다가 별도의 처리가 필요한 조건을 발견하여 정해진 처리 코드로 제어를 넘기는 일을 '''예외 (Exception)'''라고 한다. 허용되지 않은 메모리 접근과 특권 명령 실행은 모두 실행 중인 명령에 따라 동기적으로 발생하는 예외의 원인이 될 수 있다. 장치가 현재 명령어와 무관하게 보내는 하드웨어 인터럽트와 구별된다.<ref name="intel-exception">Intel, [https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A], 예외 처리, 특권 수준 및 I/O 보호.</ref> | |||
x86에서 페이지 매핑이 없거나 페이지 접근 권한을 위반하면 '''페이지 폴트 (Page Fault, #PF)'''가 발생한다. 일반 데이터 접근의 주소 형태가 x86-64의 규칙에 맞지 않는 경우나 앞에서 본 <code>HLT</code> 권한 위반은 '''일반 보호 예외 (General-Protection Exception, #GP)'''의 원인이 된다. 보호 위반이 모두 메모리 문제인 것도, 모든 메모리 문제가 #PF인 것도 아니다.<ref name="intel-exception" /> | |||
x86에서는 CPU가 #PF의 벡터 14 또는 #GP의 벡터 13으로 '''인터럽트 서술자 테이블 (Interrupt Descriptor Table, IDT)'''의 게이트를 선택하고 커널 진입 코드로 제어를 넘긴다. RV64에서는 IDT 대신 트랩 벡터와 원인 레지스터를 사용한다. 사용자 프로그램의 트랩을 S-mode 커널이 받는 구성에서는 <code>stvec</code>으로 진입하고 <code>scause</code> 등에서 원인을 확인한다. 구체적인 진입 방식이 달라도, 하드웨어가 원인을 알리고 운영체제가 처리하는 경계는 같다.<ref name="riscv-trap">[https://docs.riscv.org/reference/isa/priv/supervisor.html RISC-V Privileged Architecture: Supervisor-Level ISA], stvec·scause와 예외 처리.</ref> | |||
<pre> | |||
운영체제: 권한 모드·페이지 테이블·I/O 권한 설정 | |||
| | |||
v | |||
사용자 명령 실행 → CPU/MMU가 설정된 권한 검사 | |||
| | |||
+-----------+-----------+ | |||
| | | |||
허용된 동작 권한 위반 등 | |||
| | | |||
직접 실행 예외 → 커널 처리 | |||
</pre> | |||
예외를 받았다고 곧바로 프로그램을 종료하지는 않는다. 유효한 주소인데 필요한 페이지가 아직 준비되지 않았거나, 쓰기 시 복사 때문에 쓰기 권한을 잠시 막아 둔 경우에는 커널이 매핑을 고치고 명령을 다시 실행할 수 있다. 자세한 복구 과정은 [[운영체제 기본/가상 메모리#페이지 폴트의 처리|페이지 폴트의 처리]]에서 다룬다. | |||
반대로 허용되지 않은 사용자 접근을 복구할 수 없다면 운영체제는 프로그램에 오류를 통지한다. Linux에서는 잘못된 메모리 접근에 <code>SIGSEGV</code> 같은 '''시그널 (Signal)'''을 보낼 수 있으며, 기본 처리에 따르면 프로세스가 종료된다. CPU가 직접 시그널을 보내거나 프로그램을 죽이는 것은 아니다. 예외를 어느 시그널로 바꿀지는 아키텍처와 운영체제의 처리 규칙에 달려 있다.<ref name="signal-overview">[https://man7.org/linux/man-pages/man7/signal.7.html Linux man-pages: signal(7)], signal dispositions 및 hardware exception에서 발생하는 시그널.</ref> | |||
시그널은 보호 위반 전용 통지가 아니다. 종료 요청이나 자식 프로세스의 상태 변화도 알릴 수 있다. 발생·전달·처리와 다른 통신 방식의 관계는 [[운영체제 기본/프로세스 간 통신#시그널과 사건 통지|프로세스 간 통신]]에서 설명한다. IDT를 통한 진입과 복귀는 [[운영체제 기본/장치 제어와 입출력#처리 진입점과 IDT|장치 제어와 입출력의 IDT 절]]로 이어진다. | |||
== 시스템 콜과 요청 검증 == | |||
격리된 프로그램도 파일을 읽고 다른 프로그램과 통신해야 한다. '''시스템 콜(System Call)'''은 프로그램이 정해진 진입점을 통해 커널에 작업을 요청하는 경로다. 하드웨어가 제어를 커널로 넘기면 커널 코드가 요청을 처리하고 결과를 돌려준다. 진입·복귀 과정은 [[운영체제 기본/장치 제어와 입출력#진입, 상태 보존, 복귀|장치 제어와 입출력]]을 참조한다. 호출자는 요청할 작업과 인자를 고를 수 있지만 커널에서 실행할 임의의 명령을 지정할 수는 없다. | |||
예를 들어 데이터를 읽어 달라는 요청에는 결과를 저장할 사용자 주소와 최대 길이가 들어간다. 커널은 그 주소 범위가 사용자 공간에 속하고 요청한 방향으로 접근 가능한지 확인해야 한다. 주소와 길이를 합산할 때 정수 범위를 넘는 경우도 고려해야 한다. 호출자가 넘긴 숫자나 포인터를 신뢰하면 커널이 자신의 권한으로 금지된 접근을 대신 수행할 수 있다. <ref name="source-3">[https://mit-pdos.github.io/xv6-riscv-book/trap.html xv6: Traps and system calls].</ref> | |||
검사 시점과 사용 시점 사이의 변화도 문제다. 다른 스레드가 인자가 담긴 메모리를 바꿀 수 있다면, 검사한 내용과 실제 처리한 내용이 달라질 수 있다. 필요한 값을 커널의 관리 영역에 복사하거나, 대상의 수명을 유지하고 동기화하는 등 처리 과정 내내 가정이 유지되도록 만들어야 한다. | |||
== 격리와 협력의 비용 == | |||
경계를 넘을 때는 실행 상태를 보존하고 복원하며 인자와 권한을 확인하는 비용이 든다. 데이터를 복사해야 할 수도 있다. 다만 커널에 진입했다고 반드시 다른 스레드로 컨텍스트 스위치(Context Switch)를 하는 것은 아니다. 요청을 바로 끝내면 같은 스레드가 돌아오고, 기다려야 한다면 다른 스레드에 CPU를 넘길 수 있다. | |||
이미지 처리 기능을 별도 프로세스에 두면 그 기능의 잘못된 메모리 쓰기가 편집기의 메모리를 직접 훼손하는 일을 막을 수 있다. 대신 큰 이미지와 결과를 주고받는 통신 비용이 생긴다. [[운영체제 기본/프로세스 간 통신#공유 메모리와 동기화|공유 메모리]]로 복사를 줄이면 같은 영역에 대한 동기화와 접근 범위를 관리해야 한다. 경계를 나누는 단위와 데이터를 교환하는 방식은 함께 설계해야 한다. | |||
하드웨어가 접근 경계를 지켜도, 쓰기가 허용된 메모리에 잘못된 계산 결과를 저장하는 것까지 막지는 않는다. 공유 영역에서 두 스레드가 같은 값을 동시에 갱신하는 문제도 남는다. 하드웨어 보호는 실행과 접근의 경계를 강제하고, 그 경계 안의 계산과 [[운영체제 기본/동시성|동기화]]는 프로그램이 올바르게 구성해야 한다. | |||
== 참고 문헌 == | == 참고 문헌 == | ||
<references /> | |||
{{이전 다음 문서 | |||
|이전=← 이전 문서<br />[[운영체제 기본/추상화|추상화]] | |||
|목차=[[운영체제 기본#총론|총론 목차]] | |||
|다음=다음 문서 →<br />[[운영체제 기본/장치 제어와 입출력|장치 제어와 입출력]] | |||
}} | |||
{{운영체제 기본 둘러보기}} | |||
2026년 11월 1일 (일) 01:10 기준 최신판
프로그램 오류와 보호의 필요성
RV64 CPU를 사용하는 어떤 보드에서 GPIO (General-Purpose Input/Output, 범용 입출력)의 출력선 하나가 전원 제어 회로에 연결되어 있다고 하자. 이 보드는 그 선의 값을 0으로 바꾸면 전원이 끊기도록 설계되어 있다. GPIO 제어 레지스터는 주소 공간에 배치되어 있어, 해당 주소에 값을 쓰면 RAM의 데이터가 아니라 장치의 상태가 바뀐다. 이런 방식을 메모리 매핑 입출력(Memory-Mapped I/O, MMIO)이라고 한다. GPIO를 이용한 시스템 전원 제어는 실제로도 쓰인다.[1]
프로그램이 그 레지스터에 제한 없이 접근할 수 있는 환경이라면 다음 쓰기 한 번으로 기기의 전원을 끊을 수 있다. POWER_GPIO_OUTPUT은 이 예의 보드에서 정한 레지스터 주소를 뜻한다. GPIO의 주소와 전원 회로 연결은 RV64 명령어 집합이 아니라 보드 설계가 정한다.
#include <stdint.h>
void mistaken_write(void)
{
volatile uint32_t *power_gpio =
(volatile uint32_t *)(uintptr_t)POWER_GPIO_OUTPUT;
*power_gpio = 0;
}
프로그램의 메모리 쓰기
|
v
GPIO 제어 레지스터 → 출력선 변경 → 전원 제어 회로 → 기기 전원 차단
프로그램 입장에서는 평범한 메모리 쓰기지만, 결과는 변수 하나의 변경으로 끝나지 않는다. 저장 중이던 데이터가 손실되거나, 전원 순서와 전압을 제어하는 회로에서는 잘못된 조작이 장치 손상으로 이어질 수도 있다. 결과는 연결된 회로에 달려 있다. 장치뿐 아니라 운영체제 코드가 있는 RAM을 덮어써도 시스템 전체가 멈출 수 있다. 프로그램의 실수가 다른 프로그램과 기기 전체의 피해로 번지지 않게 하는 것이 보호가 필요한 이유다.
보호는 이런 접근이 대상에 도달하기 전에 프로그램의 실행·접근 경계를 강제하는 일이다. 운영체제는 일반 프로그램이 장치 레지스터나 커널 메모리를 직접 쓰지 못하게 설정하고, CPU는 실행 중 그 설정을 검사한다. 프로그램은 장치를 관리하는 커널 코드에 작업을 요청해야 한다. RISC-V에서는 페이지 단위 접근 권한과 물리 메모리 보호(PMP) 같은 하드웨어 수단으로 접근을 제한할 수 있다. 단순히 페이징을 끈다고 모든 보호가 사라지는 것은 아니다.[2]
보호 규칙 자체도 보호해야 한다. 프로그램이 자신의 접근 권한을 기록한 메모리를 마음대로 수정할 수 있다면 권한 검사는 의미가 없어진다. 규칙을 집행하는 코드와 상태를 보호 대상과 분리해야 한다.[3]
실행 권한, 주소, 자원의 경계
커널(Kernel)은 CPU·메모리·장치의 사용을 관리하고 프로그램의 요청을 처리하는 운영체제의 핵심 코드다. 커널 코드와 그 관리 정보도 메모리에 있으므로 일반 프로그램이 마음대로 수정하지 못하게 해야 한다.
일반 프로그램의 명령은 CPU에서 직접 실행된다. 운영체제가 그 사이에 끼어들어 명령 하나, 메모리 접근 하나마다 소프트웨어로 권한을 검사하면 실행 비용이 지나치게 커진다. 따라서 운영체제가 허용 범위와 권한을 설정하고, CPU가 실행 중 그 규칙을 검사하도록 하드웨어의 도움을 받는다. 정상적인 연산은 직접 실행하고, 허용되지 않거나 추가 처리가 필요한 동작에서만 커널로 제어를 넘긴다.[4]
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는 그 설정을 메모리 접근마다 적용한다. 정책과 설정은 운영체제가 맡고, 반복되는 주소 변환과 검사는 하드웨어가 맡는 분업이다. 프로그램이 다른 주소를 계산해도 허용되지 않은 접근까지 가능해지는 것은 아니다.[5] 페이지 테이블과 변환 결과를 캐시하는 TLB는 가상 메모리에서 이어서 설명한다.
검사에 쓰이는 설정도 보호해야 한다. 사용자 프로그램이 페이지 테이블을 바꾸거나 다른 페이지 테이블을 선택할 수 있다면 MMU가 적용하는 규칙 자체를 고칠 수 있기 때문이다. 페이지 테이블을 담은 메모리의 보호와, 주소 변환을 제어하는 레지스터의 접근 제한이 함께 필요하다.
특권 명령과 장치 접근
특권 명령 (Privileged Instruction)은 CPU의 현재 권한에 따라 실행이 제한되는 명령이다. 사용자 프로그램이 CPU를 멈추거나 예외 처리와 메모리 보호에 필요한 제어 상태를 마음대로 바꾸면 다른 프로그램과 커널도 영향을 받는다. CPU는 이를 실행하기 전에 필요한 권한이 있는지 검사한다.
- x86-64: CPU를 정지 상태로 만드는
HLT를 CPL 3에서 실행하면 일반 보호 예외#GP가 발생한다. 명령 자체가 존재하지 않는 경우의 잘못된 명령 예외#UD와 구별된다.[6] - RISC-V RV64: U-mode 프로그램이
mstatus처럼 더 높은 권한을 요구하는 제어·상태 레지스터 (Control and Status Register, CSR)에 접근하면 불법 명령 예외 (Illegal Instruction Exception)가 발생한다. 모든 CSR이 커널 전용인 것은 아니며, 레지스터마다 필요한 권한이 정해져 있다.[7]
장치에 직접 명령을 내리는 경로도 제한해야 한다. x86의 포트 입출력 IN·OUT은 CPU가 I/O 권한 수준(I/O Privilege Level, IOPL)과 필요한 경우 태스크 상태 세그먼트(Task State Segment, TSS)에 연결된 포트별 허용 비트맵을 검사하며, 허용되지 않은 접근은 #GP로 막는다. 따라서 이 명령들이 무조건 커널 모드에서만 실행된다고 단정하기보다, 운영체제가 설정한 I/O 권한을 CPU가 강제한다고 보는 것이 정확하다.[8]
메모리 매핑 입출력 (Memory-Mapped I/O, MMIO)은 장치 레지스터를 주소로 접근한다. 운영체제가 사용자 주소 공간에 해당 장치 영역을 매핑하지 않거나 필요한 권한을 주지 않으면 일반 프로그램은 그 경로로 장치를 제어할 수 없다. 허용된 일부 영역만 매핑해 주는 구성도 가능하다. 포트 입출력과 MMIO의 차이는 장치 제어와 입출력, 구체적인 매핑은 MMIO 장치 매핑으로 이어진다.
보호 위반과 예외
CPU가 명령어를 실행하다가 별도의 처리가 필요한 조건을 발견하여 정해진 처리 코드로 제어를 넘기는 일을 예외 (Exception)라고 한다. 허용되지 않은 메모리 접근과 특권 명령 실행은 모두 실행 중인 명령에 따라 동기적으로 발생하는 예외의 원인이 될 수 있다. 장치가 현재 명령어와 무관하게 보내는 하드웨어 인터럽트와 구별된다.[8]
x86에서 페이지 매핑이 없거나 페이지 접근 권한을 위반하면 페이지 폴트 (Page Fault, #PF)가 발생한다. 일반 데이터 접근의 주소 형태가 x86-64의 규칙에 맞지 않는 경우나 앞에서 본 HLT 권한 위반은 일반 보호 예외 (General-Protection Exception, #GP)의 원인이 된다. 보호 위반이 모두 메모리 문제인 것도, 모든 메모리 문제가 #PF인 것도 아니다.[8]
x86에서는 CPU가 #PF의 벡터 14 또는 #GP의 벡터 13으로 인터럽트 서술자 테이블 (Interrupt Descriptor Table, IDT)의 게이트를 선택하고 커널 진입 코드로 제어를 넘긴다. RV64에서는 IDT 대신 트랩 벡터와 원인 레지스터를 사용한다. 사용자 프로그램의 트랩을 S-mode 커널이 받는 구성에서는 stvec으로 진입하고 scause 등에서 원인을 확인한다. 구체적인 진입 방식이 달라도, 하드웨어가 원인을 알리고 운영체제가 처리하는 경계는 같다.[9]
운영체제: 권한 모드·페이지 테이블·I/O 권한 설정
|
v
사용자 명령 실행 → CPU/MMU가 설정된 권한 검사
|
+-----------+-----------+
| |
허용된 동작 권한 위반 등
| |
직접 실행 예외 → 커널 처리
예외를 받았다고 곧바로 프로그램을 종료하지는 않는다. 유효한 주소인데 필요한 페이지가 아직 준비되지 않았거나, 쓰기 시 복사 때문에 쓰기 권한을 잠시 막아 둔 경우에는 커널이 매핑을 고치고 명령을 다시 실행할 수 있다. 자세한 복구 과정은 페이지 폴트의 처리에서 다룬다.
반대로 허용되지 않은 사용자 접근을 복구할 수 없다면 운영체제는 프로그램에 오류를 통지한다. Linux에서는 잘못된 메모리 접근에 SIGSEGV 같은 시그널 (Signal)을 보낼 수 있으며, 기본 처리에 따르면 프로세스가 종료된다. CPU가 직접 시그널을 보내거나 프로그램을 죽이는 것은 아니다. 예외를 어느 시그널로 바꿀지는 아키텍처와 운영체제의 처리 규칙에 달려 있다.[10]
시그널은 보호 위반 전용 통지가 아니다. 종료 요청이나 자식 프로세스의 상태 변화도 알릴 수 있다. 발생·전달·처리와 다른 통신 방식의 관계는 프로세스 간 통신에서 설명한다. IDT를 통한 진입과 복귀는 장치 제어와 입출력의 IDT 절로 이어진다.
시스템 콜과 요청 검증
격리된 프로그램도 파일을 읽고 다른 프로그램과 통신해야 한다. 시스템 콜(System Call)은 프로그램이 정해진 진입점을 통해 커널에 작업을 요청하는 경로다. 하드웨어가 제어를 커널로 넘기면 커널 코드가 요청을 처리하고 결과를 돌려준다. 진입·복귀 과정은 장치 제어와 입출력을 참조한다. 호출자는 요청할 작업과 인자를 고를 수 있지만 커널에서 실행할 임의의 명령을 지정할 수는 없다.
예를 들어 데이터를 읽어 달라는 요청에는 결과를 저장할 사용자 주소와 최대 길이가 들어간다. 커널은 그 주소 범위가 사용자 공간에 속하고 요청한 방향으로 접근 가능한지 확인해야 한다. 주소와 길이를 합산할 때 정수 범위를 넘는 경우도 고려해야 한다. 호출자가 넘긴 숫자나 포인터를 신뢰하면 커널이 자신의 권한으로 금지된 접근을 대신 수행할 수 있다. [11]
검사 시점과 사용 시점 사이의 변화도 문제다. 다른 스레드가 인자가 담긴 메모리를 바꿀 수 있다면, 검사한 내용과 실제 처리한 내용이 달라질 수 있다. 필요한 값을 커널의 관리 영역에 복사하거나, 대상의 수명을 유지하고 동기화하는 등 처리 과정 내내 가정이 유지되도록 만들어야 한다.
격리와 협력의 비용
경계를 넘을 때는 실행 상태를 보존하고 복원하며 인자와 권한을 확인하는 비용이 든다. 데이터를 복사해야 할 수도 있다. 다만 커널에 진입했다고 반드시 다른 스레드로 컨텍스트 스위치(Context Switch)를 하는 것은 아니다. 요청을 바로 끝내면 같은 스레드가 돌아오고, 기다려야 한다면 다른 스레드에 CPU를 넘길 수 있다.
이미지 처리 기능을 별도 프로세스에 두면 그 기능의 잘못된 메모리 쓰기가 편집기의 메모리를 직접 훼손하는 일을 막을 수 있다. 대신 큰 이미지와 결과를 주고받는 통신 비용이 생긴다. 공유 메모리로 복사를 줄이면 같은 영역에 대한 동기화와 접근 범위를 관리해야 한다. 경계를 나누는 단위와 데이터를 교환하는 방식은 함께 설계해야 한다.
하드웨어가 접근 경계를 지켜도, 쓰기가 허용된 메모리에 잘못된 계산 결과를 저장하는 것까지 막지는 않는다. 공유 영역에서 두 스레드가 같은 값을 동시에 갱신하는 문제도 남는다. 하드웨어 보호는 실행과 접근의 경계를 강제하고, 그 경계 안의 계산과 동기화는 프로그램이 올바르게 구성해야 한다.
참고 문헌
- ↑ Linux 커널 문서, Subsystem drivers using GPIO, poweroff-gpio.
- ↑ RISC-V, Machine-Level ISA, Physical Memory Protection.
- ↑ Jerome H. Saltzer·Michael D. Schroeder, The Protection of Information in Computer Systems (1975), §I.
- ↑ MIT xv6: Operating system organization, 사용자 모드·커널 모드와 하드웨어 격리.
- ↑ Linux 커널 문서: Page Tables, 주소 변환과 페이지 접근 권한.
- ↑ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 2A, HLT, Protected Mode Exceptions.
- ↑ RISC-V Privileged Architecture: Control and Status Registers, CSR 접근 권한과 불법 명령 예외.
- ↑ 8.0 8.1 8.2 Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, 예외 처리, 특권 수준 및 I/O 보호.
- ↑ RISC-V Privileged Architecture: Supervisor-Level ISA, stvec·scause와 예외 처리.
- ↑ Linux man-pages: signal(7), signal dispositions 및 hardware exception에서 발생하는 시그널.
- ↑ xv6: Traps and system calls.