SVC 명령어 (Supervisor Call)
Summary
정의: 사용자 모드(EL0)에서 커널 모드(EL1)로 예외(exception)를 발생시켜 전환하는 명령어. 시스템 콜(system call)을 요청하는 표준 메커니즘이며, 커널이 사용자 프로그램을 대신해 특권이 필요한 작업(파일 입출력, 프로세스 제어, 메모리 관리 등)을 수행하도록 요청하는 통로임.
문법
SVC #imm16
imm16: 16비트 즉치값. 커널에 전달되는 값이지만, 실제 시스템 콜 번호로 직접 쓰이지는 않는 경우가 많음(플랫폼별 관례 차이, 아래 상세 설명).
동작 원리
SVC실행 시 예외 발생, 실행 흐름이 EL1(커널)의 예외 벡터 테이블로 강제 전환됨- 커널은 레지스터에 담긴 정보(시스템 콜 번호, 인자들)를 읽어 해당 작업 수행
- 작업 완료 후
ERET(Exception Return) 명령어로 사용자 모드(EL0)로 복귀 - 결과값은 관례상
X0레지스터에 담겨 반환됨
Linux 등 타 플랫폼 관례: 시스템 콜 번호를 X8에, 인자를 X0~X5에 담고 SVC #0을 호출하는 것이 표준임.
macOS(XNU 커널) 관례 — 이 부분이 질문의 핵심과 직결됨: macOS는 이 관례를 그대로 따르지 않고 독자적인 방식을 사용함.
macOS가 SVC를 직접 다루기 “싫어하는” 이유 — 구조적 배경
1. 시스템 콜 번호 자체를 X16에 담는 독자 방식
macOS/XNU 관례:
mov x16, #syscall_number // 시스템 콜 번호는 X16에
mov x0, #arg0 // 인자는 X0부터
svc #0x80 // 관례적으로 0x80 사용 (역사적으로 BSD 계열 전통)
Linux와의 차이:
| 플랫폼 | 번호 레지스터 | SVC 즉치값 |
|---|---|---|
| Linux ARM64 | X8 |
#0 |
| macOS ARM64 | X16 |
#0x80 |
이 차이 때문에 Linux 기준으로 작성된 어셈블리 예제를 macOS에 그대로 옮기면 즉시 실패함. 이전 대화들에서 예제 코드를 짤 때 항상 mov x16, #... + svc #0x80 패턴을 쓴 이유가 바로 이것임.
2. syscall 클래스 인코딩 — BSD vs Mach vs Machdep
핵심 구조: macOS(XNU)는 시스템 콜을 세 가지 클래스로 구분하며, 이 구분이 시스템 콜 번호의 상위 비트에 인코딩됨:
| 클래스 | 번호 범위(상위 비트) | 예시 |
|---|---|---|
| Mach traps | 음수 번호 영역 | mach_msg, 포트 통신 등 |
| BSD syscalls | 0x2000000 대역 |
open, read, write, exit 등 전통적 유닉스 콜 |
| Machdep | 별도 대역 | 아키텍처 종속 특수 콜 |
실전 영향: 32비트 시절(구 문서·구 예제)에는 svc #0x80과 함께 BSD 콜 번호에 0x2000000을 더해서 X16에 넣는 관례가 있었으나, 현재 64비트 ARM64 macOS에서는 어셈블러/링커 및 XNU 소스 관례상 이 오프셋 처리가 자동화되거나 다르게 취급됨. 정확한 최신 번호 매핑은 Apple의 syscall.h 헤더(오픈소스 XNU 저장소)를 직접 확인하는 것이 가장 안전함.
3. Apple의 “안정된 syscall ABI 미보장” 정책 — 가장 근본적인 이유
핵심: Apple은 공식적으로 BSD/Mach 시스템 콜 번호와 직접 SVC 호출 방식을 안정된 공개 ABI로 보장하지 않음. 즉, macOS 버전이 바뀌면 시스템 콜 번호나 호출 규약이 예고 없이 바뀔 수 있음.
이것이 “_main을 통해 libSystem을 거치는” 방식이 사실상 강제되는 이유:
| 방식 | 안정성 | 실전 사용 여부 |
|---|---|---|
직접 SVC 호출 |
불안정 (버전 간 깨질 수 있음) | 로우레벨 실험, OS 연구, 특수 목적 코드에 한정 |
libSystem.dylib을 통한 C 함수 호출 (write(), open() 등) |
안정적 (Apple이 공식 지원) | 일반적인 모든 프로그램 |
libSystem이 실제로 하는 일: printf, write, open 같은 C 표준 라이브러리 함수 내부에서 결국은 SVC를 호출하지만, 그 세부 번호 체계·클래스 인코딩·인자 배치를 캡슐화해서 Apple이 자유롭게 내부 구현을 바꿀 수 있게 해둠. 개발자는 안정된 C API만 호출하면 되고, 실제 SVC 세부사항을 알 필요가 없어짐.
4. 코드 서명(Code Signing) 및 하드닝 런타임과의 충돌
추가적 제약: macOS는 실행 파일에 대해 코드 서명(codesign) 을 강하게 요구하며, _start를 진입점으로 쓰는 순수 어셈블리 바이너리(라이브러리 링크 없이 직접 syscall만 쓰는 경우)는:
- ad-hoc 서명이라도 있어야 실행 가능한 경우가 많음
- Hardened Runtime, SIP(System Integrity Protection), Gatekeeper 등의 보안 계층과 충돌 가능성이 있음
- Apple Silicon에서는 서명 없는 바이너리 실행 자체가 더 엄격하게 제한됨
결과적으로: ld로 순수 _start + SVC 기반 바이너리를 만드는 것은 “가능은 하지만 Apple이 권장하지 않는 길”이며, 실전 프로그램은 예외 없이 _main 진입점 + libSystem 링크 방식을 사용함. _start를 직접 정의하는 방식은 학습·연구용으로만 의미가 있음.
그래도 알아야 하는 이유 — 리버싱/디버깅 관점
중요성: _main을 쓰더라도, 디스어셈블된 libSystem 내부 코드를 들여다보면 결국 이 mov x16, #N + svc #0x80 패턴이 그대로 등장함. 즉:
- 직접 작성할 일은 거의 없음
- 하지만 리버스 엔지니어링, 디버깅, 크래시 분석 시에는 반드시 마주치게 됨
lldb로write함수를 디스어셈블하면 결국 이 패턴의 SVC 호출이 최하단에 보임
실전 예시 (macOS 실제 write syscall 패턴, 참고용 근사치):
_write:
mov x16, #4 // BSD write syscall 번호 (버전별 상이할 수 있음, 참고용)
svc #0x80
b.cc return_ok // Carry Clear = 성공
// 실패 시 errno 설정 로직...
return_ok:
ret
주의: 위 번호(#4)는 역사적으로 알려진 근사치이며, Apple이 버전마다 바꿀 수 있으므로 정확한 값은 신뢰하지 말고 항상 최신 XNU 오픈소스(bsd/kern/syscalls.master)에서 직접 확인해야 함.
exit 시스템 콜 — 이전 예제들에서 반복 사용한 패턴 재확인
이전 대화의 모든 예제 코드에서 사용한 패턴:
mov x0, #0 // exit code
mov x16, #1 // exit syscall 번호
svc #0x80
이것도 사실 정식으로는 libSystem의 exit() 함수를 거치는 것이 Apple 권장 방식이나, 학습용 최소 예제(_start 기반)에서는 직접 SVC를 쓰는 것이 허용되는 몇 안 되는 실전적 예외 케이스임.
종합 결론
| 항목 | 내용 |
|---|---|
| 일반 프로그램에서의 필요성 | 낮음 (_main + libSystem 링크가 사실상 표준) |
| 리버싱/디버깅 관점 필요성 | 높음 (모든 libSystem 함수 최하단에 결국 등장) |
| Apple의 공식 입장 | 시스템 콜 번호/ABI를 공개 안정 인터페이스로 보장하지 않음 |
| macOS vs Linux 차이 | 번호 레지스터(X16 vs X8), SVC 즉치값(0x80 vs 0), 클래스 인코딩 구조 |
| 학습용 예제에서의 위치 | _start 기반 최소 예제에서만 실용적 의미 있음 |
질문하신 “맥은 저 니모닉을 별로 안 좋아 하는 것 같다”는 관찰이 정확함. Apple의 설계 철학 자체가 “SVC/syscall 세부사항은 커널 내부 구현 디테일로 숨기고, 개발자는 libSystem이라는 안정된 공개 API 계층만 사용하라”는 방향으로 강하게 짜여 있음. 이 때문에 macOS 실전 어셈블리에서 SVC는 “직접 쓰는 명령어”라기보다는 “라이브러리 내부에 숨겨진, 리버싱할 때나 마주치는 명령어”에 훨씬 가까운 위치를 차지함.