2020년 7월 1일 수요일

ARM Bare-Metal Programming - 1편

이번 blog post에서는 ARM(32bit) 용 bare metal programming(1편)에 관한 내용을 소개하고자 한다. Bare metal programming을 위한 target board로는 QEMU(processor emulator/가상 환경)을 활용(추후 STM32F103 Nucleo-64 보드를 대상으로 할 예정)하기로 하겠다.


<목차>
1. 개발 환경 준비
   - ARM GCC toolchain 설치
   - QEMU 설치
   - 간단한 예제 돌려 보기
2. Assembly Programming 기초
3. Linker & Linker Script
4. 리셋 벡터, Exception Handling 그리고 C code 진입하기
5. References


"Bringing up an ARM embedded system from scratch"


1. 개발 환경 준비
이 장에서는 bare metal programming을 위한 기본 단계로, ARM cross toolchain과 QEMU(processor emulator)를 설치한 후, (맛뵈기 차원에서) 간단한 assembly program를 하나 만들어 돌려 보도록 하겠다. 아래에서 설명하는 모든 작업은 Ubuntu 18.04 desktop에서 실시하였다.

sudo apt install gcc-arm-none-eabi
[Tip] ARM용 raw 실행 파일을 만들기 위해서는 arm-none-eabi-gcc가 설치되어야 한다.

arm-none-eabi-gcc --version
arm-none-eabi-gcc (15:6.3.1+svn253039-1build1) 6.3.1 20170620
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

sudo apt install qemu-system-arm

qemu-system-arm --version
QEMU emulator version 2.11.1(Debian 1:2.11+dfsg-1ubuntu7.28)
Copyright (c) 2003-2017 Fabrice Bellard and the QEMU Project developers

qemu-system-arm -M ?
  => qemu가 지원하는 machine의 목록을 출력해 준다. 이 중 적당한 machine을 하나 선택하도록 하자.

[그림 1.1] QEMU에서 지원하는 machine 종류 출력

다음으로 (아래 site 내용을 참조하여) arm-none-eabi-gdb를 설치해 보자.
https://acassis.wordpress.com/2018/12/27/adding-arm-none-eabi-gdb-to-ubuntu-18-04/

$ sudo dpkg -i ./libreadline6_6.3-8ubuntu2_amd64.deb 
Selecting previously unselected package libreadline6:amd64.
(데이터베이스 읽는중 ...현재 318299개의 파일과 디렉터리가 설치되어 있습니다.)
Preparing to unpack .../libreadline6_6.3-8ubuntu2_amd64.deb ...
Unpacking libreadline6:amd64 (6.3-8ubuntu2) ...
libreadline6:amd64 (6.3-8ubuntu2) 설정하는 중입니다 ...
Processing triggers for libc-bin (2.27-3ubuntu1) ...

$ sudo dpkg -i ./gdb-arm-none-eabi_7.10-1ubuntu3+9_amd64.deb 
Selecting previously unselected package gdb-arm-none-eabi.
(데이터베이스 읽는중 ...현재 318311개의 파일과 디렉터리가 설치되어 있습니다.)
Preparing to unpack .../gdb-arm-none-eabi_7.10-1ubuntu3+9_amd64.deb ...
Unpacking gdb-arm-none-eabi (7.10-1ubuntu3+9) ...
gdb-arm-none-eabi (7.10-1ubuntu3+9) 설정하는 중입니다 ...
Processing triggers for man-db (2.8.3-2ubuntu0.1) ...

[Tip] 위의 방식이 마음에 안들면 arm 개발 site에서 보다 최신의 arm toolchain을 내려 받아 설치하는 방법도 있다. 자세한 사항은 필자의 아래 blog 2장 a 절 내용을 참조하기 바란다.

Toolchain과 QEMU가 준비되었으니, 간단한 assembly program(덧셈 예제)을 하나 만들어 돌려 보도록 하자.

$ vi add.S
  => assembly code는 확장자로 .S 혹은 .s를 사용한다.
  => 아래 코드는 r0 register에 숫자 5를 r1 register에 숫자 4를 대입하고, 둘을 더해 r2 register에 저장한 후, b stop 명령을 계속 실행하면서 무한 loop에 빠지는 program이다.
  => .text는 assembler directive라고 하며, 이하의 명령이 code section에 포함됨을 알려 준다. 아래 코드에는 없으나 변수 영역(data section)을 표시하는 .data라는 directive도 있다.

[그림 1.2] add.S 파일

<여기서 잠깐 ~>
assembly code는 보통 "label:      instruction    @ comment" 형태로 구성된다.

label    메모리 상에서의 명령어(instruction)의 위치(번지)를 표시함.
instruction    ARM instruction(32 bit or 16 bit) 이나 assembler directive가 올 수 있다. assembler directive는 assembler에게 알려주는 명령에 해당하며 반드시 .(점)으로 시작한다.
@comment    글자그대로 주석이다.
------

arm-none-eabi-as -o add.o add.S
  => assembly code compile(이 경우는 compile 대신 assemble 한다고 함)은 as(assembler)를 이용한다.
  => 결과 파일은 add.o 임.

$ arm-none-eabi-ld -Ttext=0x0 -o add.elf add.o
  => 실행 가능한 파일을 생성하기 위해서는 ld(linker)를 사용해야 한다.
  => -Ttext=0x0 은 label(위의 경우는 start)의 주소를 지정하는 역할을 한다. 즉 start label은 0x0 번지에서 시작한다는 뜻이다.
   => 위 명령 실행 결과, add.elf라는 실행 파일이 만들어지게 된다.

[그림 1.3] arm-none-eabi-nm add.elf 실행 모습
[Tip] ELF(Executable and Linkable Format)실행, 오브젝트 파일, 공유 라이브러리, 또는 코어 덤프를 할 수 있게 하는 바이너리 파일(UNIX/Linux 실행 파일 format)을 말한다.

[그림 1.4] ELF 파일 형식
--------

qemu-system-arm -M realview-pb-a8 -kernel ./add.elf -S -gdb tcp::1234,ipv4
  => qemu(processor emulator)를 돌려 보자.
  => machine으로 realview-pb-a8를 선택하자.
  => -kernel option으로 add.elf를 지정하자.
  => -S option은 QEMU가 시작하자마자 일시 정지(suspend) 시키는 역할을 한다.
  => -gdb tcp::1234,ipv4는 외부에서 gdb로 연결(tcp 1234 번 port로 연결)해 올 수 있도록 해준다(내부에 gdbserver가 포함되어 있음)
[Tip] machine type은 다른 것을 선택해도 무방하나, 참고문헌 [2]를 기초로 realview-pb-a8을 일단 선택해 보았다.
[Tip] tcp port 1234는 다른 값으로 지정해도 된다.

[그림 1.5] qemu-system-arm 실행 모습

$ arm-none-eabi-gdb
  => 다른 터미널 창에서 arm 용 gdb를 실행하자.

arm-none-eabi-gdb <=======> gdbserver @ QEMU(add.elf 실행 대기 중)

(gdb) target remote:1234
  => 앞서 실행한 qemu 내의 gdbserver와 연결한다.
(gdb) c
  => 코드(add.elf)를 실행한다.
  => add.elf는 무한 loop에 빠지도록 되어 있으므로 Ctrl-C로 강제 중단하자.
(gdb) info registers
  => 현재 register 정보를 확인해 본다.
  => r0, r1, r2 register 내용을 보면, 각각 0x5, 0x4, 0x9로 add.S code에서 의도한 대로 내용이 저장되어 있다.
(gdb) x/4x 0
  => 0x00000000 메모리 번지 부터 4개의 item(4byte씩 4개)를 출력한다.

[그림 1.6] qemu-system-arm 실행 모습

(gdb) x/4iw 0x0
  => i option을 사용하면 memory에 올라와 있는 instruction을 disassemble할 수도 있다.
   0x0: mov r0, #5
   0x4: mov r1, #4
   0x8: add r2, r1, r0
=> 0xc: b 0xc


이상으로 간단한 arm toolchain & QEMU 설치 후, assembly program을 하나 작성하고 QEMU 상에서 돌려보는 과정을 소개해 보았다. 이어지는 장에서는 assembly program 작성 방법을 좀 더 깊이있게 소개(2장) 보고, linker와 linker script의 역할(3장)에 대해 소개해 보도록 하겠다.


2. Assembly Programming (정말) 기초
이번 장에서는 2개의 assembly program 예제를 가지고, Assembly Programming의 가장 기초적인 부분을 짚고 넘어가 보기로 하겠다.

a) Array의 합을 계산하는 program
코드를 바로 제시하면 다음과 같다. 우측의 주석이 잘 정리되어 있으므로 코드 흐름을 이해하는 것은 그리 어렵지 않다. 아래 program을 실행하고 나면 최종적으로 r3 register에 55가 저장되어야 한다.

[그림 2.1] Array의 합을 계산하는 program - sum-data.S

Program build에 앞서 .byte.align assembler directive의 의미를 먼저 알아보도록 하자.

<.byte directive>
1 byte(8 bits) 크기의 값들을 메모리에 연속으로 배치하고자 할 때 사용한다(byte array로 이해하면 됨). 이와 유사한 표현으로 .2byte, .4byte 등이 있는데, 각각이 의미하는 바는 각각 16 bits, 32bits array로 이해하면 된다. .byte 뒤에 기술하는 argument의 형식으로는 다음과 같은 것들이 올 수 있다.

pattern:  .byte 0b01010101, 0b00110011, 0b00001111
npattern: .byte npattern - pattern
halpha:   .byte 'A', 'B', 'C', 'D', 'E', 'F'
dummy:    .4byte 0xDEADBEEF
nalpha:   .byte 'Z' - 'A' + 1

<.align directive>
.align directive는 4 bytes(32bits) 단위로 ARM 명령어를 배치하기 위해 사용된다. 이는 instruction만 죽 나열되어 있는 상황에서는 불필요하며, instruction 중간 중간에 data bytes나 half words(16 bites)가 선언되어 있을 경우에만 의미가 있다. 4bytes 단위로 align하기 위해 필요한 경우에 padding bytes가 삽입되게 된다.

이제 code를 실행해 보자.
$ vi sum.ld
  => 먼저, 아래와 같이 간단한 linker script(의미는 3장에서 자세히 설명)를 하나 만든다.

[그림 2.2] 간단한 linker script - sum.ld

arm-none-eabi-as -o sum-data.o sum-data.S
  => assemble하여 sum-data.o object 파일을 생성한다.

arm-none-eabi-ld -T sum.ld -o sum-data.axf ./sum-data.o
  => ld 명령으로 sum-data.axf(ELF/DWARF 파일)을 생성해 낸다.
  => -T option은 linker script를 지정할 때 사용한다.

$ qemu-system-arm -M realview-pb-a8 -kernel ./sum-data.axf -S -gdb tcp::1234,ipv4
  => qemu를 사용하여 실행 파일을 구동시킨다.

다른 창(terminal)에서 arm-none-eabi-gdb를 실행해 보면 다음과 같다. 반독되는 내용이므로 구체적인 설명은 생략한다. 다만, (info register 명령 실행 결과) r3 register에 55가 저장된 것은 눈여겨 볼 부분이다.

[그림 2.3] sum-data.axf에 대해 arm-none-eabi-gdb 실행 모습

b) String의 길이를 구하는 program
역시 코드를 바로 살펴 보도록 하자. 코드 우측의 주석을 보면 알 수 있듯이 "Hello World" string의 각 byte를 하나씩 체크하면서 nul(= 0)이 나올때까지 반복하는 program이다. 물론 program의 이름에 걸맞게  loop를 돌면서 1씩 값을 증가시키고 이를 r1 register에 저장한다. Program 끝 부분에서 r1 -=1을  하는 이유는 맨 마지막 문자(nul 값)을 제외시켜야 하기 때문이다. 따라서 최종적으로 r1 register의 값이 11이면 정답이다.

[그림 2.4] string length를 계산하는 program - strlen.S

이 program에서는 두가지의 assembler directive 즉,  .asciz.equ가 눈에 띈다.

<.asciz directive>
.asciz는 string literal을 정의하기 위해 사용된다. 이와 비슷한 것으로 .ascii가 있는데, 둘간의 차이는 string 맨 끝에 null charactor(= 0)가 있고 없고이다. 즉, 이름에서 유추할 수 있듯이 .asciz는 string 맨 끝에 0이 추가되고, .ascii는 추가되지 않는다.

<.equ directive>
Assembler는 symbol table이라는 것을 관리하는데, .equ는 symbol table에 항목을 추가할 때 사용한다. 즉, 위의 예에서 ".equ  nul, 0"이 하는 일은 symbol table에 nul이라는 이름을 추가하고, 그 값이 0이라는 것을 알려준다.

그럼, 이제부터 코드를 돌려 보도록 하자. 앞서와 반복되는 내용이니 같은 설명을 반복하지는 않겠다.
$ arm-none-eabi-as -o strlen.o strlen.S
$ arm-none-eabi-ld -T strlen.ld -o strlen.axf strlen.o
$ qemu-system-arm -M realview-pb-a8 -kernel ./strlen.axf -gdb tcp::5678,ipv4
  => 이번에는 gdbserver port를 5678로 변경해 보았다.
$ arm-none-eabi-gdb
  => r1 register 값이 0xb(= 11)로 제대로 들어갔음을 알 수 있다.

[그림 2.5] string length를 계산하는 program 실행 모습

이상으로 2개의 assembly code를 예로 들어, arm assembly programming의 기초 사항을 정리해 보았다. 다음 장에서는 실행 program 생성에 중대한 역할을 하는 linker에 대해 알아 보도록 하자.

<여기서 잠깐 !>
  => ARM Assembly Programming에 대해 보다 심도있는 내용을 원하는 분은 아래 site를 참조하기 바란다.

-------------


3. Linker & Linker Script
이번 장에서는 linker와 linker script에 관해서 소개하고자 한다.

a) Linker가 하는일
먼저 linker는 아래 그림에서와 같이 여러개의 object 파일을 하나로 묶어 실행 가능한 파일(ELF)을 생성하는 역할을 한다. 이 과정에서 symbol resolution(A)relocation(B)이라는 두가지 일이 진행되게 된다.
[Tip] ld는 link editor or loader를 의미한다.

[그림 3.1] linker의 역할

Linker의 역할을 구체적으로 설명하기 위해 main.S와 sub.S로 구성된 예를 하나 들어 보자. 이 두개의 program이 하는 일은 아주 간단하다. 즉, main.S에 정의되어 있는 byte array(10, 20, 25)의 값을 더해서 그 결과를 r3 register에 저장하는 것이다. 단, 더하는 routine은 main.S가 아니라 sub.S에 정의되어 있어, main.S에서 sub.S의 (일종의)함수를 호출하는 코드라고 볼 수 있다.

[그림 3.2] main.S 코드

[그림 3.3] sub.S 코드

<.global directive>
Assembly 언어에서 label은 모두 외부에서 사용 불가하도록 되어있다(C 언어로 생각해 본다면 static 함수 선언로 선언된 경우는 외부에서 호출 불가하다는 내용과 유사하다고 볼 수 있음). 따라서 이를 외부에서 사용 가능하게 하려면 해당 label을 .global로 선언해 주어야 한다. 위의 예에서는 ".global sum" 이라고 선언되어 있으므로, main.S에서 해당 label을 사용(호출)할 수 있는 것이다.

이 상태에서 두개의 파일을 build(assemble)한 후, nm 명령으로 symbol table의 상태를 확인해 보도록 하자.

arm-none-eabi-as -o main.o main.S
arm-none-eabi-as -o sub.o sub.S

[그림 3.4] nm 명령으로 main.o와 sub.o의 symbol table 확인

위의  nm 명령 결과 중, t는 symbol이 정의되어 있음을 뜻하고, u는 정의되어 있지 않음을 뜻한다. 또한 TU 처럼 대문자로 표기된 경우는 해당 symbol이 .global로 선언되어 있을 때이다. 내용을 보니 main.o에서 sum label은 역시나 U로 표기되어 있다.

그렇다면, 위의 두 파일을 linker로 연결해 보면 어떻게 될까 ?
$ arm-none-eabi-ld -Ttext=0x0 -o sum.axf main.o sub.o

arm-none-eabi-nm sum.axf
  => 얘상대로  sum 앞에 T가 보인다.

[그림 3.5] nm 명령으로 sum.axf의 symbol table 확인

지금까지 설명한 과정을 symbol resolution(A)이라고 한다.

그렇다면 relocation(B)이란 무엇일까 ?
아래와 같이 -Ttext option을 주어 text section의 시작을 0x0 => 0x100으로 변경한 상태에서 linking을 해보면, 아래와 같이 모든 symbol의 주소(예: 00000120 T sum)가 그에 맞게 조절됨을 알 수 있는데, 이것이 relocation이다.

[그림 3.6] nm 명령으로 sum.axf의 symbol table 확인
[Tip] 내용이 너무 장황해 질 듯하여, 아주 간단하게 설명하였으나, relocation을 위해서는 section merging(서로 다른 파일에 흩어져 있는 동일 section을 하나로 통합)과 section placement(앞서 설명한 시작 주소 변경에 따른 위치 조정)라는 과정이 동반된다.

b) Linker script
앞서 언급한 대로 linker는 section merging과 section placement 절차를 수행한다. 이때 어는 section이 merge(통합)되고, 어느 위치가 메모리에 재배치되는지에 관해서는 linker script라는 파일을 통해 세세히 기술해 줄 수가 있다. 즉, linker는 linker script의 내용을 토대로 section merging & placement 과정을 진행하게 된다.


[그림 3.7] linker script 구조 설명(1)

❶ SECTIONS 명령은 section이 어떻게 merge되고 어느 위치에 배치되는지를 기술한다.
❷ . = 0x00000000 은 location counter를 의미하며 첫번째 section(여기서는 text section)이 메모리 주소 0x00000000에 위치한다는 것을 알려 준다.
❸ ❹ 입력 파일인 abc.o와 def.o에 있는 text section은 output file의 text section으로 들어감을 의미한다.

위의 linker script는 아래와 같이 *를 사용하여 단순화해서 표현할 수도 있다(보통 이렇게 많이 쓴다).

[그림 3.8] linker script 구조 설명(2)

아래 예는 .text section과 .data section을 모두 포함한 경우의 linker script 예제이다. .text section은 종전과 같이 0x0에 위치하고 있지만, .data section은 0x400 부터 배치됨을 보여준다.

[그림 3.9] linker script 구조 설명(3)

위의 linker script를 사용하는 예제 program 하나 테스트해 보자. 앞서 [그림 2.1]과 동일한 예제코드이다.

[그림 3.10] Array의 합을 계산하는 program - sample2.S

$ arm-none-eabi-as -o sample2.o sample2.S
$ arm-none-eabi-ld -T sample2.ld -o sample2.axf sample2.o

nm 명령으로 symbol table 내용을 확인해 보면, text section과 data section의 memory 주소가 linker script에 지정한 값대로 출력됨을 알 수 있다.

[그림 3.11] arm-none-eabi-nm -n ./sample2.axf 실행 결과

이상으로 linker와 linker script의 역할에 대해 간략히 살펴 보았다. 다음 장에서는 지금까지 설명한 내용을 기초로 본격적인 bare metal programming에 관한 설명에 돌입하고자 한다.


4. 리셋 벡터, Exception Handling 그리고 C Code 진입하기
지금까지 1 ~ 3장을 통해 bare metal programming을 위한 준비 사항을 정리해 보았다. 그렇다면 이제 부터는 본격적인 bare metal programming에 들어가 보도록 하자. 이절에서 소개하는 내용은 아래 책을 기초로 하였음을 밝힌다. 

[그림 4.1] 임베디드 OS 개발 프로젝트 책(인사이트 이만우 저)

a) Reset vector와 Exception vector
ARM core에 전원이 인가되면 ARM core가 가장 먼저 하는 일은 Reset vector(0x00000000 번지)를 실행하는 것이다.

[그림 4.2] ARMv7 Processor Mode [참고문헌 5]


[그림 4.3] ARM Register Sets [참고문헌 5]


[그림 4.4] ARM Current Program Status Register [참고문헌 5]

[그림 4.5] ARM Vector Table [참고문헌 5]


[그림 4.6] ARM Exception Handing [참고문헌 4]


[그림 4.7] ARM Memory Layout [참고문헌 4]


b) 메모리 map 구성 및 스택 만들고, C main 함수 호출하기

[그림 4.8] Entry.S 파일 - reset handler 수행 후, C main( )  함수 호출
 

이번 장은 (시간이 없어) 정리를 하다가 중간에 중단을 하였다. 보다 상세한 사항은 참고 문헌 [2]를 참조해 주시기 바라며, 추후 다시 정리할 수 있는 기회가 있기를 희망해 본다. 😓


5. References
[1] http://www.bravegnu.org/gnu-eprog/index.html
  => 1~ 3장의 기초 자료로 사용함.

[2] 임베디드 OS 개발 프로젝트, 이만우, 인사이트
  => 4장의 기초 자료로 사용함.
  => RTOS의 동작 원리 파악에 도움이 되는 Good book

[3] Embedded programming with Android: Bringing Up and Android System from Scratch, Roger YE, Addison-Wesley

[4] ARM System Developer’s Guide - Designing and Optimizing System Software, Andrew N. Sloss, Dominic Symes, Chris Wright, Morgan Kaufmann Publishers
   => ARM architecture 소개(too old)

   => ARMv7 architecture 소개

  => ARM  Assembly Programming 상세 소개




Slowboot

댓글 없음:

댓글 쓰기