리눅스 커널의 이해
12

ch10. system calls

linuxfilesystemkernel
SYSTEM CALLS · CH.10
LECTURE NOTE — LINUX KERNEL, CHAPTER 10

사용자 프로그램이 커널에
요청을 보내는 공식 창구

write() 함수 한 줄을 호출했을 뿐이지만, 그 이면에는 CPU 권한 전환, 레지스터 저장, 디스패치 테이블 탐색, 주소 검증, 그리고 예외 복구까지 아우르는 정교한 과정이 숨어 있습니다. 시스템 호출은 결코 단순한 함수 호출이 아닙니다.

user: write(fd,buf,n)
libc wrapper
int $0x80 / sysenter
반환 / errno
sys_write()
sys_call_table[4]
사용자 모드 커널 진입 · 디스패치 서비스 루틴 → 복귀
01

큰 그림: 시스템 호출은 "사용자 프로그램이 커널에 의뢰하는 공식 창구"입니다OVERVIEW

사용자 프로그램은 어떻게 디스크를 읽고, 파일을 쓰며, 프로세스를 생성하고, 메모리를 할당받을 수 있을까요?

만약 모든 프로그램이 하드웨어를 직접 제어한다면, 악의적이거나 버그가 있는 프로그램 하나가 메모리 전체를 훼손하거나 디스크를 망가뜨릴 수 있습니다. 그래서 운영체제는 '커널'이라는 권한 있는 관리자 층을 둡니다. 사용자 모드는 권한이 제한된 공간이고, 커널 모드는 하드웨어와 핵심 자원에 온전히 접근할 수 있는 특권 공간입니다. 시스템 호출(System Call)은 철저히 분리된 이 두 세계를 잇는 유일하고 안전한 통로입니다.

은행 금고에서 직접 돈을 꺼낼 수는 없습니다. 창구 직원에게 신분증과 출금 요청서를 제출하면, 직원이 이를 확인한 뒤 돈을 내어줍니다. 시스템 호출도 마찬가지입니다. "파일에 쓰고 싶다"거나 "메모리가 더 필요하다"고 요청하면, 커널이 권한과 유효성을 철저히 검사한 후 작업을 대신 수행합니다.

이번 장의 핵심 흐름
POSIX API와 실제 커널 시스템 호출의 차이 이해하기.
시스템 호출이 커널 내부에서 어떤 핸들러와 서비스 루틴을 거쳐 처리되는지 파악하기.
x86 아키텍처의 두 가지 진입 방식인 int $0x80sysenter의 비교.
인자 전달 방식, 사용자 주소 검증, 예외 테이블(Exception Table)과 복구(Fix-up) 메커니즘.

핵심: 겉보기엔 일반적인 "함수 호출" 같지만, 시스템 호출은 CPU 권한 수준이 변경되고, 기존 레지스터와 스택 상태가 저장되며, 커널의 공통 진입점을 거쳐 특정 서비스 루틴으로 분기한 뒤 다시 사용자 모드로 복귀하는 매우 정교한 일련의 절차입니다.

핵심 O/X 퀴즈

1. 시스템 호출은 사용자 모드 프로그램이 커널 기능을 요청하기 위한 공식 인터페이스이다.
O커널은 이 요청을 검사하고 필요한 작업을 수행합니다.
2. 사용자 프로그램은 일반적으로 디스크나 CPU 같은 하드웨어를 직접 제어해야 한다.
X직접 제어하지 않고 운영체제 커널을 통해 안전하게 접근해야 합니다.
3. 시스템 호출은 단순한 C 함수 호출과 완전히 동일하며 CPU 권한 변화가 없다.
X사용자 모드에서 커널 모드로 권한이 전환되는 특수한 제어 흐름입니다.
02

POSIX API와 시스템 호출: 우리가 호출하는 함수와 커널의 실제 요청은 다릅니다API vs SYSCALL

프로그램 코드에서 open(), write(), fork()를 호출할 때 이를 흔히 '시스템 호출'이라고 부르지만, API와 실제 커널 요청은 엄밀히 구분해야 합니다. API는 어떤 서비스를 어떻게 요청해야 하는지 정의한 '인터페이스'이고, 시스템 호출은 소프트웨어 인터럽트나 전용 명령어를 통해 커널에 명시적으로 작업을 요청하는 '행위' 자체를 뜻합니다.

API와 시스템 호출의 매핑 관계
1 API → 0 syscall
sin()이나 abs() 같은 수학 함수는 커널 개입 없이 사용자 모드에서 전적으로 처리됩니다.
1 API → 1 syscall
write() 함수는 커널의 sys_write() 시스템 호출과 1:1로 정확히 대응합니다.
1 API → N syscalls
malloc() 함수는 상황과 요청 크기에 따라 내부적으로 brk()mmap() 등 여러 시스템 호출을 선택적으로 사용합니다.
N APIs → 1 syscall
여러 종류의 API 함수가 커널 내부에서는 결국 동일한 하나의 시스템 호출을 공유하기도 합니다.

POSIX 표준은 "커널의 내부 구현 방식"이 아니라 응용 프로그램이 사용할 "API"를 규정합니다. 따라서 POSIX를 준수하는 운영체제라 하더라도 내부 구조가 반드시 Linux와 동일할 필요는 없습니다. 응용 프로그램이 기대하는 함수 시그니처와 반환값의 의미만 올바르게 제공하면 됩니다.

errno 처리: 커널은 작업 실패 시 음수 형태의 오류 코드를 반환합니다. C 라이브러리(libc)의 래퍼(wrapper) 루틴이 이를 해석하여 errno 전역 변수에 양수 오류 번호를 설정하고, 사용자 프로그램에는 -1을 반환합니다. 즉, 커널이 직접 errno 변수를 수정하는 것이 아닙니다.

핵심 O/X 퀴즈

1. POSIX 표준은 주로 커널 내부의 시스템 호출 구현 방식을 직접 규정한다.
XPOSIX는 주로 응용 프로그램이 사용하는 범용 API를 규정합니다.
2. libc의 래퍼 루틴은 시스템 호출을 사용자가 쉽게 호출할 수 있게 감싸는 역할을 할 수 있다.
O레지스터 설정, 커널 진입, 오류 처리 등의 세부 작업을 대신 수행합니다.
3. 커널은 실패한 시스템 호출에 대해 직접 libc의 errno 변수를 설정한다.
X커널은 음수 오류 코드를 반환할 뿐이며, 래퍼 루틴이 이를 가공하여 errno를 설정합니다.
03

시스템 호출 핸들러와 서비스 루틴: 공항 검색대와 목적지 게이트Figure 10-1 / sys_call_table

사용자 프로그램이 시스템 호출을 실행하면 CPU는 즉시 커널 모드로 전환됩니다. 이때 수백 개의 시스템 호출 중 어떤 작업을 원하는지는 시스템 호출 번호를 통해 전달합니다. x86 아키텍처에서는 이 식별 번호를 eax 레지스터에 담습니다.

두 핵심 개념의 구분
시스템 호출 핸들러
모든 시스템 호출이 가장 먼저 거쳐 가는 공통 진입점입니다. 레지스터 상태 저장, 호출 번호 유효성 검사, 적절한 서비스 루틴 호출, 상태 복원 및 사용자 모드 복귀를 총괄합니다.
시스템 호출 서비스 루틴
요청된 시스템 호출을 실제로 수행하는 구체적인 C 함수입니다. 관례상 xyz() 함수에 대한 서비스 루틴은 sys_xyz()라는 이름을 갖습니다.

공항의 보안 검색대가 바로 '핸들러'입니다. 모든 승객은 예외 없이 이 검색대를 통과해야 합니다. 검색을 마친 승객들은 각자의 탑승권에 적힌 게이트로 흩어집니다. write 승객은 sys_write 게이트로, fork 승객은 sys_fork 게이트로 향하는 것과 같은 이치입니다.

커널 메모리에는 sys_call_table이라는 디스패치 배열이 있습니다. 이 배열의 n번째 항목에는 시스템 호출 번호 n에 해당하는 서비스 루틴의 메모리 주소가 저장되어 있습니다. eax 값이 n이면 커널은 sys_call_table[n]을 참조하여 해당 루틴을 호출합니다. 만약 아직 구현되지 않은 번호가 전달되면, 기본적으로 sys_ni_syscall()이 호출되어 -ENOSYS 오류를 반환합니다.

핵심 O/X 퀴즈

1. 리눅스 x86에서 시스템 호출 번호는 일반적으로 eax 레지스터를 통해 전달된다.
O커널은 이 번호를 바탕으로 어떤 서비스 루틴을 실행할지 결정합니다.
2. 모든 시스템 호출은 각자 완전히 다른 커널 진입점을 사용하므로 공통 핸들러가 존재하지 않는다.
X모든 호출은 단일 공통 시스템 호출 핸들러를 거친 후 개별 서비스 루틴으로 분기합니다.
3. sys_call_table은 시스템 호출 번호와 실제 서비스 루틴의 메모리 주소를 매핑해 놓은 디스패치 테이블이다.
O배열의 n번째 항목이 시스템 호출 번호 n을 처리하는 함수 포인터를 담고 있습니다.
04

커널에 진입하고 빠져나오는 두 가지 경로: int $0x80과 sysenterTWO ENTRY PATHS

x86 리눅스 환경에서 사용자 모드에서 커널 모드로 넘어가는 방법은 크게 두 가지로 나뉩니다.

진입 및 복귀 명령어 쌍
int $0x80 / iret

오래전부터 사용된 전통적이고 범용적인 인터럽트 방식입니다. IDT의 128번 벡터(0x80)를 통해 진입하며, CPU가 많은 레지스터 상태를 자동으로 스택에 저장합니다. 호환성이 뛰어나지만 상대적으로 전환 속도가 느립니다.

sysenter / sysexit

인텔 펜티엄 II 아키텍처부터 도입된 "Fast System Call" 전용 명령어입니다. MSR(Model-Specific Register)을 활용하여 빠르게 컨텍스트를 전환합니다. 속도는 매우 빠르지만, 커널 코드가 일부 상태를 직접 수동으로 저장해야 하는 번거로움이 있습니다.

모든 하드웨어가 sysenter를 지원하는 것은 아니며, 구버전의 라이브러리들은 여전히 int $0x80을 기본으로 사용합니다. 리눅스 커널은 vsyscall page라는 메커니즘을 통해 현재 구동 중인 하드웨어 환경에 가장 최적화된 방식을 동적으로 자동 선택합니다.

int $0x80은 공항의 일반 입국 심사와 같습니다. 절차가 복잡하지만 시스템이 알아서 많은 것을 처리해 줍니다. 반면 sysenter는 VIP 전용 패스트트랙 게이트입니다. 통과 속도는 빠르지만, 사전에 등록된 정보와 엄격한 전용 규격을 정확히 맞춰야만 이용할 수 있습니다.

핵심 O/X 퀴즈

1. int $0x80은 x86 리눅스에서 전통적으로 시스템 호출을 발생시키기 위해 널리 쓰이던 명령어이다.
O소프트웨어 인터럽트 벡터 128(0x80)을 트리거합니다.
2. sysenter는 int $0x80의 오버헤드를 극복하고 더 빠른 커널 진입을 제공하기 위해 도입된 명령어이다.
O최신 아키텍처에서 고속 시스템 호출을 구현하는 핵심 명령어입니다.
3. sysenter가 널리 보급되었기 때문에 현대의 리눅스 커널은 더 이상 int $0x80을 지원하지 않는다.
X구형 프로세서와의 호환성 및 오래된 바이너리 지원을 위해 두 방식 모두 완벽히 유지해야 합니다.
05

int $0x80 진입 과정: IDT 128번 게이트 통과하기system_call() / SAVE_ALL / DPL 3

사용자 프로그램이 int $0x80 명령을 실행하면, CPU는 인터럽트 디스크립터 테이블(IDT)에서 128번 벡터를 조회합니다. 커널은 부팅 초기화 단계인 trap_init() 함수에서 set_system_gate(0x80, &system_call)를 호출하여 이 경로를 미리 세팅해 둡니다.

128번 시스템 게이트 디스크립터 구성
세그먼트 셀렉터
커널의 코드 세그먼트를 명시적으로 가리킵니다.
오프셋 (Offset)
공통 핸들러인 system_call() 함수의 시작 메모리 주소입니다.
타입 (Type)
트랩 게이트(Trap Gate)로 설정됩니다. 이는 핸들러가 실행되는 동안 마스크 가능한 하드웨어 인터럽트를 자동으로 차단하지 않음을 의미합니다.
DPL (Descriptor Privilege Level) = 3
권한 수준이 3(사용자 모드)으로 설정되어 있어, 특권이 없는 일반 사용자 프로그램도 이 게이트를 합법적으로 호출할 수 있습니다. 이것이 바로 시스템 호출이 사용자 프로그램의 '공식 진입로'가 될 수 있는 이유입니다.
1
SAVE_ALL 매크로. 시스템 호출 번호가 담긴 eax와 함께 주요 범용 레지스터의 상태를 커널 모드 스택에 안전하게 보존합니다.
2
thread_info 접근. 현재 커널 스택 포인터(esp)를 기준으로 삼아 현재 실행 중인 프로세스의 thread_info 구조체 주소를 빠르게 계산해 냅니다.
3
추적 플래그 확인. TIF_SYSCALL_TRACETIF_SYSCALL_AUDIT 같은 시스템 호출 로깅/디버깅 플래그가 켜져 있다면, do_syscall_trace()를 먼저 호출하여 기록을 남깁니다.
4
번호 범위 검사. 전달된 eax 값이 최대 한계치(NR_syscalls)를 초과하는지 검사합니다. 범위를 벗어났다면 스택에 저장된 eax 위치에 -ENOSYS를 덮어쓰고 즉시 복귀 절차를 밟습니다.
5
sys_call_table 기반 디스패치. sys_call_table[eax * 4] 위치에서 해당 서비스 루틴의 주소를 꺼내어 제어권을 넘깁니다. (예: write 시스템 호출인 경우 sys_write() 실행)

핵심 O/X 퀴즈

1. int $0x80 명령어는 IDT의 128번 벡터를 참조하여 커널의 공통 진입점인 system_call 로 제어를 넘긴다.
O운영체제 부팅 시 이 경로가 게이트로 설정됩니다.
2. 시스템 호출 게이트의 DPL이 3으로 설정되어 있기 때문에 권한이 가장 낮은 사용자 모드 코드도 이 게이트를 통과할 수 있다.
O시스템 호출 자체가 사용자 프로세스를 위해 열어둔 합법적 백도어 역할을 하기 때문입니다.
3. 전달된 시스템 호출 번호가 유효하지 않아도 커널은 오류를 무시하고 sys_call_table의 임의 위치를 강제로 실행한다.
X배열 인덱스 초과를 막기 위해 번호의 범위를 반드시 사전에 검증하며, 실패 시 -ENOSYS를 반환합니다.
06

system_call()에서 빠져나오기: 사용자 모드 복귀 전, 남은 작업 확인하기종료 경로 / iret

서비스 루틴의 작업이 완료되었다고 해서 곧바로 사용자 프로그램으로 돌아가는 것은 아닙니다. 커널은 제어권을 넘겨주기 직전에 "돌아가기 전에 추가로 처리해야 할 시스템 작업이 있는지" 꼼꼼히 살핍니다.

종료 및 복귀 판단 프로세스
반환값 기록
서비스 루틴이 반환한 결과값(eax 레지스터에 담김)을 커널 스택 내 사용자 레지스터 보존 영역(저장된 eax 위치)에 기록합니다. 이를 통해 사용자 모드로 복귀했을 때 해당 값을 읽을 수 있습니다.
인터럽트 비활성화
플래그 상태를 확인하고 후속 작업을 안전하게 결정하기 위해 로컬 하드웨어 인터럽트를 일시적으로 비활성화합니다.
thread_info 플래그 검사
대기 중인 시그널(Signal)이 있는지, 새로운 스케줄링(Rescheduling)이 필요한지 등 지연된 작업 플래그를 검사합니다.
restore_all → iret
처리할 특별한 플래그가 전혀 없다면, 이전에 저장했던 레지스터 상태들을 스택에서 팝(pop)하여 복원한 뒤, iret 명령어를 호출하여 권한을 낮추며 사용자 모드로 돌아갑니다.
work_pending 경로
만약 시그널 처리나 스케줄링이 요구된다면, 복귀를 잠시 미루고 해당 작업을 모두 완료한 후에야 사용자 공간으로 돌아갑니다.

관공서 창구에서 메인 민원 처리를 마쳤습니다. 그런데 문을 나서려는 순간 직원이 붙잡으며 말합니다. "잠시만요, 방금 추가 서류가 접수되었습니다" 혹은 "다른 부서에 들러서 서명 하나 더 하셔야 합니다." 아무 문제가 없다면 바로 귀가하겠지만, 남은 절차가 있다면 출구 앞에서 모든 문제를 깨끗이 해결하고 나서야 건물을 빠져나갈 수 있습니다.

핵심 O/X 퀴즈

1. 시스템 호출 서비스 루틴의 최종 실행 결과(반환값)는 사용자 모드로 돌아갔을 때 eax 레지스터를 통해 확인할 수 있도록 스택에 안전하게 기록된다.
O커널은 반환값을 스택의 사용자 eax 저장 위치에 의도적으로 덮어씁니다.
2. 서비스 루틴이 끝나면 커널은 어떤 예외나 검사 과정도 없이 즉시 iret 명령어를 실행하여 문맥을 전환한다.
X복귀 직전에 반드시 보류된 시그널이나 재스케줄링 요청 같은 pending 작업이 있는지 검사합니다.
3. restore_all 레이블이 이끄는 경로는 저장된 레지스터를 원상 복구하고 전통적인 iret 명령을 통해 사용자 모드로 돌아갈 때 사용된다.
O이 경로는 가장 안전하고 보편적인 복귀 루틴입니다.
07

sysenter: 고속 시스템 호출을 위한 전용 진입로MSR / TSS esp0 / sysenter_entry()

sysenter 명령어는 메모리 접근 오버헤드를 줄이기 위해 CPU 내부에 있는 세 개의 특수 레지스터(MSR: Model-Specific Register)를 적극 활용합니다.

sysenter 명령어의 핵심 MSR
SYSENTER_CS_MSR
커널 모드로 진입할 때 사용할 코드 세그먼트(CS) 셀렉터 값을 미리 담아둡니다. sysenter 실행 시 즉각적으로 이 값이 적용됩니다.
SYSENTER_EIP_MSR
명령어 실행 후 점프할 커널 진입점의 메모리 주소입니다. 리눅스에서는 sysenter_entry() 함수의 주소가 할당됩니다.
SYSENTER_ESP_MSR
커널 모드 스택과 관련된 정보를 제공합니다. 단, 직접적인 커널 스택 주소라기보다는 이를 찾기 위한 TSS 구조체 접근 단서를 제공하는 역할을 합니다.

TSS esp0 기반 스택 설정의 비밀: 사용자 모드의 라이브러리 래퍼 루틴은 보안상 현재 프로세스의 커널 스택 주소를 직접 알 수 없습니다. 리눅스는 문맥 교환(Context Switch)이 일어날 때마다 새롭게 활성화되는 프로세스의 커널 스택 포인터를 로컬 TSS 구조체의 esp0 필드에 착실히 저장해 둡니다. sysenter_entry() 진입 코드는 일단 MSR이 가리키는 임시 위치로 제어권을 넘겨받은 뒤, 이 TSS의 esp0 값을 읽어와 진짜 커널 스택 포인터를 esp 레지스터에 로드하는 방식으로 딜레마를 해결합니다.

운영체제가 부팅될 때 각 CPU 코어는 enable_sep_cpu() 함수를 통해 이 MSR 값들을 초기화합니다. sysenter는 하드웨어 인터럽트 방식보다 실행 사이클이 짧아 빠르지만, int $0x80 명령이 자동으로 스택에 푸시해주던 사용자 세그먼트 레지스터, 스택 포인터, EFLAGS, 복귀 주소(EIP) 등을 커널 진입 코드가 일일이 소프트웨어적으로 구성해야 하는 트레이드오프가 있습니다.

핵심 O/X 퀴즈

1. sysenter 명령어는 빠른 커널 모드 진입을 위해 하드웨어적으로 SYSENTER_CS_MSR, SYSENTER_EIP_MSR 등의 특수 레지스터를 참조한다.
O메모리를 읽지 않고 CPU 내부 레지스터만으로 진입 준비를 마칩니다.
2. 사용자 모드 래퍼 루틴은 현재 프로세스의 정확한 커널 스택 주소를 알아낸 뒤 이를 직접 SYSENTER_ESP_MSR에 주입해야 한다.
X커널 스택의 주소 관리는 전적으로 커널이 TSS esp0를 통해 제어하며, 사용자 모드는 이에 관여할 수 없습니다.
3. sysenter는 속도가 비약적으로 빠르지만, int $0x80 방식에 비해 일부 CPU 상태 저장 작업을 소프트웨어적으로 커널 코드가 직접 처리해야 하는 수고가 따른다.
O빠른 전환(Fast Path)을 달성하기 위해 하드웨어 자동화를 최소화한 결과입니다.
08

vsyscall page: 하드웨어 지원 수준을 사용자 공간에 알려주는 동적 브리지__kernel_vsyscall / sysenter_setup()

C 표준 라이브러리(libc)는 가능하면 빠른 sysenter를 사용하고 싶어 하지만, 코드가 실행되는 현재 CPU나 커널이 이를 지원하지 않을 수도 있습니다. 이를 해결하기 위해 커널은 초기화 과정인 sysenter_setup()에서 특별한 메모리 페이지(vsyscall page)를 하나 준비해 둡니다. 그리고 새로운 ELF 실행 파일이 메모리에 로드될 때, 이 페이지를 각 프로세스의 주소 공간 상단에 동적으로 매핑해 줍니다.

vsyscall page의 영리한 동작 방식
__kernel_vsyscall
libc 래퍼가 시스템 호출을 발생시키기 위해 일관되게 호출하는 vsyscall page 내부의 핵심 진입 함수입니다. 실제 내부 명령어가 무엇일지는 커널이 결정해 둔 상태입니다.
CPU가 sysenter를 미지원할 때
커널은 부팅 시 이 페이지 공간에 기존 int $0x80 명령어를 사용하는 코드를 배치해 둡니다.
CPU가 sysenter를 완벽히 지원할 때
커널은 이 페이지에 sysenter를 활용하는 최적화된 코드를 덮어씁니다. 이때 커널 진입 전 사용자 스택에 복귀 시 필요한 레지스터(ecx, edx, ebp)를 백업하고, ebp에 현재의 사용자 스택 포인터를 보존하는 작업도 이 코드 내에서 이루어집니다.
매우 오래된 커널 환경
만약 vsyscall page 자체가 매핑되지 않는 구식 환경이라면, libc는 이를 감지하고 스스로 int $0x80을 직접 하드코딩하여 실행하는 하위 호환성(Fallback) 로직을 탑재하고 있습니다.
1
libc 래퍼 루틴 → 시스템 호출 번호를 eax에 세팅 → __kernel_vsyscall() 함수를 호출합니다.
2
__kernel_vsyscall 내부 → ecx, edx, ebp 레지스터를 사용자 스택에 안전하게 백업 → ebp에 현재 사용자 esp 값을 복사 → sysenter 명령어를 전격 실행합니다.
3
CPU가 하드웨어 로직에 따라 MSR을 참조 → 권한 수준을 커널 모드로 격상 → 커널 내부의 sysenter_entry() 레이블로 제어가 점프합니다.
4
sysenter_entry → 앞서 다룬 TSS esp0 값을 읽어와 진짜 커널 스택 포인터를 설정 → 안전을 위해 잠가둔 인터럽트 활성화 → 사용자 세그먼트, 스택, EFLAGS 플래그, 복귀용 SYSENTER_RETURN 주소를 커널 스택에 수동으로 레이아웃합니다.
5
이후부터는 int $0x80 방식과 완벽히 동일한 공통 시스템 호출 흐름(번호 유효성 검사 → 테이블 디스패치 → 실제 서비스 루틴 실행)을 따릅니다.

핵심 O/X 퀴즈

1. vsyscall page는 프로세스 주소 공간에 매핑되어, 사용자 코드가 최적의 시스템 호출 진입 명령을 투명하게 호출할 수 있도록 돕는다.
O응용 프로그램은 내부 명령어를 몰라도 __kernel_vsyscall()만 호출하면 됩니다.
2. CPU가 최신 명령어인 sysenter를 지원하지 못하는 환경이라면, vsyscall page 메커니즘은 작동을 멈추고 프로그램을 강제 종료시킨다.
X커널은 하드웨어 수준을 파악하여 페이지 내부에 하위 호환 가능한 int $0x80 코드를 채워 넣습니다.
3. sysenter 경로에서는 기존 int 명령이 하드웨어 차원에서 알아서 백업해주던 사용자 세그먼트나 플래그 상태를 sysenter_entry() 코드가 직접 소프트웨어적으로 커널 스택에 구성해야 한다.
O진입 속도를 비약적으로 높이는 대신, 복구에 필요한 최소한의 컨텍스트 저장은 소프트웨어의 몫으로 남겨둔 아키텍처적 선택입니다.
09

sysexit와 SYSENTER_RETURN: 빠르게 빠져나오되, 원래 자리로 정확히 복귀하기sysexit / edx·ecx

서비스 루틴이 작업을 성공적으로 마치고 반환되었을 때, 지연된 처리 플래그가 없다면 커널은 iret 대신 전용 명령어인 sysexit을 이용해 사용자 모드로의 가장 빠른 복귀를 시도합니다.

sysexit 명령어 실행 전 레지스터 셋업
edx
가장 중요한 복귀 주소(SYSENTER_RETURN 레이블의 주소)를 담습니다. 사용자 공간에서 CPU가 다시 실행을 이어갈 위치입니다.
ecx
복원해야 할 사용자 모드의 스택 포인터(esp) 값을 담아둡니다.
ebp
커널 스택 프레임 정보가 악의적으로 유출되는 것을 막기 위해 0으로 클리어(Zeroing)합니다.

sysexit 명령이 CPU에서 실행되면, 하드웨어는 SYSENTER_CS_MSR+16 공식에 따라 사용자 코드 세그먼트 권한을 복원하고, edx 값을 eip에 밀어 넣습니다. 이와 동시에 SYSENTER_CS_MSR+24로 사용자 데이터 세그먼트를 셋업하며 ecx 값을 스택 포인터 esp로 복구한 뒤 사용자 모드로 완전히 돌아갑니다. 이제 제어권은 앞서 언급한 vsyscall page 영역 내의 SYSENTER_RETURN 코드로 떨어집니다.

SYSENTER_RETURN 코드 조각의 임무는 매우 단순하고 명료합니다 — 앞서 __kernel_vsyscall()이 커널로 뛰어들기 직전 사용자 스택 최상단에 푸시해두었던 ebp, edx, ecx 레지스터 값을 하나씩 팝(pop)하여 원래 상태로 복원하고, 최종적으로 ret 명령을 실행하여 이 모든 절차를 트리거했던 libc 래퍼 함수로 부드럽게 제어 흐름을 반환합니다.

들어가고 나오는 과정이 완벽한 대칭을 이룹니다. 커널 진입 시 __kernel_vsyscall()이 레지스터를 백업하고 sysenter로 급행열차를 탔다면, 복귀 시에는 커널이 sysexit으로 SYSENTER_RETURN 정거장에 승객을 정확히 내려주고, 정거장에 남겨뒀던 짐(레지스터)을 되찾아 최종 목적지로 향하게 하는 왕복 티켓 시스템과 같습니다.

주의: 시그널 처리나 스케줄링 등 잔여 작업 플래그가 하나라도 켜져 있다면, 커널은 이 고속 복귀 경로를 과감히 포기하고 공통된 느린 경로(Slow Path)로 빠져 전통적인 iret 명령어를 이용해 안전하게 돌아갑니다. 빠른 길은 환경이 완벽히 깨끗할 때만 허용됩니다.

핵심 O/X 퀴즈

1. sysexit 명령어는 sysenter와 완벽히 짝을 이루며, 사용자 모드로의 가장 빠르고 효율적인 복귀를 담당한다.
Oedx와 ecx 레지스터에 복귀 주소 및 사용자 스택 포인터 단서를 담아 고속 복귀를 수행합니다.
2. sysenter 명령으로 진입한 시스템 호출은 내부 상태와 상관없이 반드시 sysexit 명령으로만 사용자 공간으로 돌아가야 한다.
X시그널이나 스케줄링 플래그가 발견되면, 안전한 처리를 위해 공통 경로를 거쳐 iret 명령어로 우회 복귀할 수 있습니다.
3. SYSENTER_RETURN 코드 영역은 vsyscall page 내부에 위치하며, 커널 진입 직전에 저장해 두었던 사용자 범용 레지스터들을 원상 복구하는 역할을 충실히 수행한다.
O복구가 끝나면 ret 명령을 통해 원래 호출자인 래퍼 함수로 실행 흐름을 완전히 돌려줍니다.
10

시스템 호출 인자 전달: 스택이 아닌 레지스터를 통한 효율적 전달REGISTER PASSING

시스템 호출은 어떤 작업을 할지 알려주는 번호만으로는 부족합니다. write(fd, buf, count)처럼 어느 파일에, 어떤 데이터를, 얼만큼 쓸지 명시하는 구체적인 인자가 필수적입니다. x86 시스템에서는 속도 최적화를 위해 메모리에 기반한 사용자 스택 대신 CPU 레지스터에 인자를 직접 담아 커널에 전달하는 방식을 채택합니다.

레지스터역할 및 전달 내용호출 예시 (write)
eax시스템 호출 번호 (식별자)__NR_write = 4
ebx1번째 필수 인자fd (파일 디스크립터 정수)
ecx2번째 필수 인자buf (사용자 버퍼의 메모리 주소)
edx3번째 필수 인자count (기록할 데이터 바이트 수)
esi4번째 인자
edi5번째 인자
ebp6번째 인자

이 규약에 따라 최대 6개의 인자까지 레지스터만으로 넘길 수 있습니다. 만약 함수가 6개 이상의 인자를 요구하거나 전달해야 할 데이터가 거대한 구조체라면, 사용자 주소 공간 내의 구조체가 위치한 메모리 포인터 하나만을 레지스터에 담아 넘기는 간접 참조(우회) 방식을 활용합니다. 커널 진입 단계에서 SAVE_ALL 매크로가 이 레지스터들을 커널 스택에 차례대로 푸시해 놓으면, 이후 실행되는 C 언어 기반의 서비스 루틴은 이 스택 레이아웃을 바탕으로 일반적인 함수 인자를 읽듯 자연스럽게 값에 접근할 수 있습니다.

특수한 상황 — 예를 들어, fork() 계열 시스템 호출처럼 자식 프로세스 생성을 위해 부모의 전체 레지스터 컨텍스트를 완벽히 복제해야 하는 서비스 루틴의 경우 — 일반 변수가 아닌 pt_regs 구조체 포인터 타입의 인자를 선언하여, SAVE_ALL이 구성해 둔 레지스터 저장 영역에 직접적이고 광범위하게 접근합니다.

핵심 O/X 퀴즈

1. 32비트 x86 리눅스에서 시스템 호출 번호는 eax에 담기며, 함수 인자들은 ebx, ecx, edx, esi, edi, ebp 레지스터 순서로 차례로 할당되어 전달된다.
O이것이 속도 중심의 x86 시스템 호출 레지스터 전달 규약입니다.
2. 시스템 호출 인자는 일반 C 함수 호출과 동일하게, 언제나 사용자 스택 공간에 푸시된 채로 커널 공간으로 메모리 복사 과정을 거쳐 전달된다.
X성능 오버헤드를 막기 위해 메모리 스택이 아닌 초고속 레지스터를 매개체로 직접 전달되며, 진입 코드가 이를 커널 스택에 백업합니다.
3. 전달해야 할 인자의 개수가 6개를 초과하거나 구조체와 같이 부피가 큰 데이터 형식을 전달할 경우, 데이터가 위치한 사용자 메모리의 포인터 주소만 레지스터에 담아 효율적으로 우회 전달할 수 있다.
O이런 경우 래퍼 루틴과 커널은 포인터 간접 참조 방식을 통해 복잡한 데이터를 주고받습니다.
11

인자 검증과 access_ok(): 커널은 사용자 포인터를 절대 무비판적으로 신뢰하지 않습니다addr_limit.seg / PAGE_OFFSET

사용자가 악의적이거나 실수로 커널 내부 주소를 포인터 인자로 넘겼을 때, 커널이 이를 의심 없이 읽거나 쓰려고 한다면 시스템 핵심 메모리가 통째로 유출되거나 치명적으로 훼손될 수 있습니다. 따라서 커널은 사용자 공간에서 넘어온 모든 주소의 안전성을 철저히 검사해야만 합니다.

커널의 이중 메모리 주소 방어 전략
1차 방어선: 거친 검사 (access_ok)
"이 주소가 커널 전용 영역을 침범하려고 시도하는가?" — 매크로를 통해 계산된 끝 주소가 PAGE_OFFSET 경계선과 현재 프로세스의 thread_info.addr_limit.seg 상한선을 넘지 않는지 가볍게 계산해 봅니다. 이는 완전히 안전하다는 보장이 아니라, 최소한의 필요조건을 따지는 검문입니다.
2차 방어선: 세밀한 검사 (Page Fault 유도)
메모리에 실제로 접근하는 순간 권한 문제나 매핑 누락으로 인해 페이지 폴트 하드웨어 예외가 발생하면, 커널 내부의 예외 테이블(exception table)과 복구 로직(fix-up 코드)이 개입하여 이를 우아하게 -EFAULT 반환 에러로 둔갑시킵니다. (다음 섹션에서 상세히 다룹니다.)

access_ok(type, addr, size) 매크로 함수의 주된 역할은 addr+size 연산 과정에서 정수 오버플로가 발생하지 않는지, 그리고 그 덧셈의 최종 주소가 thread_info.addr_limit.seg 값을 침범하지 않는지 논리적으로 확인하는 것입니다. 일반적인 응용 프로세스의 addr_limit.seg 값은 커널 시작점인 PAGE_OFFSET과 일치합니다. 예외적으로 커널 내부에서 사용자 API를 직접 에뮬레이션해야 할 때는 get_fsset_fs 매크로를 사용해 이 상한선을 임시로 재조정하기도 합니다.

성능과 보안성 간의 아슬아슬한 균형: 시스템 호출이 일어날 때마다 해당 포인터가 완벽히 유효한 매핑 상태인지 정밀하게 페이지 테이블을 순회하며 검증한다면 엄청난 성능 하락이 발생합니다. 잘못된 포인터를 던지는 비정상적 프로그램은 통계적으로 소수이므로, 리눅스는 일단 가벼운 덧셈으로 큰 울타리 경계망(access_ok)만 친 다음, 실제 메모리를 만질 때 하드웨어가 뱉어내는 페이지 폴트에 의존하는 낙관적 방어(Optimistic Protection) 기법을 취합니다.

핵심 O/X 퀴즈

1. access_ok() 매크로는 악의적인 사용자 포인터가 커널 코어 주소 공간을 가리키는 위험한 상황을 1차적으로 차단하기 위한 논리적 범위 한계 검사이다.
OPAGE_OFFSET과 프로세스의 addr_limit.seg 상한선을 주요 기준으로 대조합니다.
2. access_ok() 검증 루틴을 무사히 통과한 주소라면 해당 포인터가 무조건 프로세스 메모리에 정상적으로 매핑되어 있고, 접근 권한 역시 완벽하게 100% 보장되는 상태임이 확증된다.
X이 검사는 단순히 '커널 영역은 아니다'라는 거친 필터링일 뿐이며, 진짜 결함 유무는 데이터 접근 순간 발생하는 페이지 폴트를 통해 판가름 납니다.
3. 만일 사용자 프로그램이 악의적으로 커널 주소를 포인터 인자로 던졌는데도 커널 차원의 엄격한 주소 검증 절차가 없다면, 커널의 기밀 메모리 데이터가 탈취되거나 파괴될 심각한 보안 구멍이 발생한다.
O이것이 바로 커널 공간과 사용자 공간의 접근을 철저히 분리하고 주소를 의심해야만 하는 근본적 이유입니다.
12

사용자 주소 공간 안전하게 접근하기: get_user, put_user, copy_from_userTable 10-1

대부분의 커널 서비스 루틴은 사용자 공간에 마련된 데이터 버퍼를 빈번하게 읽고 씁니다. 커널 코드는 보안 원칙상 아무리 사용자 포인터라 할지라도 일반적인 C 언어 포인터 다루듯 *ptr = value 형태로 직접 역참조(Dereferencing)해서는 안 되며, 반드시 커널이 제공하는 래핑된 전용 함수나 매크로를 통과해야 합니다.

Table 10-1 — 핵심 사용자 공간 접근 API 모음
get_user(x, ptr)
사용자 공간 메모리 주소에서 1, 2, 4바이트 단위의 원시 정수형 데이터를 안전하게 읽어 변수 x에 담습니다. 도중 문제가 생기면 -EFAULT 코드를 반환합니다.
put_user(x, ptr)
변수 x에 담긴 원시 정수형 데이터를 사용자 공간 특정 메모리 주소에 조심스럽게 기록합니다. 실패 시 -EFAULT가 반환됩니다.
copy_from_user(to, from, n)
임의의 사용자 공간 버퍼 블록에서 커널 내부 공간으로 주어진 n바이트만큼의 데이터를 안전하게 일괄 복사해 옵니다.
copy_to_user(to, from, n)
반대로 커널 공간에서 처리 완료된 n바이트의 데이터를 응용 프로그램이 볼 수 있게 사용자 공간 버퍼로 밀어 넣습니다.
strncpy_from_user
사용자 공간에 놓인 널(NULL) 종료 C 스타일 문자열의 끝을 찾아가며 커널로 안전하게 복사해 옵니다.
strlen_user / strnlen_user
사용자 공간에 존재하는 문자열의 길이를 측정합니다. 커널 영역 침범이나 무한 루프를 막기 위한 방어 코드가 포함되어 있습니다.
clear_user
사용자 공간의 특정 메모리 블록 크기 전체를 0(Null) 바이트로 덮어씌워 초기화합니다.

가끔 커널 코드를 보다 보면 함수 이름 맨 앞에 밑줄 두 개가 연달아 붙은 특별한 변형판(예: __get_user, __copy_from_user)을 마주칠 수 있습니다. 이 함수들은 상위 로직에서 access_ok()를 통한 주소 거친 검증이 이미 확실하게 수행되었다고 전제하여, 함수 내부의 중복된 주소 검사 루틴을 과감히 생략합니다. 이는 오버헤드를 극단적으로 줄이려는 최적화 기법입니다.

회사 건물 1층 로비의 엄격한 신원 확인 게이트(access_ok)를 상상해 보십시오. 방문증을 발급받아 이미 사무실 구역에 진입한 직원이 특정 회의실 책상을 여러 번 오가며 짐을 나를 때(데이터 반복 접근), 매 걸음마다 경비원이 따라붙어 신분증을 반복해서 요구하지는 않습니다. 가장 바깥 울타리에서 한 번 크게 신뢰도를 확인했다면, 내부에서는 제한적이지만 자유로운 접근 권한을 부여해 효율성을 높이는 원리와 동일합니다.

핵심 O/X 퀴즈

1. copy_from_user() 함수는 이름 그대로 사용자 공간 메모리에 위치한 데이터를 커널 내부 공간으로 안전하게 끌어와 복사하는 데 쓰이는 블록 단위 전송 함수이다.
O반대로 결과를 되돌려 줄 때는 짝을 이루는 copy_to_user() 함수를 활용합니다.
2. 커널 내부 C 코드를 작성할 때는 신뢰도와 속도를 위해 사용자 공간의 포인터 변수를 언제나 일반 포인터 변수처럼 직접 * 역참조 연산자를 써서 다루어도 전혀 무방하다.
X이는 커널 크래시나 치명적 보안 위협을 야기하는 금기 사항이며, 반드시 get_user, copy_from_user 같은 정해진 안전 래퍼 함수를 거쳐야만 합니다.
3. 함수 명칭 앞에 언더바 두 개(__)가 붙어있는 사용자 메모리 접근 변형 함수들은, 통상적으로 이미 주소 범위 검증이 끝났다고 믿고 내부 체크를 생략하여 실행 속도를 끌어올린다.
O동일한 버퍼 조각을 루프 안에서 빈번하게 읽고 쓸 때 성능 저하를 막아주는 유용한 우회 스킬입니다.
13

동적 주소 검사와 fix-up 메커니즘: 예외를 안전한 오류 코드로 변환하는 안전장치exception table / __ex_table / .fixup

앞서 설명한 access_ok() 함수는 PAGE_OFFSET 기반의 매우 1차원적인 범위 검사만 수행할 뿐입니다. 주소값이 PAGE_OFFSET보다 낮더라도, 실제로 해당 영역에 물리 메모리 프레임이 할당(매핑)되어 있지 않은 허공의 주소라면 어떻게 될까요? 커널이 그곳에 손을 뻗는 순간 얄짤없이 하드웨어 페이지 폴트(Page Fault) 예외가 터져 나옵니다. 하지만 명심할 것은, 커널 모드에서 발생한 페이지 폴트가 무조건적으로 "커널을 짜놓은 개발자의 치명적인 멍청한 버그(Oops)"를 의미하지는 않는다는 점입니다.

커널 모드 페이지 폴트가 터지는 4가지 대표적 시나리오
① 정상적인 Demand Paging / COW 동적 할당
사용자 주소 공간 페이지에 접근하는 건 맞는데, 아직 물리 프레임이 늦게 할당되었거나 쓰기 방지(Write-Protected)가 걸린 복제(COW) 상태일 때.
② vmalloc 영역의 지연된 동기화
올바른 커널 동적 메모리 주소지만, 커널 프로세스별 페이지 테이블 갱신이 한 템포 늦어 엔트리가 비어 있을 때.
③ 치명적인 커널 내부 버그 또는 진짜 하드웨어 고장
포인터 계산 실수나 램 칩 오류 등으로 복구 불가능한 커널 패닉(Oops) 상황.
④ 악질적이거나 실수로 잘못 전달된 사용자 인자 (오늘의 주인공)
응용 프로그램이 넘긴 사용자 버퍼 포인터 주소가 엉터리라서, 커널이 그 인자를 맹신하고 접근하다 허공을 찔렀을 때 발생하는 에러.
insn (위험을 내포한 메모리 접근 명령어의 주소)
fixup (오류 발생 시 뛰어갈 긴급 복구 코드의 주소)
__get_user_1 레이블
bad_get_user: edx 레지스터 0으로 클리어, eax에 -EFAULT 에러 담고 ret
__get_user_2 레이블
bad_get_user: edx 레지스터 0으로 클리어, eax에 -EFAULT 에러 담고 ret
strlen_user 문자열 탐색 스캔 어셈블리 명령어
.fixup 블록: 반환값 eax를 0으로 강제 세팅 후 jmp 통해 함수 강제 종료점 이동
1
실행 도중 커널 모드 권한으로 하드웨어 페이지 폴트가 유발되면 → do_page_fault() 인터럽트 핸들러가 긴급 출동합니다.
2
search_exception_tables(eip) 함수 호출 — 핸들러는 직전에 페이지 폴트 문제를 일으킨 범인 명령어의 주소(eip)를 들고 커널의 전역 예외 테이블 장부를 뒤지기 시작합니다.
3
만약 예외 테이블 장부 목록에 이 범인 명령어 주소가 등록되어 있다면 → 이는 커널 버그가 아니라 "예상된 오류 범위 내"임을 의미합니다. 핸들러는 스택에 백업되어 있던 eip 주소를 짝꿍으로 등록된 fix-up(긴급 복구) 코드의 주소로 잽싸게 바꿔치기합니다. 페이지 폴트 핸들러가 종료되면 CPU는 범인 명령어가 아닌, 이 안전한 fix-up 코드 지점부터 새롭게 실행을 이어갑니다.
4
수행권을 넘겨받은 fix-up 코드는 엉망이 된 결과 레지스터 상태를 말끔히 정리하고 + -EFAULT 상수를 반환하여 → 서비스 루틴이 "사용자가 이상한 포인터를 줬어"라는 오류 코드를 래퍼 루틴에 전달할 수 있게 징검다리 역할을 해줍니다.

커널 설계 철학은 시스템이 완벽하게 오류 없이 굴러갈 것이라고 헛된 가정을 하지 않습니다. 대신 오류가 날 법한 위험천만한 메모리 접근 지점들을 사전에 꼼꼼히 마킹(__ex_table 섹션)해 두고, 실제로 하드웨어 예외가 터지면 제어 흐름을 우아하게 잡아채어 소프트웨어적인 안전망 복구 코드(.fixup 섹션)로 미끄러지듯 연결합니다. 무시무시한 하드웨어 크래시 예외를 그저 "-1 반환"이라는 평화로운 소프트웨어 오류 코드로 마법처럼 탈바꿈시키는 핵심 장치입니다.

핵심 O/X 퀴즈

1. 커널 모드 권한 하에서 메모리 페이지 폴트가 발생하면, 상황 불문 언제나 시스템을 정지시켜야 하는 치명적인 커널 버그(Kernel Panic)로 취급하고 처리해야 한다.
X예상 가능한 궤도 안에서 사용자 포인터 메모리를 탐색하다 발생한 자연스러운 예외일 가능성이 높으며, 이는 복구 메커니즘을 통해 유연하게 처리됩니다.
2. exception table 구조체는 위험이 도사리는 사용자 공간 접근 커널 어셈블리 명령어의 주소와, 해당 명령이 실패했을 때 탈출할 비상구인 fix-up 코드의 주소를 1:1 쌍으로 강력하게 매핑해 놓은 일종의 색인표이다.
O페이지 폴트 인터럽트 핸들러가 이 색인표를 이진 탐색하여 구명줄(복구 경로)로 런타임 제어권을 옮겨버립니다.
3. 백업용 fix-up 코드가 존재하는 핵심적인 이유는 엉뚱한 사용자 메모리 주소 접근 시 뻥 터지는 하드웨어 예외를 포착해, 응용 프로그램이 알아들을 수 있는 -EFAULT 같은 정갈한 오류 반환값으로 마사지하기 위함이다.
O이런 든든한 동적 주소 예외 복구망이 있기에 리눅스 커널이 사용자 프로그램의 오작동에도 죽지 않고 버티는 것입니다.
14

커널 래퍼 매크로와 전체 과정 총정리: 시스템 호출은 한 줄짜리 함수 뒤에 숨겨진 거대한 약속입니다_syscall0~6 / 전체 흐름

일반 응용 프로그램과 달리, 커널 모드 영역에서 동작하는 커널 스레드(Kernel Thread) 데몬들은 덩치가 큰 외부 라이브러리인 libc의 래퍼 함수들을 가져다 쓸 수 없습니다. 이런 제약을 해결하기 위해 리눅스 커널 소스 트리는 필요한 시스템 호출 래퍼 루틴을 인라인 어셈블리로 아주 손쉽게 찍어내듯 선언할 수 있는 _syscall0부터 _syscall6까지의 매직 매크로 세트를 기본으로 제공합니다. 매크로 이름 뒤에 붙은 숫자는 시스템 호출 번호 자체를 제외한, 함수가 요구하는 순수 '인자의 개수'를 직관적으로 나타냅니다.

_syscall3 매크로로 찍어낸 write() 래퍼 루틴의 논리적 실행 흐름
레지스터 인자 세팅
호출 번호 eax = __NR_write (4번) 할당. 이어 1번째 인자 ebx = fd, 2번째 인자 ecx = buf 주소, 3번째 인자 edx = 기록할 count 바이트 수를 차곡차곡 담습니다.
커널 인터럽트 진입
모든 준비를 마치고 int $0x80 어셈블리 명령어를 강하게 내리쳐 커널을 깨웁니다.
반환값 최종 마사지
커널에서 돌아온 결괏값 eax가 음수 대역(오류 발생을 의미)이라면 이를 절대값(양수)으로 뒤집어 범용 errno 변수에 기록하고, 사용자 코드에는 일괄적으로 -1을 리턴합니다. 정상 성공 시에는 eax의 양수 결괏값을 그대로 토스합니다.

이제 지금까지 흩어져 있던 조각들을 모아, write() 함수가 호출되는 순간부터 끝날 때까지의 웅장한 왕복 여정을 하나의 서사시로 이어보겠습니다.

사용자 코드 영역: write(fd, buf, count) API를 코드에서 호출 → C 라이브러리(libc) 내부 래퍼 루틴이 시스템 호출 번호와 인자 3개를 eax, ebx, ecx, edx 레지스터에 맞게 정렬합니다.
vsyscall 브리지 통과: __kernel_vsyscall() 함수를 호출 → 현재 구동 중인 하드웨어 환경(CPU 지원 여부)에 맞춰 int $0x80 또는 고속 sysenter 명령어 중 최적의 경로를 타고 실행됩니다.
커널 진입과 핸들러: 시스템 권한이 특권 커널 모드로 격상 → 공통 진입점에서 SAVE_ALL 매크로가 레지스터 상태를 커널 스택에 안전하게 보존 → 현재 thread_info 구조체 접근 후 대기 중인 추적 플래그 여부를 확인하고, 요청된 시스템 호출 번호가 상한선 내에 있는지 유효성을 검토합니다.
디스패치 및 서비스 루틴 메인 실행: 디스패치 배열인 sys_call_table[4] 인덱스에서 sys_write() 함수의 주소를 찾아내 실행 권한을 위임합니다. 서비스 루틴 내부에서는 파일 디스크립터(fd)의 유효성, 쓰기 권한, 버퍼 메모리의 안정성(access_ok 체크)을 다각도로 검증한 후, copy_from_user()를 이용해 사용자 버퍼의 데이터를 안전하게 커널 캐시 메모리로 복사해 옵니다.
마무리 및 반환 준비: 서비스 루틴이 작업의 최종 성공/실패 여부를 반환값으로 넘깁니다 → 이 값은 커널 스택에 저장되어 있던 사용자 eax 레지스터 백업 슬롯 위치에 덮어쓰여집니다. 복귀 직전 마지막으로 thread_info 내에 보류된 시그널이나 컨텍스트 스위칭 재스케줄링 플래그가 없는지 면밀히 확인합니다.
사용자 모드 생환: 처리할 잔여 플래그가 없다면 가장 신속한 경로인 sysexit(또는 iret) 명령을 통해 특권 모드를 반납하고 복귀합니다 → 사용자 주소 공간에 대기 중이던 SYSENTER_RETURN 레이블 코드가 백업 레지스터들을 마저 팝(Pop)하여 원상 복구하고 → 최종적으로 libc 래퍼 루틴이 음수/양수 여부를 판단해 errno 변수 세팅 및 최종 API 성공값을 반환하면서 모든 여정이 끝납니다.

마지막 한 줄 요약: 응용 프로그램을 작성하는 개발자에게 시스템 호출은 그저 한 줄의 편리한 라이브러리 함수 호출처럼 친숙하게 다가오지만, 그 수면 아래 운영체제 커널의 입장에서는 시스템의 절대적 보안, 다양한 하드웨어 호환성, 극한의 성능 최적화, 그리고 예측 불가능한 메모리 예외 복구(Fix-up)까지 동시에, 완벽하게 통제해 내야만 하는 엄청나게 거대하고 정교한 오케스트라 연주와 같습니다.

핵심 O/X 퀴즈

1. 리눅스에서 제공하는 _syscall0부터 _syscall6 형태의 어셈블리 매크로 묶음은, 함수가 넘겨받아야 할 시스템 호출 번호 외 순수 인자의 개수에 맞춰 래퍼 루틴을 컴파일 타임에 손쉽게 만들어주는 코드 생성기 역할을 톡톡히 한다.
O뒤에 붙은 아라비아 숫자는 시스템 호출 번호를 제외하고 넘겨야 할 인자의 개수를 명시합니다.
2. write() 호출을 담당하는 래퍼 루틴 내부 로직은, 개발자가 넘긴 인자 변수값들을 아키텍처 규약에 맞게 CPU 레지스터에 정렬하여 담은 후, 트랩을 쳐서 시스템 호출 진입 명령을 실행시키는 구조로 짜여 있다.
Oeax 레지스터에는 호출 식별 번호가, 이어지는 ebx, ecx, edx 레지스터 공간에는 3개의 실제 인자값이 차례대로 들어갑니다.
3. 우리가 프로그램을 짤 때 실패한 함수가 -1을 리턴하고 전역 errno 변수값이 바뀌는 현상은, 시스템 호출이 실패했을 때 커널 내부의 서비스 루틴이 직접 유저 메모리에 접근해 errno를 강제로 조작하기 때문에 발생하는 현상이다.
X커널은 절대 libc의 구조체를 건드리지 않으며 순수하게 음수 오류 코드만 반환합니다. 래퍼 루틴이 이 음수 값을 가로채어 API 호출 규칙에 맞게 후처리 조작을 가하는 것입니다.

응용 프로그램을 작성하는 개발자에게 시스템 호출은 그저 한 줄의 편리한 라이브러리 함수 호출처럼 친숙하게 다가오지만, 그 수면 아래 운영체제 커널의 입장에서는 보안, 다양한 하드웨어 호환성, 극한의 성능 최적화, 그리고 예측 불가능한 메모리 예외 복구까지 동시에, 완벽하게 통제해 내야만 하는 정교한 오케스트라 연주와 같습니다.

강의노트 — Linux Kernel, Chapter 10 "System Calls" 기반 재구성