土日の勉強ノート

AI、機械学習、最適化、Pythonなどについて、技術調査、技術書の理解した内容、ソフトウェア/ツール作成について書いていきます

STM32(ARM Cortex-M)のELFファイルの内容を確認する

前回は、QEMU (ターゲットは STM32F4-Discovery)で動かしたサンプルソースのリンカスクリプトの内容を確認しました。

今回は、これまで使ってきたサンプルソースをビルドして生成された ELFファイル の内容を見ていきたいと思います。

それでは、やっていきます。

参考文献

マニュアル

STM32F4 のマニュアル

下記リンクのドキュメント→リファレンスマニュアル、ドキュメント→プログラミングマニュアルなどを参照してください。最近は、マニュアルが日本語化されていて、とても便利です。

デザイン/サポート | STM32, STM8ファミリはSTの32bit/8bit汎用マイクロコントローラ製品

リンカのマニュアル

https://sourceware.org/binutils/docs/ld/

はじめに

「QEMUを動かす」の記事一覧です。良かったら参考にしてください。

QEMUを動かすの記事一覧

それでは、やっていきます!

バイナリエディタ(HxD)のインストール

ELFファイルの内容を確認するのに必要なのは、まずはGNUツールとバイナリエディタがあれば十分だと思います。

GNUツールは、Ubuntu ならデフォルトで入ってるかもしれません(binutils の例えば readelf)。第1回の STM32(ARM Cortex-M)をQEMUで動かす(環境構築編) - daisukeの技術ブログ でインストールしたクロスコンパイラにも入ってます(arm-none-eabi-readelf)。

あとは、直接ファイルを見るために、バイナリエディタが必要です。バイナリエディタなら何でもいいですが、ここでは、HxD をインストールします。

mh-nexus.de

Version は、2.5.0.0 が最新のようです。

ダウンロード(HxDSetup.zip)したら、中に入って、HxDSetup.exe を実行します。

言語の選択画面になるので、日本語のままで、OKをクリックします。

言語の選択
言語の選択

インストール画面が表示されるので、次へをクリックします。

インストール画面
インストール画面

ライセンスの確認画面が表示されるので、「同意する」にチェックを入れて、次へをクリックします。

使用許諾確認の画面
使用許諾確認の画面

インストール先の指定が表示されるので、問題なければ次へをクリックします。

インストール先の指定
インストール先の指定

デスクトップ、クイック起動にアイコンを作成するかを聞かれるので、お好みで選んで、次へをクリックします。

アイコン作成の選択画面
アイコン作成の選択画面

インストール準備完了が表示されるので、インストールをクリックします。

インストール準備の完了
インストール準備の完了

インストールが完了しました。完了をクリックします。

インストール完了画面
インストール完了画面

STM32F4-DiscoveryのSTM32F407VGのデータシート情報

データシートは、STM32の他のマニュアルと同じく「https://www.stmcu.jp/design/」からダウンロードできます。

データシートによると、今回ターゲットにしている「STM32F4-Discovery」の「STM32F407VG」は、1MBのFlashメモリと、192KBのRAMを搭載しています。

素直に割り当てると、Flashメモリは、0x08000000 から 0x080FFFFF になります。RAMは、データシートを見ると、ちょっと変則的で、3か所に、0x10000000-0x2000FFFF(64KB)と 0x20000000-0x2001BFFF(112KB)と 0x2001C000-0x2001FFF(16KB)になっています。

メモリマップ
メモリマップ

これを踏まえて、ELFファイルの内容を見ていきます。

ELFファイル

参考文献の「Binary Hacks ―ハッカー秘伝のテクニック100選」を見ながらやっていきます。古い本ですが、この本の代わりは見当たりませんでした。

ELFファイルの ELF は、「Executable and Linking Format」の略で、実行可能バイナリ、オブジェクトファイルなどで使われているファイルフォーマットのことです。

fileコマンドでELFファイルかどうかを確認することが出来ます。今回のサンプルソースの実行バイナリを対象にfileコマンドを実行してみます。

$ file stm32f4discovery_sample.elf
stm32f4discovery_sample.elf: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped

32bit、リトルエンディアン(LSB)、ARM用の実行可能バイナリで、デバッグ情報が残ってるという情報が得られました。

ELFヘッダを確認する

日本語で表示されたので、readelf を使いました。ELFヘッダは、readelfコマンドの「hオプション」で確認することが出来ます。

ELFヘッダは、ELFファイルのヘッダで、後続のプログラムヘッダやセクションヘッダの構造や位置を示しています。

重要なところだけ説明します。

  • 先頭の4byteが特徴的で、ASCIIコードで「ELF」が入っています
  • ARM用の実行可能ファイル
  • エントリポイントが 0x8000cf5 となっています
  • このELFヘッダの長さは52byteとなっています
  • プログラムヘッダの始点が52byteなので、ELFヘッダの後にプログラムヘッダが続いていることが分かります
  • プログラムヘッダの長さは32byteで3個(32×3=96byte)となっています
  • セクションヘッダの長さは40byteで26個(40×26=1040byte)となっています
$ readelf -h stm32f4discovery_sample.elf
ELF ヘッダ:
  マジック:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  クラス:                            ELF32
  データ:                            2 の補数、リトルエンディアン
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI バージョン:                    0
  型:                                EXEC (実行可能ファイル)
  マシン:                            ARM
  バージョン:                        0x1
  エントリポイントアドレス:               0x8000cf5
  プログラムヘッダ始点:          52 (バイト)
  セクションヘッダ始点:          342284 (バイト)
  フラグ:                            0x5000200, Version5 EABI, soft-float ABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         3
  Size of section headers:           40 (bytes)
  Number of section headers:         26
  Section header string table index: 25

ELFヘッダの長さは52byteとなっています。バイナリエディタで開いて、ELFヘッダの部分を選択したところです。

ELFヘッダ
ELFヘッダ

プログラムヘッダ

プログラムヘッダは、各プログラムをどのメモリアドレスにロードするかの情報を示しています。

プログラムヘッダは、readelfコマンドの「lオプション」で確認することが出来ます。

3つのプログラムの構成になっているようです。最初が TEXTセクション(命令、ROM)で、2番目が DATA、BSSセクション(RAM)で、3番目はスタック領域(RAM)になっています。

  • オフセット:このELFファイル内の開始位置を示しています
  • 仮想アドレス:実行時のメモリアドレス
  • 物理アドレス:ロードするメモリアドレス
  • ファイルサイズ:このELFファイルの消費しているサイズ(ROMサイズ)
  • メモリサイズ:ターゲットの消費するサイズ?(RAMサイズ)

下層アドレスと物理アドレスが分かりにくいですが、リンカのマニュアルによると、この2つのアドレスは通常同じになるが、dataセクションでは異なる値が入るとのことです。

dataセクションは、初期値付きのグローバル変数の初期値を格納する領域です。実行時(スタートアップルーチン実行時)に、ROMから初期値を読みだして、RAM(dataセクション)にコピーします。

このROM側のアドレスが物理アドレス(ロードするアドレス)で、RAM側のアドレスが仮想アドレス(実行時のアドレス)を格納しているとのことです。

ELFファイルと一緒に生成しあれたマップファイル(stm32f4discovery_sample.map)を見ると、命令が格納されているTEXTセクッション部分は 0x08000000-0x08002AF8 までで、DATAセクション部分は、0x20000000-0x20000074 で、BSSセクションはその続きで、0x20000168 まで続いていました。

あとは、スタック領域です。16KB の SRAM の後ろの方を使っているようです。

$ readelf -l stm32f4discovery_sample.elf

Elf ファイルタイプは EXEC (実行可能ファイル) です
エントリポイント 0x8000cf5
There are 3 program headers, starting at offset 52

プログラムヘッダ:
  タイプ       オフセット 仮想Addr   物理Addr   FileSiz MemSiz  Flg Align
  LOAD           0x010000 0x08000000 0x08000000 0x02af8 0x02af8 RWE 0x10000
  LOAD           0x020000 0x20000000 0x08002af8 0x00074 0x00168 RW  0x10000
  LOAD           0x00f700 0x2001f700 0x2001f700 0x00000 0x00100 RW  0x10000

 セグメントマッピングへのセクション:
  セグメントセクション...
   00     .isr_vector .text .rodata .init_array .fini_array
   01     .data .bss
   02     ._user_heap_stack

プログラムヘッダの長さは 96byte となっています。バイナリエディタで開いて、プログラムヘッダの部分を選択したところです。

プログラムヘッダ
プログラムヘッダ

実際に、ELFファイルの TEXTセクションを見てみたいと思います。

プログラムヘッダの最初の領域が TEXTセクションでありオフセットは 0x10000 となっています。つまり、ELFファイルの先頭から 0x10000 の位置に TEXTセクションが存在しているはずです。

では、TEXTセクションには、どのようなデータが格納されているのでしょうか。

リファレンスマニュアルの「2.4 ブート設定」によると、BOOTピンによって、0x00000000 にエイリアスされるメモリを変更することが出来るとあります。通常は、Flashメモリです。

ブートモード
ブートモード

そして、リセット時、0x00000000 からスタックアドレスを SPレジスタにロードして、0x00000004 から実行するアドレス(リセットハンドラ)を取得して実行を開始するとあります。

つまり、TEXTセクションの最初の 4byte はスタックアドレスで、次の 4byte リセットハンドラのアドレスが格納されていることになります。

それでは、実際にバイナリエディタで、ELFファイルの先頭から 0x10000 の位置を見てみます。

先頭の 4byte はスタックアドレスで、0x20020000 となっています。次の 4byte はリセットハンドラのアドレスは 0x08000CF5 となっています。

プログラミングマニュアルの「2.3.4 ベクタ・テーブル」によると、ハンドラが Thumbコードの場合は最下位ビットを 1 にする必要があるとあります。よって、リセットハンドラのアドレスは 0x08000CF4 ということになります。

ちゃんと TEXTセクションのデータが格納されていました。

バイナリエディタでTEXTセクションの先頭を表示した
バイナリエディタでTEXTセクションの先頭を表示した

次は、実際に、デバッガでリセットハンドラの先頭でブレークして、PC(プログラムカウンタ)を確認してみます。すると、確かに、リセットハンドラのアドレスは 0x08000CF4 でした、スッキリです(笑)。

リセットハンドラの先頭でブレークさせたところ
リセットハンドラの先頭でブレークさせたところ

一応、2番目のRAMのセクションも見てみます。サイズは 0x74 で、グローバル変数の初期値が入っていると思います(具体的な値は分かりません)。それを超えたところには、何か別のものが入っていそうです。

DATAセクション
DATAセクション

3番目のスタックの領域も見てみましたが、FileSiz が 0 なので、何も入ってないはずで、実際は 0 でした。

セクションヘッダ

セクションヘッダは、各セクションの情報(アドレス、サイズ、アライメントなど)が格納されています。

セクションヘッダは、readelfコマンドの「Sオプション」で確認することが出来ます。

セクション名、タイプ、開始アドレス、ファイル内のオフセット、サイズ、各種フラグが確認できます。

$ readelf -S stm32f4discovery_sample.elf
There are 26 section headers, starting at offset 0x5390c:

セクションヘッダ:
  [] 名前              タイプ          アドレス Off    サイズ ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .isr_vector       PROGBITS        08000000 010000 000188 00   A  0   0  4
  [ 2] .text             PROGBITS        080001c0 0101c0 00284c 00  AX  0   0 64
  [ 3] .rodata           PROGBITS        08002a0c 012a0c 0000e4 00   A  0   0  4
  [ 4] .ARM.extab        PROGBITS        08002af0 020074 000000 00   W  0   0  1
  [ 5] .ARM              PROGBITS        08002af0 020074 000000 00   W  0   0  1
  [ 6] .preinit_array    PREINIT_ARRAY   08002af0 020074 000000 04  WA  0   0  1
  [ 7] .init_array       INIT_ARRAY      08002af0 012af0 000004 04  WA  0   0  4
  [ 8] .fini_array       FINI_ARRAY      08002af4 012af4 000004 04  WA  0   0  4
  [ 9] .data             PROGBITS        20000000 020000 000074 00  WA  0   0  4
  [10] .bss              NOBITS          20000074 020074 0000f4 00  WA  0   0  4
  [11] ._user_heap_stack NOBITS          2001f700 02f700 000100 00  WA  0   0  1
  [12] .ARM.attributes   ARM_ATTRIBUTES  00000000 020074 00002a 00      0   0  1
  [13] .comment          PROGBITS        00000000 02009e 000033 01  MS  0   0  1
  [14] .debug_info       PROGBITS        00000000 0200d1 013a83 00      0   0  1
  [15] .debug_abbrev     PROGBITS        00000000 033b54 003dc1 00      0   0  1
  [16] .debug_aranges    PROGBITS        00000000 037918 0004a0 00      0   0  8
  [17] .debug_line       PROGBITS        00000000 037db8 0069ff 00      0   0  1
  [18] .debug_str        PROGBITS        00000000 03e7b7 008d4f 01  MS  0   0  1
  [19] .debug_loc        PROGBITS        00000000 047506 0064d7 00      0   0  1
  [20] .debug_frame      PROGBITS        00000000 04d9e0 000e38 00      0   0  4
  [21] .debug_ranges     PROGBITS        00000000 04e818 001130 00      0   0  1
  [22] .debug_macro      PROGBITS        00000000 04f948 001d10 00      0   0  1
  [23] .symtab           SYMTAB          00000000 051658 001850 10     24 256  4
  [24] .strtab           STRTAB          00000000 052ea8 00094d 00      0   0  1
  [25] .shstrtab         STRTAB          00000000 0537f5 000115 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), y (purecode), p (processor specific)

おわりに

今回は、実行可能ファイルのELFファイルの内容を見てみました。プログラム本体やデータ以外に、たくさんの情報で管理されていることが分かりました。

次回は、ELFファイルをバイナリファイルに変換したり、その逆のバイナリファイルをELFファイルに変換したりしてみたいと思います。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。