ARM 코어에 전원이 들어오면 리섹 익센션이 발생하고 리셋 익셉션 핸들러가 실행된다. 해당 핸들러는 HW를 초기화하고 C언어로 작성한 코드 즉 main() 함수로 점프하는 역할을 수행한다. 해당 포스팅에서는 ARM 동작 모드 별 스택 포인터를 초기화하고 C언어 코드로 점프하는 과정을 학습한다. 실무에서는 시스템 클럭 설정, 메모리 컨트롤러 초기화 등 더 많은 작업을 수행해야 하나, QEMU는 가상 에뮬레이터이기 때문에 이것 만으로도 충분하다.
Stack Pointer를 초기화 하기 위해서는 각 동작 모드 별로 사용할 메모리 영역 등을 나누어야 한다. 즉 메모리 설계가 선행되어야 하는 것이다. QEMU는 default로 128MB의 메모리를 사용하는데, 이는 실제 임베디드 시스템에서 사용하기엔 매우 큰 용량이다. 하지만 현재는 학습하고 있는 단계이기 때문에 메모리를 넉넉하게 사용하고자 한다.
메모리 0번지에 text 영역을 배치하고, 동작 모드별 스택 영역, 전역 변수 영역, 동적 할당 영역을 순서대로 배치할 것이다.
include 디렉터리 내부에 헤더 파일들을 생성하고 메모리를 설계한대로 작성하였다.
#define INST_ADDR_START 0
#define USRSYS_STACK_START 0x00100000
#define SVC_STACK_START 0x00300000
#define IRQ_STACK_START 0x00400000
#define FIQ_STACK_START 0x00500000
#define ABT_STACK_START 0x00600000
#define UND_STACK_START 0x00700000
#define TASK_STACK_START 0x00800000
#define GLOBAL_ADDR_START 0x04800000
#define DALLOC_ADDR_START 0x04900000
#define INST_MEM_SIZE (USRSYS_STACK_START - INST_ADDR_START)
#define USRSYS_STACK_SIZE (SVC_STACK_START - USRSYS_STACK_START)
#define SVC_STACK_SIZE (IRQ_STACK_START - SVC_STACK_START)
#define IRQ_STACK_SIZE (FIQ_STACK_START - IRQ_STACK_START)
#define FIQ_STACK_SIZE (ABT_STACK_START - FIQ_STACK_START)
#define ABT_STACK_SIZE (UND_STACK_START - ABT_STACK_START)
#define UND_STACK_SIZE (TASK_STACK_START - UND_STACK_START)
#define TASK_STACK_SIZE (GLOBAL_ADDR_START - TASK_STACK_START)
#define DALLOC_MEM_SIZE (55 * 1024 * 1024)
#define USRSYS_STACK_TOP (USRSYS_STACK_START + USRSYS_STACK_SIZE)
#define SVC_STACK_TOP (SVC_STACK_START + SVC_STACK_SIZE)
#define IRQ_STACK_TOP (IRQ_STACK_START + IRQ_STACK_SIZE)
#define FIQ_STACK_TOP (FIQ_STACK_START + FIQ_STACK_SIZE)
#define ABT_STACK_TOP (ABT_STACK_START + ABT_STACK_SIZE)
#define UND_STACK_TOP (UND_STACK_START + UND_STACK_SIZE)
// CPSR setting value
#define ARM_MODE_BIT_USR 0x10
#define ARM_MODE_BIT_FIQ 0x11
#define ARM_MODE_BIT_IRQ 0x12
#define ARM_MODE_BIT_SVC 0x13
#define ARM_MODE_BIT_ABT 0x17
#define ARM_MODE_BIT_UND 0x1B
#define ARM_MODE_BIT_SYS 0x1F
#define ARM_MODE_BIT_MON 0x16
#define ARM_MODE_BIT_FLAG 0x1F
이는 ARM 아키텍처에서 발생하는 Exception의 종류이다. Exception이 발생하면 ARM 코어는 지정된 위치의 명령어를 실행하는데, 이는 Exception을 처리하기 위한 핸들러로 점프하는 코드이다. 이처럼 메모리에 각 Exception들을 처리하기 위한 핸들러로 점프하는 명령어들를 순차적으로 기록한 것 을 Exception Vector Table 이라고 부른다.
/* Entry.S */
.text
.code 32
.global vector_start
vector_start:
LDR PC, reset_handler_addr
LDR PC, undef_handler_addr
LDR PC, svc_handler_addr
LDR PC, pftch_abt_handler_addr
LDR PC, data_abt_handler_addr
B .
LDR PC, irq_handler_addr
LDR PC, fiq_handler_addr
reset_handler_addr: .word reset_handler
undef_handler_addr: .word dummy_handler
svc_handler_addr: .word dummy_handler
pftch_abt_handler_addr: .word dummy_handler
data_abt_handler_addr: .word dummy_handler
irq_handler_addr: .word dummy_handler
fiq_handler_addr: .word dummy_handler
reset_handler:
...
dummy_handler:
...
.end
링커 스크립트를 통해 vector_start 심벌을 메모리 0번지에 위치시켰는데, 해당 레이블에 위와 같은 코드를 작성하여 익셉션 테이블을 배치할 수 있다.
/* Entry.S */
#include "ARMv7AR.h"
#include "MemoryMap.h"
.text
.code 32
.global vector_start
.global vector_end
vector_start:
...
reset_handler:
/* SVC Mode Stack Init */
MRS r0, cpsr
BIC r1, r0, #ARM_MODE_BIT_FLAG
ORR r1, r1, #ARM_MODE_BIT_SVC
MSR cpsr, r1
LDR sp, =SVC_STACK_TOP
/* IRQ Mode Stack Init */
MRS r0, cpsr
BIC r1, r0, #ARM_MODE_BIT_FLAG
ORR r1, r1, #ARM_MODE_BIT_IRQ
MSR cpsr, r1
LDR sp, =IRQ_STACK_TOP
/* FIQ Mode Stack Init */
MRS r0, cpsr
BIC r1, r0, #ARM_MODE_BIT_FLAG
ORR r1, r1, #ARM_MODE_BIT_FIQ
MSR cpsr, r1
LDR sp, =FIQ_STACK_TOP
/* ABT Mode Stack Init */
MRS r0, cpsr
BIC r1, r0, #ARM_MODE_BIT_FLAG
ORR r1, r1, #ARM_MODE_BIT_ABT
MSR cpsr, r1
LDR sp, =ABT_STACK_TOP
/* UND Mode Stack Init */
MRS r0, cpsr
BIC r1, r0, #ARM_MODE_BIT_FLAG
ORR r1, r1, #ARM_MODE_BIT_UND
MSR cpsr, r1
LDR sp, =UND_STACK_TOP
/* USR, SYS Mode Stack Init */
MRS r0, cpsr
BIC r1, r0, #ARM_MODE_BIT_FLAG
ORR r1, r1, #ARM_MODE_BIT_SYS
MSR cpsr, r1
LDR sp, =USRSYS_STACK_TOP
BL main
dummy_handler:
...
.end
메모리 설계 후 작성한 헤더 파일을 include 하고 reset_handler를 작성하였다.
MRS r0, cpsr
BIC r1, r0, #0x1F
ORR r1, r1, #동작 모드
MSR cpsr, r1
LDR sp, =스택 꼭대기 주소
코드를 살펴보면 위와 같은 코드가 반복되는 것을 확인할 수 있다.
이처럼 동작 모드 별 스택 포인터를 초기화한 이후에는 BL main
을 통해 C언어의 main 함수로 점프한다. 해당 Entry.S 파일에는 main 심벌이 존재하지 않는데, 이는 다른 .c 파일에 작성하고 링크하여 해결할 수 있다.
#include "stdint.h"
void main(void) {
uint32_t* dummyAddr = (uint32_t*) (1024*1024*100); // 100MB
*dummyAddr = sizeof(long);
dummyAddr = (uint32_t*) (1024*1024*100 + 4); // 104MB
*dummyAddr = (uint32_t) 1;
}
동작을 확인하기 위해 의미 없는 코드를 작성하였다. 100MB, 104MB 메모리 영역에 값을 기록하는 코드이다.
빌드 및 디버깅 모드로 실행 후 해당 주소의 값을 확인해보면 0x0000 0004, 0x0000 0001의 값이 들어가 있는 것을 확인할 수 있다. 이를 통해 reset_handler가 실행되고 나서 C언어 코드가 실행되었음을 알 수 있다.