デバッグ情報(DWARF)を含むELFファイルを手動で作成します(ARMマイクロコントローラー用)

はじめに


最近、マイクロコントローラに興味を持ちました。 最初に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)もあります。
デバッグ情報が必要なのはなぜですか? 次のことができます。

この情報はツリー構造として保存されます。 各ツリーノードには親があり、子を持つことができ、DIE(デバッグ情報エントリ)と呼ばれます。 各ノードには、独自のタグ(タイプ)と、ノードを説明する属性のリスト(プロパティ)があります。 属性には、データや他のノードへのリンクなど、必要なものをすべて含めることができます。 さらに、ツリーの外部に情報が保存されます。
ノードは、データを記述するノードとコードを記述するノードの2つの主なタイプに分けられます。
データを記述するノード:

  1. データ型:
    • Cのint型などの基本的なデータ型(DW_TAG_base_type型のノード)
    • 複合データ型(ポインターなど)
    • 配列
    • 構造、クラス、ユニオン、インターフェース

  2. データオブジェクト:
    • 定数
    • 関数パラメーター
    • 変数
    • など


各データオブジェクトには、データが配置されているアドレスの計算方法を示すDW_AT_location属性があります。 たとえば、変数は固定アドレスを持つことができ、レジスターまたはスタック上にあり、クラスまたはオブジェクトのメンバーになることができます。 このアドレスはかなり複雑な方法で計算できるため、標準はいわゆるロケーション式を提供します。これには、特別な内部スタックマシンの一連の演算子を含めることができます。

コードを記述するノード:

  1. プロシージャ(関数)-タグDW_TAG_subprogramを持つノード。 子孫ノードには、変数の説明(関数パラメーターとローカル関数変数)が含まれる場合があります。
  2. コンパイルユニット プログラムの情報が含まれ、他のすべてのノードの親です。

上記の情報は、「。debug_info」および「.debug_abbrev」セクションにあります。

その他の情報:



ELFを作成する


elfutilsパッケージのlibelfライブラリを使用してEFLファイルを作成します。 libelfの使用に関するネットワークに関する良い記事があります- 例によるLibELF (残念なことに、その中のファイルの作成は非常に簡単に説明されています )とドキュメントがあります
ファイルの作成は、いくつかの手順で構成されます。
  1. Libelfの初期化
  2. ファイルヘッダー(ELFヘッダー)の作成
  3. プログラムヘッダーの作成(プログラムヘッダーテーブル)
  4. セクションを作成する
  5. ファイルレコード

手順をより詳細に検討してください。

Libelfの初期化

最初に、elf_version関数(EV_CURRENT)を呼び出して結果を確認する必要があります。 EV_NONEと等しい場合、エラーが発生しており、それ以上のアクションを実行できません。 次に、ディスク上に必要なファイルを作成し、そのハンドルを取得してelf_begin関数に渡す必要があります。
Elf * elf_begin( int fd, Elf_Cmd cmd, Elf *elf) 


この関数は、すべてのlibelf関数で使用される作成された記述子へのポインターを返します。エラーの場合は0が返されます。

タイトルを作成する

新しいファイルヘッダーは、elf32_newehdr関数によって作成されます。
 Elf32_Ehdr * elf32_newehdr( Elf *elf); 


エラーの場合は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; 




そのフィールドの一部は標準的な方法で入力されますが、一部は入力する必要があります。


プログラムタイトルを作成する

すでに述べたように、プログラムヘッダー(プログラムヘッダーテーブル)は、ファイルセクションとメモリセグメント間の対応表であり、ローダーが各セクションを書き込む場所を示します。 作成されるタイトルは、elf32_newphdr関数を使用して作成されます。
 Elf32_Phdr * elf32_newphdr( Elf *elf, size_t count); 


エラーまたはプログラムヘッダーへのポインターで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); 


この関数は、セクションへのポインタまたはエラー時に0を返します。
セクションを作成したら、セクションヘッダーに入力し、セクションデータ記述子を作成する必要があります。
elf32_getshdr関数を使用して、セクションヘッダーへのポインターを取得できます。
 Elf32_Shdr * elf32_getshdr( Elf_Scn *scn); 


セクションのタイトルは次のようになります。
 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; 


ヘッダーを埋めた後、elf_newdata関数を使用してセクションデータ記述子を作成する必要があります。
 Elf_Data * elf_newdata( Elf_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; 



特別なセクション

この目的のために、最低限必要なセクションのセットを作成する必要があります。

すべてのセクションは前のセクションで説明したように作成されますが、各特別なセクションには独自の特性があります。


セクション.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); 


関数はエラー時に-1を返します。 エラーテキストは、エラー文字列へのポインタを返すelf_errmsg(-1)関数を呼び出すことで取得できます。
記述子を渡すelf_end関数を使用してライブラリの操作を終了します。 以前に開いたファイルを閉じるためだけに残ります。
ただし、作成したファイルにはデバッグ情報が含まれていません。デバッグ情報は次のセクションで追加します。

DWARFの作成


libdwarfライブラリを使用してデバッグ情報を作成します。libdwarfライブラリには、ドキュメントを含むpdfファイル(libdwarf2p.1.pdf-DWARFのProducer Library Interface)が付属しています。
デバッグ情報の作成は、次の手順で構成されます。
  1. libdwarfプロデューサーの初期化
  2. ノードの作成(DIE-デバッグ情報入力)
  3. ノード属性の作成
  4. コンパイルユニットの作成
  5. 共通情報エントリの作成
  6. データ型の作成
  7. プロシージャ(関数)の作成
  8. 変数と定数を作成する
  9. デバッグ情報を含むセクションの作成
  10. ライブラリでの作業を完了する

手順をより詳細に検討してください。

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) 


この関数は、Dwarf_P_Debug-後続のすべての関数で使用される記述子、またはエラーの場合は-1を返します。エラーの場合はエラーコードがあります(dwarf_errmsg関数を使用してこのコードを渡すと、エラーメッセージのテキストを取得できます)


ノードの作成(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) 


この関数は、エラーの場合は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) 


成功した場合、エラーまたは属性記述子の場合に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) 


エラー時にDW_DLV_BADADDRを返し、成功時に属性記述子を返します。

共通情報エントリの作成

通常、関数(サブルーチン)が呼び出されると、そのパラメーターと戻りアドレスがスタックにプッシュされます(ただし、各コンパイラーは独自の方法で実行できますが)、これはすべて呼び出しフレームと呼ばれます。 デバッガーは、関数からの戻りアドレスを正しく判別し、バックトレース(現在の関数に到達した関数呼び出しのチェーン、およびこれらの関数のパラメーター)を構築するために、フレーム形式に関する情報を必要とします。 スタックに格納されているプロセッサレジスタも通常示されます。 スタック上のスペースを確保してプロセッサレジスタを保存するコードは、関数のプロローグと呼ばれ、レジスタとスタックを復元するコードはエピローグと呼ばれます。
この情報は、コンパイラに大きく依存しています。 たとえば、プロローグとエピローグは関数の最初と最後にある必要はありません。 フレームが使用される場合と使用されない場合があります。 プロセッサレジスタは他のレジスタなどに保存できます
そのため、デバッガーは、プロセッサーのレジスターが値を変更する方法と、プロシージャーに入るときにそれらが保存される場所を知る必要があります。 この情報は、コールフレーム情報-フレーム形式情報と呼ばれます。 プログラムの各アドレス(コードを含む)には、メモリ内のフレームのアドレス(Canonical Frame Address-CFA)とプロセッサレジスタに関する情報が示されます。たとえば、次のように指定できます。

情報はコード内のアドレスごとに示される必要があるため、非常に膨大であり、.debug_frameセクションに圧縮形式で保存されます。 アドレスごとにほとんど変化しないため、その変更のみが命令DW_CFA_xxxxの形式でエンコードされます。 各命令は、たとえば次のような1つの変更を示します。

.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); 


この関数は、エラー時にDW_DLV_NOCOUNTを返すか、各プロシージャのFDEを作成するときに使用するCIEハンドルを返します。これについては、「FDEプロシージャの作成」セクションで説明します

データ型の作成

プロシージャと変数を作成する前に、まずデータ型に対応するノードを作成する必要があります。多くのデータ型がありますが、それらはすべて基本型(int、doubleなどの基本型)に基づいており、他の型は基本型から構築されています。
基本タイプは、タグDW_TAG_base_typeを持つノードです。属性が必要です:

ノードには、他のオプション属性も含まれる場合があります。
たとえば、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) 



プロシージャの作成

プロシージャを作成するには、別のタイプのデバッグ情報である行番号情報を明確にする必要があります。各マシン命令をソースコードの特定の行にマッピングし、プログラムの行ごとのデバッグを可能にします。この情報は.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) 


この関数は、エラー時にファイルインデックスまたはDW_DLV_NOCOUNTを返します。
行番号に関する情報を作成するために、3つの関数dwarf_add_line_entry_b、dwarf_lne_set_address、dwarf_lne_end_sequenceがあります。これらについては以下で検討します。
プロシージャのデバッグ情報の作成は、いくつかの段階で行われます。


プロシージャシンボルの作成

手順シンボルは、上記の「.symtabセクション」セクションで説明したように作成されます。その中に、プロシージャシンボルは、これらのプロシージャのソースコードが配置されているファイルのシンボルが散在しています。最初にファイルシンボルを作成してから、プロシージャを作成します。この場合、ファイルは最新になり、次のプロシージャが現在のファイルにある場合、ファイルシンボルを再度作成する必要はありません。

属性を持つプロシージャノードの作成

最初に、dwarf_new_die関数(「ノードの作成」セクションを参照)を使用してノードを作成し、DW_TAG_subprogramをタグとして指定し、コンパイルユニット(グローバルプロシージャの場合)または対応するDIE(ローカルの場合)を親として使用します。次に、属性を作成します。

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) 


この関数は、エラー時にDW_DLV_BADADDRを返します。

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) 


この関数は、エラー時に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) 


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) 


たとえば、コンパイラはプロローグにプロシージャを作成し、そのプロシージャのレジスタ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_Unsigned dwarf_lne_set_address( Dwarf_P_Debug dbg, Dwarf_Addr offs, Dwarf_Unsigned symidx, Dwarf_Error *error) 


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) 


この関数は、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)であり、実際には、分岐、プロシージャ、および算術演算を備えた別個の言語です。この言語を完全にレビューするのではなく、実際にいくつかの指示にのみ興味があります。

アドレス式を作成するには、最初に空の式(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) 


この関数は、エラー時に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) 


この関数は、エラー時に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) 


関数は、エラー時に属性記述子またはDW_DLV_NOCOUNTを返します。
変数(およびプロシージャパラメータ)と定数は、それぞれタグDW_TAG_variable、DW_TAG_formal_parameter、およびDW_TAG_const_typeを持つ通常のノードです。次の属性が必要です。



デバッグ情報を含むセクションの作成

デバッグ情報ツリーのすべてのノードを作成したら、それを使用してエルフセクションの形成に進むことができます。これは2段階で発生します。

機能
 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) 


この関数では、次のことを行う必要があります。

完了後、dwarf_transform_to_disk_form関数は作成されたセクションの数を返します。次の手順に従って、セクションごとに0からサイクルを実行する必要があります。


ライブラリでの作業を完了する

セクションが形成されたら、libdwarf関数dwarf_producer_finishを終了できます。
 Dwarf_Unsigned dwarf_producer_finish( Dwarf_P_Debug dbg, Dwarf_Error* error) 

この関数は、エラー時にDW_DLV_NOCOUNTを返します。
この段階でのディスクへの書き込みは実行されないことに注意してください。記録は、「ELFの作成-ファイルの記録」セクションの機能を使用して行う必要があります。


おわりに


以上です。
繰り返しますが、デバッグ情報の作成は非常に広範なトピックであり、カーテンを開くだけで多くのトピックには触れませんでした。希望する人は無限に行くことができます。
質問があれば、私はそれらに答えようとします。


参照資料


ELF



ドワーフ

Source: https://habr.com/ru/post/J199490/


All Articles