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

운영체제 기본/보호: 두 판 사이의 차이

~ldmsys
역링크 갱신 완료 후 불필요한 호환 span id 제거
편집 요약 없음
 
(같은 사용자의 중간 판 12개는 보이지 않습니다)
1번째 줄: 1번째 줄:
[[운영체제 기본|목차]] · 이전: [[운영체제 기본/추상화|추상화]] · 다음: [[운영체제 기본/장치 제어와 입출력|장치 제어와 입출력]]
{{이전 다음 문서
|이전=← 이전 문서<br />[[운영체제 기본/추상화|추상화]]
|목차=[[운영체제 기본#총론|총론 목차]]
|다음=다음 문서 →<br />[[운영체제 기본/장치 제어와 입출력|장치 제어와 입출력]]
}}


== 프로그램 오류와 보호의 필요성 ==
== 프로그램 오류와 보호의 필요성 ==
다음 C 코드는 유효한 객체를 가리키지 않는 포인터로 값을 쓰려는 버그다.
메모리 보호가 없는 산업용 기기에서 계산기 프로그램과 GPIO 핀에 PWM 모터를 물려 제어를 하는 모터 제어 프로그램이 함께 실행된다고 생각해 보자. 기본적으로 계산기 프로그램은 계산 결과를 자신의 버퍼에 저장하는 역할을 하므로 모터를 제어할 이유가 하나도 없다. 그런데 계산기 프로그램이 취약하여 결과를 저장할 주소가 공격자에 의해 악의적으로 변조되어 버퍼의 주소가 아니라 GPIO 주소가 들어가면 무슨 일이 일어날까?


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


<code>NULL</code>에 <code>int *</code>라는 형식을 붙여도 값을 저장할 공간이 생기지는 않는다. C에서는 널 포인터 역참조의 동작을 정의하지 않는다. 따라서 이 코드가 반드시 특정 기계 명령을 실행하거나 정해진 오류로 종료한다고 보장할 수는 없다.<ref name="null-pointer-c">[https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf C11 위원회 초안 N1570], 6.5.3.2 문단 4와 각주 102.</ref>
계산값이 모터 제어값으로 쓰이면 기계가 예기치 못한 방식으로 움직이고, 경우에 따라 사람이 다치는 결과를 초래할 수도 있다. 정작 문제를 일으킨 계산기 프로그램은 모터 제어와는 원래 아무 관련이 없었는데도 말이다.
마찬가지로 잘못된 주소가 운영체제 코드가 있는 RAM이라면 시스템이 멈출 수도 있으며 다른 프로그램의 주소라면 그 프로그램의 동작이나 데이터를 변조할 수도 있다. 이런 문제를 막으려면 프로그램이 접근할 수 있는 영역을 그 프로그램이 본래 사용할 수 있는 영역으로 제한해야 한다. 이것이 '''보호'''가 필요한 이유다.
 
== 실행 권한, 주소, 자원의 경계 ==
커널(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는 [[운영체제 기본/가상 메모리#페이지 테이블과 주소 변환|가상 메모리]]에서 이어서 설명한다.


실제 쓰기 명령이 실행되는 경우, 일반적인 메모리 보호 환경에서는 대상의 매핑과 쓰기 권한을 검사한다. 유효한 매핑이나 권한이 없으면 접근이 차단된다. 포인터 값을 만들어 냈다고 다른 프로세스의 메모리에 접근할 권한까지 얻는 것은 아니다.<ref name="page-protection">[https://docs.kernel.org/mm/page_tables.html Linux 커널 문서: Page Tables], 주소 변환과 페이지 접근 권한.</ref>
검사에 쓰이는 설정도 보호해야 한다. 사용자 프로그램이 페이지 테이블을 바꾸거나 다른 페이지 테이블을 선택할 수 있다면 MMU가 적용하는 규칙 자체를 고칠 수 있기 때문이다. 페이지 테이블을 담은 메모리의 보호와, 주소 변환을 제어하는 레지스터의 접근 제한이 함께 필요하다.<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>


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


보호 규칙 자체도 보호해야 한다. 프로그램이 자신의 접근 권한을 기록한 메모리를 마음대로 수정할 수 있다면 권한 검사는 의미가 없어진다. 규칙을 집행하는 코드와 상태를 보호 대상과 분리해야 한다. <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>
장치 접근을 운영체제에 맡기는 목적은 응용 프로그램의 하드웨어 사용을 무작정 금지하는 데 있지 않다. 이 설계에서는 '''커널을 시스템 전체의 장치 사용을 조정하는 신뢰된 코드로 두고''', 응용 프로그램의 요청을 검증하여 허용된 동작을 수행하도록 권한을 맡긴다. 커널과 드라이버는 요청한 값과 범위, 장치의 상태와 제어 순서, 다른 작업과의 충돌을 확인한다. 응용 프로그램은 정해진 요청 경로로 장치를 사용하며, 필요하면 운영체제가 지정한 일부 장치 영역에 직접 접근할 권한을 받을 수도 있다.<ref name="source-2"/>


== 실행 권한, 주소, 자원의 경계 ==
'''CPU는 그 장치 조작의 의미가 안전한지 판단하지 않는다.''' CPU의 역할은 일반 프로그램이 운영체제의 판단을 우회하여 제한된 명령이나 장치 영역에 접근하지 못하도록 경계를 지키는 것이다. 이 분업은 커널과 드라이버가 검증을 올바르게 수행한다는 신뢰를 전제로 하므로, 그 코드의 오류까지 하드웨어 권한 검사가 막아 주지는 않는다.
하드웨어의 권한 모드를 사용하는 운영체제에서는 일반 프로그램을 제한된 권한으로 실행하고, 메모리 매핑이나 장치 제어처럼 시스템 전체에 영향을 주는 동작은 커널이 담당한다. 사용자 모드에서도 산술 연산과 허용된 메모리 접근은 CPU가 직접 실행한다. 모든 명령을 커널이 대신 실행하면 보호를 위해 지나치게 큰 비용을 치르게 된다.


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


자원별 권한은 별도로 판단한다. 사용자 모드라는 이유만으로 모든 파일 접근이 금지되는 것은 아니며, 어떤 파일을 읽을 수 있다고 해서 수정도 허용되는 것은 아니다. CPU 권한 모드는 어떤 종류의 동작을 직접 실행할 수 있는지, 주소 공간은 어떤 메모리에 접근할 수 있는지, 자원 권한은 어떤 대상에 어떤 연산을 허용할지를 나눈다. <ref name="source-2">[https://mit-pdos.github.io/xv6-riscv-book/first.html xv6: Operating system organization].</ref>
메모리 매핑 입출력 (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], 2016년 9월, 6.1~6.3절, 6.12절 및 6.15절의 General-Protection Exception과 Page-Fault Exception.</ref>
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>


x86에서 메모리 접근에 필요한 페이지 매핑이 없거나 페이지 접근 권한을 위반하면 '''페이지 폴트(page fault, #PF)'''가 발생한다. 주소의 형태 자체가 x86-64의 규칙에 맞지 않는 경우처럼 '''일반 보호 예외(general-protection exception, #GP)'''가 발생하는 경우도 있다. 즉, 잘못된 메모리 접근을 모두 #PF라고 부르는 것은 아니다.<ref name="intel-exception" />
예외를 받았다고 곧바로 프로그램을 종료하지는 않는다. 유효한 주소인데 필요한 페이지가 아직 준비되지 않았거나, 쓰기 시 복사 때문에 쓰기 권한을 잠시 막아 둔 경우에는 커널이 매핑을 고치고 명령을 다시 실행할 수 있다. 자세한 복구 과정은 [[운영체제 기본/가상 메모리#페이지 폴트의 처리|페이지 폴트의 처리]]에서 다룬다.


CPU는 #PF의 벡터 14 또는 #GP의 벡터 13으로 '''인터럽트 서술자 테이블(Interrupt Descriptor Table, IDT)'''의 게이트를 선택하고, 그 항목이 가리키는 커널 진입 코드로 실행을 넘긴다. 커널의 예외 처리 코드가 원인과 복구 가능성을 판단한다. 허용되지 않은 사용자 프로그램의 접근이라면 운영체제는 오류를 알리고, 처리되지 않은 오류는 보통 프로세스 종료로 이어진다. CPU가 직접 프로그램을 죽이는 것은 아니다.<ref name="intel-exception" />
반대로 허용되지 않은 사용자 접근을 복구할 수 없다면 운영체제는 프로그램에 오류를 통지한다. 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>


반면 #PF가 항상 잘못된 프로그램을 뜻하지는 않는다. 유효한 주소에 필요한 페이지를 아직 준비하지 않았을 뿐이라면 운영체제가 이를 마련하고 실행을 재개할 수 있다.<ref name="linux-page-tables">Linux 커널 문서, [https://cdn.kernel.org/doc/html/latest/mm/page_tables.html Page Tables], Page Faults.</ref> 이처럼 복구할 수 있는 경우는 [[운영체제 기본/가상 메모리#페이지 폴트의 처리|가상 메모리의 페이지 폴트 처리]]에서 다룬다. IDT를 통한 진입과 복귀의 구체적인 과정은 [[운영체제 기본/장치 제어와 입출력#처리 진입점과 IDT|장치 제어와 입출력의 IDT 절]]에서 이어서 설명한다.
시그널은 보호 위반 전용 통지가 아니다. 종료 요청이나 자식 프로세스의 상태 변화도 알릴 수 있다. 발생·전달·처리와 다른 통신 방식의 관계는 [[운영체제 기본/프로세스 간 통신#시그널과 사건 통지|프로세스 간 통신]]에서 설명한다. IDT를 통한 진입과 복귀는 [[운영체제 기본/장치 제어와 입출력#처리 진입점과 IDT|장치 제어와 입출력의 IDT 절]]로 이어진다.


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


예를 들어 데이터를 읽어 달라는 요청에는 대상, 결과를 저장할 주소, 최대 길이가 들어간다. 커널은 그 대상을 읽을 권한이 있는지와 결과를 돌려줄 메모리 범위가 유효한지 확인해야 한다. 주소와 길이를 합산할 때 정수 범위를 넘는 경우도 고려해야 한다. 호출자가 넘긴 숫자나 포인터를 신뢰하면 커널이 자신의 권한으로 금지된 접근을 대신 수행할 수 있다. <ref name="source-3">[https://mit-pdos.github.io/xv6-riscv-book/trap.html xv6: Traps and system calls].</ref>
예를 들어 데이터를 읽어 달라는 요청에는 결과를 저장할 사용자 주소와 최대 길이가 들어간다. 커널은 그 주소 범위가 사용자 공간에 속하고 요청한 방향으로 접근 가능한지 확인해야 한다. 주소와 길이를 합산할 때 정수 범위를 넘는 경우도 고려해야 한다. 호출자가 넘긴 숫자나 포인터를 신뢰하면 커널이 자신의 권한으로 금지된 접근을 대신 수행할 수 있다. <ref name="source-3">[https://mit-pdos.github.io/xv6-riscv-book/trap.html xv6: Traps and system calls].</ref>


검사 시점과 사용 시점 사이의 변화도 문제다. 다른 스레드가 인자가 담긴 메모리를 바꿀 수 있다면, 검사한 내용과 실제 처리한 내용이 달라질 수 있다. 필요한 값을 커널의 관리 영역에 복사하거나, 대상의 수명을 유지하고 동기화하는 등 처리 과정 내내 가정이 유지되도록 만들어야 한다.
검사 시점과 사용 시점 사이의 변화도 문제다. 다른 스레드가 인자가 담긴 메모리를 바꿀 수 있다면, 검사한 내용과 실제 처리한 내용이 달라질 수 있다. 필요한 값을 커널의 관리 영역에 복사하거나, 대상의 수명을 유지하고 동기화하는 등 처리 과정 내내 가정이 유지되도록 만들어야 한다.
44번째 줄: 95번째 줄:
경계를 넘을 때는 실행 상태를 보존하고 복원하며 인자와 권한을 확인하는 비용이 든다. 데이터를 복사해야 할 수도 있다. 다만 커널에 진입했다고 반드시 다른 스레드로 컨텍스트 스위치(Context Switch)를 하는 것은 아니다. 요청을 바로 끝내면 같은 스레드가 돌아오고, 기다려야 한다면 다른 스레드에 CPU를 넘길 수 있다.
경계를 넘을 때는 실행 상태를 보존하고 복원하며 인자와 권한을 확인하는 비용이 든다. 데이터를 복사해야 할 수도 있다. 다만 커널에 진입했다고 반드시 다른 스레드로 컨텍스트 스위치(Context Switch)를 하는 것은 아니다. 요청을 바로 끝내면 같은 스레드가 돌아오고, 기다려야 한다면 다른 스레드에 CPU를 넘길 수 있다.


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


'''최소 권한''' 원칙은 각 구성 요소에 필요한 자원과 연산만 허용하여 오류나 침해의 피해 범위를 줄인다. 그러나 허용된 출력 파일에 잘못된 결과를 쓰는 것까지 막아 주지는 않는다. 보호는 프로그램의 정확성, 공유 데이터의 동시성, 장애 후 복구를 모두 보장하는 말이 아니다. 어떤 실패를 경계 안에 가두고 어떤 협력을 허용할지 정하는 설계다.
하드웨어가 접근 경계를 지켜도, 쓰기가 허용된 메모리에 잘못된 계산 결과를 저장하는 것까지 막지는 않는다. 공유 영역에서 두 스레드가 같은 값을 동시에 갱신하는 문제도 남는다. 하드웨어 보호는 실행과 접근의 경계를 강제하고, 그 경계 안의 계산과 [[운영체제 기본/동시성|동기화]]는 프로그램이 올바르게 구성해야 한다.


== 참고 문헌 ==
== 참고 문헌 ==
<references />
<references />


----
{{이전 다음 문서
[[운영체제 기본|목차]] · 이전: [[운영체제 기본/추상화|추상화]] · 다음: [[운영체제 기본/장치 제어와 입출력|장치 제어와 입출력]]
|이전=← 이전 문서<br />[[운영체제 기본/추상화|추상화]]
|목차=[[운영체제 기본#총론|총론 목차]]
|다음=다음 문서 →<br />[[운영체제 기본/장치 제어와 입출력|장치 제어와 입출력]]
}}
 
{{운영체제 기본 둘러보기}}

2026년 11월 1일 (일) 01:53 기준 최신판

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

메모리 보호가 없는 산업용 기기에서 계산기 프로그램과 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 제어 레지스터
        |                              |
   계산 결과 보관                 모터 구동 신호 변경?
                                       |
                                 예기치 않은 기계 동작??

계산값이 모터 제어값으로 쓰이면 기계가 예기치 못한 방식으로 움직이고, 경우에 따라 사람이 다치는 결과를 초래할 수도 있다. 정작 문제를 일으킨 계산기 프로그램은 모터 제어와는 원래 아무 관련이 없었는데도 말이다. 마찬가지로 잘못된 주소가 운영체제 코드가 있는 RAM이라면 시스템이 멈출 수도 있으며 다른 프로그램의 주소라면 그 프로그램의 동작이나 데이터를 변조할 수도 있다. 이런 문제를 막으려면 프로그램이 접근할 수 있는 영역을 그 프로그램이 본래 사용할 수 있는 영역으로 제한해야 한다. 이것이 보호가 필요한 이유다.

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

커널(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.