https://www.yes24.com/Product/Goods/84909414
공부 목적으로 이만우님의 저서 "임베디드 OS 개발 프로젝트"를 따라가며, RTOS "Navilos"를 개발하는 포스트입니다. 모든 내용은 "임베디드 OS 개발 프로젝트"에 포함되어 있습니다.
개발 목적
임베디드 시스템에 많이 사용되는 RTOS를 직접 개발해 보며 다음과 같은 지식을 학습하고자 합니다.
- RTOS의 핵심 개념에 대해 학습한다.
- 운영체제 핵심 기능을 설계해 보며 학습한다.
- 펌웨어에 대한 진입장벽을 낮춘다.
- ARM 아키텍처에 대해 학습한다.
- 하드웨어에 대한 지식을 학습한다.(펌웨어가 어떻게 하드웨어를 제어하는지)
이번 챕터는 부팅하는 것을 목표로 하겠습니다.
펌웨어에서의 부팅이란?
시스템에 전원이 들어가 모든 초기화 작업을 마치고 펌웨어가 대기 상태가 될 때까지 혹은 시스템에 전원이 들어가고 ARM 코어가 리셋 익셉션 핸들러를 모두 처리한 후 C언어 코드로 넘어가기 직전까지 말합니다. 보통을 전자를 부팅이라 하지만 이번엔 후자를 부팅이라 정하겠습니다.
메모리 설계
보통 임베디드 시스템은 메모리 구조가 복잡하고 최적의 결과를 내기위해 다양한 메모리를 섞어 쓰기도 합니다. 하지만 QEMU는 소프트웨어 에뮬레이터이기 때문에 비교적 단순한 메모리 구조로 되어있고, 용량 제한도 설정하는 만큼 쓸 수 있습니다.(기본 128MB, 임베디드 시스템에선 매우 큰 용량)
실행 파일의 메모리는 크게 세 가지로 나눌 수 있습니다.
- Text 영역 : 코드가 있는 영역. 임의로 변경하면 안됨
- bss 영역 : 초기화하지 않은 변수가 있는 영역. 초기화하지 않은 전역 변수이므로 빌드 완료되어 생성된 바이너리 파일엔 심벌과 크기만 들어있음
- data 영역 : 초기화한 전역 변수가 있는 공간
임베디드 시스템이 속도가 빠르지만 용량이 작은 메모리와 속도가 느리지만 용량이 큰 메모리를 가지고 있다면 어떻게 메모리 설계를 해야할까?
- Text 영역은 빠른 메모리에 배치하고, 데이터 중에서 일부 속도에 민감한 데이터들도 링커에게 정보를 주어 빠른 메모리에 배치해야 한다.
- 나머지 data영역, bss영역은 속도가 느려도 용량이 큰 메모리에 배치한다.
메모리 설계는 다음과 같이 하도록 하겠습니다.
text 영역
- RTOS를 사용하는 펌웨어는 많아야 수십 KB면 충분하지만, 지금은 메모리가 넉넉하므로 1MB 할당
- 시작 주소 0x00000000 / 끝 주소 0x000FFFFF
data 영역, bss 영역
- 데이터를 형태와 속성에 따라 어떻게 배치할지 고민해야 합니다.
- 데이터 형태 : 동작 모드별 스택, 태스크 스택, 전역 변수, 동적 메모리 할당 영역
- 데이터 속성 : 성능 중시 데이터, 큰 공간이 필요한 데이터, 공유 데이터
- QEMU에선 별의미 없으니 그냥 쭉 배치하겠습니다.
- USR, SYS(2MB): 0x00100000 ~ 0x002FFFFF
- SVC(1MB): 0x00300000 ~ 0x003FFFFF
- IRQ(1MB): 0x00400000 ~ 0x004FFFFF
- FIQ(1MB): 0x00500000 ~ 0x005FFFFF
- ABT(1MB): 0x00600000 ~ 0x006FFFFF
- UND(1MB): 0x00700000 ~ 0x007FFFFF
태스크마다 각 1MB씩 할당할 생각이므로 64MB를 배정하겠습니다. 즉, 나빌로스의 최대 태스크는 64개 입니다. 남은 공간들은 동적 할당 메모리용으로 쓸 예정입니다.
동적 할당 영역 | 0x0490000~ 0x07FFFFFFF(55MB) |
전역 변수 영역 | 0x0480000~ 0x048FFFFFF(1MB) |
태스크 스택 영역 | 0x0080000~ 0x047FFFFFF(7MB) |
UND모드 스택 | |
ABT 모드 스택 | |
FIQ 모드 스택 | |
SVC 모드 스택 | |
USR, SYS 모드 스택 | |
Text 영역 | 0x00000000~ 0x000FFFFF(1MB) |
익셉션 벡터 테이블 만들기
이제 본격적으로 익셉션 벡터 테이블 핸들러를 작성해보겠습니다.
//Entry.S
.text
.code 32
.global vector_start
.global vector_end
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
vector_end:
reset_handler:
LDR R0, =0x10000000
LDR R1, [R0]
dummy_handler:
B .
.end
익셉션 벡터 테이블에 각 핸들러로 점프하는 코드만 작성했습니다. 각 핸들러는 아직 작성하지 않았고 무한루프를 도는 더미 코드를 호출하는 코드입니다.
디버그를 해보겠습니다.
$ make debug
$ make gdb
특별한 변화는 없지만, R1에 0x1780500이 제대로 들어간 것을 확인할 수 있습니다. 이것은 ch3에서 읽은 SYS_ID의 값과 동일한 결과입니다.
익셉션 핸들러
이 내용은 ARM 아키텍처의 기초지식을 알고 있다면 더욱 이해하기 좋습니다. 추후에 내용을 추가하도록 하겠습니다.
스택 만들기
이제부터 동작 확인용 코드가 아닌 의미있는 코드를 작성하겠습니다. 메모리 설계한 메모리 맵을 토대로 c언어 코드를 작성하겠습니다.
//스택 주소를 정의 MemoryMap.h
#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 (UNT_STACL_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 - 4)
#define SVC_STACK_TOP (SVC_STACK_START + SVC_STACK_SIZE - 4)
#define IRQ_STACK_TOP (IRQ_STACK_START + IRQ_STACK_SIZE - 4)
#define FIQ_STACK_TOP (FIQ_STACK_START + FIQ_STACK_SIZE - 4)
#define ABT_STACK_TOP (ABT_STACK_START + ABT_STACK_SIZE - 4)
#define UND_STACK_TOP (UND_STACK_START + UND_STACK_SIZE - 4)
다음은 ARM의 cpsr에 값을 설정하여 동작 모드를 바꿀 수 있는 값을 정의하겠습니다.
//동작 모드 전환 값 ARMv7AR.h
#define ARM_MODE_BIT_USR 0x10
#define ARM_MPDE_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
헤더 파일을 작성했으니 어셈블리어 코드에 포함시키겠습니다.
//Entry.S
.text
.code 32
.global vector_start
.global vector_end
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
vector_end:
reset_handler:
MRS r0, cpsr
BIC r1, r0, #0x1F
ORR r1, r1, #ARM_MODE_BIT_SVC
MSR cpsr, r1
LDR sp, =SVC_STACK_TOP
MRS r0, cpsr
BIC r1, r0, #0x1F
ORR r1, r1, #ARM_MODE_BIT_IRQ
MSR cpsr, r1
LDR sp, =IRQ_STACK_TOP
MRS r0, cpsr
BIC r1, r0, #0x1F
ORR r1, r1, #ARM_MODE_BIT_FIQ
MSR cpsr, r1
LDR sp, =FIQ_STACK_TOP
MRS r0, cpsr
BIC r1, r0, #0x1F
ORR r1, r1, #ARM_MODE_BIT_ABT
MSR cpsr, r1
LDR sp, =ABT_STACK_TOP
MRS r0, cpsr
BIC r1, r0, #0x1F
ORR r1, r1, #ARM_MODE_BIT_UND
MSR cpsr, r1
LDR sp, =UND_STACK_TOP
MRS r0, cpsr
BIC r1, r0, #0x1F
ORR r1, r1, #ARM_MODE_BIT_SYS
MSR cpsr, r1
LDR sp, =USRSYS_STACK_TOP
dummy_handler:
B .
.end
어셈블리어 코드도 헤더 파일을 포함시키는 것은 C언어와 같습니다. #include문법을 사용하여 헤더파일을 포함시킵니다.
코드 설명
- 30~64번째 줄은 모든 동작 모드를 순회하면서 스택 꼭대기 메모리 주소를 설정하는 코드임
MRS r0, cpsr
BIC r1, r0, #0x1F
ORR r1, r1, #동작 모드
MSR cpsr, r1
LDR sp, =스택 꼭대기 메모리 주소
→ 3번째 줄 #동작 모드라고 쓴 부분에 ARMv7AR.h에서 정의한 동작 모드 변경 값을 넣어 ARM 동작 모드를 변경함
→ 5번째 줄에 =스택 꼭대기 메모리 주소라고 쓴 부분에 MemoryMap.h에서 정의한 스택 꼭대기 주소 값으로 넣어 해당 동작 모드 스택 설정을 완료함
- 꼭대기 주소를 계산해서 값을 넣는 이유 : 스택은 높은 주소에서 낮은 주소로 자라는 특징을 갖고 있음 → 일반적으로 메모리는 증가하는 방향(0x00009088을 썼으면 다음 데이터는 0x00009089에서 씀) → 스택은 반대
- 스택의 꼭대기 주소 = 스택의 시작 주소 - 4 → 사실 4바이트 안빼도 됨 → 스택과 스택이 딱 붙어있는 것이 싫어서 일종의 패딩(padding)으로 4바이트를 비워두려고 사용한 것 → 이것은 나중에 디버깅할때 스택과 스택 사이를 구분하는데 사용됨
이제 빌드를 해보겠습니다.
그러나 빌드가 제대로 되지 않습니다.
에러를 보니 ARM_MODE_BIT_SVC 등의 심벌이 정의되어 있지않다고 합니다. 즉, 정의되어 있지 않다고 합니다. 이를 해결하기 위해선 헤더 파일이 있는 위치를 어셈블러에 알려주어야 합니다.
Makefile을 수정하겠습니다.
//Makefile
ARCH = armv7-a
MCPU = cortex-a8
CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-ld
OC = arm-none-eabi-objcopy
LINKER_SCRIPT = ./navilos.ld
ASM_SRCS = $(wildcard boot/*.S)
ASM_OBJS = $(patsubst boot/%.S, build/%.o, $(ASM_SRCS))
INC_DIRS = include
navilos = build/navilos.axf
navilos_bin = build/navilos.bin
.PHONY: all clean run debug gdb
all: $(navilos)
clean:
@rm -fr build
run: $(navilos)
qemu-system-arm -M realview-pb-a8 -kernel $(navilos)
debug: $(navilos)
qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4
gdb:
gdb-multiarch $(navilos)
$(navilos): $(ASM_OBJS) $(LINKER_SCRIPT)
$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS)
$(OC) -O binary $(navilos) $(navilos_bin)
build/%.o: boot/%.S
mkdir -p $(shell dirname $@)
$(CC) -march=$(ARCH) -mcpu=$(MCPU) -I $(INC_DIRS) -c -g -o $@ $<
- 14번째 줄에 헤더 파일 디렉터리 경로를 INC_DIRS라는 변수로 저장했음
- 41번째 줄에 -I 옵션으로 헤더 파일 디렉터리 경로를 지시하였음
- 41번째 $(AS)를 $(CC)로 변경하였음
- → C언어에서 #define은 전처리기에 의해서 처리되지만, arm-none-eabi-as는어셈블러일 뿐이고 전처리는 해주지 않음 → 그래서 전처리까지 하려면 arm-none-eabi-gcc를 사용해야 함
- gcc는 실행파일을 만드는 것이기 때문에 오브젝트 파일을 만들라느 -c 옵션을 추가해주었음
이번엔 빌드는 잘되지만 -mcpu=cortex-a8과 -march=armv7-a가 서로 충돌한다는 에러 메시지가 뜹니다. 그냥 무시하도록 하겠습니다.
스택 확인
스택이 제대로 초기화 되었는지 확인하겠습니다.
모든 값들이 제대로 들어가 있습니다.
메인으로 들어가자
어떻게 어셈블리어 코드에서 C언어 함수로 점프할 수 있을까요?
어셈블리 코드에서 브랜치 명령(BL)로 점프하려면 점프 대상 레이블이 같은 파일 안에 있어야 합니다. 만약 다른 파일에 있다면, 링커가 링킹할 수 있도록 레이블을 .global로 선언해야 합니다.전역 심벌은 어셈블리어에선 .global, C언어에선 extern과 같습니다.
main() 함수로 진입하는 코드를 작성하겠습니다.
꼭 main()이 시작 지점일 필요는 없습니다. main()으로 시작하는 건 관습적인 이유일 뿐입니다. 컴파일러의 시작 지점 기본값이 main()입니다.
//Entry.S에 메인 함수로 점프하는 코드 추가
MRS r0, cpsr
BIC r1, r0, #0x1F
ORR r1, r1, #ARM_MODE_BIT_SYS
MSR cpsr, r1
LDR sp, =USRSYS_STACK_TOP
BL main
boot에 Main.c 추가
//Main.c 파일 초기 코드
#include "stdint.h"
void main(void)
{
uint32_t* dummyAddr = (uint32_t*)(1024*1024*100);
*dummyAddr = sizeof(long);
}
위 코드는 언어 코드가 동작하는 것을 확인할 뿐, 유의미한 동작을 하는 코드는 아닙니다.
설명100MB 메모리s 주소 영역(0x6400000)에 의미없는 값을 쓰는 것은 의미없는 값의 타입을 long으로 설정한 것 입니다.
다시 Makefile을 수정하겠습니다.
//C언어 소스 파일을 컴파일할 수 있도록 Makefile을 변경
//Makefile
ARCH = armv7-a
MCPU = cortex-a8
CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-ld
OC = arm-none-eabi-objcopy
LINKER_SCRIPT = ./navilos.ld
MAP_FILE = build/navilos.map
ASM_SRCS = $(wildcard boot/*.S)
ASM_OBJS = $(patsubst boot/%.S, build/%.o, $(ASM_SRCS))
C_SRCS = $(wildcard boot/*.c)
C_OBJS = $(patsubst boot/%.c, build/%.o, $(C_SRCS))
INC_DIRS = include
navilos = build/navilos.axf
navilos_bin = build/navilos.bin
.PHONY: all clean run debug gdb
all: $(navilos)
clean:
@rm -fr build
run: $(navilos)
qemu-system-arm -M realview-pb-a8 -kernel $(navilos)
debug: $(navilos)
qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4
gdb:
gdb-multiarch $(navilos)
$(navilos): $(ASM_OBJS) $(C_OBJS) $(LINKER_SCRIPT)
$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS) $(C_OBJS) -Map=$(MAP_FILE)
$(OC) -O binary $(navilos) $(navilos_bin)
build/%.o: boot/%.S
mkdir -p $(shell dirname $@)
$(CC) -march=$(ARCH) -mcpu=$(MCPU) -I $(INC_DIRS) -c -g -o $@ $<
build/%.o: $(C_SRCS)
mkdir -p $(shell dirname $@)
$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) -c -g -o $@ $<
설명
- 10줄, map 파일 이름 지정 → 링커가 생성하는 파일
- 15~16줄, C_SRCS, C_OBJS 변수에 소스 파일, 오브젝트 파일 이름 저장
- 39~40줄, C_OBJS 변수 추가
빌드해보니 제대로 동작합니다. 0x64000000에 제대로 4가 저장되어 있습니다.
마치며
이번엔 지난번보다 조금 더 의미있는 코드를 작성했습니다. 진입 코드와 메모리 맵을 구성했으니 사실상 펌웨어의 초기화를 작성한 것입니다. 앞으로 여기에 살을 점점 붙여나가는 형식으로 기능을 추가할 것입니다.
현재는 진도를 더 많이 나간 상태입니다. 4장까지 진행했을때는어렵다는 생각이 들었습니다. 이미 만들어진 라이브러리 등의 함수를 갖다 쓰던 지금까지와의 코딩과는 달리 이후에 갖다 쓰기위해 정의하고 그리고 편하게 빌드하기 위해 작업한 챕터라고 이해하면 될 것 같습니다.
'임베디드' 카테고리의 다른 글
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.7 (1) | 2024.11.11 |
---|---|
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.6 (2) | 2024.11.06 |
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.5 (6) | 2024.11.04 |
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.3 (1) | 2024.10.19 |
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.0-ch.2 (0) | 2024.10.19 |