はじめに
最近、マイクロコントローラに興味を持ちました。 最初にAVR、次にARM。 マイクロコントローラーのプログラミングには、アセンブラーとCの2つの主なオプションがあります。しかし、私は
Fortプログラミング言語のファンであり、これらのマイクロコントローラーへの移植を開始しました。 もちろん、既成のソリューションはありますが、私が望んでいたものはありませんでした。gdbを使用したデバッグです。 そして、私はこのギャップを埋めるために出発しました(これまではARMのみ)。 32ビットARM Cortex-M3プロセッサ、128kBフラッシュ、および8kB RAMを備えた
stm32vldiscoveryボードがありました。
もちろん、Fortでクロス翻訳機を作成しましたが、この言語はエキゾチックであると考えられているため、記事にはコードはありません。 かなり詳細な推奨事項に限定します。 このテーマに関するネットワーク上のドキュメントと例はほとんどありません。一部のパラメーターは試行錯誤によって私が選択し、一部はgccコンパイラーの出力ファイルを分析して選択しました。 さらに、必要な最小限のデバッグ情報のみを使用しました。たとえば、再配置や他の多くのものには触れませんでした。 トピックは非常に広範であり、私はそれを私が告白し、私はそれが私にとって十分であることが判明したわずか30パーセントを理解した。
このプロジェクトに興味がある人は誰でも
ここからコードをダウンロードできます。
ELFレビュー
標準開発ツールは、プログラムを
ELF(実行可能およびリンク可能形式)ファイルにコンパイルし、デバッグ情報を含めることができます。 ここで形式仕様を見つけることができ
ます 。 さらに、各アーキテクチャには
、ARM機能などの独自の特性があり
ます 。 この形式について簡単に考えてみましょう。
ELF実行可能ファイルは、次の部分で構成されています。
1.タイトル(ELFヘッダー)
ファイルとその主な特性に関する一般情報が含まれています。
2.プログラムのタイトル(プログラムヘッダーテーブル)
これは、ファイルセクションとメモリセグメント間の対応表であり、ローダーに各セクションを書き込むメモリ領域を指示します。
3.セクション
セクションには、ファイル内のすべての情報(プログラム、データ、デバッグ情報など)が含まれます。
各セクションには、タイプ、名前、およびその他のパラメーターがあります。 「.text」セクションでは、通常、コードは「.symtab」-プログラムシンボル(ファイル名、プロシージャ、変数)のテーブル、「。strtab」-行のテーブル、接頭辞「.debug_」を持つセクション、デバッグ情報などに保存されます。 .d。 さらに、ファイルには必ずインデックス0の空のセクションが必要です。
4.セクションヘッダーテーブル
これは、セクションヘッダーの配列を含むテーブルです。
この形式については、「ELFの作成」セクションで詳しく説明します。
DWARFレビュー
DWARFは、情報をデバッグするための標準形式です。
標準は、
公式Webサイトからダウンロードできます。 また、この形式の素晴らしい簡単な概要:
DWARFデバッグ形式の紹介 (Michael J. Eager)もあります。
デバッグ情報が必要なのはなぜですか? 次のことができます。
- 物理アドレスではなく、ソースコードファイルの行番号または関数名にブレークポイントを設定します
- グローバル変数とローカル変数の値、および関数パラメーターの表示と変更
- コールスタックの表示(バックトレース)
- 1つのアセンブラー命令ではなく、ソースコードの行に沿って、ステップごとにプログラムを実行する
この情報はツリー構造として保存されます。 各ツリーノードには親があり、子を持つことができ、DIE(デバッグ情報エントリ)と呼ばれます。 各ノードには、独自のタグ(タイプ)と、ノードを説明する属性のリスト(プロパティ)があります。 属性には、データや他のノードへのリンクなど、必要なものをすべて含めることができます。 さらに、ツリーの外部に情報が保存されます。
ノードは、データを記述するノードとコードを記述するノードの2つの主なタイプに分けられます。
データを記述するノード:
- データ型:
- Cのint型などの基本的なデータ型(DW_TAG_base_type型のノード)
- 複合データ型(ポインターなど)
- 配列
- 構造、クラス、ユニオン、インターフェース
- データオブジェクト:
各データオブジェクトには、データが配置されているアドレスの計算方法を示すDW_AT_location属性があります。 たとえば、変数は固定アドレスを持つことができ、レジスターまたはスタック上にあり、クラスまたはオブジェクトのメンバーになることができます。 このアドレスはかなり複雑な方法で計算できるため、標準はいわゆるロケーション式を提供します。これには、特別な内部スタックマシンの一連の演算子を含めることができます。
コードを記述するノード:
- プロシージャ(関数)-タグDW_TAG_subprogramを持つノード。 子孫ノードには、変数の説明(関数パラメーターとローカル関数変数)が含まれる場合があります。
- コンパイルユニット プログラムの情報が含まれ、他のすべてのノードの親です。
上記の情報は、「。debug_info」および「.debug_abbrev」セクションにあります。
その他の情報:
- 行番号に関する情報(「.debug_line」セクション)
- マクロ情報(セクション ".debug_macinfo")
- 呼び出しフレーム情報(「.debug_frame」セクション)
ELFを作成する
elfutilsパッケージのlibelfライブラリを使用してEFLファイルを作成します。 libelfの使用に関するネットワークに関する良い記事があります-
例によるLibELF (残念なことに、その中のファイルの作成は非常に簡単に説明されてい
ます )と
ドキュメントがあります 。
ファイルの作成は、いくつかの手順で構成されます。
- Libelfの初期化
- ファイルヘッダー(ELFヘッダー)の作成
- プログラムヘッダーの作成(プログラムヘッダーテーブル)
- セクションを作成する
- ファイルレコード
手順をより詳細に検討してください。
Libelfの初期化
最初に、elf_version関数(EV_CURRENT)を呼び出して結果を確認する必要があります。 EV_NONEと等しい場合、エラーが発生しており、それ以上のアクションを実行できません。 次に、ディスク上に必要なファイルを作成し、そのハンドルを取得してelf_begin関数に渡す必要があります。
Elf * elf_begin( int fd, Elf_Cmd cmd, Elf *elf)
- fd-新しく開いたファイルへのハンドル
- cmd-モード(情報の読み取りにはELF_C_READ、書き込みの場合はELF_C_WRITE、読み取り/書き込みの場合はELF_C_RDWR)、オープンファイルモードに対応する必要があります(この場合はELF_C_WRITE)
- elf-アーカイブファイル(.a)を操作する場合にのみ必要です。この場合、0を渡す必要があります
この関数は、すべてのlibelf関数で使用される作成された記述子へのポインターを返します。エラーの場合は0が返されます。
タイトルを作成する
新しいファイルヘッダーは、elf32_newehdr関数によって作成されます。
Elf32_Ehdr * elf32_newehdr( Elf *elf);
- elf-elf_beginによって返されるハンドル
エラーの場合は0、構造体へのポインター-ELFファイルヘッダーを返します。
#define EI_NIDENT 16 typedef struct { unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; Elf32_Off e_phoff; Elf32_Off e_shoff; Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; Elf32_Half e_shstrndx; } Elf32_Ehdr;
そのフィールドの一部は標準的な方法で入力されますが、一部は入力する必要があります。
- e_ident-識別バイト配列。次のインデックスがあります。
- EI_MAG0、EI_MAG1、EI_MAG2、EI_MAG3-これらの4バイトには、0x7f、「ELF」という文字が含まれている必要があります。これは、elf32_newehdr関数によって既に行われています。
- EI_DATA-ファイル内のデータエンコーディングのタイプを示します:ELFDATA2LSBまたはELFDATA2MSB。 次のようにELFDATA2LSBをインストールする必要があります。e_ident[EI_DATA] = ELFDATA2LSB
- EI_VERSION-既にインストールされているファイルヘッダーバージョン
- EI_PAD-触れないでください
- e_type-ファイルタイプ。ET_NONE-タイプなし、ET_REL-移動するファイル、ET_EXEC-実行可能ファイル、ET_DYN-共有オブジェクトファイルなど。 ファイルタイプをET_EXECに設定する必要があります
- e_machine-このファイルに必要なアーキテクチャ、たとえばEM_386-Intelアーキテクチャの場合、ARMの場合、EM_ARM(40)をここに記述する必要があります-ARM アーキテクチャのELFを参照
- e_version-ファイルバージョン。EV_CURRENTに設定する必要があります
- e_entry-エントリポイントのアドレス、私たちにとっては必要ありません
- e_phoff-プログラムヘッダーファイルのオフセット、e_shoff-セクションヘッダーオフセット、塗りつぶしなし
- e_flags-プロセッサ固有のフラグ。アーキテクチャ(Cortex-M3)の場合、0x05000000(ABIバージョン5)に設定する必要があります
- e_ehsize、e_phentsize、e_phnum、e_shentsize、e_shnum-触れないでください
- e_shstrndx-セクションヘッダーを持つ行のテーブルがあるセクションの番号が含まれます。 セクションがまだないため、この番号は後でインストールします
プログラムタイトルを作成する
すでに述べたように、プログラムヘッダー(プログラムヘッダーテーブル)は、ファイルセクションとメモリセグメント間の対応表であり、ローダーが各セクションを書き込む場所を示します。 作成されるタイトルは、elf32_newphdr関数を使用して作成されます。
Elf32_Phdr * elf32_newphdr( Elf *elf, size_t count);
- elfは私たちの記述子です
- count-作成するテーブル要素の数。 セクションは1つ(プログラムコード付き)のみであるため、カウントは1になります。
エラーまたはプログラムヘッダーへのポインターで0を返します。
ヘッダーテーブルの各要素は、次の構造によって記述されます。
typedef struct { Elf32_Word p_type; Elf32_Off p_offset; Elf32_Addr p_vaddr; Elf32_Addr p_paddr; Elf32_Word p_filesz; Elf32_Word p_memsz; Elf32_Word p_flags; Elf32_Word p_align; } Elf32_Phdr;
セクションを作成する
ヘッダーを作成したら、セクションの作成を開始できます。 空のセクションは、elf_newscn関数を使用して作成されます。
Elf_Scn * elf_newscn( Elf *elf);
- elf-elf_beginによって以前に返されたハンドル
この関数は、セクションへのポインタまたはエラー時に0を返します。
セクションを作成したら、セクションヘッダーに入力し、セクションデータ記述子を作成する必要があります。
elf32_getshdr関数を使用して、セクションヘッダーへのポインターを取得できます。
Elf32_Shdr * elf32_getshdr( Elf_Scn *scn);
- scnは、elf_newscn関数から取得したセクションへのポインターです。
セクションのタイトルは次のようになります。
typedef struct { Elf32_Word sh_name; Elf32_Word sh_type; Elf32_Word sh_flags; Elf32_Addr sh_addr; Elf32_Off sh_offset; Elf32_Word sh_size; Elf32_Word sh_link; Elf32_Word sh_info; Elf32_Word sh_addralign; Elf32_Word sh_entsize; } Elf32_Shdr;
- sh_name-セクション名-セクションヘッダーの文字列テーブルのオフセット(セクション.shstrtab)-下記の「行のテーブル」を参照
- sh_type-セクションのコンテンツタイプ、SHT_PROGBITSを設定する必要があるプログラムコードのセクション、文字列テーブルのセクション-SHT_STRTAB、文字テーブルのセクション-SHT_SYMTAB
- sh_flags-組み合わせることができ、そのうち3つだけが必要なセクションフラグ:
- SHF_ALLOC-セクションがメモリにロードされることを意味します
- SHF_EXECINSTR-セクションには実行可能コードが含まれます
- SHF_STRINGS-セクションには行のテーブルが含まれます
したがって、プログラムの.textセクションには、フラグSHF_ALLOC + SHF_EXECINSTRを設定する必要があります - sh_addr-セクションがメモリにロードされるアドレス
- sh_offset-ファイル内のセクションオフセット-タッチしないでください。ライブラリがインストールされます
- sh_size-セクションサイズ-触れないでください
- sh_link-関連付けられたセクションの番号を含みます;セクションを対応する行テーブルにリンクする必要があります(以下を参照)
- sh_info-セクションのタイプに応じた追加情報、0に設定
- sh_addralign-アドレス調整、タッチしない
- sh_entsize-セクションが同じ長さの複数の要素で構成されている場合、そのような要素の長さを示し、触れないでください
ヘッダーを埋めた後、elf_newdata関数を使用してセクションデータ記述子を作成する必要があります。
Elf_Data * elf_newdata( Elf_Scn *scn);
- scn-新しいセクションへのポインタを受け取りました。
この関数は、エラー時に0を返すか、入力が必要なElf_Data構造体へのポインターを返します。
typedef struct { void* d_buf; Elf_Type d_type; size_t d_size; off_t d_off; size_t d_align; unsigned d_version; } Elf_Data;
- d_buf-セクションに書き込まれるデータへのポインター
- d_type-データ型、ELF_T_BYTEはどこにでも適しています
- d_size-データサイズ
- d_off-セクションのオフセット、0に設定
- d_align-位置合わせ、1に設定可能-位置合わせなし
- d_version-バージョン。EV_CURRENTに設定する必要があります
特別なセクション
この目的のために、最低限必要なセクションのセットを作成する必要があります。
- .text-プログラムコードを含むセクション
- .symtab-ファイル文字テーブル
- .strtabは.symtabセクションの文字の名前を含む文字列テーブルです。後者は名前自体を保存するのではなく、インデックスを保存するためです。
- .shstrtab-セクション名を含む行テーブル
すべてのセクションは前のセクションで説明したように作成されますが、各特別なセクションには独自の特性があります。
セクション.text
このセクションには実行可能コードが含まれているため、sh_typeをSHT_PROGBITSに、sh_flagsをSHF_EXECINSTR + SHF_ALLOC、sh_addrに設定して、このコードがロードされるアドレスを設定する必要があります。
セクション.symtab
このセクションには、プログラムのすべてのシンボル(関数)とそれらが記述されたファイルの説明が含まれています。 長さ16バイトのこのような要素で構成されます。
typedef struct { Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Half st_shndx; } Elf32_Sym;
セクションのデータは、ソーステキストを介して配列に渡すときに収集できます。配列へのポインターは、セクションデータ記述子(d_buf)に書き込まれます。
このセクションは通常の方法で作成され、sh_typeのみをSHT_SYMTABに設定する必要があり、.strtabセクションのインデックスがsh_linkフィールドに書き込まれるため、これらのセクションは接続されます。
セクション.strtab
このセクションには、.symtabセクションのすべての文字の名前が含まれています。 通常のセクションとして作成されますが、sh_typeをSHT_STRTABに、sh_flagsをSHF_STRINGSに設定する必要があるため、このセクションは行のテーブルになります。
セクションのデータは、ソーステキストを介して配列に渡すときに収集できます。配列へのポインターは、セクションデータ記述子(d_buf)に書き込まれます。
セクション.shstrtab
セクション-行のテーブルには、ファイルのすべてのセクションのヘッダーが含まれ、独自のタイトルが含まれます。 .strtabセクションと同じ方法で作成されます。 インデックスを作成したら、ファイルヘッダーのe_shstrndxフィールドにインデックスを書き込む必要があります。
行テーブル
行テーブルには、0バイトで終わる連続した行が含まれ、このテーブルの最初のバイトも0である必要があります。テーブルの行インデックスは、テーブルの先頭からのバイト単位のオフセットであるため、最初の行 'name'はインデックス1、次の行 ' var 'のインデックスは6です。
インデックス0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
\ 0名前\ 0 var \ 0
ファイルレコード
そのため、ヘッダーとセクションはすでに形成されているので、ファイルに書き込んでlibelfを終了する必要があります。 レコードは、elf_update関数によって生成されます。
off_t elf_update( Elf *elf, Elf_Cmd cmd);
- elf-ハンドル
- cmd-コマンド。書き込みの場合はELF_C_WRITEと等しくなければなりません。
関数はエラー時に-1を返します。 エラーテキストは、エラー文字列へのポインタを返すelf_errmsg(-1)関数を呼び出すことで取得できます。
記述子を渡すelf_end関数を使用してライブラリの操作を終了します。 以前に開いたファイルを閉じるためだけに残ります。
ただし、作成したファイルにはデバッグ情報が含まれていません。デバッグ情報は次のセクションで追加します。
DWARFの作成
libdwarfライブラリを使用してデバッグ情報を作成します
。libdwarfライブラリには、ドキュメントを含むpdfファイル(libdwarf2p.1.pdf-DWARFのProducer Library Interface)が付属しています。
デバッグ情報の作成は、次の手順で構成されます。
- libdwarfプロデューサーの初期化
- ノードの作成(DIE-デバッグ情報入力)
- ノード属性の作成
- コンパイルユニットの作成
- 共通情報エントリの作成
- データ型の作成
- プロシージャ(関数)の作成
- 変数と定数を作成する
- デバッグ情報を含むセクションの作成
- ライブラリでの作業を完了する
手順をより詳細に検討してください。
libdwarfプロデューサーの初期化
.symtabセクションで文字を作成すると同時に、コンパイル時にデバッグ情報を作成します。したがって、libelfが初期化された後、ELFヘッダーとプログラムヘッダーが作成された後、セクションが作成される前にライブラリを初期化する必要があります。
初期化には、dwarf_producer_init_c関数を使用します。 ライブラリにはさらにいくつかの初期化関数(dwarf_producer_init、dwarf_producer_init_b)があり、ドキュメントに記載されているいくつかのニュアンスが異なります。 原則として、これらのいずれも使用できます。
Dwarf_P_Debug dwarf_producer_init_c( Dwarf_Unsigned flags, Dwarf_Callback_Func_c func, Dwarf_Handler errhand, Dwarf_Ptr errarg, void * user_data, Dwarf_Error *error)
- フラグ-ビット深度、バイト順(リトルエンディアン、ビッグエンディアン)、再配置フォーマットなど、いくつかのパラメータを決定する「または」いくつかの定数の組み合わせ。これらのうちDW_DLC_WRITEおよびDW_DLC_SYMBOLIC_RELOCATIONSが必ず必要です。
- func-デバッグ情報を使用してELFセクションを作成するときに呼び出されるコールバック関数。 詳細については、以下の「デバッグ情報を使用してセクションを作成する」セクションを参照してください。
- errhand-エラーが発生したときに呼び出される関数へのポインター。 0を渡すことができます
- errarg-errhand関数に渡されるデータは0に設定可能
- user_data-func関数に渡されるデータは0に設定できます
- エラー-返されたエラーコード
この関数は、Dwarf_P_Debug-後続のすべての関数で使用される記述子、またはエラーの場合は-1を返します。エラーの場合はエラーコードがあります(dwarf_errmsg関数を使用してこのコードを渡すと、エラーメッセージのテキストを取得できます)
ノードの作成(DIE-デバッグ情報入力)
上記のように、デバッグ情報はツリー構造を形成します。 このツリーのノードを作成するには、次のものが必要です。
- dwarf_new_die関数で作成します
- 属性を追加します(各タイプの属性は、その機能によって追加されます。これについては後述します)
ノードは、dwarf_new_die関数を使用して作成されます。
Dwarf_P_Die dwarf_new_die( Dwarf_P_Debug dbg, Dwarf_Tag new_tag, Dwarf_P_Die parent, Dwarf_P_Die child, Dwarf_P_Die left_sibling, Dwarf_P_Die right_sibling, Dwarf_Error *error)
- dbg-ライブラリの初期化中に受け取ったDwarf_P_Debug記述子
- new_tag-ノードのタグ(タイプ)-ファイルlibdwarf.hにある定数DW_TAG_xxxx
- parent、child、left_sibling、right_sibling-それぞれ、ノードの親、子、左および右の隣人。 これらのパラメーターをすべて指定する必要はありません。残りのセット0の代わりに1つを指定するだけで十分です。すべてのパラメーターが0の場合、ノードはルートまたは分離されます。
- エラー-発生時にエラーコードが含まれます
この関数は、エラーの場合はDW_DLV_BADADDRを返し、成功した場合はDwarf_P_Dieノードへのハンドルを返します
ノード属性の作成
ノード属性を作成するために、dwarf_add_AT_xxxx関数のファミリー全体があります。 どの関数が必要な属性を作成する必要があるかを判断するのが困難な場合があるため、ライブラリのソースコードを数回調べました。 機能の一部をここで説明し、一部を対応するセクションで説明します。 これらはすべて、ownerdieパラメーター、属性が追加されるノードへのハンドルを取り、errorパラメーターでエラーコードを返します。
dwarf_add_AT_name関数は、名前属性(DW_AT_name)をノードに追加します。 ほとんどのノードには名前(プロシージャ、変数、定数など)が必要です。一部のノードには名前がありません(コンパイル単位など)
Dwarf_P_Attribute dwarf_add_AT_name( Dwarf_P_Die ownerdie, char *name, Dwarf_Error *error)
エラー時にDW_DLV_BADADDRを返し、成功時に属性記述子を返します。
関数dwarf_add_AT_signed_const、dwarf_add_AT_unsigned_constは、指定された属性とその署名(符号なし)値をノードに追加します。 符号付きおよび符号なしの属性は、定数値、サイズ、行番号などを設定するために使用されます。 関数形式:
Dwarf_P_Attribute dwarf_add_AT_(un)signed_const( Dwarf_P_Debug dbg, Dwarf_P_Die ownerdie, Dwarf_Half attr, Dwarf_Signed value, Dwarf_Error *error)
- dbg-ライブラリの初期化中に受け取ったDwarf_P_Debug記述子
- attr-値が設定される属性は定数DW_AT_xxxxであり、ファイルlibdwarf.hにあります
- value-属性値
成功した場合、エラーまたは属性記述子の場合にDW_DLV_BADADDRを返します。
コンパイルユニットの作成
ツリーにはルートが必要です-プログラムに関する情報(たとえば、メインファイルの名前、使用するプログラミング言語、コンパイラの名前、文字の大文字と小文字の区別(変数、関数)、プログラムのメイン関数、開始アドレス、などを含むコンパイル単位があります。など)。 原則として、属性は必要ありません。 たとえば、メインファイルとコンパイラに関する情報を作成します。
メインファイル情報
メインファイルに関する情報を保存するには、「name」属性(DW_AT_name)を使用します;「ノード属性の作成」セクションに示すように、dwarf_add_AT_name関数を使用します。
コンパイラー情報
dwarf_add_AT_producer関数を使用します。
Dwarf_P_Attribute dwarf_add_AT_name( Dwarf_P_Die ownerdie, char *producer_string, Dwarf_Error *error)
- producer_string-情報テキストを含む文字列
エラー時にDW_DLV_BADADDRを返し、成功時に属性記述子を返します。
共通情報エントリの作成
通常、関数(サブルーチン)が呼び出されると、そのパラメーターと戻りアドレスがスタックにプッシュされます(ただし、各コンパイラーは独自の方法で実行できますが)、これはすべて呼び出しフレームと呼ばれます。 デバッガーは、関数からの戻りアドレスを正しく判別し、バックトレース(現在の関数に到達した関数呼び出しのチェーン、およびこれらの関数のパラメーター)を構築するために、フレーム形式に関する情報を必要とします。 スタックに格納されているプロセッサレジスタも通常示されます。 スタック上のスペースを確保してプロセッサレジスタを保存するコードは、関数のプロローグと呼ばれ、レジスタとスタックを復元するコードはエピローグと呼ばれます。
この情報は、コンパイラに大きく依存しています。 たとえば、プロローグとエピローグは関数の最初と最後にある必要はありません。 フレームが使用される場合と使用されない場合があります。 プロセッサレジスタは他のレジスタなどに保存できます
そのため、デバッガーは、プロセッサーのレジスターが値を変更する方法と、プロシージャーに入るときにそれらが保存される場所を知る必要があります。 この情報は、コールフレーム情報-フレーム形式情報と呼ばれます。 プログラムの各アドレス(コードを含む)には、メモリ内のフレームのアドレス(Canonical Frame Address-CFA)とプロセッサレジスタに関する情報が示されます。たとえば、次のように指定できます。
- レジスタはプロシージャに保存されません
- レジスタはプロシージャ内の値を変更しません
- レジスタはCFA + nのスタックに保存されます
- レジスタは別のレジスタに格納されます
- レジスタはメモリ内のあるアドレスに保存されますが、これはかなり非自明な方法で計算できます
- など
情報はコード内のアドレスごとに示される必要があるため、非常に膨大であり、.debug_frameセクションに圧縮形式で保存されます。 アドレスごとにほとんど変化しないため、その変更のみが命令DW_CFA_xxxxの形式でエンコードされます。 各命令は、たとえば次のような1つの変更を示します。
- DW_CFA_set_loc-プログラムの現在のアドレスを示します
- DW_CFA_advance_loc-ポインターを特定のバイト数に移動する
- DW_CFA_def_cfa-スタックフレームのアドレスを示します(数値定数)
- DW_CFA_def_cfa_register-スタックフレームのアドレスを示します(プロセッサレジスタから取得)
- DW_CFA_def_cfa_expression-スタックフレームのアドレスの計算方法を示します
- DW_CFA_same_value-ケースが変更されないことを示します
- DW_CFA_register-レジスタが別のレジスタに格納されていることを示します
- など
.debug_frameセクションの要素は、Common Information Entry(CIE)とFrame Description Entry(FDE)の2つのタイプのレコードです。CIEには、多くのFDEエントリに共通の情報が含まれており、大まかに言って、特定のタイプの手順を説明しています。FDEでは、特定の各手順についても説明しています。プロシージャに入ると、デバッガは最初にCIEから命令を実行し、次にFDEから命令を実行します。私のコンパイラは、CFAがsp(r13)レジスタにあるプロシージャを作成します。すべての手順のCIEを作成します。これにはdwarf_add_frame_cie関数があります。 Dwarf_Unsigned dwarf_add_frame_cie( Dwarf_P_Debug dbg, char *augmenter, Dwarf_Small code_align, Dwarf_Small data_align, Dwarf_Small ret_addr_reg, Dwarf_Ptr init_bytes, Dwarf_Unsigned init_bytes_len, Dwarf_Error *error);
- Augmenterは、プラットフォーム固有の追加情報がCIEまたはFDEにあることを示すUTF-8エンコード文字列です。空の文字列を入れます
- code_align-バイト単位のコードのアライメント(2があります)
- data_align-フレーム内のデータ配置(-4に設定されます。これは、すべてのパラメーターがスタック上で4バイトを占有し、メモリー内で増加することを意味します)
- ret_addr_reg-プロシージャからの戻りアドレスを含むレジスタ(14個あります)
- init_bytes-DW_CFA_xxxx命令を含む配列。残念ながら、この配列を生成する便利な方法はありません。手動で作成することも、Cコンパイラーによって生成されたelfファイルでスパイすることもできます。私の場合、これには3バイトが含まれます:0x0C、0x0D、0、これはDW_CFA_def_cfaを表します:r13 ofs 0(CFAはレジスタr13にあり、オフセットは0)
- init_bytes_len-init_bytes配列の長さ
この関数は、エラー時にDW_DLV_NOCOUNTを返すか、各プロシージャのFDEを作成するときに使用するCIEハンドルを返します。これについては、「FDEプロシージャの作成」セクションで説明しますデータ型の作成
プロシージャと変数を作成する前に、まずデータ型に対応するノードを作成する必要があります。多くのデータ型がありますが、それらはすべて基本型(int、doubleなどの基本型)に基づいており、他の型は基本型から構築されています。基本タイプは、タグDW_TAG_base_typeを持つノードです。属性が必要です:- 「名前」(DW_AT_name)
- 「エンコード」(DW_AT_encoding)-指定されたベースタイプを記述するデータを意味します(たとえば、DW_ATE_boolean-論理、DW_ATE_float-浮動小数点、DW_ATE_signed-整数符号、DW_ATE_unsigned-整数符号なしなど)
- 「サイズ」(DW_AT_byte_size-バイト単位のサイズまたはDW_AT_bit_size-ビット単位のサイズ)
ノードには、他のオプション属性も含まれる場合があります。たとえば、32ビット整数の符号付きベースタイプ「int」を作成するには、タグDW_TAG_base_typeでノードを作成し、属性DW_AT_name-「int」、DW_AT_encoding-DW_ATE_signed、DW_AT_byte_size-4.を作成する必要があります。 。そのようなノードには、属性DW_AT_typeが含まれている必要があります-基本タイプへのリンクです。たとえば、intへのポインター-タグDW_TAG_pointer_typeを持つノードには、属性DW_AT_typeに以前に作成されたタイプ「int」へのリンクが含まれている必要があります。別のノードへのリンクを持つ属性は、dwarf_add_AT_reference関数によって作成されます。 Dwarf_P_Attribute dwarf_add_AT_reference( Dwarf_P_Debug dbg, Dwarf_P_Die ownerdie, Dwarf_Half attr, Dwarf_P_Die otherdie, Dwarf_Error *error)
- attrは属性です。この場合はDW_AT_typeです
- otherdie-参照されるタイプノードのハンドル
プロシージャの作成
プロシージャを作成するには、別のタイプのデバッグ情報である行番号情報を明確にする必要があります。各マシン命令をソースコードの特定の行にマッピングし、プログラムの行ごとのデバッグを可能にします。この情報は.debug_lineセクションに保存されます。十分なスペースがある場合、マトリックスの形式で格納され、そのような列を持つ命令ごとに1行が格納されます。- ソースファイル名
- このファイルの行番号
- ファイル内の列番号
- 命令が演算子または文ブロックの始まりであるかどうか
- など
このようなマトリックスは非常に大きいため、圧縮する必要があります。まず、重複した行が削除され、次に行自体が保存されるのではなく、変更のみが保存されます。これらの変更はステートマシンのコマンドのように見え、情報自体はすでにこのマシンによって「実行」されるプログラムと見なされます。このプログラムのコマンドは、たとえば次のようになります。DW_LNS_advance_pc —コマンドカウンターをあるアドレスに進めます。DW_LNS_set_file—プロシージャが定義されているファイルを設定します。DW_LNS_const_add_pc—コマンドカウンターを数バイト進めます。このような低いレベルでこの情報を作成することは難しいため、libdwarfライブラリには、このタスクを容易にするいくつかの機能があります。各命令のファイル名を保存するのはコストがかかるため、名前の代わりにそのインデックスは特別なテーブルに保存されます。ファイルインデックスを作成するには、dwarf_add_file_decl関数を使用する必要があります。 Dwarf_Unsigned dwarf_add_file_decl( Dwarf_P_Debug dbg, char *name, Dwarf_Unsigned dir_idx, Dwarf_Unsigned time_mod, Dwarf_Unsigned length, Dwarf_Error *error)
- name-ファイル名
- dir_idx-ファイルが置かれているフォルダーのインデックス。インデックスは、dwarf_add_directory_decl関数を使用して取得できます。フルパスを使用する場合、フォルダーのインデックスとして0を設定し、dwarf_add_directory_declをまったく使用しないでください。
- time_mod-ファイル変更時間、省略可能(0)
- length-ファイルサイズ、オプション(0)
この関数は、エラー時にファイルインデックスまたはDW_DLV_NOCOUNTを返します。行番号に関する情報を作成するために、3つの関数dwarf_add_line_entry_b、dwarf_lne_set_address、dwarf_lne_end_sequenceがあります。これらについては以下で検討します。プロシージャのデバッグ情報の作成は、いくつかの段階で行われます。- .symtabセクションにプロシージャシンボルを作成する
- 属性を持つプロシージャノードの作成
- FDEプロシージャの作成
- プロシージャパラメータの作成
- 行番号情報の作成
プロシージャシンボルの作成
手順シンボルは、上記の「.symtabセクション」セクションで説明したように作成されます。その中に、プロシージャシンボルは、これらのプロシージャのソースコードが配置されているファイルのシンボルが散在しています。最初にファイルシンボルを作成してから、プロシージャを作成します。この場合、ファイルは最新になり、次のプロシージャが現在のファイルにある場合、ファイルシンボルを再度作成する必要はありません。属性を持つプロシージャノードの作成
最初に、dwarf_new_die関数(「ノードの作成」セクションを参照)を使用してノードを作成し、DW_TAG_subprogramをタグとして指定し、コンパイルユニット(グローバルプロシージャの場合)または対応するDIE(ローカルの場合)を親として使用します。次に、属性を作成します。- プロシージャ名(dwarf_add_AT_name関数、「ノード属性の作成」を参照)
- プロシージャコードが始まるファイルの行番号(属性DW_AT_decl_line)、関数dwarf_add_AT_unsigned_const(「ノードの属性の作成」を参照)
- ファイル名インデックス(属性DW_AT_decl_file)、dwarf_add_AT_unsigned_const関数(「ノード属性の作成」を参照)
- プロシージャの開始アドレス(属性DW_AT_low_pc)、関数dwarf_add_AT_targ_address、以下を参照
- プロシージャの最終アドレス(属性DW_AT_high_pc)、関数dwarf_add_AT_targ_address、以下を参照
- プロシージャによって返される結果のタイプ(DW_AT_type属性-以前に作成されたタイプへのリンク。「データタイプの作成」を参照)。プロシージャが何も返さない場合、この属性を作成する必要はありません。
DW_AT_low_pcおよびDW_AT_high_pc属性は、このために特別に設計されたdwarf_add_AT_targ_address_b関数を使用して作成する必要があります。 Dwarf_P_Attribute dwarf_add_AT_targ_address_b( Dwarf_P_Debug dbg, Dwarf_P_Die ownerdie, Dwarf_Half attr, Dwarf_Unsigned pc_value, Dwarf_Unsigned sym_index, Dwarf_Error *error)
- attr-属性(DW_AT_low_pcまたはDW_AT_high_pc)
- pc_value-アドレス値
- sym_index-.symtabテーブル内のプロシージャシンボルのインデックス。オプションで、0を渡すことができます
この関数は、エラー時にDW_DLV_BADADDRを返します。FDEプロシージャの作成
「共通情報エントリの作成」セクションで説明したように、各手順では、フレーム記述子を作成する必要があります。これはいくつかの段階で発生します。- 新しいFDEの作成(共通情報エントリの作成を参照)
- 作成されたFDEを一般リストに結合します
- 作成されたFDEに指示を追加する
dwarf_new_fde関数を使用して、新しいFDEを作成できます。 Dwarf_P_Fde dwarf_new_fde( Dwarf_P_Debug dbg, Dwarf_Error *error)
関数は、エラー時に新しいFDE記述子またはDW_DLV_BADADDRを返します。dwarf_add_frame_fdeを使用して、リストに新しいFDEを添付できます。 Dwarf_Unsigned dwarf_add_frame_fde( Dwarf_P_Debug dbg, Dwarf_P_Fde fde, Dwarf_P_Die die, Dwarf_Unsigned cie, Dwarf_Addr virt_addr, Dwarf_Unsigned code_len, Dwarf_Unsigned sym_idx, Dwarf_Error* error)
- fde-受け取ったばかりのハンドル
- die-DIEプロシージャ(属性を持つプロシージャノードの作成を参照)
- cie-CIE記述子(共通情報エントリの作成を参照)
- virt_addr-プロシージャの開始アドレス
- code_len-プロシージャの長さ(バイト)
- sym_idx-シンボルインデックス(オプション、0を指定できます)
この関数は、エラー時にDW_DLV_NOCOUNTを返します。このすべての後、DW_CFA_xxxxの指示をFDEに追加できます。これは、dwarf_add_fde_instおよびdwarf_fde_cfa_offset関数を使用して行われます。最初のものは、指定された命令をリストに追加します: Dwarf_P_Fde dwarf_add_fde_inst( Dwarf_P_Fde fde, Dwarf_Small op, Dwarf_Unsigned val1, Dwarf_Unsigned val2, Dwarf_Error *error)
- fde-FDEによって作成された記述子
- op-命令コード(DW_CFA_xxxx)
- val1、val2-命令パラメーター(命令ごとに異なる、標準、セクション6.4.2呼び出しフレーム命令を参照)
dwarf_fde_cfa_offset関数は、DW_CFA_offsetステートメントを追加します。 Dwarf_P_Fde dwarf_fde_cfa_offset( Dwarf_P_Fde fde, Dwarf_Unsigned reg, Dwarf_Signed offset, Dwarf_Error *error)
- fde-FDEによって作成された記述子
- reg-フレームに書き込まれるレジスタ
- offset-フレーム内のオフセット(バイト単位ではなく、フレーム要素内、共通情報エントリの作成、data_alignを参照)
たとえば、コンパイラはプロローグにプロシージャを作成し、そのプロシージャのレジスタlr(r14)がスタックフレームに格納されます。最初のステップは、最初のパラメーターが1のDW_CFA_advance_loc命令を追加することです。つまり、pcレジスターを2バイト進め(共通情報エントリーの作成、code_alignを参照)、次にパラメーター4でDW_CFA_def_cfa_offsetを追加し(フレーム内のデータオフセットを4バイト設定)、呼び出します関数dwarf_fde_cfa_offsetはパラメーターreg = 14オフセット= 1で、CFAから-4バイトのオフセットを持つフレームにレジスタr14を書き込むことを意味します。プロシージャパラメータの作成
プロシージャパラメータの作成は、通常の変数の作成に似ています。「変数と定数の作成」を参照してください行番号情報の作成
この情報の作成は次のとおりです。- 手順の始めに、dwarf_lne_set_address関数を使用して命令ブロックを開始します
- コードの各行(または機械命令)に対して、ソースコードに関する情報(dwarf_add_line_entry)を作成します
- プロシージャの最後に、dwarf_lne_end_sequence関数を使用して命令ブロックを完了します
dwarf_lne_set_address関数は、命令ブロックが始まるアドレスを設定します。 Dwarf_Unsigned dwarf_lne_set_address( Dwarf_P_Debug dbg, Dwarf_Addr offs, Dwarf_Unsigned symidx, Dwarf_Error *error)
- offs-プロシージャのアドレス(最初の機械命令のアドレス)
- sym_idx-シンボルインデックス(オプション、0を指定できます)
0(成功)またはDW_DLV_NOCOUNT(エラー)を返します。dwarf_add_line_entry_b関数は、ソースコードの行に関する情報を.debug_lineセクションに追加します。機械語命令ごとにこの関数を呼び出します。 Dwarf_Unsigned dwarf_add_line_entry_b( Dwarf_P_Debug dbg, Dwarf_Unsigned file_index, Dwarf_Addr code_offset, Dwarf_Unsigned lineno, Dwarf_Signed column_number, Dwarf_Bool is_source_stmt_begin, Dwarf_Bool is_basic_block_begin, Dwarf_Bool is_epilogue_begin, Dwarf_Bool is_prologue_end, Dwarf_Unsigned isa, Dwarf_Unsigned discriminator, Dwarf_Error *error)
- file_index-dwarf_add_file_decl関数によって以前に取得されたソースコードファイルのインデックス(「プロシージャの作成」を参照)
- code_offset-現在のマシン命令のアドレス
- lineno-ソースコードファイルの行番号
- column_number —
- is_source_stmt_begin — 1 lineno ( 1)
- is_basic_block_begin — 1 ( 0)
- is_epilogue_begin — 1 ( , 0)
- is_prologue_end — 1 (!)
- isa — instruction set architecture ( ). DW_ISA_ARM_thumb ARM Cortex M3!
- discriminator. (, , ) . . , 0
この関数は、0(成功)またはDW_DLV_NOCOUNT(エラー)を返します。最後に、dwarf_lne_end_sequence関数が手順を完了します。 Dwarf_Unsigned dwarf_lne_end_sequence( Dwarf_P_Debug dbg, Dwarf_Addr address; Dwarf_Error *error)
0(成功)またはDW_DLV_NOCOUNT(エラー)を返します。これでプロシージャの作成が完了しました。変数と定数を作成する
一般に、変数は非常に単純です。これらには、名前、メモリ(またはプロセッサレジスタ)、データの場所、およびこのデータのタイプがあります。変数がグローバルである場合、その親はコンパイル単位である必要があります。ローカル-対応するノード(特にプロシージャのパラメーターに関しては、プロシージャは親である必要があります)。また、変数宣言を配置するファイル、行、列を指定することもできます。最も単純な場合、変数の値はある固定アドレスにありますが、多くの変数は、スタックまたはレジスターにプロシージャを入力するときに動的に作成されます。値のアドレスの計算は非常に重要な場合があります。この規格は、変数の値がどこにあるかを記述するメカニズムを提供します-アドレス式(位置式)。アドレス式は、砦のようなスタックマシン用の一連の命令(定数DW_OP_xxxx)であり、実際には、分岐、プロシージャ、および算術演算を備えた別個の言語です。この言語を完全にレビューするのではなく、実際にいくつかの指示にのみ興味があります。- DW_OP_addr-変数のアドレスを示します
- DW_OP_fbreg-ベースレジスターからの変数のオフセットを示します(通常はスタックポインター)
- DW_OP_reg0 ... DW_OP_reg31-変数が対応するレジスタに格納されることを示します
アドレス式を作成するには、最初に空の式(dwarf_new_expr)を作成し、命令(dwarf_add_expr_addr、dwarf_add_expr_genなど)を追加し、DW_AT_location属性(dwarf_add_AT_location_expression)の値としてノードに追加する必要があります。空のアドレス式を作成する関数は、ハンドルまたはエラー時に0を返します。 Dwarf_Expr dwarf_new_expr( Dwarf_P_Debug dbg, Dwarf_Error *error)
式に命令を追加するには、dwarf_add_expr_gen関数を使用します。 Dwarf_Unsigned dwarf_add_expr_gen( Dwarf_P_Expr expr, Dwarf_Small opcode, Dwarf_Unsigned val1, Dwarf_Unsigned val2, Dwarf_Error *error)
- expr-命令が追加されるアドレス式の記述子
- opcode-命令コード、定数DW_OP_xxxx
- val1、val2-命令パラメーター(標準を参照)
この関数は、エラー時にDW_DLV_NOCOUNTを返します。変数のアドレスを明示的に設定するには、前の関数の代わりにdwarf_add_expr_addr関数を使用する必要があります。 Dwarf_Unsigned dwarf_add_expr_addr( Dwarf_P_Expr expr, Dwarf_Unsigned address, Dwarf_Signed sym_index, Dwarf_Error *error)
- expr-命令が追加されるアドレス式の記述子
- address-変数のアドレス
- sym_index-.symtabテーブル内の文字のインデックス。オプションで、0を渡すことができます
この関数は、エラー時にDW_DLV_NOCOUNTも返します。最後に、dwarf_add_AT_location_expr関数を使用して、作成したアドレス式をノードに追加できます。 Dwarf_P_Attribute dwarf_add_AT_location_expr( Dwarf_P_Debug dbg, Dwarf_P_Die ownerdie, Dwarf_Half attr, Dwarf_P_Expr loc_expr, Dwarf_Error *error)
- ownerdie-式が追加されるノード
- attr-属性(この場合はDW_AT_location)
- loc_expr-以前に作成されたアドレス式のハンドル
関数は、エラー時に属性記述子またはDW_DLV_NOCOUNTを返します。変数(およびプロシージャパラメータ)と定数は、それぞれタグDW_TAG_variable、DW_TAG_formal_parameter、およびDW_TAG_const_typeを持つ通常のノードです。次の属性が必要です。- 変数/定数名(dwarf_add_AT_name関数、「ノード属性の作成」を参照)
- 変数が宣言されているファイルの行番号(DW_AT_decl_line属性)、dwarf_add_AT_unsigned_const関数(「ノード属性の作成」を参照)
- ファイル名インデックス(属性DW_AT_decl_file)、dwarf_add_AT_unsigned_const関数(「ノード属性の作成」を参照)
- 変数/定数データ型(属性DW_AT_type-以前に作成された型へのリンク。「データ型の作成」を参照)
- アドレス式(上記を参照)-変数またはプロシージャパラメータに必要
- または値-定数の場合(属性DW_AT_const_value、「ノード属性の作成」を参照)
デバッグ情報を含むセクションの作成
デバッグ情報ツリーのすべてのノードを作成したら、それを使用してエルフセクションの形成に進むことができます。これは2段階で発生します。- まず、dwarf_transform_to_disk_form関数を呼び出す必要があります。この関数は、セクションごとに必要なelfセクションを作成するために作成した関数を呼び出します
- 各セクションについて、dwarf_get_section_bytes関数は、対応するセクションに書き込む必要があるデータを返します
機能 dwarf_transform_to_disk_form ( Dwarf_P_Debug dbg, Dwarf_Error* error)
作成したデバッグ情報をバイナリ形式に変換しますが、ディスクには何も書き込みません。作成されたelfセクションの数またはエラー時にDW_DLV_NOCOUNTを返します。同時に、各セクションに対してコールバック関数が呼び出されます。これは、ライブラリを初期化するときにdwarf_producer_init_c関数に渡しました。この関数を自分で作成する必要があります。その仕様は次のとおりです。 typedef int (*Dwarf_Callback_Func_c)( char* name, int size, Dwarf_Unsigned type, Dwarf_Unsigned flags, Dwarf_Unsigned link, Dwarf_Unsigned info, Dwarf_Unsigned* sect_name_index, void * user_data, int* error)
- name-作成されるelfセクションの名前
- サイズ-セクションサイズ
- type-セクションタイプ
- フラグ-セクションフラグ
- リンク-セクション通信フィールド
- info-セクション情報フィールド
- sect_name_index-再配置を伴うセクションのインデックスを返す必要があります(オプション)
- user_data-ライブラリ初期化関数で設定したのと同じように渡されます
- エラー-ここでエラーコードを渡すことができます
この関数では、次のことを行う必要があります。- 新しいセクションを作成します(elf_newscn関数、セクションの作成を参照)
- セクションヘッダーを作成します(elf32_getshdr関数、同上)。
- 正しく記入してください(同書参照)。セクションのヘッダーフィールドは関数のパラメーターに対応しているため、これは簡単です。sh_addr、sh_offset、sh_entsizeが0に設定され、sh_addralignが1に設定されていないフィールド
- 作成されたセクションのインデックス(関数elf_ndxscn、「セクション.symtab」を参照)を返すか、エラーが発生した場合は-1を返します(エラーコードをエラーに設定して)
- 「.rel」セクションもスキップする必要があります(この場合)。関数から戻るときに0を返します
完了後、dwarf_transform_to_disk_form関数は作成されたセクションの数を返します。次の手順に従って、セクションごとに0からサイクルを実行する必要があります。- dwarf_get_section_bytes関数を使用してセクションに書き込むためのデータを作成します。
Dwarf_Ptr dwarf_get_section_bytes( Dwarf_P_Debug dbg, Dwarf_Signed dwarf_section, Dwarf_Signed *elf_section_index, Dwarf_Unsigned *length, Dwarf_Error* error)
- dwarf_section-セクション番号。0..nの範囲でなければなりません。nは、dwarf_transform_to_disk_form関数によって返される数値です。
- elf_section_index-データを書き込むセクションのインデックスを返します
- length-このデータの長さ
- エラー-使用されていません
関数は、受信したデータへのポインターまたは0を返します(
作成するセクションがもうない場合) - 現在のセクションのデータ記述子を作成し(elf_newdata関数、セクションの作成を参照)、それを設定します(同書を参照)。
- d_buf-前の関数から受信したデータへのポインター
- d_size-このデータのサイズ(同上)
ライブラリでの作業を完了する
セクションが形成されたら、libdwarf関数dwarf_producer_finishを終了できます。 Dwarf_Unsigned dwarf_producer_finish( Dwarf_P_Debug dbg, Dwarf_Error* error)
この関数は、エラー時にDW_DLV_NOCOUNTを返します。この段階でのディスクへの書き込みは実行されないことに注意してください。記録は、「ELFの作成-ファイルの記録」セクションの機能を使用して行う必要があります。おわりに
以上です。
繰り返しますが、デバッグ情報の作成は非常に広範なトピックであり、カーテンを開くだけで多くのトピックには触れませんでした。希望する人は無限に行くことができます。質問があれば、私はそれらに答えようとします。参照資料
ELF
ドワーフ