본문 바로가기

시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리

ARM32- 스택 푸쉬(Stack Push) Userspace -> Kernel Space

리눅스 드라이버 및 리눅스 커널 코드가 실제 실행되는 공간은 어디일까요? 
 
커널에서 어떤 코드던 프로세스가 실행되는 운동장은 스택이에요. 
프로그램이 실행되면서 스택에 Push/Pop 동작을 엄청나게 자주하는데요.
 
예전 실리콘 벨리에서 어떤 스타트업 회사가 기획했던 아이디어라고 하는데요.
리눅스 커널 API를 하드웨어로 처리하는 아이디어를 구현하려고 했다고 하네요. 
그 아이디어의 근거는 리눅스 커널에서 자주 사용하는 API는 몇 개로 한정되어 있다는 거에요.
 
그럼 너무 자주 실행되는 함수면 어떤 동작을 자주 할까요?
스택 Push/Pop이 겠죠. 그래서 inline을 함수를 쓰는 거에요. inline 함수는 컴파일러가 호출되는 함수 내에 inline 함수를
추가하기 때문에 스택 Push/Pop 동작을 줄 일 수 있어요. 대신 inline 함수의 심볼 정보는 찾을 수 없죠.
 
대부분의 Generic Linux Kernel API는 inline 함수 타입이랍니다.
 
그럼 함수가 호출될 때 파라미터와 지역 변수를 스택에 Push/Pop하는 동작(유식한 말로 Calling Convention이라고 하죠)은 다른 글에서 다루기로 하고요.
쉽게 Stack Push을 소개하려고 해요.
 
User 공간에서 Kernel 공간으로 이동하고 싶을 때, 시스템 콜을 호출해야 한다고 하죠. 
(와이파이 콜과 시스템 콜을 혼돈해서는 안되요. 시스템 콜을 와이파이 콜로 이해하고 있는 돌대가리 관리자가 갑자기 생각나네요.)
 
아래 콜 스택은 바인더 "Binder:410_1:790" 프로세스가 ioctl 시스템 콜을 하는 동작이에요.
crash> bt 790
PID: 790    TASK: da175040  CPU: 0   COMMAND: "Binder:410_1"
bt: WARNING:  stack address:0xda233da0, program counter:0xc0fd96a0
 #0 [<c0fd96a0>] (__schedule) from [<c09d31d8>]
 #1 [<c09d31d8>] (binder_thread_read) from [<c09d8460>]
 #2 [<c09d8460>] (binder_ioctl) from [<c0247cc4>]
 #3 [<c0247cc4>] (do_vfs_ioctl) from [<c0247dfc>]
 #4 [<c0247dfc>] (sys_ioctl) from [<c0106b00>]
    pc : [<b4551998>]    lr : [<b452515f>]    psr: 200d0010
    sp : b382e888  ip : 00000000  fp : 00000000
    r10: 00000008  r9 : b40b126c  r8 : b40b12a0
    r7 : 00000036  r6 : b40b1240  r5 : c0186201  r4 : b382e8a0
    r3 : 00000000  r2 : b382e8a0  r1 : c0186201  r0 : 00000020
    Flags: nzCv  IRQs on  FIQs on  Mode USER_32  ISA ARM
 
오라 그런데, Crash Tool에서 래지스터를 찍어주네요? 이게 뭘까요?
crash> task 790
PID: 790    TASK: da175040  CPU: 0   COMMAND: "Binder:410_1"
struct task_struct {
  state = 1,
  stack = 0xda232000,
 
"Binder:410_1" 프로세스의 스택이 자라는 주소(0xda232000+0x2000)를 가보면, 그 비밀을 알 수 있어요.
참고로 ARM32 비트 아키텍쳐에서는 스택 사이즈가 0x2000, ARM64 비트는 0x4000으로 고정되어 있어요.
 
자, 0xda234000 주소로 가 볼까요?
crash> eval 0xda232000+0x2000
hexadecimal: da234000  (3573968KB)
 
crash> rd 0xda233f80 -e 0xda234000
da233f80:  00000000 c0247dfc 00000000 b382e8a0   .....}$.........
da233f90:  c0186201 b40b1240 00000036 c0106cc4   .b..@...6....l..
da233fa0:  da232000 c0106b00 b382e8a0 c0186201   . #..k.......b..
da233fb0:  00000020(R0) c0186201(R1) b382e8a0(R2) 00000000(R3)    ....b..........
da233fc0:  b382e8a0(R4) c0186201(R5) b40b1240(R6) 00000036(R7)   .....b..@...6...
da233fd0:  b40b12a0(R8) b40b126c(R9) 00000008(R10) 00000000(R11)   ....l...........
da233fe0:  00000000(R12) b382e888(R13) b452515f(R14) b4551998(PC)   ........_QR...U.
da233ff0:  200d0010 00000020 00000000 00000000   ...  ...........
 
유저 공간에서 커널 공간으로 이동하기 직전에 실행되었던 레지스터를 커널 스택 공간에 Push를 하는 거죠?
Crash Tool로 스택 덤프를 보니 눈에 잘 안들어온다고요? 그럼 T32로 볼까요?
 
아래 명령어를 써 봅시다. 아래 T32 명령어는 메모리 공간의 덤프를 심볼 정보와 함께 표현해주죠. 참 유용한 명령어죠.
d.v %y.l 0xda233f80
 
위 Crash Tool 보다 좀 눈에 잘 들어오죠?
________address|_data________|value_____________|symbol
NSD:DA233F80| 00 00 00 00  0x0
NSD:DA233F84| FC 7D 24 C0  0xC0247DFC         \\vmlinux\fs/ioctl\sys_ioctl+0x4C
NSD:DA233F88| 00 00 00 00  0x0
NSD:DA233F8C| A0 E8 82 B3  0xB382E8A0
NSD:DA233F90| 01 62 18 C0  0xC0186201         \\vmlinux\time/timer\perf_trace_hrtimer_init+0x75
NSD:DA233F94| 40 12 0B B4  0xB40B1240
NSD:DA233F98| 36 00 00 00  0x36               
NSD:DA233F9C| C4 6C 10 C0  0xC0106CC4         \\vmlinux\Global\sys_call_table
NSD:DA233FA0| 00 20 23 DA  0xDA232000
NSD:DA233FA4| 00 6B 10 C0  0xC0106B00         \\vmlinux\Global\ret_fast_syscall
NSD:DA233FA8| A0 E8 82 B3  0xB382E8A0
NSD:DA233FAC| 01 62 18 C0  0xC0186201         \\vmlinux\time/timer\perf_trace_hrtimer_init+0x75
NSD:DA233FB0| 20 00 00 00  0x20               //<<-- R0
NSD:DA233FB4| 01 62 18 C0  0xC0186201    //<<-- R1
NSD:DA233FB8| A0 E8 82 B3  0xB382E8A0    //<<-- R2
NSD:DA233FBC| 00 00 00 00  0x0               //<<-- R3
NSD:DA233FC0| A0 E8 82 B3  0xB382E8A0   //<<-- R4
NSD:DA233FC4| 01 62 18 C0  0xC0186201   //<<-- R5
NSD:DA233FC8| 40 12 0B B4  0xB40B1240   //<<-- R6
NSD:DA233FCC| 36 00 00 00  0x36             //<<-- R7
NSD:DA233FD0| A0 12 0B B4  0xB40B12A0  //<<-- R8
NSD:DA233FD4| 6C 12 0B B4  0xB40B126C  //<<-- R9
NSD:DA233FD8| 08 00 00 00  0x8              //<<-- R10
NSD:DA233FDC| 00 00 00 00  0x0        //<<-- R11
NSD:DA233FE0| 00 00 00 00  0x0        //<<-- R12
NSD:DA233FE4| 88 E8 82 B3  0xB382E888   //<<-- R13(SP)
NSD:DA233FE8| 5F 51 52 B4  0xB452515F   //<<-- R14
NSD:DA233FEC| 98 19 55 B4  0xB4551998  //<<-- PC
NSD:DA233FF0| 10 00 0D 20  0x200D0010
NSD:DA233FF4| 20 00 00 00  0x20               \\vmlinux\Global\up_b_offset+0x18
NSD:DA233FF8| 00 00 00 00  0x0
NSD:DA233FFC| 00 00 00 00  0x0
NSD:DA234000| 00 00 00 00  0x0   //<<-- stack bottom 주소
 
Userspace 레지스터 중에서 주목해야 할 놈이 있어요. 바로, R7이죠.
R7는 시스템 콜 번호를 저장하고 있어요.
 
Crash Tool로 시스템 콜 번호를 확인할 수 있거든요. 아래 "sys -c"  명령어로 시스템 콜 번호와 시스템 콜 함수 위치를 함께 확인할 수 있어요.
crash> sys -c
NUM  SYSTEM CALL                FILE AND LINE NUMBER
  0  sys_restart_syscall        ../home001/AustinKim/kernel/signal.c: 2503
  1  sys_exit
  2  sys_fork                   ../home001/AustinKim/kernel/msm-3.18/kernel/fork.c: 1729
  3  sys_read
  4  sys_write
  5  sys_open                   ../home001/AustinKim/fs/open.c: 1064
  6  sys_close                  ../home001/AustinKim/fs/open.c: 1125
  7  sys_ni_syscall
  8  sys_creat                  ../home001/AustinKim/fs/open.c: 1089
  9  sys_link
  a  sys_unlink
  b  sys_execve  
// 생략
 34  sys_umount
 35  sys_ni_syscall
 36  sys_ioctl
 37  sys_fcntl
 
역시 sys_ioctl의 시스템 콜 번호는 0x36이네요. 물론 R7도 0x36이지만요.
 
Reference: ARM 프로세서의 주요 기능
 
ARM 프로세서는 왜 배워야 할까
ARM 프로세서 학습하는 방법의 문제점
ARM 프로세서 소개  
ARM 아키텍처를 구성하는 주요 기능
   ● 어셈블리 명령어란  
   ● ARM의 동작 모드와 익셉션 레벨   
 
Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자