good github to practice Arm assembly instruction

 

https://github.com/Apress/programming-with-64-bit-ARM-assembly-language

데이터 처리 명령어
 
 - move
 - 산술 명령어
 - 비트 shift
 - 논리 비트 shift
 
메모리 연산
 
 - ldr
 - str
 
비교와 브랜치
 
 - 플래그 설정 명령어
 - 컨디션 코드
 - 조건부 브랜치
 - 브랜치
 
시스템 제어 명령어
 
 - 트랩 관련 명령어
 - 시스템 레지스터 설정
 - PSTATE 각종 필드 설정
 - 스택 동작 방식 설정
 
배리어 명령어
 
 - DSB
 - DMB
 - ISB
 
캐시 설정 명령어
 
 - DC {C,CI,I}SW, Xx
 - TLBI ALLE{1..3}{IS}

 

< '시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리' 저자>
 
 
* 유튜브 소개 영상
16진수인 0x7fffffff는 이진수로는 아래와 같다.
0111 1111 1111 1111 1111 1111 1111 1111 (2진수)
2147483647 (10진수)
  (static int) __boot_cpu_mode = 2147483647 = 0x7FFFFFFF 
 
 
0x7fffffff를 'LSR 4'로 수행하면;
 
0111 1111 1111 1111 1111 1111 1111 1111(0x7FFF FFFF, 2,147,483,647)
( >> 4)
0000 0111 1111 1111 1111 1111 1111 1111(0x07FF FFFF, 134,217,727)
 
결과는 0x07FF_FFFF, 10진수로는 134,217,727이다.
 
이제 음수 값을 확인하자.
 
16진수인 0x80000001는 이진수로는 아래와 같다.
1000 0000 0000 0000 0000 0000 0000 0001 (2진수)
2147483647 (10진수)
  (static int) __boot_cpu_mode = -2147483647 = 0x80000001 
 
가장 왼쪽에 있는 MSB 비트가 1이므로 음수를 표현한다.
 
0x80000001를 'LSR 4'로 수행하면;
 
1000 0000 0000 0000 0000 0000 0000 0001(0x8000 0001, -2,147,483,647)
( >> 4)
0000 1000 0000 0000 0000 0000 0000 0000(0x0800 0000, 134,217,728)
 
결과가 양수인 134,217,728이다. 
 
-2,147,483,647 를 'LSR 4' 한 결과가 -134,217,727로 나오게 하려면 어떻게 해야 할까?
 
Arithmetic Shift Right 명령어를 실행하자.
 
1000 0000 0000 0000 0000 0000 0000 0001(0x8000 0001, -2,147,483,647)
( >>> 4)
1111 1000 0000 0000 0000 0000 0000 0000(0xF800 0001, -134217727)
 
실행 결과는 아래와 같다.
 
  (static int) __boot_cpu_mode = -134217727 = 0xF8000001  
 
 
link:
 
 
 
.
이번에는 ARMv8 아키텍처에서 제공하는 명령어 중에 C 언어의 3항 연산자가 연상되는 csel를 알아봅시다.
 
다음은 분석하려는 코드입니다.
 
                    115|                printk(!error ? "okay\n" : "failed\n");
    MX:FFFFFF8009BB2404|F0FFE748                                    adrp    x8,0xFFFFFF800989D000
    MX:FFFFFF8009BB2408|F0FFE289                                    adrp    x9,0xFFFFFF8009805000
    MX:FFFFFF8009BB240C|91026108                                    add     x8,x8,#0x98      ; x8,x8,#152
    MX:FFFFFF8009BB2410|91192529                                    add     x9,x9,#0x649     ; x9,x9,#1609
    MX:FFFFFF8009BB2414|7100027F                                    cmp     w19,#0x0         ; error,#0
    MX:FFFFFF8009BB2418|9A880120                                    csel    x0,x9,x8,eq
 
[1] w19가 0인 경우
 
   * 'cmp     w19,#0x0         ; error,#0' //실행 후 CPSR의 Z 필드는 1로 업데이트됨
   * 'csel    x0,x9,x8,eq'                      // x9를 x0에 저장함
 
[2] w19가 0이 아닌 경우
 
   * 'cmp     w19,#0x0         ; error,#0' //실행 후 CPSR의 Z 필드는 0으로 업데이트됨
   * 'csel    x0,x9,x8,eq'                      // x8를 x0에 저장함
 
 
 
CSEL
Conditional select, returning the first or second input.
 
Syntax
CSEL  Wd, Wn, Wm, cond    ; 32-bit general registers
CSEL  Xd, Xn, Xm, cond    ; 64-bit general registers
 
 
아래는 conditional bit
 
 
Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자
 
 
 
* yield는 nop와 같은 명령어로써, SMP 시스템의 ARM 버스가 yield이 실행된 CPU 코어에 낮은 우선 순위를 부여하도록 한다. 
yield는 프로세스가 특별히 할 일이 없을 때 실행된다.
 
ARM 문서 내용
 
A similar use might be in modifying the arbitration priority of the snoop bus in a multiprocessor (MP) system. Defining such an instruction permits binary
compatibility between SMT and SMP systems.
 
 
출처: DDI0406C_arm_architecture_reference_manual.pdf 
 
YIELD
YIELD is a hint instruction. Software with a multithreading capability can use a YIELD instruction to indicate to the
hardware that it is performing a task, for example a spin-lock, that could be swapped out to improve overall system
performance. Hardware can use this hint to suspend and resume multiple software threads if it supports the
capability.
For more information about the recommended use of this instruction see The Yield instruction on page A4-176.
 
 
A4.8.1 The Yield instruction
In a Symmetric Multi-Threading (SMT) design, a thread can use the YIELD instruction to give a hint to the processor
that it is running on. The YIELD hint indicates that whatever the thread is currently doing is of low importance, and
so could yield. For example, the thread might be sitting in a spin-lock. A similar use might be in modifying the
arbitration priority of the snoop bus in a multiprocessor (MP) system. Defining such an instruction permits binary
compatibility between SMT and SMP systems.
 
ARMv7 defines a YIELD instruction as a specific NOP (No Operation) hint instruction.
The YIELD instruction has no effect in a single-threaded system, but developers of such systems can use the
instruction to flag its intended use on migration to a multiprocessor or multithreading system. Operating systems
can use YIELD in places where a yield hint is wanted, knowing that it will be treated as a NOP if there is no
implementation benefit.
cbnz 명령어는 Compare Branch None-zero의 약자입니다.
명령어의 이름 그대로 레지스터의 값이 0이 아닌 경우, 지정된 주소로 브랜치하는 명령어입니다.
 
아래 코드를 봅시다.
 
01 and     x2,x1,#0x3F      ; x2,x1,#63
02 cbnz    x2,0xFFFFFF8008083728   ; x2,work_pending
 
01번째 줄 코드를 봅시다.
x1이 0x1이면 x2는 0x1, x1이 0x3f이면 x2는 0x3f가 됩니다.
 
이 경우 x2는 0x1 혹은 0x3f이므로 0이 아니니, non-zero가 됩니다.
 
그 다음 02번째 줄을 실행하면 work_pending 레이블의 시작 주소인 0xFFFFFF8008083728로 브랜치합니다.
 
01~02 번째 줄은 C 코드로 다음고 같이 표현할 수 있겠네요.
 
---
x2 = x1 & 0x3f;
 
if (x2)
   b 0xFFFFFF8008083728 ; b work_pending
---
우리가 외국인이 어떤 사람인지 잘 알려면 외국어를 배워야 합니다. 외국어를 직접 말하면서 외국인과 대화를 해야 그 사람의 성격을 제대로 알 수 있습니다.
 
그렇다면 프로그래머 입장에서 ARM 프로세서가 어떤 동작을 하는 지 파악하려면 무엇을 배워야 할 까요? 바로 ARM 어셈블리 명령어입니다.
 
미국인과 대화를 하려면 영어를 말하듯 ARM 프로세서와 대화를 하려면 ARM 어셈블리 명령어를 사용해야 합니다. 즉, ARM 어셈블리 명령어는 ARM 프로세서와 소통하는 대화하는 언어라고 말할 수 있습니다.
 
ARM 어셈블리 명령어를 사용하면 ARM 프로세서와 어떻게 대화를 할 수 있을까요?
이번에는 프로그래머 입장에서 조금 더 구체적으로 ARM 프로세서와 대화하는 패턴을 예로 들겠습니다.
 
쉽게 설명을 하면 ARM 프로세서와는 대화하는 패턴을 2가지로 요약할 수 있습니다. 
 
   ● ARM 프로세서에게 일을 시킨다.
   ● ARM 프로세서의 상태 정보를 읽어 오고 싶다.
   
프로그래머 입장에서 위에서 언급한 패턴을 벗어나는 대화를 하는 경우는 드뭅니다. 
 
ARM 어셈블리 명령어의 포멧 
 
ARM 어셈블리 명령어는 명령어와 명령어를 수행한 결과를 저장하는 레지스터로 구성돼 있습니다.
다음 그림을 보면서 ARM 어셈블리 명령어의 포멧을 설명하겠습니다.
 

 
그림 1.3 어셈블림 명령어의 포멧
 
위 그림에서 표기된 필드 중 왼쪽 부분부터 살펴봅시다.
 
   ● OP Code: mov, add, sub, str, ldr와 같이 어셈블리 명령어의 기능을 나타내는 필드입니다. 
   ● cond: 어셈블리 명령어를 조건부로 실행하기 위해 OP Code 뒤에 붙여서 사용되는 필드입니다. 명령문의 추가 옵션입니다.
   ● Rd: 연산을 수행한 다음의 결과를 저장하는 레지스터인 목적 레지스터가 위치하는 필드입니다. 이 필드에는 R0~R15 레지스터가 올 수 있습니다. 
   ● Rn: Operand1로 레지스터가 위치하는 필드이며 R0~R15 레지스터가 사용됩니다. 
   ● Rm: Operand2로 레지스터뿐만 아니라 상수, 주소, 비트 시프트 연산식과 같은 등 다양한 값이 위치하는 필드입니다.
 
이번에는 다음과 같이 간단한 명령어를 예를 들어 어셈블리 명령어를 소개합니다. 
 
   ●  mov r1, #0x1
   
위 명령어를 mov라는 명령어인데 레지스터에 데이터를 저장하는 역할을 합니다. 
mov란 명령어를 실행하면 실행 결과는 어디에 저장될까요? 바로 레지스터 r1입니다.
   
처음에 ARM 어셈블리 명령어를 보고 공포에 질려 공부를 포기하는 분들이 많지만 ARM 어셈블리 명령어의 포멧은 다음과 같이 정말로 간단합니다.  
 
ARM 어셈블리는 ARM의 기본 개념과 더불어 배울 수 있는 개념이므로 어셈블리 명령어의 의미와 ARM  프로세서의 동작 원리를 확인할 수 있습니다.
 
---
"이 포스팅이 유익하다고 생각되시면 공감 혹은 댓글로 응원해주시면 감사하겠습니다. "혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!"
 
​Thanks,
Guillermo Austin Kim(austindh.kim@gmail.com)
---
이번 포스트에서는 ARM 아키텍처를 이루는 주요 개념에 대해서 설명하겠습니다.
ARM 아키텍처를 배우려면 수 많은 세부 기능을 배워야 하는데 그 중 핵심을 요약하면 다음과 같습니다.
 
    ○ ARM 어셈블리 명령어
    ○ 레지스터 세트 
    ○ 익셉션 
    ○ 함수 호출 규약
 
이 중에서 먼저 ARM 어셈블리 명령어에 대해서 살펴보겠습니다. 
 
 ARM 프로세서에게 어셈블리 명령어란
 
우리가 외국인이 어떤 사람인지 잘 알려면 외국어를 배워야 합니다.
외국어를 직접 말하면서 외국인와 대화를 해야 그 사람의 성격을 제대로 알 수 있습니다.
 
그렇다면 프로그래머 입장에서 ARM 프로세서가 어떤 동작을 하는 지 파악하려면 무엇을 배워야 할 까요?
바로 ARM 어셈블리 명령어입니다.
 
미국인과 대화를 하려면 영어를 말하듯 ARM 프로세서와 대화를 하려면 ARM 어셈블리 명령어를 사용해야 합니다.
즉, ARM 어셈블리 명령어는 ARM 프로세서와 소통하는 대화하는 언어라고 말할 수 있습니다.
 
ARM 어셈블리 명령어를 사용하면 ARM 프로세서와 어떻게 대화를 할 수 있을까요?
이번에는 프로그래 입장에서 조금 더 구체적으로 ARM 프로세서와 대화하는 패턴을 예로 들겠습니다.
 
쉽게 설명을 하면 ARM 프로세서와는 대화하는 패턴을 2가지로 요약할 수 있습니다. 
 
   ○ ARM 프로세서에게 일을 시킨다.
   ○ ARM 프로세서의 상태 정보를 읽어 오고 싶다.
   
프로그래머 입장에서 위에서 언급한 패턴를 벗어나는 대화를 하는 경우는 드뭅니다. 
 
ARM 어셈블리 명령어의 포멧 
 
ARM 어셈블리 명령어는 명령어와 명령어를 수행한 결과를 저장하는 레지스터로 구성돼 있습니다.
예를 들어 다음 명령어를 설명하겠습니다.
 
   mov r1, #0x1
   
위 명령어를 mov라는 명령어인데 레지스터에 데이터를 저장하는 역할을 합니다. 
mov란 명령어를 실행하면 실행 결과는 어디에 저장될까요? 바로 레지스터 r1입니다.
   
처음에 ARM 어셈블리 명령어를 보고 공포에 질려 공부를 포기하는 분들이 많은데요.
ARM 어셈블리 명령어의 포멧은 다음과 같이 정말로 간단합니다.  
 
  ○ 명령어가 있고 명령어를 실행한 결과는 레지스터에 저장된다.
 
ARM 어셈블리와 공포심 
 
ARM 프로세서를 배울 때 가장 먼저 만나는 게 어셈블리 명령어입니다. 
기본적인 ARM 어셈블리 명령어를 소개하고 어셈블리 명령어가 동작하는 원리를 그림으로 설명하는 경우가 대부분입니다.
많은 개발자분들이 ARM 프로세서를 배울 때 어셈블리 명령어를 배울려고 노력하는 것도 사실입니다.
 
그런데 많은 개발자들은 ARM 어셈블리 명령어를 배울 때 부담스러워 합니다. 
어떤 분들은 ARM 어셈블리 명령어를 공부하는게 너무 고통스럽다고 합니다. 그러면서 다음과 같이 불만을 토로합니다.
 
  ○ ARM 어셈블리 명령어의 갯수가 너무 많다 외우기 어렵다.
 
그런데 실제 개발을 하면 자주 분석하는 ARM 엇메블리 명령어의 갯수는 20개 내외이고, 정말 자주 사용하는 어셈블리 명령어는 10개 이내이다.
 
그래서 자주 사용하는 ARM 어셈블리 명령어의 동작 원리를 제대로 파악해놓고, 처음 만나는 어셈블리 명령어는 그 때 그 때 용법을 파악해서 찾으면 됩니다.
 
우리가 처음 영어를 공부할 때를 생각해봅시다. 처음 영어를 배울 때 영어 사전에 있는 영어 단어를 무리해서 암기하려는 분이 있어요. 당연한 이야기지만,여러분이 영어 사전에 담긴 단어를 모두 외우려고 하면 부담되고 힘이 들 것입니다.
 
하지만 중고등학교에서 배우는 영어 단어로를 제대로 활용해도 외국인과 소통하는데 아무런 지장이 없습니다. 
 
ARM 어셈블리 명령어는 레지스터와 함께 구성돼 있습니다.
레지스터는 별 게 아니다. ARM 코어 내부에서 연산 결과를 저장하는 메모리 공간에 불과합니다.
 
레지스터만을 사용해 ARM 어셈블리 명령어를 수행하면 ARM 코어는 가장 빠른 사이클 내에 연산을 수행합니다.
한 가지 예를 들어볼까요?
 
mov r1, r2
 
위 명령어를 실행하면 레지스터 r2에 있는 데이터를 레지스터 r1에 저장합니다. 이런 명령어를 실행하면 ARM 코어는 가장 빠른 사이클 내에 명령어를 처리합니다.
 
그래서 최적화 레벨을 높혀서 ARM-GCC 컴파일러를 사용하면 되도록 레시스터를 사용해 연산 결과를 저장하려고 노력합니다.
그럼 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: For more information on 'Linux Kernel';
 
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1
 
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2
 
 
 
 
 
push & 스택 푸쉬
리눅스 커널 함수를 어셈블리 코드로 열어보면 바로 push란 명령어가 눈에 보입니다. 그럼 정말 맞는지 샘플 코드를 볼까요? 아래 코드는 리눅스 커널 핵심 함수입니다. 각 함수에서 가장 먼저 실행되는 명령어가 push죠.
NSR:C0FF413C|__schedule:   push    {r4-r11,r14}
NSR:C0FF4140|              add     r11,r13,#0x20    ; r11,r13,#32
NSR:C0FF4144|              ldr     r3,0xC0FF4948
 
NSR:C017B0C4|handle_irq_event_percpu:  push    {r0-r2,r4-r11,r14}
NSR:C017B0C8|                          cpy     r3,r13           ; r3,sp
NSR:C017B0CC|                          ldr     r6,[r0,#0x4]
 
그럼 이 push란 명령어가 실행되면 어떤 동작을 할까요? 당연히 스택에 레지스터를 저장하죠. 이 때 스택 주소가 업데이트 됩니다. 그런데 이 동작은 ARM 프로세스의 함수 호출 규약과 연관돼 있습니다. 그래서 두 코드 “push {r4-r11,r14}”, “push {r0-r2,r4-r11,r14}”가 실행되면 스택 메모리에 정확히 어떤 값들이 업데이트되는지 확인이 필요합니다.
 
자 이제 시작하기 전에 한 가지 조건을 말씀드릴께요. 여기서 각각 레지스터가 담고 있는 값들은 레지스터 번호와 같다고 가정할께요. 실제 코어 덤프에선 이런 패턴의 레지스터를 볼 수는 없는데요. 좀 더 이해를 돕기 위해서 이렇게 레지스터 번호와 레지스터가 담고 있는 값을 통일 시켰습니다.
r0: 0, r1: 1, r2: 2, r3: 3, r4: 4, r5: 5, r6: 6, … ,r14: start_kernel, 스택 주소: 0xD000D000
 
push {r4-r11,r14}
아래 함수가 실행되기 전에 스택 주소는 0xD000D000입니다.
NSR:C0FF413C|__schedule:   push    {r4-r11,r14}
 
"push {r4-r11,r14}" 명령어가 실행되기 전에 0xD000D000 스택주소 근처 메모리 들은 아래 값을 갖고 있습니다.
메모리주소       값
1 NSD:D000CFD8|0x0
2 NSD:D000CFDC|0x0   
3 NSD:D000CFE0|0x0 
4 NSD:D000CFE4|0x0    
5 NSD:D000CFE8|0x0  
6 NSD:D000CFEC|0x0   
7 NSD:D000CFF0|0x0     
8 NSD:D000CFF4|0x0     
9 NSD:D000CFF8|0x0      
10 NSD:D000CFFC|0x0
11 NSD:D000D000|0x0  // <<-- 스택주소
 
"push {r4-r11,r14}" 명령어가 실행되면 2가지 동작을 한번에 수행합니다. 
1. 스택 주소가 업데이트 됩니다. 스택에 푸쉬되는 레지스터 갯수만큼이죠. {r4-r11,r14} 명령어의 의미는 {r4, r5, r6, r7, r8, r9, r10, r11, r14} 인데요. 레지스터 갯수가 9개이고 32 비트 아키텍처이므로 4 x 9 = 36, 즉 36 바이트만큼 스택 공간을 확보합니다.
0xD000D000 - 0x24(36: 10진수) = 0xD000CFDC, // 10진수 36은 16진수로는 0x24 값이죠.
 
0x24 바이트 만큼 스택 공간 확보 후 새로운 스택 주소는  0xD000CFDC 가 됐습니다.
 
2. 레지스터 스택 푸쉬 
이전 동작에서 0x24 크기만큼 스택 공간을 확보했습니다. 이제는 레지스터들을 스택에 푸쉬할 차례입니다. {r4, r5, r6, r7, r8, r9, r10, r11, r14} 9개 레지스터들을 스택에 저장합니다.
메모리 주소     값
12 NSD:D000CFD8|0x0
13 NSD:D000CFDC|0x4   // <<-- 스택주소 
14 NSD:D000CFE0|0x5 
15 NSD:D000CFE4|0x6    
16 NSD:D000CFE8|0x7   
17 NSD:D000CFEC|0x8   
18 NSD:D000CFF0|0x9     
19 NSD:D000CFF4|0x10     
20 NSD:D000CFF8|0x11      
21 NSD:D000CFFC|0xC19008D4 \\vmlinux\init/main\start_kernel
22 NSD:D000D000|0x0
 
21번째줄 덤프에서 r14을 저장하고, 0xD000CFDC 부터 0xD000CFF8 메모리 주소에 레지스터를 저장합니다. 0xD000CFDC 메모리에는 r4,  0xD000CFE0 메모리에는 r5가 푸시된 거죠. 쭉 이런 방식으로 0xD000CFF8 메모리에는 r11을 저장합니다. 메모리 주소에 어떤 순서로 레지스터를 푸시하는지잘 기억하세요.
 
이번에는 handle_irq_event_percpu 함수 내 “push {r0-r2,r4-r11,r14}” 명령어가 수행되면 어떤 동작을 하는지 알아볼까요?
 
 
push {r0-r2,r4-r11,r14}
역시 이번에도 각각 레지스터가 담고 있는 값들은 레지스터 번호와 같다고 가정할게요. 
r0: 0, r1: 1, r2: 2, r3: 3, r4: 4, r5: 5, r6: 6, … ,r14: start_kernel, 스택 주소: 0xD000D000 
 
아래 함수가 실행되기 전에 스택 주소는 0xD000D000입니다.
NSR:C017B0C4|handle_irq_event_percpu:  push    {r0-r2,r4-r11,r14}
 
push 명령어가 실행하기 전 0xD000D000 스택주소 근처 메모리 들은 아래와 값을 갖고 있습니다.
메모리주소           값
1  NSD:D000CFD0|0x0
2  NSD:D000CFD4|0x0
3  NSD:D000CFD8|0x0
4  NSD:D000CFDC|0x0
5  NSD:D000CFE0|0x0
6  NSD:D000CFE4|0x0
7  NSD:D000CFE8|0x0
8  NSD:D000CFEC|0x0
9  NSD:D000CFF0|0x0
10 NSD:D000CFF4|0x0
11 NSD:D000CFF8|0x0
12 NSD:D000CFFC|0x0
13 NSD:D000D000|0x0
14 NSD:D000CFEC|0x0   
15 NSD:D000CFF0|0x0     
16 NSD:D000CFF4|0x0     
17 NSD:D000CFF8|0x0      
18 NSD:D000CFFC|0x0
19 NSD:D000D000(스택주소)|0x0
 
"push {r0-r2,r4-r11,r14}" 명령어가 실행되면 이번에도 다음과 같이 2가지 동작을 한번에 수행합니다. 
 
1. 스택 주소가 업데이트 됩니다. 스택에 푸쉬되는 레지스터 갯수만큼이죠. 
{r4-r11,r14} 명령어의 의미는 {r0, r1, r2, r4, r5, r6, r7, r8, r9, r10, r11, r14} 인데요. 
레지스터 갯수가 12개이고 32 비트 아키텍처이므로 4 x 12 = 48, 즉 48 바이트만큼 스택 공간을 확보합니다.
0xD000D000 - 0x30(48: 10진수) = 0xD000CFDC, // 10진수 48은 16진수로는 0x24 값이죠.
 
0x30 바이트 만큼 스택 공간 확보 후 새로운 스택 주소는   0xD000CFDC 가 됐습니다.
 
 
2. 레지스터 스택 푸쉬 
0x30 크기만큼 스택 공간을 확보했습니다. 이제는 레지스터들을 스택에 푸쉬할 차례입니다. 
{r0, r1, r2, r4, r5, r6, r7, r8, r9, r10, r11, r14} 12 개 레지스터들을 스택에 저장합니다.
메모리주소     값
NSD:D000CFD0(새로운 스택)|0x0   
NSD:D000CFD4|0x1        
NSD:D000CFD8|0x2       
NSD:D000CFDC|0x4      
NSD:D000CFE0|0x5        
NSD:D000CFE4|0x6      
NSD:D000CFE8|0x7   
NSD:D000CFEC|0x8      
NSD:D000CFF0|0x9     
NSD:D000CFF4|0x10     
NSD:D000CFF8|0x11      
NSD:D000CFFC|0xC19008D4 \\vmlinux\init/main\start_kernel
NSD:D000D000|0x0
 
push란 어셈블리 명령어에 대해서 배웠습니다. 실제 메모리에 push 명령어가 실행되면 어떤 값들이 저장되는지 알아봤구요. 다음에는 ldr이란 명령어를 살펴볼께요.

+ Recent posts