おしながき

ELFファイルフォーマット

  • .eh_frameセクションの構造と読み方

DWARFファイルフォーマット

NCURSESライブラリ

  • NCURSES Programing HOWTO ワタクシ的ほんやく
    1. Tools and Widget Libraries
    2. Just For Fun !!!
    3. References
  • その他、自分メモ
  • NCURSES雑多な自分メモ01


最近の更新 (Recent Changes)

2019-09-24
2013-10-10
2013-10-03
2013-10-01
2013-09-29
目次に戻る:DWARFファイルフォーマット

TAG詳細(その04) インライン関数編 ※inlined_subroutine

というわけで、次は「インライン関数」です。 これ、C言語とかでは、高速化の1つのテクニック的な「おまけ?みたいな機能」の扱いを受ける位の状況で、代わりにまぁ「inline」って書くだけ、みたいな位のモンなんですが。。。ことDWARF上での表現は「そーとー難しい」です。
原文も、これだけの内容にしては、長く、そして英語が難しい。。。ので、どこまで訳せているか、あんま自信なしです。という前提で、以下デス。

インライン関数表現の概要

  • (C言語とかで)「inline」的なキーワードを付けて宣言した関数(インラインされる関数)は、「DW_TAG_subprogram」タグで表現されます。(つまり、一般の関数とおんなじ感じです)
  • 「DW_TAG_subprogram」なタグのDIE(つまり関数の情報)のうち、以下のどっちかに該当する場合は、DW_AT_inlineなAttributeを持ってます。
    • 1) 「inline」的なキーワードで明確にインラインしてねって頼んだ場合 (まぁ、この場合はDW_AT_inline付いてもらわんと困るワナ)
    • 2) コンパイラが(勝手な判断?ありがたい最適化判断?)によって暗黙にinline関数としてしまった場合
  • DW_AT_inlineなAttributeは、ソースコード上でのインライン宣言の有無、コンパイラが実際にインライン展開したかどうか、を示すモンで以下下表の値もつです。
    No. 属性名 インライン
    宣言の有無
    コンパイラが
    インライン展開
    したかどうか
    1 DW_INL_not_inlined 0x00 × ×
    2 DW_INL_inlined 0x01 ×
    3 DW_INL_declared_not_imlined ×
    4 DW_INL_declared_inlined


「抽象インスタンス」と「具体インスタンス」ってなに?

例えば、以下の様なケースがあったとします。(C言語で)

inline int funcA(int n)   {    /* 関数A */
    return n + 10;
}

int funcB(void)   {    /* 関数B */
   int  result;
   result = funcA(5);
   printf("%d", result );
   return result;
}
この時、

  • 関数A: インライン展開される側
  • 関数B: 関数A(funcA)をインライン展開する

という関係になるはずです。 ただし、これいっつも関数B内に関数Aが取り込まれるかというと、そうでもなくて、コンパイラの最適化(勝手、とも、都合、ともいう)で、せっかく関数Aに「inline」って教えてあげたにもかかわらず、ちゃっかり「インライン展開されずに、ふつーの関数Aとして使われる」的な事態にもなりえるのです。

ので、DWARFではこのケースでの「関数A」と「関数B内に展開される関数A」、それぞれの情報を持たせるようにできています。
そして、このケースで

  • 関数A(すなわち、インライン展開されてしまうかもしれない側) → 「抽象インスタンス」
  • 関数B(すなわち、インライン展開で他の(インライン宣言された)関数を取り込んでしまうかもしれない側) → 「実体インスタンス」

と定義してるみたいです。

以下、この「抽象インスタンス」「実体インスタンス」のそれぞれ、およびインライン展開される場合、されない場合、などでのこいつらのDWARF表現方法になりますんで、これは押えておく必要ありっぽいです。


抽象インスタンス

DW_AT_inlineを持っている(一般的にはDW_TAG_subprogramのタグを持つ?)DIE、つまり上記例での関数Aは、引数やら関数内の変数宣言やら、を示す「子DIE」を持ってることが一般的です。
んで、これらは(その内容次第だけど)、場合によっては孫、ひ孫、と子孫がツリーになることも、あるわけですね。
このとき、

  • この子孫のそれぞれを、「抽象インスタンスエントリ」
  • DW_AT_inlineを持っているDIE(親)を、「抽象インスタンスルート」
  • DW_AT_inlineを持ってるDIE(親)から、以下続く子、孫、ひ孫。。。のツリーを、「抽象インスタンスツリー」

と呼んじゃうことにします。(これは、原文での「決め」なんで、まぁこーいうもんだってことで理解です)

ただし、1つ例外があります。
インライン関数は、例えば上の例で、仮に関数C(こいつもinline指定)があって、関数A内からこれをcallしていたケースで、コンパイラ君がお気を使ってくれて、わざわざ関数B内でのコール箇所に、関数Aはもちろん関数Cまで全部展開しきった状態にしてくれたとします。
このケースでちょっと気づきたいのは、「インライン関数がネスト(入れ子)」になる可能性があるってことです。
この時、関数CにもAにも「抽象インスタンスツリー」ができるわけですが、関数A内の「抽象インスタンスツリー」の子か孫か、どっかに「関数Cの抽象インスタンスツリー」が単純に考えると、登場する可能性があります。
これ自体はやむを得ないのですが、この時、「関数Aの抽象インスタンスツリーは、関数Cの抽象インスタンスツリーの部分を除いた、子や孫、子孫だけのツリー」になるってのが、例外です。


んで、この「抽象インスタンスツリー」内の「抽象インスタンスエントリ」(つまり、子や孫、子孫になるDIE)には、(本来そのDIEが持つTAGとしては、持っているべきAttributeであっても)インライン展開されるか、(コンパイラ都合で)インライン展開されないか、によって値が変わってしまうAttributeは含まないことがある。という重大な注意事項があるです。
(原文には、「含んではいけない!」ということはない、とも明記してあるので注意です)
例えば、「DW_AT_low_pc」「DW_AT_high_pc」「DW_AT_ranges」「DW_AT_entry_pc」「DW_AT_location」「DW_AT_return_addr」「DW_AT_start_scope」「DW_AT_segment」など、マシンコードアドレスの範囲を示すAttributeが好例です。
(こいつらは、展開されない場合はちゃんとマシンコードアドレス空間内のアドレスを貰え定住できますが、インライン展開された場合は、他の関数のマシンアドレス内に埋め込まれるわけですから、アドレスは住み込み先の住所になるわけです)
なお、これは上でも書いた様に、ソースコード上「inline」と書かなくても、コンパイラが勝手にあなたはinlineなので、と決めつけて、住み込みにした場合も同様です。

※本当は、原文のこの後にもちょっと注意?的な文があるのですが、関係代名詞が複雑すぎて翻訳できません。。。orz (原文 pp52一番上)


実体インスタンス(インライン展開されちゃった場合)

インライン展開可能な関数のインライン展開部分は、「DW_TAG_inlined_subroutine」なるTAGでその情報が示されます。つまり、上の例では、関数B内で関数Aを呼び出してる部分ですね。
ということで、まずは、このTAGの特徴や注意事項なんか、箇条書きにしてみますです。

  • インライン化が発生した箇所を表現するTAGの直下の子DIEとして表現される、と原文書いてあります
    • ので、よーはDW_TAG_subprogramなDIE(=関数)の直下の子DIEとして、「DW_TAG_inlined_subroutine」がくる、ってことになるはずです
  • 「DW_AT_low_pc & DW_AT_high_pc」のコンビか、「DW_AT_ranges」なAttributeを持ってます。
    • で、これらの値に、インライン展開後のマシン命令のコードアドレスが入っています。(前者コンビなら、範囲が連続していて、後者なら連続していない場合)
  • 「DW_AT_entry_pc」なAttributeを持っていれば、こいつには「(インライン展開された関数部分の)インライン展開後の最初のマシン命令アドレス」がはいっています。
  • 「DW_AT_call_file」「DW_AT_call_line」「DW_AT_call_column」
    • インライン展開が発生したソースコード上の場所を、それぞれ以下として表現しています。
      • 「DW_AT_call_file」 → ソースファイル番号(.debug_line上のファイルテーブルの番号?)
      • 「DW_AT_call_line」 → ソースファイルの行番号
      • 「DW_AT_call_column」 → ソースファイルの列番号
    • 原文にも注意書きがありますが、インライン展開が発生した箇所(インライン関数をcallしている箇所)のファイル番号、行/列番号です。 インライン関数を利用している関数(=関数B自体)のファイル、行/列ではないので注意!

次に、また面倒な話なんですが。。。

  • 「DW_TAG_inlined_subroutine」タグのDIEの子DIE → 「実体インラインインスタンスエントリ」
  • 「DW_TAG_inlined_subroutine」タグを持つDIE → 「実体インラインインスタンスルート」
  • 「DW_TAG_inlined_subroutine」タグを持つDIEと、その子、孫、ひ孫、子孫。。。のDIE全部 → 「実体インラインインスタンスツリー」

と呼ぶことにします。
ここで、1つ注意したいのが、この名称っす。これ「実体インラインインスタンス」と「インライン」が明記されています。これは、コンパイラ都合でインライン展開されなかった場合、つまり「アウトライン」になってしまった場合と区別するために、こーしています。

んで、こちらも「抽象インスタンスxxx」とおんなじで、他の「実体インラインインスタンスツリー」にネストされることあありますが、こちらも抽象インスタンスと同様で、「実体インスタンスツリー」の子、孫。。。には、「実体インスタンスツリー」は含まないものとします。


「抽象インスタンス」と「実体インラインインスタンス」の関係

この2つですが、まぁなんとなく想像付く部分もあるわけですが、やっぱり関係があります。ということで、どーいう関係なのかちょっと覗きます。

  • まず「実体インラインインスタンスツリー」は、ある1つの「抽象インスタンスツリー」とのみ関係を持っています。
    • これは、ある意味とーぜんです。(抽象インスタンスである、関数Aを関数Bがインラインしちゃってるわけですが、この関数Aは、また別の関数DやEでもインラインは可能なので)
    • 逆は成立しません! これは、関数B内で関数Aを取り込んでいる部分(実体インラインインスタンスツリー)が、関数Aと全く別の関数Fの両方を、1箇所で2つの関数をインラインしていることはないですので
  • 「実体インラインインスタンスエントリ」では、(抽象インスタンスの対応するエントリには存在していても)実体インスタンスでの情報を特定できないAttributeを削除したりすることがあるです。
    • これでは困るので、解決策として、「実体インラインインスタンスエントリ」と関連する「抽象インスタンスエントリ」へのリンク情報を「DW_AT_abstract_origin」の値として持つようになっています。
      • これによって、ドロップされちゃったAttributeは、「DW_AT_abstract_origin」の値から、「抽象インスタンスエントリ」内のAttributeを検索するようにして取得できるのですね。
  • 逆に「抽象インスタンスでは(DW_AT_low_pcのように、特定できないため)ドロップしていたAttributeのうち、「実体インラインインスタンス」では特定可能なものは、「実体インラインインスタンスエントリ」内に含む必要があります
    • DW_AT_low_pcなど、コードアドレスものは、この決まりによって、「実体インラインインスタンス」に含まれるので、最終的にソースとマシン命令の突合はインライン展開された関数でも可能になるワケです。
  • もし「実体インラインインスタンスツリー」内のDIE(実体インラインインスタンスエントリ)が、ソースファイルの宣言箇所との関連を示す情報を持っているなら、それらの情報はインラインされる側の関数(=抽象インスタンス)のファイル番号、行/列番号となる
    • これはつまり、上の例でれば、関数B内で関数Aがインライン展開された前提で、そのインライン展開された関数AのDIE(=実体インラインインスタンスエントリ)にソースの行/列番号の属性を含んでいたら、この行や列は、関数A自体のソース行/列になるよ、ってことです。
      これは、なんかヘンなようにも聞こえますが、こうしてもらわないと、デバッガでインライン関数を追えなくなるので、正しいと思いますです。
  • DW_AT_abstract_originを介してペアになっている「実体インラインインスタンスエントリ」と「抽象インスタンスエントリ」は、どちらも同じDW_TAG_xxxを持っています。
    • 例えば、ローカル変数なら実体/抽象とも関連するDIEは「DW_TAG_variable」のエントリになるです。(まー、これはとーぜんか)
    • 但し、「実体インラインインスタンスルート」と「抽象インスタンスルート」の関係の場合だけは、成立しません!
      • 「実体インラインインスタンスルート」はDW_TAG_inlined_subroutine、「抽象インスタンスルート」はDW_TAG_subprogramを持っているからです。(上の説明のとおり)

これらの点を前提として、「抽象インスタンス」と「実体インラインインスタンス」の構造や内容は、ほぼ類似しています。(というか、そーでないと、困る!)
ただ、以下3点は、例外です。。。

  • 例外1) DW_AT_abstract_originなAttributeのみ持っていて、かつ子DIEを持たないor(持っていたとしても上の理由で)削除された「実体インラインインスタンスエントリ」は、削除されることがある
    • 子DIEや他のAttributeを持っていないということは、デバッグ情報としては意味ないです。ので、ムダだから消しちゃうよ。ってことみたいです。
      • 例:C言語なら、インクルードした型、構造体/共用体、クラスなど
    • もし「実体インラインインスタンスツリー」のDIEが関連するインライン関数で宣言されてるDIEを参照する必要があり、さらにその実体インラインインスタンスエントリが存在しない場合は、「抽象インスタンスエントリ」を見てください。(って書いてるけど、???ですな)
  • 例外2) DW_AT_nameなAttributeを持っていず、さらに他のDIEからの参照もされていない「抽象インスタンスツリー」のエントリと関連する「実体インスタンスツリー」のDIEは、削除されちゃうことがあるです。
    • これは、「抽象インスタンスツリー」のDIEが持つ付加的な情報が、「実体インスタンスツリー」では不要であるため、とのことです。
      • 例:抽象インスタンスツリー上(つまり、インライン宣言された関数上)で宣言された変数が、あるインライン展開先での実際の値が「定数」となった場合などです。(まーたしかにそーか)
  • 例外3) 実体インスタンスツリーは、あるインライン展開先では特定することができる(ため、実体側だけで生成される)新しいDIEを表現するために、抽象インスタンスツリーのDIEとは関連しないDIEを含むことがある。
    • このケースでは、抽象インスタンスツリーのDIEとは関連しないから、DW_AT_abstract_originなAttributeは持たないです。(そりゃそーだ) 代わりに、新たに生成されるDIEはこれらの情報を直接的に持つ必要があります。
    • これは、多くのインライン展開で必要そうでないDIEを抽象インスタンスツリー上から削除してよし、と認めることを意味しているです。また、想定からはずれた(一般的でない)インライン展開では、実体インスタンスツリーにDIEを追加していい、ということです。(直訳です。。。)

実体アウトラインインスタンス (インライン展開されなかった場合)

インライン関数は、コンパイラによるありがたい「最適化」の結果による判断や、時にはコンパイラの「一身上の勝手な都合」によって、インライン宣言してあげたのにインライン展開しないことがあります。
で、このケースになると、インライン関数は、通常ふつーの関数として存在し、コード領域上にもきちんとアドレスを陣取る訳です。
こーなった場合は、上の説明の「抽象インスタンス」と何にも変わらなくなるよーにも思えますが、同じく上の説明であげた通り抽象インスタンスにはDW_AT_low_pcなど、実コードアドレス等の情報がないも含めて、DWARFでは以下2つを並存させる形で扱ってるです。

  • あくまでインライン展開されるかどうか分からないことを前提とした情報をもつ「抽象インスタンス」
  • 結果としてインライン展開されず、別の関数として存在しているけどその存在の実体を表すための「実体アウトラインインスタンス」


ということで、以下はこの「実体アウトラインインスタンス」の説明でごわすです。

  • 「実体アウトラインインスタンス」のDWARFでの表現方法は、基本的には前で説明した「実体インラインインスタンス」と同じ
    • 「実体アウトラインインスタンス」自体のDIEは、DW_AT_abstract_originなAttributeを持っていて、こいつが関連する「抽象インスタンス」を指す点も、「実体インラインインスタンス」と全く同じです。
  • 但し、以下2点だけは「実体アウトラインインスタンス」と「実体インラインインスタンス」の「相違点」となるです。
    • 相違点1) インライン宣言された関数の「実体アウトラインインスタンス」の「実体アウトラインインスタンスルート」となるDIEのTAGは、インライン宣言された関数自体のタグとなる
      • つまり、一般的にはDW_TAG_subprogramとなる。これは、「抽象インスタンス」でのインライン関数のTAGと同じ。
        • 実体インラインインスタンスは、DW_TAG_inlined_subroutineです。このTAGは実体アウトラインインスタンスでは使いません。これが決定的に異なるです。
        • でもよくよく考えてみると当り前で、「実体アウトラインインスタンス」の時は、インライン関数が単独でコード空間に存在(居住)しているわけですから、まぁふつーの関数(DW_TAG_subprogram)となるですわな。
    • 相違点2) 「実体アウトラインインスタンスツリー」のルートDIEは、通常、対応する「抽象インスタンス」のルートDIEと同じ親DIEによって所有されるです。
      • これは、つまり、例えばあるCソースhoge.c内にインライン関数宣言した関数Aがいて、この関数がコンパイラの気分でインライン展開されなかったとき、
        抽象インスタンス、および実体アウトラインインスタンスは、どちらもDW_TAG_subprogramな関数として存在するわけ。
        んで、hoge.cは、DW_TAG_compile_unitになるだろーから、この2つは、DW_TAG_compile_unitが親、ってことtで正しそーですね。
      • なお、原文上は、「一般的に同じ親になる」が、必ず「同じ親でなければならない」ということはない、とも明記してあります。


ネストされたインライン関数

言語やコンパイラによっては、他の関数やサブルーチンをある関数やサブルーチンで論理的に入れ子(ネスト)することができるものがあるです。(何の言語か、忘れた。。。)
んで、さらに外部やネストされたサブルーチンのインライン化ができるものまであります。ので、ちょっと触れておきますです。

※あ、ちなみに「関数ネスト」は少なくともC言語(C++もそのはず)では、該当しないので、意図的に関数ではなく「サブルーチン」って書いています。


  • インラインサブルーチンをネストしているインライン展開しないサブルーチンに、ネストしたサブルーチンのインライン化情報を持たせるため、DWARFではネストされたインラインサブルーチンに対して「抽象インスタンスツリー」「実体インラインインスタンスツリー」の両方をこれまでの説明の通り持たせてます。
    • よって、基本はあんま変わらんってこと。
  • インラインされた側のサブルーチンでは、以下のルールが適用されます。
    • ルール1) ネストされたサブルーチンの「抽象インスタンスツリー」は、原則上記説明の通りに表現されるですが、「外部の抽象インスタンスツリーには含まれない」というルールだけはネストの場合は無視です
    • ルール2) ネストされたサブルーチンの「抽象インスタンスツリー」は常に外のサブルーチン(ネストしている側)の実体インスタンスツリー内からは削除されます。
      • つまり、2重のネストがなされているとき、1重目の実体インスタンスツリー内に、2重目の抽象インスタンスツリーが入る、なんてことが起こり得るけどそれは消すよ、って意味でしょうか?
    • ルール3) ネストされたサブルーチンの「実体インスタンスツリー」は、外のサブルーチン(ネストしている側)の抽象インスタンスツリー内からは削除されます。
      • ルール2の逆ですね。
    • ルール4) ネストされたサブルーチンの「実体インスタンスツリー」は、外のサブルーチン(ネストしている側)の実体インスタンスツリーとして、これまでの説明の通りに表現されるですが、(ルール1と同様に)「外部の実体インスタンスツリーには含まれない」というルールが除外されます。
      • なお、これは「実体インスタンスツリー」が、実際にインライン展開された場合、またコンパイラ都合でインライン展開されなかった場合、いずれのケースでも適用されます。


※ネストされたインライン関数、についてはワタクシが関数ネストできる言語を知らないので、良くワカランのですが、まぁこげな感じで原文記載ありです。


目次に戻る:DWARFファイルフォーマット