제2장: Linux 시스템 호출
Source: Dev.to
Source: …
Linux 시스템 콜 – 커널의 “정문”
이 게시물은 Ultimate Container Security Series의 일부로, 컨테이너 보안을 기본 개념부터 런타임 보호까지 다루는 구조화된 다파트 가이드입니다. 시리즈 구조, 범위 및 업데이트 일정에 대한 개요는 시리즈 소개 게시물 여기를 참고하세요.
1. Linux 실행 “세계”
| 세계 | 설명 |
|---|---|
| Userspace (사용자 공간) | 사용자‑대면 애플리케이션이 실행되는 영역(웹 서버, 브라우저, 편집기, CLI 도구, 백그라운드 서비스 등). 제한된 영역으로, 애플리케이션은 하드웨어에 직접 접근하거나 핵심 시스템 자원을 관리할 수 없습니다. 이러한 제한은 안정성을 높이며, 앱이 충돌해도 전체 OS가 보통은 유지됩니다. |
| Kernel space (커널 공간) | Linux 커널이 존재하는 영역. 메모리, 프로세스, 스케줄링, 하드웨어, 드라이버, 파일시스템, 네트워킹, 보안 등을 제어합니다. CPU, RAM, 디스크 등 하드웨어와 직접 상호작용하며 전체 권한을 가집니다. |
2. 시스템 콜은 어디에 들어가나요?
애플리케이션은 낮은 권한을 가진 사용자 공간에서 실행됩니다.
애플리케이션이 커널 권한이 필요한 작업을 수행하려면—예를 들어:
- 파일 열기
- 데이터 읽기/쓰기
- 프로세스 생성
- 메모리 할당
- 네트워크 트래픽 전송
- 현재 시간 조회
—커널에 요청해야 합니다.
그 요청은 시스템 콜 인터페이스(또는 syscall 인터페이스)를 통해 이루어집니다.
정의 (쉽게 말하면) – 시스템 콜은 사용자‑공간 애플리케이션이 Linux 커널에 서비스를 요청하는 프로그래밍 방식이며, 안전하고 제어된 방법으로 수행됩니다.
구분이 필요한 이유
- 보안 및 안정성 – 사용자 프로그램이 하드웨어나 커널 메모리에 직접 접근할 수 없게 함으로써 위험을 방지합니다.
- 제어된 진입점 – 시스템 콜은 커널로 들어가는 제한적이고 검증된 진입점을 제공합니다.
모든 작업이 커널을 필요로 하는 것은 아닙니다. 예를 들어 문자열 토큰화는 완전히 사용자 공간에서 이루어집니다. 파일, 디바이스, 네트워킹, 프로세스 관리와 관련된 작업은 모두 시스템 콜이 필요합니다.
Linux에는 300개 이상의 시스템 콜이 포함되어 있으며(정확한 수는 커널 버전 및 CPU 아키텍처에 따라 다름) 제공합니다.
3. 흔히 사용되는 시스템 콜 (예시)
| 프로그램이 원하는 작업 | 시스템 콜 |
|---|---|
| 파일 읽기 | read() |
| 파일 쓰기 | write() |
| 파일 열기 | open() |
| 새 프로그램 시작 | execve() |
| 프로세스 생성 | fork() |
| 메모리 할당 | mmap() |
| 네트워크 데이터 전송 | send() |
| 현재 시간 얻기 | clock_gettime() |
전체 목록은 매뉴얼 페이지 man 2 syscalls에서 확인할 수 있습니다.
4. 시스템 콜의 고수준 흐름
프로그래머 입장에서는 시스템 콜이 일반 함수 호출처럼 보이지만, 실제로는 제어된 커널 모드 전환을 수행합니다.
전형적인 흐름
- 사용자 애플리케이션이 표준 라이브러리 함수(예:
read())를 호출합니다. - 해당 함수가 시스템 콜 번호를 사용해 시스템 콜을 트리거합니다.
- CPU가 사용자 모드에서 커널 모드로 전환됩니다.
- Linux 커널이 요청된 작업을 실행합니다.
- 결과(또는 오류)가 애플리케이션에 반환됩니다.
예시: read(fd, buffer, size)는 해당 파일 디스크립터에 대한 커널의 read 구현을 호출하고, 읽은 바이트 수를 반환합니다(오류 시 ‑1 반환, 상세 내용은 errno에 저장).
5. 고수준 언어에서 시스템 콜 사용하기
애플리케이션 개발자는 보통 “원시” 시스템 콜을 직접 호출하지 않습니다. 대신 래퍼 함수를 사용합니다:
| 언어 | 래퍼 제공처 |
|---|---|
| C / C++ | glibc (예: read(), write(), open()) |
| Go | syscall 패키지(또는 고수준 os 패키지) |
이 래퍼들은:
- 인자를 검증하고 정렬한다
- 커널 모드 전환을 수행한다
- 친숙한 형태로 결과를 반환한다
6. 최소 C 예제 – stdout에 출력하기
#include <stdio.h>
int main(void) {
// ...
}
const char msg[] = "Hello, World!\n";
/* write() is a glibc wrapper around the write syscall */
write(1, msg, sizeof(msg) - 1); /* fd 1 = stdout */
return 0;
}
단계별로 무슨 일이 일어나나요?
write(1, msg, sizeof(msg) - 1)이 사용자 공간에서 호출됩니다.write()(glibc) 는 시스템 콜을 준비합니다(시스템 콜 번호와 인수를 적절한 레지스터에 넣음).- CPU 가 시스템 콜 인터페이스를 통해 커널 모드로 전환됩니다.
- 커널이 다음을 검증합니다:
- 파일 디스크립터 1이 유효한지,
- 프로세스가 해당 디스크립터에 쓸 권한이 있는지,
msg가 접근 가능한 메모리를 가리키는지.
- 커널이 바이트들을 stdout(보통 터미널)으로 씁니다.
- 커널이 쓰여진 바이트 수를 반환하고, 실행이 사용자 공간으로 복귀합니다.
코드가 매우 단순해 보이지만, 중요한 점은 파일, 프로세스, 네트워킹, 메모리 매핑 등 모든 상호작용이 시스템 콜을 통해 이루어진다는 것입니다.
7. 컨테이너와 시스템 콜
컨테이너는 호스트 Linux 커널 위에서 실행되는 프로세스일 뿐입니다.
- 컨테이너는 별도의 커널을 갖지 않으며 호스트 커널을 공유합니다.
- 시스템 콜은 컨테이너 프로세스가 그 커널과 상호작용하는 유일한 방법입니다.
따라서 컨테이너가 수행하는 모든 작업—파일 읽기, 소켓 열기, 프로세스 생성—은 모두 시스템 콜을 통해 흐릅니다. 애플리케이션 코드는 호스트에서 실행되든 컨테이너 안에서 실행되든 동일한 방식으로 시스템 콜을 사용합니다.
8. 보안상의 함의
컨테이너가 호스트 커널에 의존하기 때문에, 시스템 콜은 강력한 보안 제어 지점이 됩니다:
- 프로세스가 강력한 시스템 콜을 호출할 수 있다면, 강력하고 잠재적으로 위험한 작업을 수행할 수 있습니다.
- 최소 권한이 중요합니다: 모든 애플리케이션이 모든 시스템 콜을 필요로 하는 것은 아닙니다.
- 컨테이너화된 애플리케이션이 사용할 수 있는 시스템 콜을 제한함으로써 공격 표면을 줄일 수 있습니다.
핵심: 공격자가 컨테이너화된 앱을 장악하면, 그 프로세스가 허용된 시스템 콜(및 권한)에 따라 할 수 있는 피해 규모가 크게 달라집니다.
따라서 컨테이너 하드닝은 커널 노출을 최소화하는 데 초점을 맞춥니다—예를 들어 seccomp 프로파일, AppArmor, SELinux, 혹은 기타 메커니즘을 사용해 컨테이너가 호출할 수 있는 시스템 콜을 제한합니다.
9. 다음 내용은?
다음 장에서는 이 기반을 바탕으로 다음을 설명합니다:
- 컨테이너가 어떻게 격리, 자원 관리, 보안 경계를 제공하는지
- 런타임 보호 기술(seccomp, capabilities, namespaces 등)
- 실제 워크로드에 적용할 수 있는 실용적인 하드닝 단계
계속 기대해 주세요!
컨테이너 보안 제어
- seccomp – 시스템 호출을 제한합니다.
- Capabilities – 불필요한 권한을 제거합니다.
- Namespaces & cgroups – 격리와 자원 제한을 제공합니다.
이후 장에서는 이 아이디어를 직접 확장하여 컨테이너가 경계를 어떻게 만들고, 이를 어떻게 강화할 수 있는지 보여줄 것입니다.
추가 자료
- Tutorial – Write a System Call → 튜토리얼 – 시스템 호출 작성
- The Definitive Guide to Linux System Calls → 리눅스 시스템 호출에 대한 결정적인 가이드
이 글은 Ultimate Container Security Series의 한 부분으로, 컨테이너 보안 개념을 실용적으로 정리하고 설명하기 위한 지속적인 노력의 일환입니다. 관련 주제를 탐색하거나 다음에 다룰 내용을 보고 싶다면, Series Introduction에서 전체 로드맵을 확인할 수 있습니다.