그럼 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;
5 [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 (ARM Processor)
# Reference: For more information on 'Linux Kernel';
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2
'시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리 > 4장: 어셈블리 명령어' 카테고리의 다른 글
[ARM] 어셈블리 명령어란 (0) | 2023.06.10 |
---|---|
[ARM] ARM 아키텍처의 주요 개념: 어셈블리 명령어 (0) | 2023.06.10 |
arm instruction(명령어) - push & 스택 푸쉬 (0) | 2023.06.10 |
arm instruction(명령어) - cmn (0) | 2023.06.10 |
arm instruction(명령어) - strleb (0) | 2023.06.10 |