Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


arm instruction(명령어) - ldr [ARM] Instruction

그럼 ldr 명령어의 정의에 대해서 같이 배워볼까요? LDR 명령어는 메모리에서 워드를 레지스터로 읽어 드리는 동작입니다. 자 그럼 아래 명령어를 예를 들어 같이 볼까요? 참고로 R1은 0xD2FB0000라고 하겠습니다.
ldr r0, [r1]

그런데 0xD2FB0000메모리 주소에는 00000001란 값이 있다고 가정할께요.
메모리주소        값
NSD:D2FB0000|>00000001 C50F6000 00000004 40400040

“ldr r0, [r1]” 명령어가 수행되면 r0은 0000000으로 업데이트 됩니다. r1(0xD2FB0000)이 갖고 있는 메모리 값을 r0에 로딩하는 동작이죠. 그럼 아래와 같은 명령어가 실행되면 어떻게 업데이트 될까요?
ldr r0, [r1,#0x4]

r1에서 0x4만큼 더한 주소는 0xD2FB0004입니다. 그런데 이 메모리 공간에 0xC50F6000 값이 있습니다. 따라서 r0은 0xC50F6000으로 업데이트 됩니다.
r0 = 0xC50F6000 = *(0xD2FB0000+0x4) = *(r1+0x4)

그런데 ldr 명령어를 이런 방식으로 익히면 바로 머리 속에서 사라질 확률이 높습니다. ldr 명령어는 반드시 어셈블리 코드에 대응하는 C 코드를 함께 보면서 익혀야 오래 남거든요.
그럼 한 걸음 더 들어가서 ldr 명령어를 배워볼까요?우선 ldr이란 명령어를 만나면 C코드로 2가지 패턴을 그리면 좀 더 이해가 빠릅니다. 그럼 첫 번째 유형부터 살펴볼까요?


ldr r0, [주소]
아래와 같은 유형의 ldr 명령어를 보면 전역 변수에 접근하고 있다고 보면 됩니다.
ldr r0, [주소]

그럼 C코드와 어셈블리 코드를 함께 살펴볼까요? 이전 장에서 다룬 do_DataAbort 함수의 4번째 줄을 볼게요.
1  NSR:C010036C|do_DataAbort:     push    {r4-r8,r14}
2  NSR:C0100370|                  cpy     r5,r1            ; r5,fsr
3  NSR:C0100374|                  and     r12,r5,#0x400    ; r12,r5,#1024
4  NSR:C0100378|                  ldr     r1,0xC01004D0

4번째 줄 코드를 보면 "ldr r1,0xC01004D0"란 명령어가 보이죠? 이런 명령어가 전역 변수에 접근합니다. 그럼 정말로 이 명령어가 전역 변수에 접근하는지 살펴볼까요?
0xC01004D0 메모리에 어떤 값이 있는지 확인하면 0xC1A195A4이 있네요.
메모리 주소   값
NSD:C01004D0|>C1A195A4 C1B8BAD4 C1001E24 C14517E4 

"sym 0xC1A195A4" 크래시 유틸리티 명령어로 0xC1A195A4 주소가 어떤 심볼인지 확인하니 fsr_info란  전역 변수라고 출력하네요.
crash> sym 0xC1A195A4
c1a195a4 (d) fsr_info

다음 크래시 유틸리티 명령어로 fsr_info 전역 변수가 위치한 메모리를 확인해도 같은 정보(0xc1a195a4)를 확인을 할 수 있습니다.
crash> p &fsr_info
$1 = (struct fsr_info (*)[32]) 0xc1a195a4 <fsr_info>

그럼 이제 실제 do_DataAbort 함수의 C코드 구현부를 볼까요? 4번째 줄 코드를 보면 "fsr_info" 란 전역 변수에 접근하고 있죠.
1 asmlinkage void __exception
2 do_DataAbort(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
3 {
4 const struct fsr_info *inf = fsr_info + fsr_fs(fsr);
5 struct siginfo info;

ldr    r0, [r1, 오프셋] 
ldr 명령어를 아래 형식으로 쓰면, 포인터 연산을 하고 있는 경우가 많습니다.
ldr    r0, [r1, 오프셋]

가령 아래와 같이 어떤 포인터 멤버 변수에 접근하는 코드죠. current는 현재 구동 중인 프로세스의 태스크 디스크립터의 포인터를 가져오는데요. 구조체는 struct task_struct입니다.  struct task_struct 구조체 멤버 중에 stack이 있는데요. 프로세스에 할당된 스택 주소를 담고 있습니다.
void *stack_ptr = current->stack;

그럼 실제 리눅스 커널 코드를 열어보면서 살펴볼게요. 그럼 잠깐 아래 코드를 볼까요?
1 NSR:C015BFEC|put_prev_entity:  push    {r4-r11,r14}
2 NSR:C015BFF0|                  cpy     r5,r0            ; r5,cfs_rq
3 NSR:C015BFF4|                  ldr     r3,[r1,#0x1C]

첫번째와 두 번째 줄 코드를 볼까요? 스택에 r4, r5, r6, r7, r8, r9, r10, r11, r14 레지스터를 푸시하고 r0 레지스터를 r5에 저장합니다. 참고로 r0는 함수에 전달되는 파라미터를 저장합니다. 
이번에는 세번째 줄 코드를 볼까요. 드디어 ldr이란 명령어가 보이네요.
3 NSR:C015BFF4|                  ldr     r3,[r1,#0x1C]

이 명령어는 r1에서 0x1c만큼 떨어진 메모리에 있는 값을 r3에 로딩하는 동작입니다. 이를 이해하기 쉽게 수식으로 표현하면 다음과 같아요.
r3 = *(r1+0x1c)

자 여기서 잠깐, ARM 함수 호출 규약에서 파라미터를 어떻게 처리하는지 잠깐 알아볼 필요가 있어요.  
함수에 전달하는 파라미터는 r0에서 r5에 저장해서 전달합니다. 그럼 아래 C 코드를 함께 보면서 살펴볼까요?
1 static void put_prev_entity(struct cfs_rq *cfs_rq, struct sched_entity *prev)
2 {
3 if (prev->on_rq)
4 update_curr(cfs_rq); 

위 코드 첫번 째 줄 함수 선언부를 보면 put_prev_entity 함수는 2개 파라미터를 받아 처리합니다.  각각 파라미터의 구조체는 다음과 같죠.
첫번째 파라미터: struct cfs_rq *cfs_rq 
두번째 파라미터: struct sched_entity *prev

그런데 put_prev_entity 함수가 호출되는 순간에는 위 파라미터들은 다음 레지스터로 전달됩니다. r0은 struct cfs_rq *cfs_rq, r1는 struct sched_entity *prev 구조체 포인터 변수를 담고 있습니다.
r0: struct cfs_rq *cfs_rq 
r1: struct sched_entity *prev

그럼 원래 봤던 코드로 돌아가면, 아래 함수 두 번째 줄 코드는 r0(struct cfs_rq *cfs_rq 구조체)를 r5에 저장하고, 세번째 코드의 r1는 struct sched_entity *prev 구조체라는 걸 알 수 있죠.
1 NSR:C015BFEC|put_prev_entity:  push    {r4-r11,r14}
2 NSR:C015BFF0|                  cpy     r5,r0            ; r5,cfs_rq
3 NSR:C015BFF4|                  ldr     r3,[r1,#0x1C]

그럼 여기서 한 가지 의문이 생겼습니다. 왜 "ldr r3,[r1,#0x1C]" 명령어에서  r1에서 0x1C만큼 떨어진 메모리 주소에 접근할까요?.
[r1,#0x1C]

그 이유는 struct sched_entity 구조체 내 on_rq 멤버가 0x1c 오프셋에 위치해 있기 때문입니다. 
조금 더 쉽게 설명드리면, struct sched_entity->on_rq에 접근하기 위해서죠. 이 정보는 아래 크래시 유틸리티 명령어로 확인할 수 있습니다. 다음 5번과 10번째 줄 메시지를 눈여겨 보세요.
crash> struct -o  sched_entity
1 struct sched_entity {
2   [0x0] struct load_weight load;
3    [0x8] struct rb_node run_node;
4   [0x14] struct list_head group_node;
 [0x1c] unsigned int on_rq;
6   [0x20] u64 exec_start;
7   [0x28] u64 sum_exec_runtime;

8 crash> struct -o  sched_entity.on_rq
9 struct sched_entity {
10   [0x1c] unsigned int on_rq;
11 }

그럼 만약 struct sched_entity->exec_start 멤버에 접근하면 ldr 명령어는 어떻게 될까요? 
struct sched_entity 구조체 내 exec_start 멤버가 0x20에 위치하니까, 다음 명령어가 쓰이겠죠.
ldr     r3,[r1,#0x20]

이렇게 ARM 어셈블리 명령어 중 ldr를 만나면 C 코드로 어떻게 구현됐는지 머리 속으로 그리면서 분석하면 조금 더 오랫동안 이 명령어가 머리 속에 남습니다. 이 점 참고하시고요.
ldr이란 명령어는 어느 함수나 볼 수 있습니다. 그래서 이 명령어를 제대로 이해 못하면 어셈블리 코드 자체를 볼 수 없습니다. 그러니 조금 낯설더라도 꾸준히 이 명령어를 익히시길 바래요.


Reference(프로세스 관리)
4.9 프로세스 컨택스트 정보는 어떻게 저장할까?
 4.9.1 컨택스트 소개
 4.9.2 인터럽트 컨택스트 정보 확인하기
 4.9.3 Soft IRQ 컨택스트 정보 확인하기
 4.9.4 선점 스케줄링 여부 정보 저장
4.10 프로세스 디스크립터 접근 매크로 함수
 4.10.1 current_thread_info()
 4.10.2 current 매크로란
4.11 프로세스 디버깅
 4.11.1 glibc fork 함수 gdb 디버깅
 4.11.2 유저 프로그램 실행 추적 


덧글

댓글 입력 영역