임베디드 OS 개발 프로젝트를 읽고 작성하였습니다.
시작하기 전에
코딩 들어간다..!!
3장 일단 시작하기
3.1 리셋 벡터(reset vector)
: 메모리 주소 0x00000000
ARM 코어에 전원이 들어가면 ARM 코어가 가장 먼저 메모리 주소 0x00000000에서 32bit를 읽어서 명령을 바로 실행 실행함
우리가 할 일 ?
메모리 주소 0x00000000에 명령어를 넣어 주는 것
(1) boot 디렉터리 만들기
(2) Entry.S 작성
스켈레톤(skeleton) 코드 (= 아무 의미 없는 코드)
.text #.end가 나올 때까지 모든 코드가 text 섹션
.code 32 #명령어의 크기가 32비트
.global vector_start #C 언어 지시어인 extern과 같은 일
.global vector_end #주소 정보를 외부 파일에서 심벌로 읽을 수 있게 설정
vector_start: #레이블 선언
MOV R0, R1 #R1의 값을 R0에 넣어라
vecotr_end: #레이블 선언
.space 1024, 0 #해당 위치부터 1024바이트를 0으로 채우라는 명령
.end #text 섹션이 끝났음을 알리는 지시어
< text 섹션 >
실행 파일 : 소프트웨어를 구성하는 요소를 파일 시스템에 바이너리로 만든 것
소프트웨어를 구성하는 요소
데이터를 기준 : 데이터(변수)와 데이터를 변경하는 코드(변수의 값을 변경하는 로직)
이 코드를 바이너리로 변경해서 모아 놓은 것을 실행파일에서 text 섹션이라고 함
(3) Entry.S 파일 컴파일
코드 3.2 해석은 더보기 클릭!
arm-none-eabi-as -march=armv7-a -mcpu=cortex-a8 -o Entry.o ./Entry.S | |
arm-none-eabi-as | 어셈블리어 소스 파일 컴파일, ARM용 어셈블러 |
-march=armv7-a | 아키텍처를 armv7-a로 설정 |
-mcpu=cortex-a8 | cpu를 cortex-a8로 성공 |
-o | 오브젝트 파일을 만든다 |
Entry.o | 컴파일에 성공하면 만들어지는 파일 |
./Entry.S | 컴파일하는 파일 |
* RealViewPB가 사용하는 ARM 코어가 cortex-a8
arm-none-eabi-objcopy -O binary Entry.o Entry.bin | |
arm-none-eabi-objcopy | 바이너리만 뽑아내는 명령 |
-O binary | 심벌 정보 등이 포함되어 있는 바이너리만 뽑아내기 위한 옵션 |
Entry.o | 오브젝트 파일 (=GCC로 컴파일할 때 -c 옵션으로 만드는 오브젝트 파일) |
Entry.bin | 컴파일에 성공하면 만들어지는 파일 |
hexdump Entry.bin | |
hexdump | 바이너리 내용 확인 |
Entry.bin | 확인할 대상 파일 |
000000 0001 e1a0 0000 0000 0000 0000 0000 0000 | 0001 e1a0 => MOV R0, R1 |
000010 0000 0000 0000 0000 0000 0000 0000 0000 |
|
* | 반복되는 값을 표시하지 않고 앞의 값(0)이 계속 나온다는 의미 |
0000400 0000 0000 | ARM은 4바이트 단위로 메모리 주소를 관리, 0000404의 앞 주소 |
0000404 | 마지막 주소 |
3.2 실행 파일 만들기
ELF 파일
: 여러 실행 파일 형식 중에서 대표적인 형식으로 리눅스의 표준 실행 파일 형식
QEMU가 펌웨어 파일을 읽어서 부팅하려면 입력으로 지정한 펌웨어 바이너리 파일이 ELF 파일 형식이어야 함
ex) 코드 3.2 Entry.o 파일
arm-none-eabi-as로 생성한 Entry.o 파일 => ELF 파일
바이너리만 뽑아내기 위해 arm-none-eabi-objcopy를 사용한 것
링커
여러 오브젝트 파일을 묶어서(linking) 하나의 실행 파일로 만드는 프로그램
ELF 파일을 만들려면 링커(Linker)의 도움이 필요함
오브젝트 파일을 만들고 그 오브젝트 파일들을 묶어서 실행 파일 하나를 만드는 것은 윈도우나 리눅스의 실행파일을 만드는 과정과 완전히 똑같다.
링커 스크립트
링커가 동작하기 위해 링커에 정보를 던져 주는 파일
보통 윈도우나 리눅스용 애플리케이션을 만들 때는 링커에 신경을 쓰지 않음
(사용하는 운영체제에 맞는 링커 스크립트가 해당 운영체제의 라이브러리에 기본값으로 포함되어 있기 때문)
but 펌웨어를 개발할 때는 해당 펌웨어가 동작하는 하드웨어 환경에 맞춰서 펌웨어의 섹션 배치를 세세하게 조정해야 할 일이 많이 있음
-> ⭐ 링커 스크립트로 링커의 동작을 제어하여 원하는 형태의 ELF 파일을 생성 ⭐
가장 간단한 형태의 링커 스크립트
소스코드 디렉터리 트리의 최상위 디렉터리에 저장 (boot에 저장했다 Desktop로 이동함)
ENTRY(vector_start) #시작 위치의 심벌 지정
SECTIONS #3~20번째 줄까지의 블록이 섹션 배치 설정 정보를 가지고 있는 것이라고 알려주는 것
{
. = 0x0; #첫 번째 섹션이 메모리 주소 0x00000000에 위치함
.text : #text 섹션의 배치 순서 지정(추가 정보를 입력하면 배치 메모리 주소까지 지정할 수 있음)
{
*(vector_start) #메모리 주소 0x00000000에 리셋 벡터가 위치해야 하므로 vector_start 심벌이 먼저 나옴
*(.text .rodata) #이어서 .text 섹션
}
.data : #data 섹션 메모리에 배치
{
*(.data)
}
.bss : #bss 섹션 메모리에 배치
{
*(.bss)
}
}
코드 3.2 해석은 더보기 클릭
arm-none-eabi-ld -n -T ./navilos.ld -nostdlib -o navilos.axf boot/Entry.o | |
arm-none-eabi-ld | 실행 파일 만들기 |
-n | 링커에 섹션의 정렬을 자동으로 맞추지 말고 지시하는 옵션 |
-T | 링커 스크립트 파일명을 알려주는 옵션 |
./navilos.ld | 파일 위치 / 링커 스크립트 파일명 |
-nostdlib | 링커가 자동으로 표준 라이브러리를 링킹하지 못하도록 지시하는 옵션 |
-o | |
navilos.axf | 링커가 동작을 완료하면 생성되는 파일 |
boot/Entry.o |
arm-none-eabi-objdump -D navilos.axf | |
arm-none-eabi-objdump -D | 디스어셈블(disassemble)해서 내부가 어떻게 되어 있는지 출력 |
navilos.axf | 대상 파일 |
navilos.axf: file format elf32-littlearn
00000000 <vector_start>: #vector_start가 메모리 주소 0x00000000에 잘 배치되어 있음
0: e1a00001 mov r0,r1 #디스어셈블한 명령가 mov r0, r1, 기계어로 0xE1A00001임
00000004 <vector_end>:
...
코드 3.1에서 작성한 코드가 디스어셈블한 결과로 잘 나온 것을 확인 가능
3.3 QEMU에서 실행해 보기
실행 파일을 만드는 데 성공!!
BUT 실행이 안 됨
ELF 파일 포맷으로 만든 실행 파일이지만, 리눅스 커널에서 동작하지 않는 섹션 배치로 만들어져 있기 때문
게다가 리눅스용 라이브러리가 하나도 없음
실행 방법
(1) ARM 개발 보드에 다운로드해서 동작을 확인
(2) QEMU로 실행
QEMU로 실행
코드 3.5 해석은 더보기 클릭
qemu-system-arm -M realview-pb-a8 -kernel navilos.axf -S -gdb tcp::1234,ipv4 | |
qemu-system-arm | 실행 파일 만들기 |
-M | 머신 지정 |
realview-pb-a8 | 지정한 머신 이름 |
-kernel | ELF 파일 이름 지정 |
navilos.axf | ELF 파일 이름 |
-S | stop 상태에서 gdb를 실행시키는 옵션 |
-gdb | gdb와 연결해서 디버깅을 하려고 사용하는 옵션 |
tcp::1234,ipv4 |
현재 ubuntu에서는 gdb-arm-none-eabi가 검색되지 않는다!
그래서 gdb-multiarch를 설치해서 사용하였다. 리눅스 버전 18.04 이후로는 gdb-multiarch로 통합되었음
target remote:1234 => 1234번 포트로 원격 디버깅을 연결하겠다는 명령
1234번 포트는 코드 3.5에서 QEMU를 실행할 때 지정했던 원격 디버깅 포트임
메모리 출력 명령(x/4x)으로 0x00000000 메모리 주소에서 4바이트를 출력
결과 : 0x01 0x00 0xa0 0xe1임 (한 바이트씩 출력한 값)
이 값을 4바이트로 묶어서 표현하면 0xE1A00001 코드3.4에 나오는 값과 일치함
이 말은 navilos.axf 파일에 있는 코드 데이터가 QEMU의 메모리로 제대로 다운로드가 되었다는 뜻
📢추가 - QEMU로 실행하기📢
QEMU : 하드웨어 가상머신 / Emulator
터미널 1 -> QEMU를 생성하여 서버를 열기
터미널 2 -> gdb를 이용해서 그 서버에 접속
📢오류📢
메모리 출력 명령(x/4x)으로 0x00000000 메모리 주소에서 4바이트를 출력
결과 : 0x01 0x00 0xa0 0xe1임 (한 바이트씩 출력한 값)
=> gdb에서 x 명령어로 바이트를 출력하기 위해서는 x/b를 사용해야 함. 즉 4바이트 출력 => x/4b
x/4x는 4개의 16진수 출력!!
* 현재까지의 소스 코드 트리
3.4 빌드 자동화하기
Makefile 만들기
navilos.axf 파일을 얻기 위해서1. arm-none-eabi-as로 어셈블리어 파일을 컴파일2. arm-none-eabi-ld로 링킹
if 어셈블리어 파일이 하나 더 생기면
1. arm-none-eabi-as로 어셈블리어 파일을 컴파일 한 번 더
2. arm-none-eabi-ld에 입력 파일명 추가 후 링킹
매우 매우 귀찮은 작업 => Makefile을 만들어 빌드 자동화!
코드 3.7 해석은 더보기 클릭
툴 체인(tool chain) : 크로스 컴파일에 관여하는 여러 유틸리티들을 묶어 부름 (4~7번째 줄)
ARCH = armv7-a #RealViewPB의 아키텍처 정보, 39번째 줄에서 사용
MCPU = cortex-a8 #CPU 정보, 39번째 줄에서 사용
CC = arm-none-eabi-gcc #4~7 크로스 컴파일러 실행 파일의 이름
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 #최종 목표, ELF 파일 이름
navilos_bin = build/navilos.bin #최종 목표, 바이너리 파일 이름
.PHONY: all clean run debug gdb
all: $(navilos)
clean:
@rm -fr build
run: $(navilos) #QEMU를 실행하는 명령
qemu-system-arm -M realview-pb-a8 -kernel $(navilos)
debug: $(navilos) #QEMU와 gdb를 연결할 때 사용하는 명령
qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4
gdb: #gdb로 간결하게 처리, arm-none-eabi-gdb 대신 gdb-multiarch 사용
gdb-multiarch
$(navilos): $(ASM_OBJS) $(LINKER_SCRIPT) #링커로 navilos.axf 파일을 생성하는 명령(+navilos.bin)
$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS)
$(OC) -O binary $(navilos) $(navilos_bin)
build/%.o: boot/%.S #자동으로 *.S 파일을 *.o 파일로 컴파일하는 명령
mkdir -p $(shell dirname $@)
$(AS) -march=$(ARCH) -mcpu=$(MCPU) -g -o $@ $< #-g 옵션 : 디버깅 심벌을 실행 파일에 포함
* 11~12번째 줄 해석
어셈블리어 소스 파일 목록과 오브젝트 파일 목록, make의 빌트인(built-in) 함수
코드 | 해석 |
ASM_SRCS = $(wildcard boot/*.S) | boot 디렉터리에서 확장자가 S인 파일 이름을 모두 ASM_SRCS 변수에 값으로 넣으라는 의미 |
ASM_OBJS = $(patsubst boot/%.S, build/%.o, $(ASM_SRCS)) | boot 디렉터리에서 확장자가 S인 파일 이름을 찾아서 확장자를 o로 바꾼 다음 디렉터리도 build로 바꿔 ASM_OBJS 변수에 값으로 넣으라는 의미 |
ASM_SRCS는 boot/Entry.S를 저장하고 있음
ASM_OBJS는 build/Entry.O를 저장하고 있음
make 명령 실행
* 현재까지의 소스 코드 트리
3.5 하드웨어 정보 읽어오기 - 데이터시트를 읽는 방법
레지스터
: 하드웨어가 소프트웨어와 상호작용하는 인터페이스
펌웨어를 개발할 때는 하드웨어를 더 많이, 자세히 알아야 함
하드웨어와 상호작용한다는 것 => 쉽게 말해서 하드웨어에서 정보를 읽어오고 하드웨어에 정보를 쓰는 작업을 한다
어떻게 하드웨어에서 정보를 읽어오고 쓰는걸까?
⭐레지스터라는 것을 이용⭐
데이터시트
해당 하드웨어가 가지고 있는 레지스터의 목록과 설명 그리고 레지스터에 어떤 값을 썼을 때 하드웨어가 어떻게 동작하는지를 적어 놓은 문서
펌웨어 개발자가 어떤 하드웨어를 제어하는 펌웨어를 작성할 때는 그 하드웨어의 레지스터 사용법을 알아야 함
=> 하드웨어의 레지스터 사용법은 데이터시트에 나와 있음
하드웨어 정보 읽기
코드 3.8 해석은 더보기 클릭
.text
.code 32
.global vector_start
.global vector_end
vector_start:
LDR R0, =0x10000000 #R0에 0x10000000이라는 숫자를 넣으라는 것
LDR R1, [R0] #R0에 저장된 메모리 주소(0x10000000)에서 값을 읽어서 R1에 넣으라는 것
vector_end:
.space 1024, 0
.end
메모리 주소 0x10000000에는 어떤 값이 있음? -> RealViewPB의 데이터시트를 봐야 함
<RealViewPB의 데이터시트>
레지스터 주소 0x10000000 : ID Register (하드웨어를 식별할 수 있는 정보를 가진 레지스터, 읽기 전용)
첫 번째 실행
R0에 0x10000000이 저장되어 있는 것을 확인
두 번째 실행
R1에 0x1780500이라는 값이 들어 있음
3.6 요약
이번 장에서는 하드웨어에서 제대로 동작하는지 확인해 봤으며,
하드웨어의 레지스터에 접근해 봤으며 결과를 확인하려고 gdb도 사용해 봤다.
의도한 대로 동작하지 않거나 동작하는 것을 확인하기 위해서 디버거(gdb)를 사용하는 일련의 과정을 계속 반복하는 것이 펌웨어 개발 작업!
'개발 > 임베디드 os 개발 프로젝트' 카테고리의 다른 글
5장 UART (3) | 2023.06.15 |
---|---|
임베디드 OS 개발 프로젝트 부록 A.1장 (0) | 2023.06.04 |
4장 부팅하기 (0) | 2023.06.04 |
2장 개발 환경 구성하기 (0) | 2023.04.16 |
1장 임베디드 운영체제 (0) | 2023.03.28 |