https://www.yes24.com/Product/Goods/84909414
공부 목적으로 이만우님의 저서 "임베디드 OS 개발 프로젝트"를 따라가며, RTOS "Navilos"를 개발하는 포스트입니다. 모든 내용은 "임베디드 OS 개발 프로젝트"에 포함되어 있습니다.
개발 목적
임베디드 시스템에 많이 사용되는 RTOS를 직접 개발해 보며 다음과 같은 지식을 학습하고자 합니다.
- RTOS의 핵심 개념에 대해 학습한다.
- 운영체제 핵심 기능을 설계해 보며 학습한다.
- 펌웨어에 대한 진입장벽을 낮춘다.
- ARM 아키텍처에 대해 학습한다.
- 하드웨어에 대한 지식을 학습한다.(펌웨어가 어떻게 하드웨어를 제어하는지)
이번 챕터는 우선 코드를 작성해 보고, 빌드 자동화와 디버깅을 하는 내용입니다.
리셋 벡터
ARM 코어에 전원이 들어가면 가장 먼저 하는 일이 리셋 벡터를 실행하는 일입니다.
- 리셋 벡터: 메모리 주소 0x00000000에 저장된 명령
-> ARM 코어는 전원이 들어오면 메모리 0x00000000에서 32비트를 읽어 명령을 실행합니다.
우리가 할 일은?
메모리 0x00000000에 명령어를 넣어주면 됩니다.
boot 디렉터리를 만들겠습니다.
이름이 boot니까 앞으로 부팅 관련된 소스 코드를 이곳에 넣겠습니다.
//directory 생성
mkdir boot
// 확인
tree
앞으로 tree 명령어를 통해 디렉터리 파일들 정보를 확인할 예정입니다.
스켈레톤 코드를 만들고 앞으로 살을 붙여나갈 것입니다.
//최초로 작성한 Entry.S
.text
.code 32
.global vector_start
.global vector_end
vector_start:
MOV R0, R1
vector_end:
.space 1024, 0
.end
코드 설명
- .text는 .end가 나올 때까지 모든 코드가 text 섹션이라는 의미
- text 섹션이 뭐지? → 실행 파일의 형식을 알아야 함소프트웨어를 구성하는 요소를 파일 시스템에 바이너리로 만든 것→ 데이터 기준으로 구성 요소를 나눈다면? → 데이터와 데이터를 변경하는 코드로 구성됨코드를 바이너리로 모아 놓은 것을 text 섹션이라고 부름
- → 변수 = 데이터, 변수의 값을 변경하는 로직 = 코드
- 소프트웨어를 구성하는 요소는 기준에 따라 여러 가지로 나눌 수 있음.
- 실행 파일이란?
- .code 32는 명령어 크기가 32비트라는 뜻
- .global 은 C 언어 지시자인 extern과 같은 역할
→ vector_start, vector_end의 주소 정보를 외부 파일에서 심벌로 읽을 수 있게 설정하는 것
- vector_start는 vector_start라는 레이블을 선언한 것
- MOV R0, R1은 별 의미없는 코드 → R1의 값을 R0에 넣으라는 것(R0, R1은 레지스터)
- vector_end는 vector_end라는 레이블을 선언한 것
- .end → text 섹션이 끝났다는 것
위 코드에서 중요한 것은 메모리 주소입니다. 확인해 보겠습니다.
//터미널
$arm-none-eabi-as -march=armv7-a -mcpu=cortex-a8 -o Entry.o ./Entry.S
$arm-none-eabi-objcopy -O binary Entry.o Entry.bin
$hexdump Entry.bin
명령어 설명
실행 파일 만들기
QEMU가 펌웨어 파일을 읽어서 부팅하려면 입력으로 지정한 펌웨어 바이너리 파일이 ELF 파일 형식이어야 합니다.
- ELF 파일: 리눅스 표준 실행 파일 형식 -> Entry.o도 ELF파일 -> 그래서 바이너리를 뽑아내기 위해 arm-none-eabi-objcopy를 이용한 것
ELF 파일을 만들려면 링커가 있어야 합니다. 이는 실행 파일을 만드는 과정과 유사합니다.
일단 링커 스크립트 코드를 만들어 보겠습니다.
// 최초로 작성한 링커 스크립트 navilos.ld
ENTRY(vector_start)
SECTIONS
{
. = 0x0;
.text :
{
*(vector_start)
*(.text .rodata)
}
.data :
{
*(.data)
}
.bxs :
{
*(.bss)
}
}
코드 설명
- ENTRY 지시어는 시작 위치의 심벌을 지정함
- SECTIONS 지시어는 3~20번째 줄까지의 섹션 배치 설정 정보를 가지고 있는 것이라고 알려주는 것
- . =0x0;은 첫 번재 섹션이 메모리 주소 0x000000000에 위치한다는 것을 알려주는 것
- .text는 text 섹션의 배치 순서를 지정함
추가 정보를 입력하면 배치 메모리 주소까지 지정할 수 있음
→ 현 시점에선 필요없으니 생략
추가 정보가 없으면 링커는 시작 주소부터 순서대로 섹션 데이터를 배치함
- 0x00000000에는 리셋 벡터가 있어야 하므로 vector_start 심벌을 먼저 입력하고, .text를 적었음
- 이어서 data 섹션과 bss 섹션을 연속되도록 설정하였음
이제 이 링커를 가지고 실행 파일을 만들어 보겠습니다.
//터미널
//실행 파일 만들기 -> navilos.axf 파일이 생성됨
$arm-none-eabi-ld -n -T ./navilos.ld -nostlib -o navilos -o navilos.axf boot/Entry.o
//어떻게 생겼는지 출력해보자
$arm-none-eabi-objdump -D navilos.axf
//-n 옵션은 링커에 섹션의 정렬을 자동으로 맞추지 않도록 함
//-T 옵션은 링커 스크립트의 파일명을 알려주는 옵션
//-nostdlib 명령은 링커가 자동으로 표준 라이브러리를 링킹하지 못하도록 지시하는 옵션임
vector_start가 0x00000000에 잘 배치된 것을 볼 수 있습니다.
디스어셈블한 결과를 보면 Entry.S에서 작성한 코드가 잘 담겨있습니다.
기계어 자체는 0xE1A00001입니다.
QEMU 실행
ELF 파일 포맷의 실행 파일은 만들었으나 실행은 안될 것입니다.
$ ./navilos.axf
bash: ./navilos.axf: cannot execute binary file: Exec format error
에러의 이유는 리눅스 커널에서 동작하지 않는 섹션 배치로 만들어졌기 때문입니다.
실행하는 방법은 두 가지입니다.
1. 실제 ARM 개발보드에 다운로드해서 확인하기
2. QEMU로 실행하기
당연하게도 QEMU로 실행하겠습니다.
$ qemu-system-arm -M realview-pb-a8 -kernel navilos.axf -S -gdb tcp::1234,ipv4
// -M 옵션으로 머신을 지정한다 → realview-pb-a8로 머신을 설정했음
// -kernel 옵션으로 ELF 파일 이름을 지정함
// -S 옵션은 QEMU가 동작하자마자 바로 일시정지(suspend)되도록 지정하는 옵션임
// -gdb tcp::1234, ipv4는 gdb와 연결하는 소켓 포트를 지정하는 옵션임
// -S 와 -gdb는 디버깅하기 위해 사용하는 것임
화면에 나오는 것이 없기 때문에 어떤 게 실행되는지 모릅니다. gdb를 통해 메모리를 확인해봐야 합니다.
우선 gdb를 설치하고, QEMU와 연결해야 합니다.
기존 터미널을 두고, 새로운 터미널에서 해야 합니다.
// 설치 확인
$ gdb-multiarch
// 설치
$ sudo apt install gdb-multiarch
// gdb와 QEMU를 연결하는 명령
$ gdb-multiarch navilos.axf
//메모리 출력
// gdb 프롬포트가 나오면 target remote:1234를 입력하자
// 1234 포트로 원격 디버깅을 연결하겠다는 것이다.
(gdb) target remote:1234
// 출력
// 메모리 출력 명령어 -> 1바이트씩 4바이트를 출력함
(gdb) x/4b 0
0x0 <vector_start>: 0x01 0x00 0xa0 0xe1
출력값을 모아서 표현하면 0xE1A00001입니다.
→ navilos.axf 파일에 코드 데이터가 QEMU 메모리에 제대로 다운로드되어있음
위 명령어를 통해 우리 만든 펌웨어 바이너리가 QEMU에서 잘 작동하는 것을 확인할 수 있습니다.
빌드 자동화
//최초로 작성한 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))
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)
$(LDrm ) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS)
$(OC) -O binary $(navilos) $(navilos_bin)
build/%.o: boot/%.S
mkdir -p $(shell dirname $@)
$(AS) -march=$(ARCH) -mcpu=$(MCPU) -g -o $@ $<
설명
$ make all
$ make debug
이제 위 두 명령어만 사용하여 빌드와 디버그 하면 됩니다.
하드웨어 정보 읽어오기 - 데이터 시트 읽는 방법
하드웨어에서 정보를 어떻게 읽기, 쓰기를 할까?
레지스터를 이용합니다.
- 레지스터 : 하드웨어가 소프트웨어와 상호작용하는 인터페이스
어떤 하드웨어를 제어하려면 해당 하드웨어의 레지스터 사용법을 알아야 합니다. 이는 데이터 시트를 통해 확인해야 합니다.
하드웨어에서 정보를 읽어오는 간단한 작업을 해보겠습니다.
//하드웨어에서 정보를 읽어오는 코드 Entry.S
.text
.code 32
.global vector_start
.global vector_end
vector_start:
LDR R0, =0x10000000
LDR R1, [R0]
vector_end:
.space 1024, 0
.end
설명
데이터 시트에서 레지스터 주소 0x10000000을 찾으면 SYS_ID 레지스터가 나옵니다. 이는 하드웨어를 식별할 수 있는 정보를 가진 읽기 전용의 레지스터입니다. 펌웨어가 여러 하드웨어를 제어할 때 주로 사용됩니다.
위처럼 데이터 시트에 설명은 되어 있으나 어떻게 사용해야 하는지 예제는 없습니다.
-> 제조사에서 예제를 제공하는 애플리케이션 노트를 제공하기도 합니다.
위 표를 보면 상수로 기본값을 정의한 항목들이 있습니다. → HBI, ARCH
SYS_ID를 제대로 읽었으면, 결괏값에 0x178과 0x5가 포함되어야 합니다.
빌드하고 gdb를 통해 확인해 보겠습니다.
$ make
$ make debug
$ make gdb
//gdb와 연결
(gdb) target remote:1234
(gdb) file build/navilos.axf
//디버깅 심벌을 제대로 읽었는지 확인
//Entry.S의 작성한 코드가 출력되어야 함
(gdb) list
//아직까진 QEMU에서 코드가 한줄도 실행되지 않음
// -> 레지스터에 아무런 정보가 없어야 함
// 아래 명령어로 확인해보자
(gdb) info register
// 여기엔 레지스터가 0으로 초기화되어 있어야 함
// 소스코드 한 줄 실행하는 명령 step의 축약어
(gdb) s
// 다시 확인
(gdb) info register
// 여기엔 r0가 0x100000000이 저장되어 있을것임
(gdb) s
// 축약 명령
(gdb) i r
// 여기엔 r0에 저장된 0x10000000에 저장된 값을 r1에 넣었으니
// r1엔 0x1780500이 저장되어 있어야 함
여기까지 확인하면 펌웨어와 QEMU가 제대로 동작하는지 확인할 수 있습니다.
Entry.S 파일 내용이 그대로 나옵니다.
→ 심벌 정보는 제대로 로딩되었음
현시점 QEMU는 한 줄도 실행되지 않아야 합니다. → 레지스터엔 아무런 정보가 없어야 함
확인해 보니 레지스터 값이 모두 0입니다. 한 줄 실행 후 다시 확인하겠습니다.
예상대로 R0에 0x10000000이 저장되어 있습니다. 이제 다음 줄을 실행하면 ARM 코어는 메모리 주소 0x10000000에서 값을 읽어 R1에 넣을 것이고, 그 값이 0x178을 포함하는지 확인하면 됩니다.
(i r은 info register의 축약어, s는 step의 축약어)
데이터 시트에서 설명하는 값과 일치함을 확인할 수 있습니다.
요약
이번 챕터에서 하드웨어가 제대로 동작하는지 확인해 보려고 하드웨어의 레지스터에 접근해 봤고, 결과를 확인하려고 gdb도 써 봤습니다. 앞으로 작업하는 모든 내용은 본질적으로 이번 챕터에서 한 일을 반복입니다. 코드를 만들고 하드웨어 레지스터에서 정보를 읽거나 값을 써서 하드웨어를 제어하고, 동작을 확인하기 위해 디버거(gdb)를 계속 사용할 것입니다.
참고
저자 이만우님 깃허브 https://github.com/navilera/Navilos
RealViewPB 데이터시트 https://developer.arm.com/documentation/dui0417/d/?lang=en
'임베디드' 카테고리의 다른 글
[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.4 (3) | 2024.10.31 |
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.0-ch.2 (0) | 2024.10.19 |