개발 입문

안태욱·2023년 1월 6일
0
post-thumbnail

1. 하드웨어 정보 조사하기

임베디드 소프트웨어는 하드웨어와 상호작용하는 데 목적이 있다. 레지스터를 통해 하드웨어에 데이터를 쓰거나 읽어올 수 있는데, 임베디드 개발자는 target 시스템의 레지스터 사용법을 알고 있어야 한다. 이러한 정보는 해당 하드웨어의 데이터시트에 기록되어 있다.

데이터시트

해당 하드웨어가 가지고 있는 레지스터의 목록과 그에 대한 설명, 그리고 레지스터에 어떤 값을 썼을 때 하드웨어가 어떻게 동작하는지를 적어 놓은 문서이다.

해당 시리즈에서는 realview-pb-a8 을 target 시스템으로 사용하고 있는데, ARM사에서 운영하는 ARM 인포센터에서 해당 데이터시트를 찾을 수 있다.

잠시 데이터시트를 살펴보면, 0x1000 0000 - 0x1000 0FFF는 System register라고 설명하고 있는 것을 확인할 수 있다.

System register들 중 0x0000의 offset을 가지는 레지스터를 확인하면 SYS_ID라는 이름을 가진 레지스터임을 확인할 수 있다.SYS_ID는 System Identifier의 약자로 하드웨어를 식별할 수 있는 정보를 가진 레지스터이다. 이는 읽기 전용의 레지스터로, 여러 하드웨어에 같은 펌웨어를 사용할 때 현재 동작하고 있는 하드웨어가 무엇인지 구분하기 위해 사용한다.

조금 더 뒤로 넘어가보면 해당 레지스터의 구조와 각 비트들이 무엇을 의미하는지 확인할 수 있는데, HBI와 ARCH 필드에는 초기값이 설정되어 있음을 확인할 수 있다.


이처럼 데이터시트에는 해당 하드웨어에 대한 설명들이 나와 있으므로 임베디드 소프트웨어 개발자는 자신이 개발하고 있는 target 시스템의 데이터시트를 항상 찾아보면서 개발을 진행해야 한다.


2. 소스 작성

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

작업 디렉터리 하위에 boot 디렉터리를 생성하고 Entry.S 파일을 작성하였다.

  • .text ~ .end : 내부의 코드는 text 섹션에 들어갈 코드
  • .code 32 : 명령어의 크기가 32
  • .global : C 언어 지시어인 extern 역할로, 선언한 vector_xxx 주소 정보를 외부 파일에서 심벌로 읽을 수 있게 해줌

vector_start:

  • R0 레지스터에 0x 1000 0000 저장
  • R0에 저장된 주소로부터 값을 읽어 R1 레지스터에 저장

vector_end:

  • 해당 위치부터 1024 바이트를 0으로 채움

위 데이터시트를 참고한 내용에 따르면 0x1000 0000 주소는 SYS_ID 레지스터에 해당하는 주소였다. 즉 위의 vector_start의 코드는 SYS_ID 레지스터에 저장된 값을 읽어서 R1 레지스터에 저장하는 코드라고 볼 수 있다.

어셈블러로 컴파일 해보기

$ 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
  • arm-none-eabi-as : 어셈블리어 소스 파일을 컴파일
  • arm-none-eabi-objcopy
    • -O binary : 바이너리만 카피

이렇게 생성한 파일의 바이너리 내용을 확인해보면 다음과 같다.

링커 스크립트


ENTRY(vector_start)

SECTIONS {

    . = 0x0;

    .text : {
        *(vector_start)
        *(.text .rodata)
    }

    .data : {
        *(.data)
    }

    .bss : {
        *(.bss)
    }


}

작업 디렉터리 최상단에 navilos.ld 파일을 작성하였다.

QEMU가 펌웨어 파일을 읽어서 부팅하려면 입력으로 지정된 펌웨어 바이너리 파일이 ELF 파일 형식이어야 한다. 링커를 통해서 여러 목적 파일들을 묶어 실행파일을 만들 수 있다. 이 때 목적 파일 또한 ELF 파일인데, 각 섹션들을 메모리의 어느 주소에 위치시킬지 지정하기 위해 링커 스크립트를 작성한다. 보통 윈도우나 리눅스용 어플리케이션을 작성할 때는 운영체제 내부에 포함된 링커 스크립트를 사용하는데, 펌웨어를 개발할 때는 하드웨어 환경에 맞춰줘야 하기 때문에 이렇게 섹션 배치를 세세하게 조정하곤 한다.

  • ENTRY() : 시작 위치의 심벌을 지정

text 섹션을 0x00에 위치시켰는데, 그 중 vector_start 심벌을 제일 먼저 배치하였다.


3. 실행 파일 만들기

$ arm-none-eabi-ld -n -T ./navilos.ld -o navilos.axf boot/Entry.o
  • -n : 링커에 섹션의 정렬을 자동으로 맞추지 말라고 지시하는 옵션
  • -T : 링커 스크립트의 파일명을 알려주는 옵션
  • -nostdlib : 링커가 자동으로 표준 라이브러리를 링킹하지 못하도록 지시하는 옵션

위 명령을 실행하고 나면 navilos.axf 파일이 생성된다.

axf 파일

arm executable format 즉 .axf는 elf의 변종 중 하나이다. .axf는 elf 헤더, .text, .data, .bss 에서는 elf와 동일한 규격을 사용하지만 .debug는 DWARF2.0 규격을 따른다.

DWARF2.0

elf 포맷에는 debug section이 있는데, 이 영역에 어떤 형식으로 debug 정보를 넣을 것인가에 대한 여러가지 규칙이 존재한다. ARM의 경우는 DWARF 방식을 사용하며 이를 통해 ICD로 디버깅을 진행할 수 있다.

ICD

In-Circuit-Debugging의 약자로, 하드웨어 레벨의 실기간 디버깅을 가능하게 하여주는 강력하고 효과적인 mcu 디버깅 툴이다. 타켓 mcu가 실제 회로에 장착되어 있어도 프로그램을 실행하고 멈추는 등 라인단위로 실행을 할 수 있게한다. 대표적으로는 Trace32 디버거가 있다.


$ arm-none-eabi-objdump -D navilos.axf

위 명령을 통해 axf 파일을 디스어셈블하여 내부를 확인할 수 있다.

결과를 확인해보면 vector_start가 링커 스크립트에서 작성한대로 메모리 주소 0x0에 배치된 것을 확인할 수 있다. 또한 Entry.bin에서 확인한 0201 e3a0 1000 e590 명령어를 디스어셈블한 결과를 확인할 수 있다.


QEMU에서 실행하기

위에서 생성한 axf 파일을 호스트 PC에서 실행하면 위와 같은 오류 메시지가 출력된다. 이는 호스트 PC의 아키텍처에 맞는 컴파일러가 아닌 target 보드에서 실행하기 위해 크로스컴파일 했기 때문이다. QEMU를 사용하면 navilos.axf 파일을 실행할 수 있다.

  • -M : 머신 지정
  • -kernel : 실행할 ELF 파일 이름 지정
  • -S : 동작하자 마자 바로 QEMU를 정지시키는 옵션
  • -gdb tcp:1234,ipv4 : 디버거와 연결하는 소켓 포트를 지정하는 옵션

위 명령만으로는 작성한 프로그램이 잘 다운로드 되어 실행하고 있는지 알 수 없다. 따라서 별도의 디버깅을 통해 메모리를 확인해봐야 한다.


메모리 확인해보기

gdb-multiarch 명령을 실행한 후 target remote:1234를 입력한다. 이는 1234번 포트를 통해 원격 디버깅을 연결하기 위함이다.

  • x/4x : 메모리 출력 명령

위 명령을 통해 0x0의 주소를 확인해보면 axf 파일을 디스어셈블하여 확인했었던 명령을 볼 수 있다. 이는 vector_start에 작성한 코드에 대한 명령어였는데, 이를 통해 작성한 프로그램이 QEMU에 잘 다운로드 되었음을 알 수 있다.


Makefile



# RealViewPB archtecture & CPU info
ARCH = armv7-a
MCPU = cortex-a8

# cross compiler command
# tool chain
CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-ld
OC = arm-none-eabi-objcopy

LINKER_SCRIPT = ./navilos.ld

# assembly source & object 
ASM_SRCS = $(wildcard boot/*.S)
ASM_OBJS = $(patsubst boot/%.S, build/%.o, $(ASM_SRCS))

# final target
navilos = build/navilos.axf
navilos_bin = build/navilos.bin

.PHONY: all clean run debug gdb

all: $(navilos)

clean:
	@rm -fr build

# qemu execute commad
run: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos)

# gdb-multiarch
debug: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4

# arm-none-eabi-gdb
gdb:
	gdb-multiarch

# link
$(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 $@)
	$(AS) -march=$(ARCH) -mcpu=$(MCPU) -g -o $@ $<

지금까지 navilos.axf 파일을 얻기 위해서 arm-none-eabi-as로 어셈블리어 파일을 컴파일하고 arm-none-eabi-ld를 통해 링킹을 진행하였다. 만약 어셈블리어 파일이 추가되거나 기존의 파일이 변경된다면 이러한 과정을 계속해서 반복해야 한다. 이를 좀 더 간편하게 하기 위해서 Makefile을 사용하여 빌드를 자동화 할 수 있다.

  • tool chain : 크로스 컴파일에 관여하는 여러 유틸리티들
  • makefile의 빌트인 함수를 사용
    • $(wildcard boot/* .S) : boot 디렉터리에서 확장자가 .S인 파일들을 모두 ASM_SRCS 변수에 넣는 코드
    • $(patsubst boot/%.S, build/%.o, $(ASM_SRCS)) : ASM_SRCS에 있는 파일들의 이름에서 확장자를 .o로 바꾸어 build 디렉터리 하위에 넣고, ASM_OBJS 변수에 넣는 코드
  • all : 최종 생성할 타깃을 지정
  • run : QEMU 실행 명렁
  • debug : QEMU 디버깅 모드로 실행 명령
  • gdb : gdb-multiarch 실행 명령
  • $(navilos): $(ASM_OBJS) $(LINKER_SCRIPT) : 링커로 navilos.axf 파일을 생성하는 명령
    • 추가적으로 navilos.bin 도 함께 생성
  • build/%.o: boot/%.S : .S 파일을 .o 파일로 컴파일하는 명령

이후에는 make gdb, make run 등 간단한 명령을 통해 빌드를 진행할 수 있다.



4. 디버깅 해보기

$ make
$ make debug

Makefile을 작성하였기 때문에 위 두가지 명령어로 빌드 및 QEMU를 디버깅 모드로 실행할 수 있다.

$ make gdb
(gdb) target remote:1234

gdb-multiarch를 실행하고 난 이후에는 마찬가지로 target remote:1234로 연결하여 디버깅을 진행할 수 있다.

  • file : ELF 파일에 포함되어 있는 디버깅 심벌을 읽는 명령
    • 컴파일 시 -g 옵션을 넣어서 디버깅 심벌을 실행 파일에 포함시켜줘야 함

list 명령을 입력하면 Entry.S 파일의 내용이 그대로 출력되는 것을 확인할 수 있다.

  • info register : 레지스터들 상태를 출력

현재 QEMU는 -S 옵션을 주어 실행했기 때문에 동작하자마자 코드를 실행하지 않고 정지된 상태이다. 때문에 r0 레지스터에는 값이 들어있지 않아야하는데, 실제로 레지스터 상태를 출력해보면 0이 들어있는 모습을 확인할 수 있다.

부가적으로 sp(stack pointer)나 pc(program counter)를 살펴보면 마찬가지로 0이 들어있음을 알 수 있다.

  • s : 소스 코드를 한 줄 실행 (step의 단축 명령)

s를 통해 vector_start의 첫 번째 명령인 LDR R0, =0x10000000를 실행한 이후 다시 레지스터의 상태를 확인하였다. 이번에는 r0에 0x1000 0000이 들어가 있는 모습을 확인할 수 있다.

  • i r : info register의 단축 명령

vector_start의 두 번째 명령인 LDR R1, [R0]을 실행한 후의 레지스터 상태이다. 이는 곧 SYS_ID 레지스터의 값을 R1 레지스터에 복사한 것으로 해석할 수 있다.

데이터시트에서 SYS_ID 레지스터의 HBI, ARCH 필드는 각각 0x178, 0x5의 초기값을 가졌는데, 이 값들을 위 디버깅 결과에서 동일하게 확인할 수 있다.



5. 요약

해당 포스팅에서는 하드웨어 정보를 조사하기 위해 데이터시트를 읽고, 이를 활용하여 어셈블리어로 코드를 작성하였다. 또한 이를 빌드하고 디버깅을 통해 동작을 확인하였다. 이러한 과정들은 임베디드 소프트웨어 개발의 큰 흐름이라고 볼 수 있다. 이후의 개발에서도 이와 마찬가지로 데이터시트를 읽고, 레지스터에 값을 읽고 쓰는 코드를 작성하며, 빌드 및 디버깅하며 동작을 확인하는 과정을 반복한다.

profile
책 읽는 개발자

0개의 댓글