Skip to content

Commit 89d9987

Browse files
committed
add lesson5 translations
1 parent 4523549 commit 89d9987

File tree

3 files changed

+512
-0
lines changed

3 files changed

+512
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
## 5.3: Exercises
2+
3+
1. task가 사용자 모드에서 실행될 때, 일부 시스템 레지스터에 접근해보아라. 이 경우 동기적 예외가 발생하는지 확인해라. 이 예외를 처리하려면 `esr_el1` 레지스터를 사용해 시스템 콜과 구분지어라.
4+
1. 현재 task의 우선순위를 설정하기 위한 새로운 시스템 콜을 구현해보아라. task가 실행되는 동안 동적으로 우선 순위를 바꾸는 방법을 보여라.
5+
1. lesson05의 소스코드가 qemu에서 실행되도록 수정하라. 다음 링크를 참고하라.
6+
7+
[https://github.com/s-matyukevich/raspberry-pi-os/issues/8](https://github.com/s-matyukevich/raspberry-pi-os/issues/8)
8+
##### Previous Page
9+
10+
5.2 [User processes and system calls: Linux](../../docs/lesson05/linux.md)
11+
12+
##### Next Page
13+
14+
6.1 [Virtual memory management: RPi OS](../../docs/lesson06/rpi-os.md)

translations/ko/lesson05/linux.md

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
## 5.2: User processes and system calls
2+
3+
RPi OS의 시스템 콜 구현은 리눅스에서 거의 복사해왔기 때문에 이번 챕터에서 설명할 것은 많지 않다. 하지만 리눅스에서 특정 시스템 콜이 어디에서 어떻게 구현되었는지 알 수 있도록 소스코드를 사용할 것이다.
4+
5+
### Creating first user process
6+
7+
먼저 첫 번째 사용자 프로세스가 어떻게 생성되는지에 대해 알아보자. [start_kernel](https://github.com/torvalds/linux/blob/v4.14/init/main.c#L509) 함수부터 살펴보는 것이 좋겠다. 이 함수는 linux/init/main.c에 있다. 앞에서 살펴본 바와 같이 이 함수는 커널 부팅 과정의 초기에 호출되는 최초의 아키텍처 독립적인 함수이다. 이 함수에서 커널 초기화를 시작하며, 커널 초기화 도중에 첫 번째 사용자 프로세스를 실행하는 것이 이해될 것이다.
8+
9+
실제로 `start_kernel`의 흐름을 따라가다 보면 곧 [kernel_init](https://github.com/torvalds/linux/blob/v4.14/init/main.c#L989) 함수를 발견할 것이다. kernel_init 함수는 다음 코드를 포함한다.
10+
11+
```
12+
if (!try_to_run_init_process("/sbin/init") ||
13+
!try_to_run_init_process("/etc/init") ||
14+
!try_to_run_init_process("/bin/init") ||
15+
!try_to_run_init_process("/bin/sh"))
16+
return 0;
17+
```
18+
이것이 우리가 찾던 것인 듯하다. 이 코드로부터 리눅스 커널이 `init` 사용자 프로그램을 어디에서 시작해서 어느 순서로 찾는지를 추론할 수 있다. 그러면 `try_to_run_init_process` 함수는 [execve](http://man7.org/linux/man-pages/man2/execve.2.html)시스템 콜을 처리하기 위해 [do_execve](https://github.com/torvalds/linux/blob/v4.14/fs/exec.c#L1837)함수를 실행한다. 이 시스템 콜은 binary executable file을 읽어서 현재 프로세스에서 실행한다.
19+
20+
`execve` 시스템 콜은 lesson9에서 자세히 살펴볼 것이다. 지금은 이 시스템 콜의 가장 중요한 작업은 executable file을 구문 분석(parsing)하고 해당 내용을 메모리에 로드하는 것이고 이는 [load_elf_binary](https://github.com/torvalds/linux/blob/v4.14/fs/binfmt_elf.c#L679) 함수에서 수행된다. 여기서 우리는 executable file이 [ELF](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format) 형식이라고 가정한다. `load_elf_binary` 함수의 끝부분에서 아키텍처 별 [start_thread](https://github.com/torvalds/linux/blob/v4.14/arch/arm64/include/asm/processor.h#L119) 함수가 호출된다. RPi OS에서는 이를 `move_to_user_mode` 루틴의 프로토타입으로 사용했으며, 다음 코드가 우리가 주로 관심 있는 코드이다.
21+
```
22+
static inline void start_thread_common(struct pt_regs *regs, unsigned long pc)
23+
{
24+
memset(regs, 0, sizeof(*regs));
25+
forget_syscall(regs);
26+
regs->pc = pc;
27+
}
28+
29+
static inline void start_thread(struct pt_regs *regs, unsigned long pc,
30+
unsigned long sp)
31+
{
32+
start_thread_common(regs, pc);
33+
regs->pstate = PSR_MODE_EL0t;
34+
regs->sp = sp;
35+
}
36+
```
37+
`start_thread`가 실행될 때, 현재 프로세스는 커널 모드에서 동작한다. `start_thread`는 저장된 `pstate`, `sp`, `pc` 필드를 설정하는 데 사용되는 현재 `pt_regs` 구조체에 접근할 수 있다. 이 논리는 RPi OS의 `move_to_user_mode` 함수와 정확히 동일하므로 생략하겠다. 기억해야 할 것은 `start_thread` 함수가 `kernel_exit` 매크로가 사용자 모드로 프로세스를 이동시키는 방식으로 저장된 프로세서 상태를 준비한다는 것이다.
38+
39+
### Linux syscalls
40+
41+
기본 시스템 콜 메커니즘은 리눅스 및 RPi OS에서 정확히 동일하다. 이제 이미 익숙한 [clone](http://man7.org/linux/man-pages/man2/clone.2.html) 시스템 콜을 사용하여 이 메커니즘의 자세한 부분을 이해해보자. [glibc clone wrapper](https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/unix/sysv/linux/aarch64/clone.S;h=e0653048259dd9a3d3eb3103ec2ae86acb43ef48;hb=HEAD#l35) 함수부터 시작하자. 이 함수는 RPi OS의 [call_sys_clone](https://github.com/s-matyukevich/raspberry-pi-os/blob/master/src/lesson05/src/sys.S#L22) 함수와 동일하게 동작하지만, 이전 함수에서 전달인자의 온전성을 검사하고 적절하게 예외를 처리한다는 점이 다르다. 꼭 이해하고 기억해야 할 것은 두 가지 경우 모두 `svc` 명령어를 사용해 동기적 예외를 생성하고, 시스템 콜 번호는 `x8` 레지스터에 저장하고 모든 전달인자는 `x0`-`x7` 레지스터에 저장하여 전달한다.
42+
43+
다음으로 `clone` 시스템 콜 정의를 살펴보자. 이 코드는 [여기](https://github.com/torvalds/linux/blob/v4.14/kernel/fork.c#L2153) 에서 찾을 수 있으며 다음과 같다.
44+
45+
```
46+
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
47+
int __user *, parent_tidptr,
48+
int __user *, child_tidptr,
49+
unsigned long, tls)
50+
{
51+
return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
52+
}
53+
```
54+
[SYSCALL_DEFINE5](https://github.com/torvalds/linux/blob/v4.14/include/trace/syscall.h#L25) 매크로는 5개의 파라미터로 시스템 콜을 정의한다는 의미의 5를 이름에 포함한다. 이 매크로는 새로운 [syscall_metadata](https://github.com/torvalds/linux/blob/v4.14/include/trace/syscall.h#L25) 구조체를 할당한 뒤 값을 채우고, `sys_<syscall name>` 함수를 생성한다. 예를 들어 `clone` 시스템 콜의 경우 `sys_clone` 함수가 정의된다. 이 함수는 실제로 하위 계층 아키텍처 코드로부터 호출되는 시스템 콜 handler이다.
55+
56+
시스템 콜이 실행될 때, 커널은 시스템 콜 번호로 시스템 콜 handler를 찾을 방법이 필요하다. 이를 위한 가장 쉬운 방법은 시스템 콜 handler에 대한 포인터 배열을 생성하여 각 시스템 콜 번호를 배열의 인덱스로 사용하는 것이다. 이러한 접근 방식을 RPi OS에서 사용했으며 리눅스에서도 동일한 접근 방식을 사용한다. 이 배열을 [sys_call_table](https://github.com/torvalds/linux/blob/v4.14/arch/arm64/kernel/sys.c#L62) 이라고 하며 다음과 같이 정의된다.
57+
58+
```
59+
void * const sys_call_table[__NR_syscalls] __aligned(4096) = {
60+
[0 ... __NR_syscalls - 1] = sys_ni_syscall,
61+
#include <asm/unistd.h>
62+
};
63+
```
64+
65+
모든 시스템 콜은 초기에 `sys_ni_syscall` 함수를 가리키도록 할당된다. 여기서 ni는 존재하지 않음을 의미한다. `0`번 시스템 콜과 현재 아키텍처에 대해 정의되지 않은 모든 시스템 콜은 이 handler를 유지한다. `sys_call_table` 배열에 있는 다른 모든 handler는 [asm/unistd.h](https://github.com/torvalds/linux/blob/v4.14/include/uapi/asm-generic/unistd.h) 헤더파일에 다시 쓰여진다. 이 파일은 간단히 시스템 콜 번호와 핸들러 함수 간의 맵핑을 제공한다.
66+
67+
### Low-level syscall handling code
68+
69+
`sys_call_table`이 어떻게 생성되고 채워지는지를 보았다. 이제 하위 계층 시스템 콜 처리 코드에서 어떻게 사용되는지 알아보자. 다시 말하지만 기본 메커니즘은 RPi OS와 거의 동일하다.
70+
71+
일부 시스템 콜은 동기적 예외이며 모든 exception handler는 exception vector table에 정의되어 있다는 것을 알고 있다. 우리가 관심 있는 handler는 EL0에서 생성된 동기적 예외를 처리하는 것이다. 각 예외가 올바른 handler를 찾도록 하는 것을 [el0_sync](https://github.com/torvalds/linux/blob/v4.14/arch/arm64/kernel/entry.S#L598)라고 한다.
72+
```
73+
el0_sync:
74+
kernel_entry 0
75+
mrs x25, esr_el1 // read the syndrome register
76+
lsr x24, x25, #ESR_ELx_EC_SHIFT // exception class
77+
cmp x24, #ESR_ELx_EC_SVC64 // SVC in 64-bit state
78+
b.eq el0_svc
79+
cmp x24, #ESR_ELx_EC_DABT_LOW // data abort in EL0
80+
b.eq el0_da
81+
cmp x24, #ESR_ELx_EC_IABT_LOW // instruction abort in EL0
82+
b.eq el0_ia
83+
cmp x24, #ESR_ELx_EC_FP_ASIMD // FP/ASIMD access
84+
b.eq el0_fpsimd_acc
85+
cmp x24, #ESR_ELx_EC_FP_EXC64 // FP/ASIMD exception
86+
b.eq el0_fpsimd_exc
87+
cmp x24, #ESR_ELx_EC_SYS64 // configurable trap
88+
b.eq el0_sys
89+
cmp x24, #ESR_ELx_EC_SP_ALIGN // stack alignment exception
90+
b.eq el0_sp_pc
91+
cmp x24, #ESR_ELx_EC_PC_ALIGN // pc alignment exception
92+
b.eq el0_sp_pc
93+
cmp x24, #ESR_ELx_EC_UNKNOWN // unknown exception in EL0
94+
b.eq el0_undef
95+
cmp x24, #ESR_ELx_EC_BREAKPT_LOW // debug exception in EL0
96+
b.ge el0_dbg
97+
b el0_inv
98+
```
99+
100+
여기서 현재 예외가 시스템 콜인지 아닌지 알기 위해 `esr_el1`레지스터가 사용된다. 시스템 콜인 경우 [el0_svc](https://github.com/torvalds/linux/blob/v4.14/arch/arm64/kernel/entry.S#L837) 함수가 호출된다. 이 함수에 대해 살펴보자.
101+
102+
```
103+
el0_svc:
104+
adrp stbl, sys_call_table // load syscall table pointer
105+
mov wscno, w8 // syscall number in w8
106+
mov wsc_nr, #__NR_syscalls
107+
el0_svc_naked: // compat entry point
108+
stp x0, xscno, [sp, #S_ORIG_X0] // save the original x0 and syscall number
109+
enable_dbg_and_irq
110+
ct_user_exit 1
111+
112+
ldr x16, [tsk, #TSK_TI_FLAGS] // check for syscall hooks
113+
tst x16, #_TIF_SYSCALL_WORK
114+
b.ne __sys_trace
115+
cmp wscno, wsc_nr // check upper syscall limit
116+
b.hs ni_sys
117+
ldr x16, [stbl, xscno, lsl #3] // address in the syscall table
118+
blr x16 // call sys_* routine
119+
b ret_fast_syscall
120+
ni_sys:
121+
mov x0, sp
122+
bl do_ni_syscall
123+
b ret_fast_syscall
124+
ENDPROC(el0_svc)
125+
```
126+
127+
Now, let's examine it line by line.
128+
129+
```
130+
el0_svc:
131+
adrp stbl, sys_call_table // load syscall table pointer
132+
mov wscno, w8 // syscall number in w8
133+
mov wsc_nr, #__NR_syscalls
134+
```
135+
136+
처음 세 줄에서는 어떤 레지스터의 별칭인 `stbl`, `wscno`, `wsc_nr` 변수가 초기화된다. `stbl`은 시스템 콜 테이블의 주소를 가지고, `wsc_nr`은 총 시스템 콜 개수를 가지며, `wscno``x8` 레지스터에 저장된 현재 시스템 콜 번호를 뜻한다.
137+
```
138+
stp x0, xscno, [sp, #S_ORIG_X0] // save the original x0 and syscall number
139+
```
140+
RPi OS에서 다루었듯이 시스템 콜이 종료된 후 `x0``pt_regs` 영역에 덮여씌워진다. `x0` 레지스터의 기존 값이 필요한 경우, `pt_regs`구조체의 별도 필드에 저장된다. 마찬가지로 시스템 콜 번호도 `pt_regs`에 저장된다.
141+
```
142+
enable_dbg_and_irq
143+
```
144+
145+
인터럽트와 debug exception을 활성화한다.
146+
147+
```
148+
ct_user_exit 1
149+
```
150+
151+
사용자 모드에서 커널 모드로의 전환하는 것을 기록한다.
152+
153+
```
154+
ldr x16, [tsk, #TSK_TI_FLAGS] // check for syscall hooks
155+
tst x16, #_TIF_SYSCALL_WORK
156+
b.ne __sys_trace
157+
158+
```
159+
160+
현재 task가 시스템 콜 추적 프로그램에서 실행되는 경우에 `_TIF_SYSCALL_WORK` 플래그가 set되어야 한다. 이 경우 `__sys_trace` 함수가 호출된다. 일반적인 경우만 살펴보기 위해 이 함수는 넘어가겠다.
161+
162+
```
163+
cmp wscno, wsc_nr // check upper syscall limit
164+
b.hs ni_sys
165+
```
166+
167+
현재 시스템 콜 번호가 총 시스템 콜 개수보다 큰 경우, 사용자에게 오류를 리턴한다.
168+
169+
```
170+
ldr x16, [stbl, xscno, lsl #3] // address in the syscall table
171+
blr x16 // call sys_* routine
172+
b ret_fast_syscall
173+
```
174+
175+
시스템 콜 번호는 시스템 콜 테이블 배열에서 handler를 찾기위한 인덱스로 사용된다. Handler 주소는 `x16` 레지스터에 로드되어 실행된다. 마침내 `ret_fast_syscall` 함수에 이르렀다.
176+
177+
```
178+
ret_fast_syscall:
179+
disable_irq // disable interrupts
180+
str x0, [sp, #S_X0] // returned x0
181+
ldr x1, [tsk, #TSK_TI_FLAGS] // re-check for syscall tracing
182+
and x2, x1, #_TIF_SYSCALL_WORK
183+
cbnz x2, ret_fast_syscall_trace
184+
and x2, x1, #_TIF_WORK_MASK
185+
cbnz x2, work_pending
186+
enable_step_tsk x1, x2
187+
kernel_exit 0
188+
```
189+
190+
중요한 것은 첫 번째 줄에서 인터럽트가 비활성화되고 마지막 줄에서 `kernel_exit` 매크로가 호출된다는 것이다. 그 밖의 모든 것은 특수한 경우의 처리와 관련이 있다. 따라서 이 함수는 시스템 콜이 실제로 종료되고 사용자 프로세스로 실행이 옮겨지는 곳이다.
191+
192+
### Conclusion
193+
194+
이제 시스템 콜을 생성하고 처리하는 것을 다 살펴보았다. 이 과정은 상대적으로 간단하지만 운영체제에서 필수적이다. 왜냐하면 커널이 API를 설정하고, API가 사용자 프로그램과 커널 간의 유일한 통신 수단임을 확인하기 때문이다.
195+
196+
##### Previous Page
197+
198+
5.1 [User processes and system calls: RPi OS](../../docs/lesson05/rpi-os.md)
199+
200+
##### Next Page
201+
202+
5.3 [User processes and system calls: Exercises](../../docs/lesson05/exercises.md)

0 commit comments

Comments
 (0)