Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

10206
629
98792


[라즈베리파이] 동기화 - 스핀락(spinlock): spin_lock() 어셈블리 코드 분석 9장. 커널동기화(스핀락/뮤텍스)

이전 시간에 인라인 어셈블리 코드를 살펴봤는데, 이번에는 어셈블리 코드를 분석하겠습니다. 실제 ARM 프로세서 입장에서 스핀락을 어떻게 실행하는지 정확히 파악하려면 어셈블리 코드를 봐야 합니다. 어셈블리 코드를 분석해야 스핀락 핵심 개념을 알 수 있다는 겁니다. 이전 절에 다룬 내용을 이해했으면 그리 어렵지 않습니다.

분석하려는 코드는 다음과 같습니다.
1  80704b60 <_raw_spin_lock>:
2  80704b60: e1a0c00d  mov ip, sp
3  80704b64: e92dd800  push {fp, ip, lr, pc}
4  80704b68: e24cb004  sub fp, ip, #4
5  80704b6c: e52de004  push {lr} ; (str lr, [sp, #-4]!)
6  80704b70: ebe82672  bl 8010e540 <__gnu_mcount_nc>
7  80704b74: f590f000  pldw [r0]
8  80704b78: e1903f9f  ldrex r3, [r0]
9  80704b7c: e2832801  add r2, r3, #65536 ; 0x10000
10 80704b80: e1801f92  strex r1, r2, [r0]
11 80704b84: e3310000  teq r1, #0
12 80704b88: 1afffffa  bne 80704b78 <_raw_spin_lock+0x18>
13 80704b8c: e1a02823  lsr r2, r3, #16
14 80704b90: e6ff3073  uxth r3, r3
15 80704b94: e1530002  cmp r3, r2
16 80704b98: 0a000003  beq 80704bac <_raw_spin_lock+0x4c>
17 80704b9c: e320f002  wfe
18 80704ba0: e1d030b0  ldrh r3, [r0]
19 80704ba4: e1530002  cmp r3, r2
20 80704ba8: 1afffffb  bne 80704b9c <_raw_spin_lock+0x3c>
21 80704bac: f57ff05b  dmb ish
22 80704bb0: e89da800  ldm sp, {fp, sp, pc}

spin_lock() 부터 __raw_spin_lock(), do_raw_spin_lock(), arch_spin_lock() 함수 들은 모두 인라인 타입 함수입니다. 따라서 컴파일을 하면 위에 언급된 함수들은 심볼이 없습니다.
spin_lock() -> _raw_spin_lock() -> __raw_spin_lock() -> do_raw_spin_lock() -> arch_spin_lock()

예를 들어 다음 get_task_exe_file() 함수 6번째 줄 코드에서 spin_lock() 함수를 호출하는 것처럼 보입니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/kernel/fork.c#L1025]
1 struct file *get_task_exe_file(struct task_struct *task)
2 {
3 struct file *exe_file = NULL;
4 struct mm_struct *mm;
5
6 task_lock(task); // spin_lock(&p->alloc_lock);
7 mm = task->mm;

위 함수를 바이너리 유틸리티를 써서 어셈블리 코드를 보면 다음과 같습니다.
1 80119974 <get_task_exe_file>:
2 80119974: e1a0c00d  mov ip, sp
3 80119978: e92dd818  push {r3, r4, fp, ip, lr, pc}
4 8011997c: e24cb004  sub fp, ip, #4
5 80119980: e52de004  push {lr} ; (str lr, [sp, #-4]!)
6 80119984: ebffd2ed  bl 8010e540 <__gnu_mcount_nc>
7 80119988: e1a04000  mov r4, r0
8 8011998c: e2800c05  add r0, r0, #1280 ; 0x500
9 80119990: eb17ac72  bl 80704b60 <_raw_spin_lock>

C 코드에서는 7번째 줄 코드와 같이 spin_lock() 함수를 호출했는데 어셈블리 코드에서는 위 9번째 줄 코드와 같이 _raw_spin_lock() 함수를 호출하는 겁니다.

어셈블리 코드를 분석에 앞서 _raw_spin_lock() 함수에 전달하는 인자들의 개수와 유형을 꼼꼼히 살펴볼 필요가 있습니다.
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
__raw_spin_lock(lock);
}

_raw_spin_lock 함수로 1개 인자를 전달하며 인자의 타입은 raw_spinlock_t *lock입니다. ARM 함수 호출 규약으로 함수 인자는 r0 레지스터로 전달되는데 r0에 포인터 타입 raw_spinlock_t 구조체 변수 주소가 있습니다. r0이란 메모리 공간에 스핀락 변수가 있다는 의미입니다. 이 점을 머릿속으로 그리면서 어셈블리 코드 분석을 시작합시다.

먼저 8번째 줄 코드를 보겠습니다.
8  80704b78: e1903f9f  ldrex r3, [r0]

r0는 0xb93b4a78이고 이 메모리 공간에 0x00010001 값이 있으니 r3은 0x00010001로 변경됩니다. 0xb93b4a78 메모리 공간에 스핀락 인스턴스가 있습니다. r3에 스핀락 인스턴스 owner와 next를 모두 저장한 겁니다. r3는 스핀락 인스턴스를 의미합니다.

다음 9번째 줄 코드를 보겠습니다.
9  80704b7c: e2832801  add r2, r3, #65536 ; 0x10000

다음 수식으로 r3에 0x10000을 더합니다. 
r2 = 0x0002|0001 = 0x0001|0001 + 0x0001|0000

이는 스핀락 지역 변수 next를 +1만큼 더하는 동작입니다. ticket의 자료구조 패턴을 활용한 명령어입니다.

다음은 10번째 줄 코드입니다.
10 80704b80: e1801f92  strex r1, r2, [r0]

r2에는 ticket next만 +1만큼 더한 결괏값인 0x00020001을 저장하고 있습니다. 이 값을 r0 메모리 공간에 저장합니다. 스핀락 인스턴스 멤버인 next를 +1만큼 증감하는 동작입니다. 스핀락 next 값을 +1만큼 증감시키는 겁니다. 만약 r0 메모리 공간에 0x00020001이 제대로 저장됐으면 r1은 1로 변경됩니다.

다음은 11~12번 줄 코드입니다. 
8  80704b78: e1903f9f  ldrex r3, [r0]
9  80704b7c: e2832801  add r2, r3, #65536 ; 0x10000
...
11 80704b84: e3310000  teq r1, #0
12 80704b88: 1afffffa  bne 80704b78 <_raw_spin_lock+0x18>

만약 0x00020001이 r0 메모리 공간에 저장을 못하면 8번째 줄 코드인 80704b78 코드 주소로 이동해서 다시 스핀락 변수를 읽습니다.

어셈블리 코드를 C 언어 형태로 변환해서 같이 표현하면 다음과 같습니다.
8  80704b78: e1903f9f  ldrex r3, [r0]
9  80704b7c: e2832801  add r2, r3, #65536 ; 0x10000
10 80704b80: e1801f92  strex r1, r2, [r0]
11 
12 static inline void _raw_spin_lock(arch_spinlock_t *lock)
13 {
14 unsigned long tmp;
15 u32 newval;
16 arch_spinlock_t lockval;
17
18 lockval = &lock->slock;
19 newval = lockval.tickets.next + 1;
20 &(lock.tickets.next) = newval;

8~10번 줄 코드를 18~20번 줄 코드에 대응합니다. 

조금 더 알기 쉽게 표현하면 다음 그림과 같습니다.
 
어셈블리 코드를 분석할 때 C 언어로 변환하면 더 오랫동안 분석한 내용을 머릿속에 기억할 수 있습니다.

이번에는 _raw_spin_lock 함수의 후반부 코드를 보겠습니다.
13 80704b8c: e1a02823  lsr r2, r3, #16
14 80704b90: e6ff3073  uxth r3, r3
15 80704b94: e1530002  cmp r3, r2
16 80704b98: 0a000003  beq 80704bac <_raw_spin_lock+0x4c>
17 80704b9c: e320f002  wfe
18 80704ba0: e1d030b0  ldrh r3, [r0]
19 80704ba4: e1530002  cmp r3, r2
20 80704ba8: 1afffffb  bne 80704b9c <_raw_spin_lock+0x3c>

13번 줄 코드를 보면 r2와 r3 레지스터로 연산을 합니다.
8  80704b78: e1903f9f  ldrex r3, [r0]
...
13 80704b8c: e1a02823  lsr r2, r3, #16

r2는 ticket에서 next 값을 +1만큼 증감한 0x00020001입니다. r3는 처음 스핀락 값을 저장하고 있으니 0x00010001입니다. r3 레지스터는 스핀락 구현부 코드에서 가장 중요한 역할을 하니 집중해서 볼 필요가 있습니다.

lsr은 Logical Shift Right 이란 용어의 약자로 피연산자를 오른쪽으로 Shift 하는 명령어입니다. r3가 0x00010001인데 이 값을 왼쪽으로 16만큼 왼쪽으로 비트 Shift 연산을 하는 동작입니다.

연산 결과는 다음과 같습니다. 
0x00010001(r3)
+lsr
=============
0x00000001(r2)

반복하면서 설명해 드리지만, 어셈블리 코드를 볼 때는 그 의미를 파악하면서 읽을 필요가 있습니다. r3에는 next와 owner가 포함된 ticket 값이 있습니다. 0x00010001입니다. 이 중에 next 값을 r2에 저장하는 동작입니다.

다음 14번째 줄 코드를 보겠습니다.
14 80704b90: e6ff3073  uxth r3, r3

uxth이라는 낯선 어셈블리 명령어입니다. 낯선 어셈블리 코드를 보면 겁먹지 마시고 구글링으로 "uxth arm" 검색어로 키워드를 입력하면, 다음과 같이 ARM Infocenter 사이트에서 관련 정보를 얻을 수 있습니다.
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0489i/CIHHJCFE.html

확인하면 uxth는 Zero extend Halfword의 약자로 16비트에서 상위 8비트를 Clear 하는 동작입니다.

연산 결과는 다음과 같습니다. 이 어셈블리 명령어는 next와 owner 중 owner를 r3에 저장하는 임무를 수행합니다.
0x00010001(r3)
uxth
================
     0x0001

13~14줄 명령어를 같이 늘어놓고 해석하면 r3(0x00010001) 있는 next를 r2에 저장하고 owner를 r3에 저장하는 동작입니다.
13 80704b8c: e1a02823  lsr r2, r3, #16
14 80704b90: e6ff3073  uxth r3, r3

0x0001 | 0001(r3)
 next     owner
  r2        r3

lsr와 uxth 단 2개 최소 어셈블리 명령어로 해당 CPU가 효율적으로 실행할 수 있도록 최적화 아이디어가 반영된 코드입니다.

다음 15번째 줄 코드를 분석할 차례입니다.
15 80704b94: e1530002  cmp r3, r2
16 80704b98: 0a000003  beq 80704bac <_raw_spin_lock+0x4c>
...
21 80704bac: f57ff05b  dmb ish

r3(owner)와 r2(next) 값을 비교해서 16번 줄 코드와 같이 두 값이 같으면 0x80704bac 주소로 이동해서 _raw_spin_lock() 함수를 빠져나옵니다.

여기서 r3의 의미를 조금 더 생각해 봅시다.
1  80704b60 <_raw_spin_lock>:
...
8  80704b78: e1903f9f  ldrex r3, [r0]

r3은 _raw_spin_lock 함수에 전달되는 스핀락 값을 저장하고 있습니다. 이때 owner와 next가 같으면 스핀락을 획득한 적이 없으니 next를 +1만큼 증감시키고 _raw_spin_lock() 함수를 빠져나오는 겁니다.

그런데 만약 ticket 값이 0x00020001이면 next가 2이고 owner가 1이므로 누군가 스핀락을 획득했다고 봐야 합니다. 만약 처음 r3가 0x00020001이면 15번 줄 코드를 실행할 때는 r3는 owner인 0x1이고 r2는 next인 0x2입니다.
15 80704b94: e1530002  cmp r3, r2
16 80704b98: 0a000003  beq 80704bac <_raw_spin_lock+0x4c>

이미 스핀락을 누군가 획득했을 경우 동작은 다음 시간에 알아보겠습니다.


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)

#Reference(커널 동기화)
커널 동기화 기본 개념 소개
레이스 발생 동작 확인
커널 동기화 기법 소개
스핀락
뮤텍스란
커널 동기화 디버깅

핑백

덧글

댓글 입력 영역