1. 개요
System Call대다수의 운영체제들[1]은 커널 모드(Kernel Mode)[2]와 사용자 모드(User Mode)가 구분되어 있으며, 커널 모드는 Ring0에 사용자 모드는 Ring3에 속한다. (Ring 1~2는 장치 드라이버 전용이다)[3]
커널 모드는 커널 및 커널에 붙는 드라이버들이 작동되는 영역으로 모든 컴퓨터 리소스에 접근할 수 있다. 그리고 하나의 가상 메모리 영역만을 공유하여 커널과 드라이버가 서로 접근할 수 있다. 다만 그 만큼 불안정해서 만일 커널이나 드라이버 중 하나라도 오류가 일어나면 치명적이다.
사용자 모드는 일반 프로그램들이 작동되는 영역으로 컴퓨터 리소스에 제한적으로 접근이 가능하고 프로그램들은 프로세스로 작동한다. 프로세스의 가상 메모리 영역은 다른 프로세스가 볼 수 없으므로 다른 프로세스와 정보를 주고받기 위해서는 프로세스간 통신을 이용해야 한다. 대신 프로세스들은 각각 독립적으로 작동하기 때문에 한 프로세스에서 오류가 발생해도 그 프로세스만 종료될 뿐이지 운영체제와 다른 프로세스에 영향을 미치지 않아 안정성이 높다.
일반적인 프로그램들은 사용자 모드에서 실행되므로 커널 모드에 대한 직접적인 접근이 불가능하다. 하지만 커널에 접근할 수 없으면 사용자 모드의 프로세스들이 파일을 쓰거나 불러올 수 없고 그래픽 처리와 같은 거의 모든 작업을 할 수 없다. 따라서 커널에 요청하여 커널 모드에서 처리하고 그 결과를 사용자 모드의 프로그램에게 전달하는 것이 바로 시스템 콜이다.
CPU에는 특권 레벨(Privilege Level)이라는 개념이 존재한다. 특권 레벨마다 실행할 수 있는 명령어들이 다르다. 특권 레벨이 낮을수록 권한이 많아지고 높을수록 권한이 적어진다. 특권 레벨이 0인 경우 가장 많은 권한을 가지게 되며 특권 레벨 0에서만 실행할 수 있는 명령어를 특권 명령(Privileged Instruction)이라고 부른다. 특권 레벨 0이 아닌 다른 레벨에서 실행할 경우 CPU는 실제로 실행시키지 않고 예외를 발생시킨다.[4] 따라서 컴퓨터의 하드웨어를 직접 제어헤야 하는 운영체제와 드라이버는 특권 레벨 0(커널 모드)에서 실행되고 일반 프로그램은 특권 레벨 3(사용자 모드)에서 실행된다.
예를 들어 인텔 x86 CPU의 HLT 명령어는 더 이상의 명령어 실행을 중단시키고 인터럽트나 리셋 등 특정 동작에 의해서만 동작을 재개하는 명령어다. 만약 이 명령어를 일반 프로그램에서 사용하면 어떻게 될까? 그러면 컴퓨터가 아예 멈춰버릴 수 있다. 따라서 HLT 명령어는 특권 명령에 해당되어 커널 모드가 아닌 사용자 모드에서 실행시키면 CPU는 실행시키지 않고 예외를 발생시킨다.
참고로 ARM CPU에서는 PL(ARMv7까지, Privilege Level)/EL(ARMv8 이후, Execution Level)라는 비슷한 개념이 존재한다. x86 CPU와 다르게 레벨이 높을수록 권한이 많아지고 낮을수록 권한이 작아진다. PL0/EL0는 일반적인 앱, PL1/EL1는 커널, PL2/EL2는 하이퍼바이저, PL3/EL3는 보안 펌웨어에 해당한다.
시스템 콜은 프로그램의 거의 모든 코드의 실행에서 발생하며 파일 생성이나 쓰기 또는 읽기, 키보드 입력, 그래픽 출력, 스레드 생성 및 제어 같은 것도 시스템 콜을 통해 커널에 요청하여 커널 모드에서 처리한다. 예로 Windows API 중 유저레벨 API중 하나인
CreateFile()
을 실행시키면 내부적으로 Private API인 NtCreateFile()
가 실행되는데 이 함수는 결과적으로 커널 모드의 IoCreateFile()
함수가 실행된다.윈도우에서 커널 함수가 사용자 모드에 노출되는 것은 ntdll.dll이며 실제 구현체는 윈도우 커널인 ntoskrnl.exe이다. ntdll.dll의 대부분 함수는 커널에서 구현되고 ntdll.dll를 통해 이들 함수가 사용자 모드에 노출된다. [5]
디바이스 드라이버는 커널 모드에서 실행되므로 시스템 콜을 통하지 않고 바로 커널 함수를 실행할 수 있다.
2. 작동 방식
사용자 모드에서 커널 모드로 들어가는 방법은 INT[6]나 SYSENTER[7] 명령어를 이용한다.커널 모드에 들어가면 시스템 콜 테이블[8]에서 요청한 함수의 주소를 가져와서 실제 함수를 호출하고 IRETD(INT 2E를 사용할 경우) 또는 SYSEXIT(SYSENTER를 사용할 경우) 명령어를 통해 사용자 모드로 복귀하게 된다.
x64 CPU에서는 SYSCALL 명령어가 존재한다. 64비트 운영체제에서는 SYSENTER 대신 SYSCALL 명령어를 사용한다. 여기서 사용자 모드로 복귀할 때 SYSRET 명령어를 사용한다.
2.1. Microsoft Windows
윈도우에서의 시스템 콜 라이브러리는 ntdll.dll이며 그 외의 Windows API를 호출한다해도 내부적으로 ntdll.dll의 함수를 호출한다.예를 들어서 kernel32.dll의 프로세스를 생성하는 CreateProcess 함수를 호출하면 CreateProcess는 내부 함수인 CreateProcessInternal 함수를 호출한다. 이는 후킹이나 탐지를 막기 위해 내부 함수를 사용하는 것이다. 그리고 CreateProcessInternal는 ntdll.dll의 NtCreateUserProcess( Windows Vista 이전에는 NtCreateProcess) 함수를 호출하는 식이다.
ntdll.dll의 Zw이나 Nt 접두사를 가진 함수는 사용할 시스템 콜 인덱스 번호를 EAX 레지스터에 저장하고 INT 2E/KiFastSystemCall(32비트)/KiSystemCall64(64비트) 루틴을 호출하게 되는데 KiFastSystemCall(32비트)/KiSystemCall64(64비트)는 SYSENTER/SYSCALL 명령어를 사용하여 커널 모드로 접근하여 KiSystemService 루틴으로 이동하게 된다.[9]
KiSystemService 루틴에서 시스템 서비스 서술자 테이블 (SSDT, KeServiceDescriptorTable)의 시스템 콜 테이블 (KiServiceTable)에서 EAX 레지스터에 저장된 인덱스 번호의 함수 주소를 가져와 호출시킨다. 호출 후 결과 값을 받으면 IRETD/SYSEXIT/SYSRET 명령어를 사용하여 사용자 모드로 복귀하는 것으로 시스템 콜 과정이 끝난다.
GUI 프로그램일 경우 그래픽과 메시지 등을 담당하는 기능이 커널 드라이버인 win32k.sys에서 담당하므로 시스템 콜 테이블을 가져오는 과정에서 KeServiceDescriptorTable이 아닌 KeServiceDescriptorTableShadow 구조체를 가져온다. 이 구조체는 커널 자체(KiServiceTable)와 win32k.sys의 시스템 콜 테이블이 모두 들어가 있다. 여기서 win32k.sys의 시스템 콜 테이블은 W32pServiceTable이다. win32k.sys의 시스템 콜도 역시 win32u.dll라는 라이브러리로 사용자 모드에 노출된다. user32.dll과 gdi32.dll이 win32u.dll의 함수를 사용한다.
윈도우의 시스템 콜은 Nt과 Zw 접두사로 나누어져 있는데 Nt 접두사는 사용자 모드에서 사용하고 Zw 접두사는 커널 모드에서 사용한다.[10] 이 둘의 차이점은 파라미터 값 검증 부분에 있다. 기본적으로 사용자 모드에서 나오는 파라미터 값을 신뢰할 수 없으므로 파라미터 값을 검증하는 과정[11]을 거치게 되는데 Nt 함수는 파라미터 검증을 수행하나 Zw 함수는 커널 모드에서 나오는 값이기에 신뢰할 수 있으므로 수행하지 않는다.[12]
사용자 모드는 PASSIVE_LEVEL 수준에서만 동작하며 커널 모드에서 사용자 모드로 전환되려고 할 때 현재 IRQL가 PASSIVE_LEVEL이 아닐 경우 블루스크린이 발생한다. 오류 코드는 IRQL_GT_ZERO_AT_SYSTEM_SERVICE(0x0000004A).
3. 확장
오픈소스 운영체제라면 커널 소스를 수정하면서 시스템 콜 테이블에 새로운 함수를 추가할 수 있다. 다만 Microsoft Windows와 macOS와 같이 오픈소스가 아닌 운영체제들은 커널 모드에서 동작하는 드라이버/kext 패키지라도 시스템 콜 테이블에 새로운 함수를 추가할 수 없다.4. 중요성
시스템 콜은 운영체제에게 있어서는 매우 중요한 요소이다. 사용자 모드에 있는 프로그램이 시스템 함수를 직접 호출할 수 없으므로 따로 프로그램이 커널 호출을 요청하는 시스템을 만들어서 커널이 처리해야할 일을 프로그램으로부터 받아서 처리하는 것이다. 만약에 없으면? 사용자 모드에서 아무것도 할 수 없게 된다! 그래픽 출력이라거나 파일 다루는 등 행위도 커널의 도움으로 이루어진다.5. 후킹
시스템 콜은 과정상 지정된 테이블을 참조하여 호출하므로 드라이버를 이용하면 시스템 콜 테이블의 타켓 시스템 함수 주소를 다른 것으로 수정할 수 있다. 악성코드가 강제 종료 방지 등을 위해 후킹하기도 하나 안티 바이러스나 보안 프로그램도 자가 보호 등을 위해 시스템 콜 테이블을 수정하기도 한다. 그러다보니 몇몇 악성코드는 백신 무력화 방법 중 하나로 시스템 콜 테이블을 원상복구시키기도 한다.시스템 콜 테이블을 직접 후킹하는 방법 외에는 시스템 콜 전 단계인 IDT이나 GDT, MSR에서 시스템 콜 실행 주소를 수정하는 방법도 있다.
하지만 시스템 콜은 매우 빈번하게 발생하므로 후킹을 통해 과정을 추가하면 컴퓨터 성능이 저하될 수 있고 시스템 콜 테이블을 잘못 건들면 하면 커널 패닉 등 안정성 문제가 발생할 수 있기 때문에 시스템 콜 테이블 같은 내부 커널 영역을 수정할 수 없도록 막아놓기도 한다.
대표적으로 윈도우 64비트에서 작동하는 보안 기능인 커널 패치 보호(KKP, Kernel Patch Protection, 비공식적으로 패치가드(PatchGuard))는 시스템 콜 테이블 수정 등의 커널 내부 수정[13]이 확인되면 즉시 블루스크린을 일으킨다.
6. 창작물에서
소드 아트 온라인에서는 작중에서 키리토가 시스템 커맨드를 사용할 때 이 용어를 쓴다. 사실 온라인 게임은 서버와 통신하여 플레이하는데 서버 측에서 API를 공개하여 클라이언트가 이를 이용해 서버에 요청하고 서버 측에서 처리한다는 점[14]에서 운영체제의 시스템 콜이랑 유사하다. 이는 게임 뿐만 아니라 서버와 통신하는 모든 프로그램에게 해당된다.7. 기타
실제 시스템 콜은 기계어 코드로만 호출이 가능하므로 대부분의 운영체제는 저수준 언어 또는 운영체제를 구현한 언어로 작성된 라이브러리나 헤더 파일의 형태로 API를 제공한다. UNIX의 경우 C언어로 작성되고 컴파일시 변환된다. 라이브러리 함수는 대부분 시스템 콜 기계어 코드로 직접 변환되지는 않고 여러가지 과정이 더 있어서 느린 경우가 많다.
[1]
TempleOS와 같이 Ring0으로만 작동하는 운영체제들은 예외
[2]
macOS,
리눅스 등지에서는 슈퍼바이저 모드(Supervisor Mode)라고도 부른다.
[3]
Ring는 '
보호 링'이라고 하며, CPU의 기능이다. 기본적으로 운영체제는 컴퓨터의 모든 값을 읽고 쓸 수 있다. 하지만 운영체제에서 돌아가는 프로세스도 운영체제와 똑같이 할 수 있게 하면, 안정성 및 보안성의 문제가 뒤따를 수 밖에 없기 때문에 운영체제는 CPU의 기능인 보호 링을 사용하여, 커널 모드와 사용자 모드를 나누게 된다. 거의 모든 운영체제들은 커널 모드는 Ring0에, 사용자 모드는 Ring3에 두는 것이 일반적이며, Ring1~2는 사용하지 않는다.
[4]
Windows의 경우 사용자 모드 프로세스가 특권 명령을 실행할 경우 STATUS_PRIVILEGED_INSTRUCTION(0xc0000096) 예외가 발생한다.
[5]
ntdll.dll의 접두사 중에서 Nt 또는 Zw가 있는데, 이 그룹들은 바로 시스템 호출이다. 이들 함수가 호출되면 커널 모드에 들어가서 SSDT를 통해 같은 의미를 가진 ntoskrnl.exe의 함수를 호출한다.
[6]
소프트웨어
인터럽트 발생 명령어로 IDT(Inturrupt Descriptor Table)의 특정 번호에는 시스템 콜 실행 주소가 저장되어 있는데 해당 번호로 소프트웨어적 인터럽트를 발생시켜 커널모드에 접근한다. 윈도우는 2E, 리눅스는 0x80에 시스템 콜 발생 주소가 등록되어 있다. 다만 이 방법은 부하가 커서 느려지기 때문에 현재의 운영체제들은 SYSENTER 명령어를 이용하게 된다.
[7]
CPU가 제공하는 명령어로 팬티엄 2부터 추가되었다. MSR(0x176)를 사용하여 커널 모드에 접근한다.
[8]
윈도우에서는 SSDT(System Service Descriptor Table, 실제 시스템 콜 테이블은 커널 외부로 드러나지 않는 KiServiceTable이다.)
[9]
다만 SYSENTER의 경우 KiFastCallEntry 루틴을 거쳐서 KiSystemService를 호출한다.
[10]
Zw 함수는 사용자 모드의 ntdll.dll에도 존재하나 호출해도 Nt 함수와 동일하게 동작한다.
[11]
사용자 모드에서 오는 값들을 검증하는 함수로 ProbeForWrite와 ProbeForRead가 있다.
[12]
이는 ProbeForWrite와 ProbeForRead 함수는 사용자 메모리에 대해서만 검증할 수 있기 때문이다. 만약 검증할 메모리 주소가 커널 메모리 주소라면 항상 예외를 발생시킨다.
[13]
KKP가 검사하는 항목은 IDT/GDT, SSDT, Processor MSRs, Kernel Image, Kernel Objects, 이외에도 Unexported Kernel Symbols 등... 즉 드라이버가 수정할 수 없는 항목들은 전부 포함된다.
[14]
대부분의 연산을 클라이언트에서 처리하는 게임도 있지만 당연히 핵 문제에 매우 취약하므로 소규모 게임이 아닌 이상 이 방식을 사용하지 않는다.