임베디드 해킹 - ARM 아키텍쳐

2025. 3. 26. 18:36카테고리 없음

임베디드에 주로 사용되는 아키텍처는 Arm이다. 

 

ARM 의 역사


"Advanced Risc Machine"

 

1980년대에 "Acorn Computer Ltd"라는 회사가 처음 디자인했지만, 1980년대에 해체되고 Arm에 전념하는 ARM 홀딩스라는 새로운 회사로 재구성되었다.

ARM 홀딩스는 프로세서를 직접 제조하는 것이 아니라, Arm 아키텍처의 설계를 바탕으로 라이선스를 판매하고 이를 통해 수익을 창출하는 회사이다.

 

1996년, ARM 홀딩스는 Texas Instruments, Samsung, Nokia와 Arm 라이선스에 대한 계약을 체결하였다.

이후에도 Arm TrustZone, Cortex 시리즈와 같은 새로운 기능 및 프로세서를 계속 개발해 나가며, 이로 인해 Arm 아키텍처는 임베디드, IoT(Internet of Things), 그리고 특히 스마트폰 시장에서 엄청난 성장을 이루었다. 이러한 Arm 기반의 프로세서는 스마트폰을 넘어 Apple의 MacBook에 탑재된 M1 칩처럼, 노트북 시장에서도 활약할 수 있는 잠재력을 보여주고 있다.

 

ARM 프로세서 버전


Arm의 시작을 알린 것은 1985년도에 나온 Arm1이고, 이후로 Arm2, Arm250, Arm2a 그리고 Arm610등등 다양한 프로세서가 나왔고 동시에 ISA(Instruction Set Architecture)의 버전 또한 계속 업그레이드 되었다.

점점 많아지는 프로세서와 ISA 버전을 관리하기 위해 Arm 계열, ISA 버전, Arm 코어를 구분하여 나누고 있다.

ISA 버전에 따라서 바이너리 명령어가 바뀌기 때문에, 임베디드 기기 프로세서의 호완성을 맞추려면 해당 프로세서의 ISA 버전을 우선 확인해야한다.

 

ARM 610

 

ARM 아키텍쳐


Arm은 16비트, 32비트, 64비트를 하고, 이전까지의 ISA 버전은 16비트, 32비트만 지원했지만 ARMv8-A 버전부터는 64비트도 지원하기 시작했다.

32비트 arm은 AArch32, arm32라고 불리며 64비트 arm은 AArch64, arm64라고 불린다. 라즈베리파이 4도 ARMv8-A 버전을 사용하는 Cortex-A72 기반 프로세서를 사용하고 Apple의 M1 칩 또한 ARMv8-A 버전의 ISA를 사용하고 있다.

 

Arm / Thumb mode

Arm이 16비트를 지원하는 방식은 바로 mode를 나누는 것이다. Arm 프로세서에는 32비트의 Arm 모드( 명령어는 모두 4 바이트) , 16비트의 Thumb 모드 ( Thumb 모드의 명령어는 2 바이트 )가 있다. Arm 프로세서는 프로그램 실행 중 이 두 모드를 번갈아 사용할 수 있다.

Arm 프로세서의 모드 전환 기능은 주로 코드의 최적화를 위해 사용된다.

16비트 Thumb 모드를 사용하면 명령어의 크기가 2바이트로 줄어들기 때문에 메모리 공간을 더 효율적으로 사용할 수 있어,  메모리 크기가 제한된 임베디드 같은 곳에서 중요한 역할을 한다.

32비트 Arm 모드는 처리 성능이 더 높은 복잡한 계산을 필요로 하는 고성능 작업에 더 적합하다.

 

레지스터

Arm 레지스터는 r0~r12의 범용 레지스터와 r13~r15, Current Program Status Register (CPSR)의 특수 레지스터로 구성되어 있으며 모두 4 바이트의 크기를 가지고 있다.

 

 

r0 ~ r3 레지스터는 scratch/temporary 레지스터로 어떤 함수에서도 자유롭게 사용할 수 있지만,  r4에서 r11까지의 레지스터는 callee-save 레지스터이며, 함수가 호출될 때 r4부터 r11의 데이터는 함수가 종료된 후에도 원래의 상태를 유지해야 한다. 따라서 callee가 r4에서 r11까지의 레지스터를 사용하려면, 데이터를 스택에 저장해두고 함수가 끝날 때 pop을 통해 데이터를 복구해야한다.

 

Thumb 모드의 경우에는 범용 레지스터로 r0 ~ r7까지만 사용하게 되고 Frame Pointer로 사용되던 r11을 r7으로 대체하여 사용다.

 

* PC 레지스터의 경우 Arm의 Pipelining 특성 때문에 실제 실행되는 명령어의 두 줄 앞에 존재한다.  fetch 되는 명령어가 PC에 있고 decode 되는 명령어의 경우 PC-4(Thumb 모드의 경우 PC-2), 실제로 execute 되는 명령어의 경우 PC-8(Thumb 모드의 경우 PC-4)에 있다.

 

CPSR 레지스터

 

31:28 비트는 Flag Field로 방금 처리된 연산 결과의 상태를 나타내고, Flag로는 NZCV가 존재한다.

  • Negative flag : 결과값이 음수일 때 1로 설정
  • Zero flag : 결과값이 0일 때 1로 설정
  • Carry flag : 덧셈에서 Carry 발생 시 1로 설정, 뺄셈에서 Borrow 발생 시 0으로 설정
  • oVerflow flag : 덧셈 / 뺄셈에서 Signed Overflow가 발생 시 1로 설정

7:6 비트에 있는 field는 IRQFIQ를 비활성화 하는 비트로 1로 설정하면 해당 인터럽트가 비활성화 된다.

바로 뒤에 있는 T bit는 Arm과 Thumb 모드를 나타내며, 0은 Arm, 1은 Thumb 모드입니다.

뒤에 4:0 비트는 현재 프로세서 모드를 나타낸다. 

User 모드를 제외한 6개의 모드는 Privileged 모드로 스스로 모드를 바꿀 수 있다.

 

Banked 레지스터

각 모드는 각자의 Register set 을 가지고 있다.

 

예를 들어 User 모드에서 Abort 모드로 이동 시, 현재 r0~r12 레지스터를 스택에 저장 , CPSR 레지스터는 Saved Program Status Register (SPSR) 레지스터에 저장한다.

이후 Abort 모드에서 User 모드로 이동할 때는 SPSR 데이터를 CPSR로 옮기고 스택에 저장한 User 모드의 레지스터를 가져다.

 

각각의 모드는 banked register를 가지고 있다. banked register는 각 모드 별로 물리적으로 존재하는 레지스터이며 banked register가 아닌 레지스터들은 User & System 모드의 레지스터를 공유하게 된다.  공유하는 레지스터는 모드 변경 시 기존 모드의 레지스터 값을 스택에 넣어 현재 컨텍스트를 저장하게 된다.

 

 

ARM 어셈블리


명령어{s}{condition} Rd, Rn, {Operand2}

 

  • s : 실행 결과에 따라 CPSR flag field를 설정다
  • condition : CPSR flag field에 따라 해당 명령어를 실행합니다. 기본은 AL (ALWAYS)로, 어떤 상태이든 실행한다.
    • AL 이외에도 EQ, NE, GE, LE, GT, LT와 같은 옵션들이 존재한다.
CMP r5, r6 // CMP는 s가 없어도 flag field를 설정
MOVEQ r0, r1
MOVNE r2, r3
SUBS r4, r5, r6  // 명령어 뒤에 s를 붙여 flag field 설정
MOVEQ r0, r1     // r5와 r6이 같다면 실행
MOVNE r2, r3     // r5와 r6이 같지 않다면 실행
MOV(AL) r4, r5   // 무조건 실행
  • Rd : destination register로 명령어의 실행 결과가 들어간다.
  • Rn : 첫 번째 피연산자이다. 레지스터, immediate 값을 넣을 수 있다.
  • Operand2 : 두 번째 피연산자이다. 레지스터, immediate 값 혹은 shift 값이 들어갈 수 있다.

Operand2는 아래와 같은 다양한 형태가 될 수 있다.

Shift     : MOV R0, R0, LSL #1 (R0에 Logical shift left 1을 하고 R0에 넣습니다.)
Immediate : ADD R0, R1, #2     (R1에 2를 더하고 R0에 넣습니다.)
Register  : ADD R0, R1, R2     (R1에 R2를 더하고 R0에 넣습니다.)

 

 

ARM 어셈블리 명령어

 

 

1. 데이터 이동 

여기서 LDR 과 STR 의 operand2 에는 indirect/ direct 두 형태가 들어갈 수 있다.

 

1-1 Direct 형태 

 

2. Indirect 형태

LDR r0, [base register, {offset}]  ; offset은 선택적으로 들어갈 수 있습니다.

 

2-1 base 레지스터

LDR r0, [r1]
LDR r0, [r15]

 

2-2 offset : immediate , register, scaled register 중 하나 선택

 

 

 

2-3 오프셋을 추가하는 과정에서 인덱싱도 3가지로 나뉜다. 

 

 

산술연산과 논리연산 & 비교와 분기

 

BX와 BLX 명령어가 실행될 때, 분기하는 레지스터 값의 LSB가 1 이라면 Thumb 모드로, 0 이라면 Arm 모드로 변환한다.

  • bx R1(0x400401) → Thumb mode
  • bx R1(0x400400) → Arm mode

*분기란 ? jump/call 이 없고 branch ( 분기) 가 있으며 실행의 흐름을 바뀌게 하는거

 

스택

 

 

사용법

 

시스템 콜

 

ARM 함수의 프롤로그와 에필로그 

 

# Prologue
push {fp, lr}
add fp, sp, #4
sub sp, sp, #12

...

# Epilogue
sub sp, fp, #4
pop {fp, pc}

 

 

rm의 함수 프롤로그와 에필로그 또한 x86-64의 프롤로그와 에필로그와 유사하지만, x86-64에서 call 명령어를 사용하면 반환 주소가 스택에 저장되는 반면, arm은 bl, blx 명령어를 사용하면 LR에 반환 주소가 저장되는 차이점이 있다.

 

1. push {fp, lr} 

스택에 FP와 LR을 넣는다. 이때 오른쪽부터 왼쪽 순서로 스택에 push가 된다

 

2. add fp, sp, #4 

FP를 SP + 4로 옮긴다.

 

3.sub sp, sp, #12

SP를 12만큼 빼며 함수 내에서 사용될 local 변수들의 자리를 만든다.

 

ARM 함수 에필로그

 1. sub sp, fp, #4

SP에 fp - 4의 값을 넣는다.  SP는 이전에 push 했던 FP가 있는 주소에 위치하게 된다.

 

2. pop {fp, pc} 

왼쪽에서 오른쪽으로 pop을 진행한다. 이전에 저장해둔 FP, LR을 FP, PC에 pop 다.