[RTOS] 3장: 리셋 벡터, 링커 스크립트, Makefile로 빌드 자동화

리셋 벡터

.text @ .end가 나올 때까지 text 섹션
	.code 32 @ 명령어의 크기는 32비트
	
	.global vector_start @ C의 extern과 같은 심벌. 외부파일에서 이 주소정보를 심벌로 읽을 수 있음 
	.global vector_end

	vector_start: @ 레이블 선언
		MOV R0, R1 @ 의미없는 명령어
	vector_end: @ 레이블 선언
		.space 1024, 0 @ 해당 위치부터 1024바이트 만큼을 0으로 채우기
.end @ text 섹션 종료
@ arm-none-eabi-as -march=armv7-a -mcpu=cortex-a8 -o Entry.o ./Entry.S
@ 어셈블리어를 컴파일하여 오브젝트 파일(ELF) 생성

@ arm-none-eabi-objcopy -O binary Entry.o Entry.bin
@ 심벌정보 등을 제외하고, 바이너리만 카피

@ hexdump Entry.bin
@ 바이너리내용을 brute하게 16진법으로 확인


0001 e1a0 는 MOV R0, R1이다.
그 뒤 쭉 0이 나오는데,
* 표시는 앞의 값(0)이 계속 반복된다는 뜻.
404로 끝나는데, 0x00000400까지 계속 0이라는 뜻.(ARM은 4바이트 단위로 주소를 관리한다)
0x400 == 0d1024이므로 이는 맞다.


ELF 파일

QEMU 가 펌웨어를 부팅하려면 펌웨어 바이너리 파일이 ELF 형식이어야 한다.
ELF는 여러 실행파일형식 중 하나인데, 리눅스의 표준 실행파일형식이기도 하다.

우리가 arm-none-eabi-as 로 생성한 .o 파일도 ELF 파일이다.
이 중, 심벌정보 등등을 제외하고 바이너리만 뽑아내기 위해 arm-none-eabi-objcopy 를 사용했다.

바이너리만 뽑아낸다고? 모든 파일은 결국 이진수아닌가? 여기선 기계어화된 code만 따로 '바이너리' 라고 명칭하는거겠지?

어쨌든, ELF 파일을 만들기 위해서는 링커가 필요한데
링커는 여러 .o 파일을 묶어서 하나의 실행파일로 만드는 프로그램이다.
링커가 제대로 동작하기 위해, 링커스크립트가 필요하다. 링커에 정보를 던져주는 파일이다.

보통 윈도우나 리눅스용 애플리케이션을 만들 때는 링커에 신경을 쓰지 않습니다. 사용하는 OS에 맞는 링커스크립트가 해당 OS의 라이브러리에 기본포함되어있기 때문입니다. 하지만, 펌웨어를 개발할 때는 해당 펌웨어가 동작하는 하드웨어 환경에 맞춰 펌웨어의 섹션배치를 세세히 조정할 일이 많습니다. 그래서 링커스크립트로 링커의 동작을 제어하여 원하는 형태의 ELF 를 생성합니다.

ENTRY(vector_start)
SECTIONS
{
    . = 0x0;


    .text :
    {
        *(vector_start)
        *(.text .rodata)
    }
    .data :
    {
        *(.data)
    }
    .bss :
    {
        *(.bss)
    }
}
 arm-none-eabi-ld -n -T ./navilos.ld -nostdlib -o navilos.axf boot/Entry.o
 => 실행파일 만들기
 -n: 링커에게 섹션의 정렬을 자동으로 맞추지 말라고 지시
 -T: 링커스크립트의 파일명 알려줌
 -nostdlib: 링커가 자동으로 표준라이브러리를 사용하지 못하게 지시
arm-none-eabi-objdump -D navilos.axf

그 결과 axf 파일이 생성됨을 확인할 수 ㅣㅆ고,
arm-none-eabi-objdump -D 명령을 통해 disassemble하여 내부를 출력할 수 있다.

00000000 주소에 vector_start 가 잘 배치되어있고
이는 디스어셈블한 결과 보이는 명령어는 mov r0 r1 으로 알맞다.
참고로, 해당 명령을 기계어로 하면 0xE1A0001이다.


QEMU 실행

./navilos.axf                             [22/02/5 | 10:59:25]
zsh: exec format error: ./navilos.axf

실행안된다.
그 이유는, axf가 ELF는 맞는데 리눅스커널에서 동작하지 않는 섹션배치이기 때문이다.
애초에 리눅스용 라이브러리가 하나도 없다.

그럼 어떻게 실행하느냐?
여기서 QEMU가 나온다.
물론, 실제 ARM 개발보드에 다운로드하여 동작을 확인할 수도 있겠지만.

실패한다.
이는 WSL2에서 Xwindow 지원없이 GUI 프로그램을 돌리려고 할 때 나타난다고 한다(GUI 아닌데..?). 참고링크
링크따라 설정을 하고 난 뒤, 실행하니 아래 창이 나왔다.


어쨌든,
-M: 머신을 지정
-kernel: ELF파일명을 지정
-S: QEMU가 동작하자마자 바로 일시정지(디버깅용)
-gdb tcp::1234,ipv4: gdb와 연결할 소켓포트(디버깅용)

gdb-multiarch를 실행해보자.
책에 나오는 arm-none-eabi-gdb는 지원이 중단된 모양인지 설치가 안되어,
해당 패키지까지 통합된 gdb-multiarch를 썼다.

호기심에 일반 gdb를 실행시켜봤는데, 잘 되지 않았다.

gdb-multiarch가 크로스GDB라고 하는데, 그 기능이 지원이 안된다면 호스트가 x86일 때 x86만 원격으로 가능한 것일까?


빌드 자동화

navilos.axf 파일을 얻기 위해 우리가 해야 할 일은 아래와 같다.
1. arm-none-eabi-as로 어셈블리를 컴파일
2. arm-none-eabi-as로 링킹

ARCH = armv7 # 아키텍처 정보
MCPU = cortex-a8 # cpu 정보 => 둘 다 39번줄에서 사용

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) # make의 빌트인 함수: "boot 디렉터리에서 .S는 전부 ASM_SRCS 변수에 값으로 넣으라", 즉 boot/Entry.S 저장
ASM_OBJS = $(patsubst boot/%.S, build/%.o, $(ASM_SRCS)) # make의 빌트인 함수: "boot 디렉터리에서 .S를 찾아 전부 .o로 바꾸고 디렉터리도 build로 바꿔 ASM_OBJS 변수에 값으로 넣어라", 즉 build/Entry.o 저장

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-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 실행 자동화
	gdb-multiarch

$(navilos): $(ASM_OBJS) $(LINKER_SCRIPT) # 링커로써, axf 파일 생성 및 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 $@ $<


마지막 에러는 xwindow가 켜져있어서 그렇다.

앞으로

make all
make debug

로 테스트를 편하게 할 수 있다.


데이터시트 읽기: Entry.S 수정

하드웨어에서 정보를 읽어오는 코드로 변경하고자 한다.

어떻게 하드웨어에서 정보를 읽어오고 쓰는 걸까요? 바로 레지스터라는 것을 이용합니다. 레지스터는 하드웨어가 소프트웨어와 상호작용하는 인터페이스입니다. 따라서 펌웨어 개발자가 어떤 하드웨어를 제어하는 펌웨어를 작성할 때는, 그 하드웨어의 레지스터 사용법을 알아야 하고 이는 데이터시트에 나와있습니다.

.text @ .end가 나올 때까지 text 섹션
	.code 32 @ 명령어의 크기는 32비트
	
	.global vector_start @ C의 extern과 같은 심벌. 외부파일에서 이 주소정보를 심벌로 읽을 수 있음 
	.global vector_end

	vector_start: @ 레이블 선언
		LDR R0, =0x10000000 @ R0 레지스터에 10000000 넣기
		LDR R1, [R0] @ R0에 담긴 값을 주소로써 해석하여, 해당 주소에 있는 값을 읽어 R1에 넣기: 즉, R1에 10000000 넣기
	vector_end: @ 레이블 선언
		.space 1024, 0 @ 해당 위치부터 1024바이트 만큼을 0으로 채우기
.end @ text 섹션 종료

그럼 10000000 주소가 무엇일까?
REALVIEWPB 데이터시트에서 레지스터주소 0X1000000을 찾으면 ID REGISTER 라고 나온다.

읽기전용이며, 여러 하드웨어에 같은 펌웨어를 쓴다면 펌웨어가 지금 동작하고 있는 하드웨어가 어떤 하드웨어인지를 알아내기 위한 용도이다.

따라서 REALVIEWPB의 ID REGISTER에도 어떤 고윳값이 들어가서 펌웨어가 식별할 수 있도록 해야한다. 이 코드는 그 고윳값을 읽는 것이다.

데이터시트를 보면, [31:0]에서 27:16은 0X178, 11:8은 0X5 가 들어있어야 함을 확인할 수 있다.
GDB로 체크해보자.

gdb-multiarch
target remote:1234
file build/navilos.axf

list
info register

s
info register

s
i r

s로 한 줄 실행시 레지스터 바뀜 확인

s로 한 줄 더 실행시 레지스터 또 바뀜 확인

참고로 명령어가 한 줄 나오는건, 실행된 명령어가 아니라 앞으로 실행할 바로다음 명령어를 의미한다.

데이터시트대로, 0x1780500의 [31:0]에서 27:16은 0X178, 11:8은 0X5 가 들어있어야 함을 확인할 수 있다.

보드 리비전은 Rev A, 버스 아키텍쳐는 AXI라는 정보를 알 수 있다.

참고로, 이런 수준의 예제는 "APPLICATION NOTE" 라고, 데이터시트와는 별도로 불리는데 제조사에서 제공한다면 큰 참고가 된다고 한다.


이로써 처음으로 의미있는 동작을 하는 펌웨어를 만들어 실행했다.
우리는 이 펌웨어로 보드 리비전과 버스 아키텍처 정보를 얻어냈다.

다음엔 부팅을 구현할 것이다.

좋은 웹페이지 즐겨찾기