Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

15192
888
89788


[라즈베리파이] 시스템 콜 - 소프트웨어 인터럽트 벡터(vector_swi) 코드 분석 11장. 시스템 콜

커널 공간에서 시스템 콜 실행 출발점은 vector_swi 레이블입니다. svc 명령어를 실행하면 소프트웨어 인터럽트 벡터인 vector_swi 레이블으로 브랜치하기 때문입니다. vector_swi 레이블 어셈블리 코드 분석으로 ARM 리눅스 커널에서 시스템 콜이 어떻게 동작하는지 살펴봅시다.

vector_swi 레이블 어셈블리 코드는 다음과 같습니다.
1  80107ee0 <vector_swi>:
2  80107ee0:  e24dd048  sub  sp, sp, #72 ; 0x48
3  80107ee4:  e88d1fff    stm   sp, {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip}
4  80107ee8:  e28d803c  add   r8, sp, #60 ; 0x3c
5  80107eec:  e9486000  stmdb  r8, {sp, lr}^
6  80107ef0:  e14f8000   mrs r8, SPSR
7  80107ef4:  e58de03c  str lr, [sp, #60] ; 0x3c
8  80107ef8:  e58d8040  str r8, [sp, #64] ; 0x40
9  80107efc:  e58d0044  str  r0, [sp, #68] ; 0x44
10 80107f00: e3a0b000  mov  fp, #0
11 80107f04: ee11cf10   mrc 15, 0, ip, cr1, cr0, {0}
12 80107f08: e59fa0b0  ldr sl, [pc, #176] ; 80107fc0 <__cr_alignment>
13 80107f0c: e59aa000  ldr sl, [sl]
14 80107f10: e13a000c  teq sl, ip
15 80107f14: 1e01af10  mcrne  15, 0, sl, cr1, cr0, {0}
16 80107f18: e92d500f  push   {r0, r1, r2, r3, ip, lr}
17 80107f1c: eb0355a9  bl 801dd5c8 <trace_hardirqs_on>
18 80107f20: e8bd500f  pop {r0, r1, r2, r3, ip, lr}
19 80107f24: f1080080  cpsie i
20 80107f28: e1a096ad  lsr r9, sp, #13
21 80107f2c: e1a09689  lsl r9, r9, #13
22 80107f30: e28f808c  add r8, pc, #140 ; 0x8c
23 80107f34 <local_restart>:
24 80107f34: e599a000 ldr sl, [r9]
25 80107f38: e92d0030 push {r4, r5}
26 80107f3c: e31a00f0  tst sl, #240 ; 0xf0
27 80107f40: 1a000008 bne 80107f68 <__sys_trace>
28 80107f44: e3570e19 cmp r7, #400 ; 0x190
29 80107f48: e24fee11 sub lr, pc, #272 ; 0x110
30 80107f4c: 3798f107 ldrcc pc, [r8, r7, lsl #2]
31 80107f50: e28d1008 add r1, sp, #8
32 80107f54: e357080f  cmp r7, #983040 ; 0xf0000
33 80107f58: e2270000 eor r0, r7, #0
34 80107f5c: 2a0010db bcs 8010c2d0 <arm_syscall>
35 80107f60: e3a08000 mov r8, #0
36 80107f64: ea00d333  b 8013cc38 <sys_ni_syscall>
위와 같은 어셈블리 코드는 어떻게 추출할까요?

리눅스 커널을 빌드하면 리눅스 커널 폴더에 심볼 정보가 포함된 vmlinux 파일이 생성됩니다. 라즈베리파이에서 제공하는 objdump 란 바이너리 유틸리티를 쓰면 vmlinux 에서 어셈블리 코드를 추출할 수 있습니다. 명령어는 다음과 같습니다.
root@raspberrypi:/home/pi# vmlinux –d > linux_kernel_assemble.txt
ret_fast_syscall 레이블 동작은 3단계로 구분할 수 있습니다.
1단계: 프로세스 스택 공간에 유저 공간에서 실행했던 레지스터 세트를 푸시
2단계: 시스템 콜 핸들러를 실행한 후 복귀할 주소를 ret_fast_syscall 레이블로 저장
3단계: 시스템 콜 테이블에 접근해서 시스템 콜 핸들러 함수 분기

먼저 1단계 코드를 분석하겠습니다. 1번째 줄 코드를 보겠습니다.
1  80107ee0 <vector_swi>:
2  80107ee0:  e24dd048  sub  sp, sp, #72 ; 0x48

sp는 r13 레지스터로 현재 실행 중인 코드의 스택 주소를 저장합니다. sub 명령어를 실행해서 스택 주소를 0x48만큼 뺍니다. 스택 주소를 0x48만큼 빼는 동작은 0x48 바이트만큼 스택 공간을 확보한다는 의미입니다.

vector_swi 레이블을 실행할 때는 ARM 프로세서 레지스터 세트는 유저 공간에서 소프트웨어 인터럽트를 발생하기 직전 정보를 저장하고 있습니다. 

3번째 줄 코드를 보겠습니다.
3  80107ee4:  e88d1fff    stm   sp, {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip}

프로세스 스택 공간에 r0에서 r12 레지스터까지 푸시(저장)합니다. r0~r12 레지스터는 유저 공간에서 실행 중인 레지스터를 의미합니다.
4  80107ee8:  e28d803c  add   r8, sp, #60 ; 0x3c

스택 주소를 0x3c만큼 더해서 r8 레지스터에 저장합니다.
스택 주소를 이동해서 스택 공간에 다른 데이터를 저장하기 전 주로 실행하는 명령어입니다.
5  80107eec:  e9486000  stmdb  r8, {sp, lr}^

r13, r14 레지스터를 푸시할 스택 공간을 0으로 비웁니다.
20~21번째 줄 코드를 분석하겠습니다.
20 80107f28: e1a096ad  lsr r9, sp, #13
21 80107f2c: e1a09689  lsl r9, r9, #13

현재 실행 중인 스택 주소 프로세스 최상단 스택 주소에 접근해 r9 레지스터에 저장합니다.
프로세스 최상단 주소에 있는 struct thread_info 구조체 flags 멤버에 접근하기 위해서입니다.
  
다음 vector_swi 레이블에서 가장 중요한 22번째 줄 코드를 보겠습니다.
22 80107f30: e28f808c  add r8, pc, #140 ; 0x8c

현재 실행 중인 코드 프로그램 카운터에서 0x8c만큼 더해서 r8 레지스터에 저장합니다.
22번째 줄 코드가 0x80107f30 주소에 있으니 프로그램 카운터는 0x80107f30 주소를 저장하고 있을 것입니다. 그런데 ARM 프로세스는 파이프라인 방식으로 어셈블리 명령어를 병렬로 처리하므로 0x80107f2c 주소 기준으로 -0x4 바이트만큼 오프셋을 계산할 필요가 있습니다.

0x80107f2c 주소에서 0x8c만큼 더해서 다음과 같이 r8 레지스터에 저장하라는 코드입니다.
r8 = 0x80107fc4 = 0x80107f2c + 0x8c

r8 레지스터에 저장된 0x80107fc4 주소에 어떤 심볼이 있는 지 확인해 봅시다.
sys_call_table 심볼이 보입니다.
80107fc4 <sys_call_table>:
80107fc4:8012c6f4 801212c0 8011c100 8026ab24
80107fd4:8026abc4 80268508 80267108 8013cc38
80107fe4:80268558 8027a428 8027a0e4 80271d68
80107ff4:80267abc 8013cc38 80279f74 80267dc8
...
801085c4:804ddd18 80225d30 8013cc38 80271d98 
801085d4:8013cc38 80202d50 8023cf84 8026b224  
801085e4:8026af60 8026affc 80241e9c 80241eb8 
801085f4:80241f24 8013cc38 8013cc38 8013cc38

80107fc4 주소부터 80xx_xxxx 주소가 배열같이 정렬되어 있습니다. 이 주소는 시스템 콜 핸들러 함수 입니다.

이해를 돕기 위해 Trace32로 sys_call_table 테이블을 확인해 보겠습니다.
다음과 같이 명령어를 입력합시다.
d.v %y.l sys_call_table
________address|value_______|symbol
    NSD:80107FC4|0x8012D4E0   \\vmlinux\kernel/signal\sys_restart_syscall
1   NSD:80107FC8|0x80121E08   \\vmlinux\exit\sys_exit
2   NSD:80107FCC|0x8011C6D0   \\vmlinux\fork\sys_fork
3   NSD:80107FD0|0x802844FC   \\vmlinux\read_write\sys_read
4   NSD:80107FD4|0x8028459C   \\vmlinux\read_write\sys_write
5   NSD:80107FD8|0x80281788   \\vmlinux\open\sys_open
6   NSD:80107FDC|0x80280380   \\vmlinux\open\sys_close
7   NSD:80107FE0|0x8013DE74   \\vmlinux\sys_ni\compat_sys_epoll_pwait
8   NSD:80107FE4|0x802817D8   \\vmlinux\open\sys_creat
9 NSD:80107FE8|0x80294770   \\vmlinux\fs/namei\sys_link
10 NSD:80107FEC|0x80294428   \\vmlinux\fs/namei\sys_unlink
11 NSD:80107FF0|0x8028BE98   \\vmlinux\exec\sys_execve
12 NSD:80107FF4|0x80280D3C   \\vmlinux\open\sys_chdir
13 ...
14 NSD:801085E4|0x80284BAC   \\vmlinux\read_write\sys_preadv2
15 NSD:801085E8|0x80284C48   \\vmlinux\read_write\sys_pwritev2
16 NSD:801085EC|0x8013DE74   \\vmlinux\sys_ni\compat_sys_epoll_pwait
17 NSD:801085F0|0x8013DE74   \\vmlinux\sys_ni\compat_sys_epoll_pwait
18 NSD:801085F4|0x8013DE74   \\vmlinux\sys_ni\compat_sys_epoll_pwait
19 NSD:801085F8|0x80289668   \\vmlinux\fs/stat\sys_statx
20 NSD:801085FC|0x8013DE74   \\vmlinux\sys_ni\compat_sys_epoll_pwait
21 NSD:80108600|0x8013DE74   \\vmlinux\sys_ni\compat_sys_epoll_pwait

확인 결과 sys_call_table 심볼에는 sys_xxx() 패턴으로 구현된 시스템 콜 핸들러 함수 주소가 0x80107FC4~0x80108600 주소 구간에 저장되어 있습니다.

0x80107FC8~0x80107FD0 주소에 단지 0x80121E08, 0x8011C6D0, 0x802844FC 값이 있을 뿐인데, 주소를 심볼 단위로 변환해서 보여주는 Trace32 "d.v %y.l [심볼]" 명령어로 바로 주소에 대응하는 함수를 알게 된 것입니다. 
2   NSD:80107FC8|0x80121E08   \\vmlinux\exit\sys_exit
3   NSD:80107FCC|0x8011C6D0   \\vmlinux\fork\sys_fork
4   NSD:80107FD0|0x802844FC   \\vmlinux\read_write\sys_read

24~25번째 줄은 시스템 콜 디버깅을 위한 코드입니다.
24 80107f34: e599a000 ldr sl, [r9]
25 80107f38: e92d0030 push {r4, r5}
26 80107f3c: e31a00f0  tst sl, #240 ; 0xf0
27 80107f40: 1a000008 bne 80107f68 <__sys_trace>

24번째 줄 코드는 r9 레지스터 저장된 프로세스 스택 최상단 주소로 struct thread_info 구조체에 접근해서 struct thread_info 첫 번째 멤버인 flags를 sl(r10) 레지스터에 저장하는 동작입니다.

flags 값으로 시스템 콜을 ftrace로 출력할지 결정하며 ftrace 에서 시스템 콜 디버깅 이벤트를 켰을 경우 __sys_trace 레이블을 실행하는 코드입니다. ftrace 로그 분석 방법은 디버깅 절에서 더 다룹니다.

28번째 줄 코드를 보겠습니다.
28 80107f44: e3570e19 cmp r7, #400 ; 0x190

r7 레지스터는 시스템 콜 번호를 저장하고 있다는 점을 기억합시다.

cmp는 r7 레지스터에 저장된 값과 400을 비교하는 명령어입니다.
r7 레지스터에 저장된 시스템 콜 번호와 시스템 콜 최대값인 400과 비교하는 것입니다.

제대로 시스템 콜 번호를 지정했으면 시스템 콜 번호가 400보다 적을 겁니다. 하지만 유저 모드에서 직접 시스템 콜 번호를 r7에 잘못 지정해서 소프트웨어 인터럽트를 발생했을 경우를 위한 예외 처리 코드입니다.

cmp 명령어 결과는 ARM 프로세서 CPSR(Current Program Status Register) 레지스터의 Zero와 Carry비트에 업데이트 됩니다. 정상적으로 r7 레지스터에 시스템 콜 번호가 저장돼있으면 r7 레지스터는 400보다 적을 것이므로 Z와 C 비트는 1로 변경되지 않습니다.

만약 r7에 저장된 시스템 콜 번호가 400이거나 400보다 크면 CPSR 레지스터 C 비트는 1로 바뀝니다.

이 명령어 결과로 시스템 콜 핸들러 함수로 분기하는 다음 30번째 줄 코드 실행 흐름에 영향을 끼칩니다.
30 80107f4c: 3798f107 ldrcc pc, [r8, r7, lsl #2]

ldrcc 명령어는 ldr+cc 조합 명령어인데 CPSR 레지스터 C 비트가 1이 아닐 때만 ldr 명령어를 수행합니다. 리눅스 관점으로 이 명령어를 분석하면 시스템 콜 번호가 400 이하 일 때만 시스템 콜 테이블에 지정된 핸들러 함수로 분기합니다.

29번째 줄 코드를 봅시다. 
29 80107f48: e24fee11 sub lr, pc, #272 ; 0x110

이번에는 2단계 코드를 볼 차례입니다.
이 명령어를 실행하는 코드 주소에서 0x110 만큼 뺄샘 연산을 한 결과를 lr(r14) 복귀 레지스터에 저장합니다.
lr = 0x80107e44 = 0x80107f44 - 0x110

0x80107e44 주소에는 다음과 같이 ret_fast_syscall 레이블이 저장돼 있습니다.
80107e44 <ret_fast_syscall>: 
80107e44: e5ad0008  str r0, [sp, #8]!
80107e48: f10c0080  cpsid i

시스템 콜 핸들러 실행을 마무리한 다음에 복귀할 주소를 ret_fast_syscall 레이블로 지정한 것입니다.

ftrace로 시스템 콜 실행 후에 호출되는 함수 콜스택을 보면 다음 8번째 줄 로그와 같이 ret_fast_syscall 레이블을 볼 수 있습니다.
1 lxpanel-731   [002] 118.058060: mutex_lock+0x14/0x130 <-kstat_irqs_usr+0x24/0x44
2 lxpanel-731   [002] 118.058069: <stack trace>
3 => seq_read+0x1dc/0x504
4 => proc_reg_read+0x6c/0x90
5 => __vfs_read+0x3c/0x134
6 => vfs_read+0x9c/0x164
7 => SyS_read+0x4c/0xa0
8 => ret_fast_syscall+0x0/0x28

7번에서 3번째 줄 함수 방향으로 함수를 실행한 후 다시 3번째 줄 함수에서 7번째 줄 함수로 복귀합니다. 이후 8번째 줄에서 보이는 ret_fast_syscall 레이블을 실행합니다.

3단계 코드를 볼 차례입니다.
마지막 30번째 줄 코드를 보겠습니다. 역시 vector_swi 레이블에서 가장 중요한 코드입니다.
30 80107f4c: 3798f107 ldrcc pc, [r8, r7, lsl #2]

시스템 콜 테이블에 접근해 시스템 콜 번호에 해당 하는 핸들러 함수로 실행 흐름을 바꾸는 코드입니다.

ldrcc 어셈블리 명령어는 ldr+cc 조합으로 구성돼 있습니다. ARM CPSR 레지스터의 C(Carry) 비트가 1로 설정되어 있으면 ldr 명령어를 실행하지 않습니다.

ARM CPSR 레지스터의 C(Carry) 비트는 어떤 조건에서 1로 설정될까요? 

28번째 줄 어셈블리 명령어에서 시스템 콜 번호가 400보다 같거나 클 경우 CPSR 레지스터 Carry 비트를 1로 설정합니다.
28 80107f44: e3570e19 cmp r7, #400 ; 0x190
...
30 80107f4c: 3798f107 ldrcc pc, [r8, r7, lsl #2]

유저 공간에서 적절한 시스템 콜 번호를 지정했을때만 시스템 콜 핸들러를 실행하는 예외 코드입니다.

ldrcc 명령어 동작 조건에 대해 알아 봤으니 30번째 줄 명령어 전체 동작 분석을 시작하겠습니다. 30번 줄 명령어는 이해하기 쉽게 다음 과정으로 변환시킬 수 있습니다.
ldrcc pc, [r8, r7, lsl #2]
pc = *(r8 + (r7 << 2))

r7에 저장된 값을 왼쪽으로 2만큼 비트 연산(Logical Shift Left: lsl)을 한 결과를 r8 레지스터에 더합니다. 계산 결과로 이 메모리 주소에 있는 값을 pc에 로딩하는 것입니다.

이 명령어 실행을 할 때 r7, r8 레지스터가 어떤 값을 갖고 있는지 먼저 점검합시다. 

r8 레지스터는 시스템 콜 테이블 주소 위치를 담고 있고, r7은 유저 공간에서 지정한 시스템 번호를 저장하고 있습니다.

r7이 시스템 콜 번호인4 그리고 시스템 콜 데이블 심볼 주소인 0x80107fc4를 r8이 저장하고 있다고 가정하고 이 명령어 실행 과정을 확인합시다.
ldrcc pc, [r8, r7, lsl #2]

[계산 과정]
pc = *(r8 + (r7 << 2))
pc = *(r8 + (4 << 2))
pc = *(0x80107fc4 + 0x10)
pc = *(0x80107fd4)
pc =  0x8028459c   \\vmlinux\read_write\sys_write

각 명령어 실행 결과 프로그램 카운터 레지스터는 sys_write() 함수 주소인 0x8028459c를 저장하게 됩니다.

시스템 콜 테이블인 sys_call_table 주소에 있는 메모리 정보는 다음과 같습니다.
________address|value_______|symbol
0   NSD:80107FC4|0x8012D4E0   \\vmlinux\kernel/signal\sys_restart_syscall
1   NSD:80107FC8|0x80121E08   \\vmlinux\exit\sys_exit
2   NSD:80107FCC|0x8011C6D0   \\vmlinux\fork\sys_fork
3   NSD:80107FD0|0x802844FC   \\vmlinux\read_write\sys_read
4   NSD:80107FD4|0x8028459C   \\vmlinux\read_write\sys_write 
5   NSD:80107FD8|0x80281788   \\vmlinux\open\sys_open

sys_call_table 심볼에는 4바이트 단위로 시스템 콜 핸들러 주소가 저장돼 있습니다. 라즈베리파이가 32 비트 ARM 아키텍처를 적용했으니 심볼은 4바이트(32비트) 단위 주소인 것입니다. 

ARM 프로그램 카운터 레지스터가 어떤 주소로 변경되면 해당 주소를 실행한다는 의미입니다. ARM 아키텍처에서 프로그램 카운터 레지스터에 저장된 주소에 있는 기계어를 Fetch하기 때문입니다.

이번 시간에 시스템 콜 핵심 어셈블리 코드를 분석했습니다. 유저 공간에서 시스템 콜을 실행하면 코드 공간으로 스위칭되는 진입점 코드를 확인한 것입니다. 리눅스 커널을 이론으로 이해하는 것보다 어셈블리 코드를 분석하면 더 오랫동안 머리 속에 남습니다.

#Reference 시스템 콜


Reference(워크큐)
워크큐(Workqueue) Overview

.

    핑백

    덧글

    댓글 입력 영역