10장 시스템 콜 김 태 훈 API와 시스템 콜의 차이 API는 특정 서비스를

Download Report

Transcript 10장 시스템 콜 김 태 훈 API와 시스템 콜의 차이 API는 특정 서비스를

10장 시스템 콜
김태훈
API와 시스템 콜의 차이
• API는 특정 서비스를 어떻게 받을지를 지정하는
함수
• 시스템 콜은 소프트웨어 인터럽트를 통해 커널에
하는 직접적인 요청
• libc 에서 정의하는 API중 일부는 시스템 콜을 호
출하는 목적으로만 사용하는 “래퍼루틴”이 있다.
• 보통 각 시스템 콜마다 대응하는 래퍼 루틴이 하나
씩 있으며, 래퍼 루틴은 애플리케이션에서 사용할
API를 정의한다.
– 그러나 그 역은 성립하지 않는다. 즉, API는 특정 시스
템 콜에 대응할 필요가 없다.
시스템 콜 핸들러와 서비스 루틴
• 사용자 모드 프로세스가 시스템 콜을 호출하면, CPU는 커널
모드로 전환해서 커널함수를 실행하기 시작한다.
• 커널은 서로 다른 많은 시스템 콜을 구현하므로 사용자 모드
프로세스는 자신이 원하는 시스템 콜을 구별하는 ‘System Call
Number’를 매개 변수로 전달해야 한다.
– eax 레지스터를 사용한다.
• 모든 시스템 콜은 정수 값을 반환한다.
–
–
–
–
양수나 0은 성공, 실패는 음수이다.
에러가 발생한 경우 errno변수를 통해 에러 코드는 나타낸다.
커널은 error변수를 사용하지 않음.
대신 래퍼 루틴이 시스템 콜에서 돌아온 후 변수를 설정하는 일
을 한다.
시스템 콜 핸들러와 서비스 루틴
• 시스템 콜 핸들러의 역할
– 레지스터 내용을 커널 모드 스택에 저장한다.
– ‘System Call Service Routine’이라는 C함수를 호출하여 시스템
콜을 처리한다.
– 핸들러에서 빠져 나온다.
• 커널 모드 스택에 저장된 값을 레지스터들에 로딩하고, CPU
는 커널모드에서 사용자 모드로 다시 돌아온다.
시스템 콜 핸들러와 서비스 루틴
• 그림 10-1
– SYSCALL, SYSEXIT는 사용자 모드에서 커널 모드로,
커널 모드에서 사용자 모드로 CPU를 전환하는 실제
어셈블리 명령어가 있는 곳을 나타낸다.
• 커널은 시스템 콜 번호와 해당 시스템 콜 서비스
루틴을 서로 연계하기 위해 ‘System Call Dispatch
Table’을 사용한다.
–
–
–
–
이 테이블은 sys_call_table 배열에 들어 있다.
NR_syscalls개의 엔트리를 포함한다.
__NR_syscall_max개 (279개)
이 값은 구현 가능한 시스템 콜의 최대 개수로, 실제
구현하고 있는 시스템 콜 수를 의미하지는 않는다.
시스템 콜로의 진입과 복귀
• 시스템 콜을 호출하는 두 가지 방법
– int $0x80 어셈블리 명령을 수행한다.
• 과거 버전에서 사용자 모드에서 커널 모드로 전환
하는 유일한 방법
– sysenter 어셈블리어를 수행한다.
• 2.6 커널 이상 부터 지원함.
• 펜티엄 2 프로세서 부터 지원함.
• 사용자 모드로 전환하는 두 가지 방법
– iret 어셈블리 명령어를 수행한다.
– sysexit 어셈블리 명령어를 수행한다.
시스템 콜로의 진입과 복귀
• 두 가지 방법이 혼재함으로 인해 고려해야 할
사항.
– 커널은 int $x80 명령어만 사용하는 라이브러리와
sysenter를 사용하는 라이브러리를 동시에 지원해
야 한다.
– sysenter를 사용하는 라이브러리는 int $x80만 지
원하는 커널에 대처해야 한다.
– 커널과 표준 라이브러리는 sysenter 명령어를 포
함하지 않는 구식 프로세서와 sysenter 명령어를
포함하는 최신 프로세서에서 모두 실행될 수 있어
야 한다.
int $0x80 명령어를 통한 시스템 콜
발생
• trap_init 함수는 128번 (0x80)에 해당하는 ‘Interrupt
Descriptor Table’ 엔트리를 다음과 같이 설정한다.
– set_system_gate(0x80, &system_call);
– 이는 gate descriptor의 필드를 다음 값으로 지정한다.
• 세그먼트 셀렉터
– __KERNEL_CS
• 오프셋
– system_call() 예외 핸들러에 대한 포인터이다.
• 종류(Type)
– 15로 설정.
– 예외 종류가 Trap이고, 해당 핸들러는 마스크 가능한 인터럽트를 금지하지 않음.
• DPL(Descriptor Privilege Level)
– 3으로 설정
– 사용자 모드의 프로세스가 예외 핸들러를 호출할 수 있게 한다.
• 사용자 모드 프로세스가 int $0x80 명령어를 발생시킬 때 CPU
는 커널 모드로 전환하고 system_call 주소에서 명령어를 수행
한다.
system_call() 함수
• 시스템 콜 번호와 함께 예외 핸들러가 사
용할 수 있도록 eflags와 cs, eip, ss, esp 레
지스터를 제외한 모든 CPU레지스터를 스
택에 저장한다.
• SAVE_ALL 매크로는 ds와 es를 각각 커널
데이터 세그먼트의 세그먼트 셀렉터로 설
정한다.
system_call() 함수
• ebx 레지스터에 현재 프로세스의 thread_info 자료
구조의 주소를 저장한다.
• thread_info 자료 구조의 flags 필드를 검사하여 디
버거의 추적이 있는지 확인한다.
– TIF_SYSCALL_TRACE, TIF_SYSCALL_AUDIT
– 이 경우, do_syscall_trace() 함수를 시스템 콜 실행 전
후에 호출한다.
• 사용자 모드 프로세스가 전달한 시스템 콜 번호가
올바른지 검사한다.
• 시스템 콜 번호가 잘못된 것이면 eax를 저장했던
스택위치에 –ENOSYS값을 저장한 후
resume+userspace()를 호출한다.
sysenter 명령어를 통한 시스템 콜
발생
•
low-latency system call
•
다음의 세 가지 레지스터를 사용한다.
– SYSENTER_CS_MSR
•
커널 코드 세그먼트의 세그먼트 셀렉터
•
커널 진입점의 선형 주소
•
커널 스택 포인터
– SYSENTER_EIP_MSR
– SYSENTER_ESP_MSR
•
sysenter 명령어 수행시,
– SYSENTER_CS_MSR의 내용을 cs 레지스터에 복사한다.
•
커널 영역의 코드 세그먼트 정보
•
sysenter_entry() 함수의 주소
•
TSS(Task State Segment)의 주소
– SYSENTER_EIP_MSR의 내용을 eip 레지스터에 복사한다.
– SYSENTER_ESP_MSR의 내용을 esp 레지스터에 복사한다.
– SYSENTER_CS_MSR의 값에 8을 더한 후 ss 레지스터에 값을 로딩한다.
vsyscall
• 커널이 정의한 실행 코드를 사용자 공간에서
참조할 수 있도록 하기 위해 library에서 실행
시키는 것.
– 실행하고 있는 CPU에 따라 int 0x80을 사용하거
나, sysenter 명령을 사용한다.
– 장점 : 커널 호출이 불필요한 경우, 실제 시스템
콜을 발생하지 않고 처리를 끝낸다.
• 예) gettimeofday
• 표준 라이브러리의 래퍼 루틴이 시스템 콜을
호출할 때는 항상 __kernel_vsyscall() 함수를
호출한다.
vsyscall
• CPU가 sysenter를 지원하지 않는 경우
__kernel_vsyscall:
int $0x80
ret
• CPU가 sysenter를 지원하는 경우
__kernel_vsyscall:
pushl %ecx
pushl %edx
pushl %ebp
movl %esp, %ebp
sysenter
시스템 콜로의 진입
1.
2.
3.
4.
5.
6.
eax 레지스터에 시스템 콜 번호를 로딩한다.
__kernel_vsyscall() 함수는 ecx, edx, ebp 레지스터 값을 스택에
push한다.
esp(사용자 스택 포인터)를 ebp에 복사한다.
sysenter 명령어를 수행한다.
CPU는 현재 모드를 커널 모드로 전환한다.
SYSENTER_EIP_MSR 레지스터가 가리키는 sysenter_entry()함수를
호출한다.
1.
2.
3.
4.
커널 스택 포인터를 설정한다.
지역 인터럽트를 허용한다.
다음을 스택에 push한다.
1.
2.
3.
4.
사용자
사용자
사용자
리턴시
데이터 세그먼트(__USER_DS)
스택 포인터
코드 세그먼트의 세그먼트 셀렉터
수행될 명령어 주소
래퍼 루틴이 전달한 레스터 초기 값을 ebp 레지스터에 복구한다.
시스템 콜에서 빠져 나오기
• eax 레지스터에 시스템 콜의 반환 값을 저장한다.
• 지역 인터럽트를 금지한다.
• 현재 thread_info의 flag를 검사한다.
sysenter
ebp
edx
ecx
sysexit
sysexit 명령어
• 커널 모드에서 사용자 모드로 전환한다.
• sysexit 명령어 수행시 CPU는,
– SYSENTER_CS_MSR 레지스터 값에 16을 더해서
cs 레지스터에 로딩한다.
• 사용자 코드의 세그멘트 셀렉터 로딩
– edx 레지스터 값을 eip 레지스터에 복사한다.
– SYSENTER_CS_MSR 레지스터에 24를 더해서 ss
레지스터에 로딩한다.
• 사용자 데이터 세그먼트의 세그먼트 셀렉터 로딩
– ecx 레지스터 값을 esp 레지스터에 복사한다.
SYSENTER_RETURN 코드
• sysenter로 시작된 시스템 콜이 iret이나
sysexit 명령에 의해 종료중일때 수행된다.
• vsyscall 페이지에 저장되어 있다.
SYSENTER_RETURN:
popl %ebp
popl %edx
popl %ecx
ret
• 스택에 저장된 레지스터의 값을 복구한 후,
표준 라이브러리의 래퍼 루틴으로 제어권을
반환한다.
매개 변수 전달
•
매개변수의 형식
•
필수 매개 변수
•
일반적인 C프로그램에서 매개변수는 스택을 통하여 전달된다.
•
•
시스템 콜 서비스 루틴 수행 전 매개 변수를 CPU 레지스터에 저장한다.
저장된 매개 변수를 커널 모드의 스택으로 복사한다.
– 실제 값(숫자), 사용자 주소 공간의 변수, 사용자 공간의 자료구조 주소
– eax 레지스터로 전달하는 시스템 콜 번호
– 예) fork()를 호출하면, int $0x80 or sysenter를 실행하기 전에 eax 레지스터를
__NR_fork로 설정한다.
– 그러나 시스템 콜은 모드가 변경되는 특별한 함수이기 때문에 양쪽 어느 곳의 스택도
사용할 수 없다.
– 사용자 모드의 스택을 커널 모드의 스택으로 직접 복사는 난해하므로 사용하지 않는
다.
매개 변수 전달
• 매개 변수를 레지스터로 전달하기 위한 두가지 조건
– 각 매개 변수는 레지스터 크기인 32비트를 넘을 수 없다. :
32비트 CPU의 경우에만 해당
– 레지스터의 수는 제한적이므로 6개를 넘길 수 없다.(eax 포
함)
• 6개를 초과하는 매개 변수 전달법
– 하나의 레지스터를 매개 변수 값을 저장하고 있는 프로세
스 주소 공간 내의 메모리 영역을 가르키게 한다.
• 사용되는 레지스터의 종류
– eax, ebx, ecx, edx, esi, edi, ebp
매개변수 확인
• 공통적인 검사 유형
– 매개 변수가 주소인 경우 커널은 이 주소가 프
로세스 주소 공간 내에 있는지 검사해야 한다.
• 선형 주소가 프로세스 주소 공간에 속하는지, 속하
면 주소를 포함하는 메모리영역이 해당 접근 권한
에 있는지 확인한다.
– 초기 커널의 형태 : 전달된 매개 변수를 모두 검사하므로
시간이 오래 걸림.
• 선형 주소가 PAGE_OFFSET보다 낮은지 확인한다.
– 즉, 커널용으로 예약한 주소 범위에 들어가지 않는지..
– 커널 2.2 이후 부터 사용
프로세스 주소 공간 접근
•
프로세스 주소 공간에 접근을 위한 매크로 제공
함수명/매크로명
읽기/쓰기
읽기/쓰기 사이즈
접근 범위 체크
get_user
읽기
1, 2, 4 바이트 정수값
있음
1, 2, 4 바이트 정수값
없음
1, 2, 4 바이트 정수값
있음
1, 2, 4 바이트 정수값
없음
__get_user
put_user
쓰기
__put_user
copy_from_user
읽기
임의 사이즈
있음
__copy_from_user
읽기
임의 사이즈
없음
copy_to_user
쓰기
임의 사이즈
있음
__copy_to_user
쓰기
임의 사이즈
없음
strncpy_from_user
읽기
임의사이즈(NULL종료)
있음
__strncpy_from_user
읽기
임의사이즈(NULL종료)
없음
strlen_user
읽기
문자열 길이 리턴. 복사하지
않음
있음
strnlen_user
읽기
문자열 길이 리턴. 복사하지
않음. n까지 카운트
있음
clear_user
쓰기 (zero clear)
임의 사이즈
있음
__clear_user
쓰기 (zero clear)
임의 사이즈
없음
동적 주소 검사 – 수선 코드 (The
Fix-up Code)
• 커널 모드에서 페이지 폴트가 일어날 수
있는 네 가지 경우
– 해당 페이지 프레임이 존재하지 않거나 읽기
전용 페이지에 쓰려고 한 경우.
– 해당 페이지 테이블 엔트리가 초기화되지 않
은 경우
– 커널함수의 버그 또는 일시적인 H/W에러
– 시스템 콜에 전달한 주소의 메모리 영역에 읽
기/쓰기를 할 때 해당 주소가 프로세스 주소
공간에 들어 있지 않은 경우
예외 테이블
• 사용자 공간에 접근 하는 명령어 개수는 매우 적다.
• 프로세스 주소 공간에 접근하는 각 커널 명령의 주소를 ‘예외 테이
블(Exception Table)’ 구조체로 모은다.
– 만일 페이지 폴트 예외가 발생하면, do_page_fault 핸들러를 이용해 예
외 테이블을 검사한다.
– 예외를 일으킨 명령의 주소를 찾으면 시스템 콜 매개 변수가 원인이 되
고, 그렇지 않으면 다른 버그 때문이다.
struct exception_table_entry
{
unsigned long insn, fizup
};
• 예외 테이블의 엔트리
– insn : 프로세스 주소 공간에 접근하는 명령어의 주소
– fixup : 예외가 발생할 때 호출할 어셈블리 코드 주소
예외 테이블
• 예외 테이블은 커널을 컴파일할 때 링크된다.
– 예외 테이블 항목은 __ex_table 섹션에 저장된다.
– 예외가 발생했을 때 처리 코드는 커널 코드 세그
먼트 중 .fixup 섹션에 저장된다.
• search_exception_table() 함수
– 예외 테이블에 지정한 주소가 있는지 찾는다.
– 주소가 테이블에 있으면 해당
exception_table_entry 의 포인트를,
– 없으면 0을 반환한다.
커널 래퍼 루틴
• 시스템 콜은 주로 사용자 모드 프로세스에
서 사용한다.
– 하지만 커널스레드에서도 사용할 수 있도록
래퍼 루틴(Wrapper Routine) 선언을 위해
_syscall0 ~ _syscall6 까지 매크로를 선언하고
있다.
– 매크로의 숫자는 시스템 콜이 사용하는 매개
변수의 개수이다.
• 여기서는 6개가 넘는 매개변수는 사용할 수 없다.
커널 래퍼 루틴
• 각 매크로는 (2 + 2xn)개의 매개변수를 받는
다.
–예
• _syscall0(int, fork)
– int : 반환타입
– fork : 시스템 콜 이름
• _syscall3(int, write, int, fd, const char*, buf, unsigned
int, count)
–
–
–
–
–
–
int : 반환타입
write : 시스템 콜 이름
int : type, fd : 첫번째 매개 변수
const char * : type, buf : 두번째 매개 변수
unsigned int : type, count : 세번째 매개 변수
int write(int fd, const char *buf, unsigned int count)