みねっちょのマイコン関係ブログ

組込開発系フリーソフトやハードの情報発信ブログ

WSL に標準添付の ARM 用クロスコンパイラで OS 無しベアメタルのビルドをする

サイト内 Google 検索:


最終更新: 2021-03-29
本記事では、Windows10 で WSL 上の Ubuntu-20.04 に標準パッケージとしてインストール可能な ARM 用クロスコンパイラを使って、OS 無しベアメタル用バイナリをビルドする手順を紹介します。コンパイラ浮動小数点ハードウェア有り32ビット用の ARMHF を使用しています。尚、クロスコンパイラのインストール手順の詳細や確認方法は、【こちらの別記事】に記載しています。

目次:

前提条件:

  • Windows 10 を使用していること
  • WSL (Windows Subsystem for Linux) の Ubuntu-20.04 がインストール済なこと
  • Ubuntu 上に crossbuild-essential-armhf パッケージがインストール済なこと
    (sudo apt install crossbuild-essential-armhf。詳細な手順は【こちらの別記事】に記載。)

ベアメタルとは:

 ベアメタルとは、ボードに OS の載って居ない状態を指します。OS が載って居ないので、システムコールは使えませんし、printf 等も使えません。OS に標準添付のスタートアップルーチン (crt0.o) も使えません。起動したら、一番最初に実行されるプログラムを作成する事になります。本記事では、スタートアップルーチンには必要最小限な数行の物を自作します。

概略手順:

  1. ベアメタル専用のスタートアップルーチン (いわゆる crt0.o) のアセンブラソースコードを作成します。標準ライブラリのソースコード crt0.s と紛らわしいので、名前は start.s とします。
  2. スタートアップルーチンとメインルーチンは -c オプションを使って別々にアセンブルコンパイルをする必用があります。理由は、C コンパイラが標準ライブラリの crt0.o を読み込まない様にする為です。コンパイル時は、ICE や ソースコードデバッガ用に DWARF2 デバッグ情報付きで出力します。
  3. 別々にアセンブルコンパイル済のオブジェクト (start.o と main.o) をリンカでリンクして、ELF (Executable and Linkable Format) 形式の実行可能ファイルを作成します。
    ICE やデバッガで高級言語レベルのソースコード デバッグを行う場合には、この DWARF2 情報付き実行可能ファイルとコンパイルに使用したソースコードを使用します。
  4. 作成された ELF 形式の実行可能ファイルを C 言語のソースコード付きで逆アセンブルして内容を確認します。
  5. インテルヘキサ (HEX) ファイルを出力します。
    詳細は後述しますが、モトローラSレコード (MOT) ファイルや、Verilog シミュレーション用のファイル、素のバイナリファイル等も出力可能です。

Ubuntu 上で実行するコマンドで書くと次の通りです。

arm-linux-gnueabihf-as start.s -g -gdwarf-2 -o start.o
arm-linux-gnueabihf-gcc main.c -g -gdwarf-2 -marm -Og -c -o main.o
arm-linux-gnueabihf-ld start.o main.o -Bstatic -Ttext 0x0 -o main.out
arm-linux-gnueabihf-objdump main.out --disassemble-all --source --section=.text
arm-linux-gnueabihf-objcopy main.out -O ihex main.hex

アセンブラ記述の中に C のソースコードが挟み込まれる様にダンプした結果は、次の通りです。 (スタックポインタの値は 0x10000000 に設定しています。)

f:id:minettyo:20210324163214j:plain

このダンプリストにおいて、[sp]、[sp, #4] はそれぞれオート変数 (自動変数) a、b が 格納されるスタック上の場所です。変数 a と b に「volatile」宣言をしていますが、これは変数が「揮発性」なので毎回必ず値を代入するように、コンパイラに最適化の抑制を指示するものです。これを宣言しておかないと、この例ではプログラムが余りにも簡単過ぎる為、コンパイラに最適化をさせると 3+4 の答 7 を戻り値として r0 に返すだけのアセンブラになってしまいます。変数 c には「volatile」宣言をしていないので、「-Og」と指定したコンパイラの最適化によりスタック上のオート変数が除去されています。つまり、「return (a+b)」に最適化されたという事です。

 

GDB ベースの GUI ソースコードデバッガ Insight (GDBtk) の内蔵シミュレータで動作を確認すると、次の通りです。デバッガは、緑にハイライトされた C 言語ソースコードを実行する直前で停止しています。
(画像はクリックまたはピンチすると拡大します。)

f:id:minettyo:20210324163508j:plain

  • GDB ベースの GUI ソースコードデバッガでの動作確認は別記事で紹介予定です。
  • Ubuntu 標準パッケージの QEMU エミュレータでの確認は別記事で紹介予定です。
  • 作成したプログラムを Raspberry Pi ボードで実行する方法は別記事で紹介予定です。

スタートアップ ルーチンの作成とアセンブル

C 言語のメインルーチンを呼ぶ為のアセンブラソースと言うと難しく考えがちですが、普通のプロセッサでは次の2つを行うだけ動きます。後述の複雑な事をしなければこれだけです。その他は、複雑な事をしたくなった時に都度拡張して行くことにます。

  1. スタックポインタの設定
  2. メインルーチンへの分岐

以上を踏まえて、出来るだけ必用最低限の設定だけを行うアセンブラ ソースコードを作成してみます。

但し、このやり方で作成するスタートアップルーチンは簡易的な物です。まず、割り込みや例外処理に対応していません。また、正式な C 言語の仕様には準拠しておらず、グローバル変数に明示的に記載された .data セクションの初期値の ROM から RAM へのコピーと、グローバル変数または static 変数に初期値が記載されていない場合の .bss セクションのゼロ初期化を行っていません。
初期値の記載が無い変数の初期化については、次のサイトに詳しい説明があります。

www.ertl.jp

 以下のコードを WSL のターミナル上にカット&ペーストすると、start.s というファイルが出来ます。

cat <<! > tmp.txt
.align 2 .global _start .text _start: ldr sp, =#0x10000000
bl main nop; nop; nop; nop ! /usr/bin/unexpand tmp.txt > start.s /bin/rm tmp.txt

上のコードでは即値オペランドに「=」という疑似コードを使用したロード命令を使用しています。この記述の仕方をしておくと、アセンブラやリンカが適切な命令に自動的に置き換えを行います。この例では、ldr 命令が mov 命令に置き換えられます。また、スタックポインタ sp (r13) レジスタに即値を直接代入していますが、これは ARM ステートでも動作する CPU の場合のみ可能です。Thumb ステートのみの命令セットの場合には上位番号 (r8~r15) の汎用レジスタには即値の直接代入は出来ませんでの、次の様に一旦下位番号 (r0~r7) の汎用レジスタに即値を代入した後に、下位番号のレジスタからスタックポインタ  sp (r13) に mov を行う2行に分ける必要が有ります。

        ldr r1, =#0x10000000
        mov sp, r1

尚、Cortex-M シリーズの場合には、スタックポインタの設定とリセットベクタの指定方式が異なります。これに付いては別途記事にする予定です。

使用するアセンブラの疑似コードに依存しない書き方をしたい場合には、シフト命令を使用すると良いでしょう。同様に、次のコードを WSL のターミナル上にカット&ペーストすると、start.s というファイルが出来ます。

cat <<! > tmp.txt
.align 2 .global _start .text _start: mov r1, #1 lsl r1, #24 mov sp, r1 bl main nop; nop; nop; nop !
/usr/bin/unexpand tmp.txt > start.s
/bin/rm tmp.txt

「.」で始まる最初の3行は疑似命令です。「.align 2」は以降に配置される各プログラム ブロックの先頭を2の2乗の区切りに配置することを意味します。アセンブラ ソースコードの先頭に書くのが習わしです。「.global _start」は、_start というラベルがグローバルであること、つまり他のオブジェクトファイルからも参照可能なことを意味します。「.text」は、実行ファイル中の TEXT セクション、つまりプログラムが格納される領域の始まりであることを宣言します。

4行名の「_start:」はラベルです。「_start」というのは、「main」とペアの特殊な予約済のラベルです。「_start」は「main」関数を呼び出すスタートアップ ルーチン (群) の先頭に置かれます。

5行目以降がプログラムの本体です。ARM ステートと Thumb ステートの両方に使えるように、少し曖昧なアセンブリ記述をしています。「mov r1, #1」はレジスタ r1 に即値 1 を代入すること、「lsl r1, #24」はレジスタ r1 の値を左に24ビット論理シフトすること、「mov sp, r1」はレジスタ r1 の値をスタックポインタに代入することを意味します。以上3つの機械語で、スタックポインタに 0x1000000 という値を代入しています。

bl というのはサブルーチンコール用の分岐命令で、次に実行すべき命令の番地をリンクレジスタ lr レジスタに格納してから分岐します。呼び出す main 関数が Thumb ステートで記述されている場合、この命令はリンカで自動的に blx に置き換えられます。

以降の「nop」は、C 言語のメインルーチンから戻って来た時に実行されます。セミコロン「;」は2つ以上のニーモニック (機械語) を同一行に書く場合の区切り記号です。main ルーチンから戻った時のブレークポイント設定用に、適当に4個入れてあります。

ソースコードが出来たら、アセンブルしてオブジェクトを作成します。このアセンブラでは、何も指定しないと ARM ステート用のオブジェクトを出力します。「-mthumb」オプションを指定すると、Thumb ステート用のオブジェクトを出力します。

「-g -gdwarf-2」の意味は、次のセクションで説明します。アセンブラで記述したソースコードに付けても大きなメリットは無いですが、元のソースコードやコメント行がディスアセンブルしてダンプするコードに挟まる様になります。

arm-linux-gnueabihf-as start.s -g -gdwarf-2 -o start.o

尚、アセンブルリストを見たい場合には、コマンドに「-a」オプションを追加すれば標準出力にプリントアウトされます。

C ソースコードコンパイル

次のサンプルプログラムをコンパイルします。cat コマンドから ! までを WSL のターミナル上にカット&ペーストすると、main.c というファイルが出来ます。

cat <<! > main.c
int main() {
  volatile int a, b;
int c; a=3;
b=4; c=a+b; return c; } !

このソースコードコンパイルしてオブジェクトを作成します。「.out」の実行可能ファイルではなく、オブジェクト (.o) 止まりとなるよう「-c」オプションを付けます。「-c」を付けないと、リンカが起動されて、Linux 用の標準ライブラリのスタートアップルーチン crt0.o (_start ラベルを含む) が先頭に付いた実行可能ファイル (.out) が作成されてしまいます。

arm-linux-gnueabihf-gcc main.c -g -gdwarf-2 -marm -Og -c -o main.o

コンパイル時にデバッガ用オプションとして「-g」と「-gdwarf-2」を同時に指定します。これは、ICE (In-Circuit Emulator) 等で使用される標準的なデバッグ情報 DWARF バージョン2 を、出力される実行可能ファイルに付加する為です。DWARF2 を指定しておけば、殆どの ICE やソースコードデバッガで読めます。DWARF4だと、読める ICE の機種やデバッガが限られます。

このコンパイラでは、何も指定しないと Thumb ステートの命令が出力されます。ARM ステート用の命令を出力する為には「-marm」オプションを指定します。アセンブラと動作が逆なので注意が必要です。GDB ではバージョン7以降が必要となります。

「-Og」 は最適化オプションの1つですが、ディスアセンブル時に元の C 言語で書かれたソースコード中にブレークポイントを貼れる程度までの最適化を行います。-g オプションと共に使用すると、最適化を行いつつ C 言語でのソースコード デバッグも行えます。「-O2」 を指定してしまうと、ソースコードデバッガでブレークポイントを正しく貼れない場合があります。

また、C のソースコード中で、変数定義の冒頭で volatile 宣言を行っていますが、これは変数を「揮発性」とみなして毎回代入を行うよう、コンパイラに最適化の抑止を指示する為の宣言です。

尚、アセンブルリストを見たい場合には、コマンドに「-Wa,-a」オプションを追加すると標準出力にプリントアウトされます。-Wa は C コンパイラのドライバがアセンブラに渡すオプションの指定になります。リンカにオプションを渡したい場合は -Wl です。

リンカで実行可能ファイル (.out) を作成する:

arm-linux-gnueabihf-ld start.o main.o -Bstatic -Ttext 0x0 -o main.out

まず、リンカに指定するオブジェクトの順番は非常に大事です。作成される実行可能ファイル上では、オブジェクトは ld に指定した順番通りに配置されます。従って、スタートアップルーチン start.o は必ず先頭に指定せねばなりません。

このルールは、ライブラリのリンク順番にも影響し「-l」オプションにも適用されます。例えば、算術演算ライブラリを使用する為に「#include <math.h>」を指定した場合、「start.o -lm main.o」と指定すると、スタートアップルーチン、算術演算ライブラリ、main.c プログラムの順に配置されます。普通は分岐命令の飛び先が遠くならない様に配慮しますので、「start.o main.o -lm」と指定します。命令キャッシュの容量が限られている場合、この指定の順番は性能に大きく影響します。速度が大変重要なプログラムを実行する場合など、ライブラリ (/usr/arm-linux-gnueabihf/lib/libXXX.a) の中から ARMHF 用の ar コマンドで必要なオブジェクト (.o) だけを抜き出して個別に配置の順番を指定することも有ります。

「-Bstatic」は、ダイナミックリンク ライブラリ (.so) を使用せずに、静的ライブラリ (.a) をリンクするという意味です。

「-T」オプションは、セクションのアドレスを指定します。「-Ttext 0x0」は、「.text」セクションを 0x0 番地に配置するという意味です。尚、C 言語の中で「初期値を持つグローバル変数」や構造体を使うなど、.data セクションが必要となる場合には「-Tdata 番地」を指定します。「初期値を持たないグローバル変数」や「初期値を持たない static 変数」を使用する場合は、.bss セクションの指定「-Tbss 番地」も指定します。

作成された実行可能ファイル (.out) の確認を行う:

コンパイル後に、ARMHF 用の objdump コマンドを使って、プログラムがリンカで指定した通り 0x0 番地に格納されている事を確認します。「--disassemble-all」または「-D」オプションは、デバッグ情報も含めた全ての情報をダンプします。「--source」オプションを指定すると、ディスアセンブルしたコードの中に C のソースコードが表示されます。「--sections」オプションを使うと、ダンプするセクションを指定出来ます。

arm-linux-gnueabihf-objdump main.out --disassemble-all --source --section=.text

ここで注目すべきは、もし C ソースコードコンパイル時に「-marm」オプションを指定しなかった場合には、start.s のアセンブラソースの中に記述した「bl」命令を、リンカが自動的に「blx」命令に置き換えることです。blx 命令は、分岐と同時に ARM ステートと Thumb ステートとの切り替えを行います。分岐先として指定されたアドレスの最下位ビットが 1 である (実際の分岐先アドレスに1を加えておく) とステートが切り替わります。尚、実際の分岐先アドレスが奇数になる訳では無く、余りビットが命令セットとして利用されているだけです。

さて次に、 file コマンドを使用して実行可能のファイルが、ARM用 の 32 ビット ELF フォーマット、EABI5 (Embedded Application Binary Interface #5) バージョン1である事を確認します。 更に、ARMHF 用の objdump に「--dwarf=info」オプションを指定して、 出力された DWARF のバージョンが 2 である事を確認しておきます。MinGW 用の GCC-4.8.1 以降の x86 用のコンパイラ (ARM 用に非ず) の場合は、「-g」と「-gdwarf-2」オプションを同時に指定しても DWARF バージョン4 を出力し、Ver. 7 以降の GDB でないと読み込めませんので、注意が必要です。

file main.out
arm-linux-gnueabihf-objdump --dwarf=info main.out | head

ヘキサファイルと素のバイナリの出力:

インテルヘキサ (Intellec Hex) 形式やモトローラ S レコード形式のヘキサ (MOT) ファイルの出力には objcopy コマンドを使用します。指定するオプションが「-O ihex」でインテルヘキサ (Intellec Hex) 形式、「-O srec」でモトローラ S レコード (MOT) 形式のヘキサファイル、「-O verilog」で Verilog 用の ROM コードファイルが出力されます。

また、ベアメタルのボード用に、Linux の実行可能形式 (ELF) ではなく素のバイナリファイルが必要になる事もあります。「-O binary」と指定すると、ELF 形式ではない素のバイナリファイルが出力されます。Linux の od コマンドで確認すると、確かに素のバイナリファイルになっています。

このバイナリファイルのヘキサダンプを加工すると、objcopy コマンドで出力するよりも読み易い Verilog シミュレーション用の ROM コードファイルを作成出来ます。尚、od (オクタルダンプ) コマンドは何も指定しないとアドレスもデータもオクタル (8進) でダンプ表示しますので、注意が必要です。

2021-03-29 訂正:
また、32ビット ROM 用にダンプする場合、アドレスを4で割らなければなりません。更に、od コマンドによる連続値の省略で「*」が出力されないよう「-v」オプションが必要です。

arm-linux-gnueabihf-objcopy main.out -O ihex main.hex
arm-linux-gnueabihf-objcopy main.out -O srec main.mot
arm-linux-gnueabihf-objcopy main.out -O verilog main.verilog
arm-linux-gnueabihf-objcopy main.out -O binary main.binary
od -A d -t u4 -v main.binary | \
awk '{printf("@%06x %08x %08x %08x %08x\n", $1/4, $2, $3, $4, $5)}' > main.v

更なる確認予定 (今後の記事の予定):

Ubuntu-20.04 の標準パッケージである、QEMU (Ubuntu 上のコマンド名は qemu-system-arm) とGDB (gdb-multiarch) を使用して、Linux 用のオブジェクトとしてシミュレーションとデバッグが出来る筈なのですが、一寸頓挫しています。qemu-system-arm には、Digilent / Avnet 社製 ZEDBoard、Xilinx 社製 ZC702 評価ボード、もしくは ADI 社製 SDR 学習キット ADALM-Pluto 等に搭載されている Cortex-A9 コア搭載の Xilinx 社製 FPGA内蔵 Zynq SoC (モデル名は xilinx-zynq-a9 ) や、Raspberry Pi 2 (モデル名は raspi2 ) 用のシミュレーション モデルも入って居ます。

尚、Raspberry Pi 2 は既に古い製品なので、今から購入するのであれば、「Raspberry Pi 3 Model B+ (Pi3B+) 技適付き」をお薦めします。「技適マーク付き」でないと日本国内でワイヤレス LAN の使用が違法となります。また、Pi4B は更に新しいのですが、消費電力が若干多く、5.1V 3A 出力、USB Type-C の特殊な AC アダプタが必要となります。

また、Insight (GDBtk) を WSL 上にビルドして、ベアメタル (OS無しの状態) での GUI デバッグも可能ですが、こちらも途中まで出来て居ますので、順次記事にして行きます。


【ARM 関係の目次へ戻る】   【WSL 関係の目次へ戻る】


【広告】