LoginSignup
2
4

簡単なベアメタル ARM プログラムはどうやって作るのか調べてみた

Last updated at Posted at 2020-07-02

はじめに

以前、QEMU 上で Raspberry pi 用のベアメタルなバイナリを動作させてみたのですが、そのコードをほとんど理解していなかったので、今回はそのコードを理解してみようと思います。 基本的には、前回動かしたコードを理解するために必要な部分だけお勉強します。

動かしたソースコード

今回調べたことは、「QEMUでRaspberry Pi 3のUARTをベアメタルで動かす - Qiita」 で紹介されているコードに関する挙動です。 ソースコードは以下の URL から見れると思います。 https://qiita.com/eggman/items/045bf5525fcf78209b79#%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89

起動時の処理

英語ですが、ここに懇切丁寧に起動時の処理が書かれています。

https://wiki.osdev.org/ARM_RaspberryPi_Tutorial_C#Testing_your_operating_system_.28Real_Hardware.29

必要なファイルは以下の3つ

  • bootcode.bin: 最初に GPU に読み込まれて実行されるバイナリ。なお、 Raspberry pi 4 では ROM に入っているので、SD カードに入れる必要はないらしい。
  • fixup.dat: ハードウェア関連の情報が入っているファイル。起動のためには必須。
  • start.elf: Raspberry Pi のファームウェアのイメージ (PC でいうところの BIOS 相当)。これは GPU で実行されるらしい。

Raspberry Pi は最初に起動するのが CPU ではなく GPU らしく、ARM CPU は起動直後は hlt 状態だそうです。

起動の流れは以下の通りです。

  1. Raspberry Pi の電源投入。
  2. GPU が bootcode.bin を実行
  3. GPU で start.elf を実行 (ここで config.txt と cmdline.txt を読み込む)
  4. GPU から CPU がキックされる
  5. CPU で カーネルイメージ (kernel*.img) を実行

カーネルイメージとカーネルイメージの起動時ついても、先ほどと同じ OSDev のページに書いてます。 https://wiki.osdev.org/ARM_RaspberryPi_Tutorial_C#Pi_3.2C_4

  • カーネルイメージ(kernel.img など)ファイルとその他必要なファイルを Bootable な FAT ファイルシステム内に置くと起動できる。 Raspberry pi の起動で必要なファイルについてはこちらを参考 –> https://www.raspberrypi.org/documentation/configuration/boot_folder.md
  • カーネルイメージのファイル名を kernel8.img というイメージを 64bit モードで起動, kernel7.img という名前のイメージだと 32 bit モードを起動になる。
    • 尚、kernel8.img と kernel7.img の両方をファイルシステム内の置くとどうなるのかはよくわからない。
  • エントリーポイント 64bit モードで起動する際は 0x80000 (先頭から 512 KiB), 32 bit モードの際は 0x8000 (先頭から 32 KiB)。
  • Pi 3 と Pi 4 では上記起動に関する仕様は同じ。ただし、周辺機器に関しては MMIO ベースアドレスなどが異なる。
  • 最新のファームウェアでは、起動時には Core 0 のみで動作、その他のコアはスピンループで待機
    • ちなみに、その他のコアを起こすには、 0xE0 (core 1), 0xE8 (core 2) or 0xF0 (core 3) にエントリーポイントになるコードのアドレスを書き込んで何かイベントを起こせば良さそう。 ただイベントに関しては少し複雑そうだったので、また今度調べようと思います。

ちなみに QEMU はこの仕様通りではなく、いきなり *.elf から実行できるみたいですが、これについては今後暇があれば調べて見ます。

ELF フォーマット

参考 ページ:

ELF ファイルを作るためには最低限、以下のセクションを定義する必要があるようです。

  • .text: コードを置くセクション
  • .rodata: Read only な定数や文字列を置くセクション
  • .data: 初期値を持つ変数などを置くセクション
  • .bss: 初期値を持たない変数を置くセクション

Linker Script

参考ページ

先述の ELF フォーマットになるように、リンカスクリプトでバイナリファイル内のセクションの定義などをする必要があります。 簡単なアプリケーションをコンパイルする時にはリンカスクリプトなんか書きませんが、これはデフォルトのセクション定義が利用されるからのようです。 しかし今回は 0x80000 番地から実行を開始するという制約があり、デフォルトのセクション定義では困るので自分でセクション定義を書く必要があります。

私はそもそもリンカースクリプトのリの字もわかっていなかったので、今回リンカスクリプトの基本的なことを調べてみました。 ただし前回使ったリンカースクリプトに関係する部分のみ触れて、それ以外のことには基本的には触れません。

セクション名とシンボル

アセンブラ内において、セクションとシンボル名は以下のように現れます。

  • セクション名はアセンブラ内で .section .text.boot のように参照されるもの
  • シンボルは変数名や関数名

シンボルの参照について

リンカスクリプト内では、プログラムで定義されているシンボルの先頭に _ を付けて参照します。

  • 例: アセンブラ内 _start –> リンカスクリプト内 __start

リンカスクリプトの文法

以降では、リンカスクリプトの予約語や文法について調べたことを書いてみます。

  • ENTRY – ENTRY ポイントのシンボル

    • コンパイラの最適化などでエントリーポイントが消されないようにするために指定する。
    • ここで指定したからといって、エントリーポイントのアドレスが良しなに設定されるわけではない。
    • エントリーポイントのアドレスは, SECTIONS で別途設定する必要がある。
    • ここで指定するシンボル名はプログラム内で使われるもの? (= 先頭に _ を付けない)
  • SECTIONS {…} – Section の定義を行うコードブロック

    • {...} 内にセクションの定義を記述します。
  • <セクション名> : { …} – セクションの定義

    • SECTIONS ブロック内に書くことで、 <セクション名> で指定されているセクションを定義します。
  • ドット(.) – ロケーションカウンタ

    • 現在のアドレスを保持する。
    • . を参照すると絶対アドレスが返ってくる。
    • 一方 section の定義内で . に値を代入するときは、相対アドレス (section の先頭アドレスからのオフセット) を指定する必要がある。
    • オブジェクトを配置すると、その分勝手にインクリメントされる。
  • オブジェクトファイル名(セクション名) (アスタリスク(*)も可) – オブジェクトを配置

    • オブジェクトファイル内で定義されたセクションの中身を当該位置に配置するという意味
    • * はワイルドカードであり、オブジェクトファイルとセクション名それぞれに使える
    • *(.text.boot) は任意のオブジェクトファイルにある .text.boot という名前のセクションにあるオブジェクトを置くという意味。
    • 実際に boot.S 内に .text.boot セクションが定義されており、このセクションコードが明示的に .text セクションの先頭 (=0x80000) に置かれるようにリンカスクリプトが書かれている。
  • KEEP()

    • リンカによるリンクの省略を避けるために使う。
    • C 言語で言う volatile のようなイメージ?
  • ALIGN(n)

    • ロケーションカウンタ(?)の値をnで割り切れる値までインクリメントする(アライメントする)。

ARM アセンブラ

レジスタとリテラルについて

  • 参考ページ(32bit arm): https://qiita.com/edo_m18/items/a7c747c5bed600dca977

  • 参考ページ(64bit arm): https://www.mztn.org/dragon/arm6403reg.html

  • 32bit arm では 16 個の 32bit 汎用レジスタ(r0-r15)、64bit arm では31個の 64bit 汎用レジスタ(x0-x30) がある。

    • 64bit arm での w0-w31 は対応する番号の汎用レジスタ(x0-x31)の下位 32 bit へアクセスできる (rax と eax のような関係) 。
  • sp はスタックポインタを保持

    • 32bit arm では r13 レジスタと共有
    • 64bit arm では専用のレジスタ
  • pc はプログラムカウンタ (x86 でいう eip や rip)。

    • 32bit arm では r15 レジスタと共有
    • 64bit arm では専用のレジスタ
  • lr はリンクレジスタで、リターンアドレスを保持する。bl 命令などで値が入れられる?

    • 32 bit arm では r14 レジスタと共有
    • 64 bit arm では x30 レジスタと共有
  • リテラルは #1234 みたいな感じで表られる。

分岐命令について

arm では Intel でいう Jump を無条件分岐と呼ぶらしく、これを b から始まる命令 (Branch の頭文字) で行う。

  • bl: Branch Link, ジャンプ元の次のアドレス (リターンアドレス)をリンクレジスタに保持。 RET 命令で戻れる。
    • x86 でいう call 命令に近い立ち位置
    • x86 と違い、リターンアドレスはスタックではなくレジスタに格納される。
    • x86 と違い、ハードウェアによるスタックの操作はない

スタックについて

参考ページ: ARMアセンブリでLチカ http://idken.net/posts/2016-12-25-arm_asm1/

  • スタックの扱い方が x86 とは違い、ハードウェアによるスタックの操作はないらしく、ソフトウェア的にスタックを実現することになる。
    • (ソフトウェアによるスタックの操作や関数の呼び出し規約はいつか調べたい)
  • 下記のルールでソフトウェアスタックを実現することが一般的らしい
    • スタックは下位アドレスから上位アドレスに伸びる (=アドレスの値が小さくなる方向に伸びる)
    • スタックポインタは スタックの先頭を指している (スタックの先頭の値が入っているアドレスをさしており、次に値を入れる場所を指しているわけではない)
  • 今回のアセンブラコードでは、 0x80000 から上位アドレスに向かって伸びるスタックを配置するようになっている。

Raspberry Pi の起動時のレジスタ初期値

UART

C 言語のコードの説明に入る前に、Raspberry Pi 4 における UART の仕様について簡単に説明します。

データシートなどの情報について

ハードウェアを制御するソフトウェアを記述するうえで、ハードウェアのデータシートは必要不可欠です。 というわけで、その在りかをまず押さえましょう。

BCM 2711 における UART のレジスタの場所

ソフトウェアから UART へアクセスするには、物理メモリ空間上にマップされたレジスタである MMIO レジスタ(MMIO については後でもう少し説明します)に対してメモリアクセスをするとよいです。 なので、これらのレジスタが物理アドレス上のどの番地にあるかを知る必要があります。 結論から言うと, 0xfe201000 番地から始まる領域に MMIO レジスタが配置されています。 たあ、これを先ほど示したデータシートにどのように書かれているのか、少し見てみましょう。

  • BCM 2711 のデータシートには 0x7e201000 番地に UART のレジスタがマップされている (MMIO レジスタ、詳細は口述)と書かれている。
  • ただし、このアドレスは GPU からみたアドレス である。
  • ARM CPU から見えるアドレス上では、GPU 上から見える 0x7c0000000x80000000 の範囲は 0xfc0000000xff800000 にマップされている。
    • BCM 2711 のデータシートの Figure 1 に書いている
    • (なんかマップ先の方が範囲狭いんだけどいいのか?)
  • このため、ARM CPU からは 0xfe201000 番地に UART のレジスタがマップされているように見える。

UART のレジスタについて

今回コード内で使っている UART のレジスタは下記の二つのみなので、これらのみ説明を書きます。

  • DR: Data register
    • 下位 8 bit がデータ送受信用のフィールド
    • 送信したいデータはこの 8 bit に書き込み、受信したデータを受け取りたい場合は、この 8 bit から読み込む
    • そのほかにも、転送エラーを示すビットがあるが、今回は使っていない
  • FR: Flag register
    • 4 bit 目が RXFE (Receive FIFO empty) で、受信用の FIFO もしくはレジスタが空であるか否かを示すフラグ
    • 5 bit 目が TXFF (Transmit FIFO Full) で、送信用の FIFO もしくはレジスタが満杯であるか否かを示すフラグ

C 言語のコード

いよいよ C 言語のコードに入っていきます。

メモリのアドレスについて

コードを読む前に確認しておくべきこととして、メモリアドレスの種類があります。 普通 OS 上でアプリケーションが動作する場合、アドレスはプロセスの仮想アドレスを使います。 仮想アドレスはプロセスごとに独立したアドレス空間上のアドレスであり、仮想アドレスは MMU によって物理アドレスに変換されます。 しかし、 今回は MMU が有効になっていない環境で動作するプログラムですので、 C のコードで扱うアドレスは物理アドレスになります。 これ MMIO レジスタへアクセスするコードを書く際に知っておく必要があります。

MMIO

MMIO (Memory mapped I/O) とは決められた物理アドレス上にデバイスのレジスタがマッピングされており、そのアドレスに read/write すると物理メモリではなくデバイスのレジスタに対しての read/write になるというものです。 先述の UART では, 0xFE201000 からオフセット 0x0 の位置に DR レジスタが、オフセット 0x18 の位置に FR レジスタがあります。 今回は、先述の通り MMU が有効になっておらず C 言語上で扱うメモリアドレスは仮想アドレスではなく物理アドレスであるため、以下のように直接物理アドレスを指定してメモリにアクセスすれば、レジスタへアクセスできます。

/* dr_value and fr_value are uint_32 variable */

/*Read from FR register*/
fr_value = *(uint_32*)(0xFE201000 + 0x18);

/*Write to DR register*/
*(uint_32*)(0xFE201000 + 0x0) = dr_value;

あまり見慣れないコードかもしれませんが、ポインタ変数をアドレスの定数に置き換えたと思えば理解しやすいかもしれないです。

残りの C 言語のコードは特に変哲もないコード(個人の感想です)だと思うので特に解説はないです。

終わりに

随分と長くなりましたが、とりあえずこれで Raspberry pi 上でベアメタルなコードが起動して UART で通信する仕組みが理解できたような気がします。

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4