Explore "Full-Stack" in depth!

情報系の専門学校で、今は機械学習に的を絞って学習中。プログラミングを趣味でやりつつ、IT系のあらゆる知識と技術を身に付けるべく奮闘中。

オブジェクトファイルのシンボルテーブルを最低限理解する。

目次

概要

お久しぶりです。
最近やりたい事が沢山あってブログをかけずにいましたが、

に述べている通りです。

今日はELFバイナリに埋め込まれるシンボルテーブルについて軽く解説しようと思います。
自作コンパイラとかやってる人には ほぼ必須の知識ですし、
バイナリ解析( Exploitation勢含む)が好きな人には楽しいお話だと思います。

今回の記事を作成する上で、

docs.oracle.com

こちらのサイトを参考にしました。
ご存知Oracle社のドキュメントなので安心出来ますね。

解説中に登場するソースコードは基本的にGolangで記述されています。
対応する構造体等は /usr/include/elf.h に記載されているので参考にしてください。

対象読者

  • バイナリ解析大好きな人
    • でも触りはじめたばかりであまり良くわからない人
  • ネイティブコンパイラの自作やってる人
    • アセンブリコードの生成だけではなく機械語まで吐き出したい人
    • 正直 この方面の人の為の記事

本題

ELFフォーマット復習

まず前提として、ELFバイナリの形式を確認しましょう。

f:id:orangebladdy:20190516173803g:plain

こちらのサイトより引用させて頂きました。

ld等のリンカはセクションごとに解析を行い、
最終的に一つの 実行形式を生成します。

OSに組み込まれた ELFローダはセグメントごとに解析し、
プログラムを メモリ上の適切な位置 にロードし実行します。

注意
メモリ管理の文脈で セグメント・セグメンテーションという単語をよく目にしますが、
ELFバイナリでいう セグメントとは少し意味が異なるので注意してください。

各セクションは機械語列で構成されており、
セクションの情報はファイル末尾に( オプションで )付属する
Section Header Table上の各エントリが保持しています。

type Elf64_Shdr struct {
    Name      uint32 //セクション名。後述
    Type      uint32 //セクションの種類
    Flags     uint64 //セクションに許すフラグ
    Addr      uint64
    Offset    uint64
    Size      uint64
    Link      uint32
    Info      uint32
    Alignment uint64
    EntrySize uint64
}

セクション名のメンバが 非符号付き整数 付き整数であることに違和感を持った方もいるかもしれません。
これは .shstrtabセクション上のインデックスを表しています。
.shstrtabセクションとは、 セクション名を保持する文字列テーブル です。

あるC言語のプログラムをhexdumpすることでその存在を確認できます。

f:id:orangebladdy:20190516175034j:plain

この表現方法は後述するシンボル名も採用しているため解説が必要でしょう。

文字列表現は、
(null byte) <section name1> (null byte) <section name2>のようになっています。

セクションヘッダテーブルの開始位置はELFヘッダ( 64bitであればバイナリの先頭64バイト )に
格納されています。


シンボルテーブル

今回深掘りする.symtabセクションもセクション列のうちの一つであり、

  • 変数
  • 関数
  • ファイル

等の情報を格納するセクションになっています。
シンボルテーブルは次に示す構造体で管理されています。

動的リンクされたファイル( DYN )については`、 .dynsym`セクションに、プログラムで使用されているシンボルの情報が格納されています。

type ELF64_Sym struct {
    Name  uint32 //シンボル名
    Info  uint16 //後述
    Other uint16 //後述
    Shndx uint16 //どのセクションに属したシンボルかを表現するインデックス
    Value uint64 //後述
    Size  uint64 //シンボルが持つバイト数
}

特筆すべきフィールドについて解説をしていきます。

シンボル名 name

これは .strtabという、
シンボル名を格納するセクション のインデックス値を指しています。

readelf -S a.out | grep -A20 .strtabの実行結果を示します。

f:id:orangebladdy:20190516183142j:plain

ここでいう0x201はセクションのサイズを示し、
0x1628がセクション列先頭からの位置を示します。

シンボルの情報を格納する info

まずはreadelfコマンドを使って、
実際にシンボルテーブルの内容を見てみます。

f:id:orangebladdy:20190516183729j:plain

f:id:orangebladdy:20190516183743j:plain

なにやらいろいろと情報が出てきています。

少し複雑に見えますね。

Infoメンバは、
シンボルの情報を格納しています。

具体的には、

  • バインディング…リンクの可視性と動作
  • タイプ…関連付けられた実体の分類

等です。

ST_BINDというフィールドとST_TYPEというフィールドを、

func NewInfo(bind uint16, ty uint16) uint16 {
    return ((bind << 4) + (ty & 0xf))
}

のように計算して、
ELF64_Sym.Infoメンバに格納する事で実現出来ます。

抽象的で分かりづらいですよね。
実際にコードを見てみましょう。

ST_BIND

   /* st_bind */
    STB_LOCAL  = 0
    STB_GLOBAL = 1
    STB_WEAK   = 2
    STB_LOOS   = 10
    STB_HIOS   = 12
    STB_LOPROC = 13
    STB_HIPROC = 15

上記のように定義された定数のどれかに当てはまるように、
各シンボルの情報を分類するのです。

次のようなコードをアセンブルし、
オブジェクトファイルを生成してみます。

int x = 300;
int y = 700;
int main(void){ return x + y;}

このコードで言うところのx,yはいわゆる グローバル変数ですよね。
gcc -c c.cとして生成したオブジェクトファイルのシンボルテーブルを見てみましょう。

f:id:orangebladdy:20190516184806j:plain

BINDのカラムを見る感じ、
定義したグローバル変数の値が GLOBALとして表現されています。

int main(void){ 
    int x = 300;
    int y = 700;
    return x + y;
}

のようにすると、このシンボルテーブルには表示されなくなります。
main関数内でしか生存しない変数なので。

ここで 「上記コードのx,yはローカル変数なのにLOCALがつかないの?」と思った方もいるかもしれませんが、
スコープがmain関数に限定されているような変数は 他ファイルに影響を与えません

LOCALバインドが付与されるようなケースは、

static int x;
static int y;
int main(void) {
    x = 300;
    y = 700;
    return x + y;
}

ですね。

Symbol table '.symtab' contains 11 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS c.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
     5: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    4 x
     6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    4 y
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    7
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
    10: 0000000000000000    40 FUNC    GLOBAL DEFAULT    1 main

のようになります。

残りの定数についても触れておきます。

  • STB_WEAK…ウィークシンボル。GLOBALと似ているが、GLOBALより優先順位が低い
  • STB_LOOS,STB_HIOS…OS固有のセマンティクスの為に予約
  • STB_LOPROC,STB_HIPROC…プロセッサ固有のセマンティクスのために予約

STB_WEAKSTB_GLOBALの違いについて、
こちらのドキュメントから引用します。

リンカーがいくつかの再配置可能オブジェクトファイルを結合する場合は、同じ名前を持つ複数の STB_GLOBAL シンボルは定義できません。ただし、定義された大域シンボルが存在している場合、同じ名前のウィークシンボルが現れてもエラーは発生しません。リンカーは大域定義を使用し、ウィーク定義を無視します。

同様に、共有シンボルが存在している場合にそれと同じ名前のウィークシンボルが現れても、エラーは発生しません。リンカーは共通定義を使用し、ウィーク定義を無視します。共通シンボルは、SHN_COMMON を保持する st_shndx フィールドを持ちます。「シンボル解決」を参照してください。

リンカーがアーカイブライブラリを検索すると、未定義または一時的な大域シンボル定義が存在するアーカイブメンバーが抽出されます。メンバーの定義は、大域シンボルまたはウィークシンボルになります。

リンカーはデフォルトでは、未定義のウィークシンボルを解決するためのアーカイブメンバーを抽出しません。解決されていないウィークシンボルは、値 0 を持ちます。-z weakextract を使用すると、このデフォルトの動作をオーバーライドします。このオプションを使用すると、ウィーク参照がアーカイブメンバーを抽出できます。

まとめると、

  • 複数ファイルに同名の STB_GLOBALバインドを持つシンボルは存在できない
    • STB_GLOBALSTB_WEAKバインドを持つ同名シンボルがある場合STB_GLOBALが使用される

ということのようですね。

ST_TYPE

   /* st_type */
    STT_NOTYPE        = 0  /* Symbol type is unspecified */
    STT_OBJECT        = 1  /* Symbol is a data object */
    STT_FUNC          = 2  /* Symbol is a code object */
    STT_SECTION       = 3  /* Symbol associated with a section */
    STT_FILE          = 4  /* Symbol's name is file name */
    STT_COMMON        = 5  /* Symbol is a common data object */
    STT_TLS           = 6  /* Symbol is thread-local data object*/
    STT_NUM           = 7  /* Number of defined types.  */
    STT_LOOS          = 10 /* Start of OS-specific */
    STT_GNU_IFUNC     = 10 /* Symbol is indirect code object */
    STT_HIOS          = 12 /* End of OS-specific */
    STT_LOPROC        = 13 /* Start of processor-specific */
    STT_HIPROC        = 15 /* End of processor-specific */

のような定数が定義されています。

それぞれ次の意味を持ちます。

  • NOTYPE-種類が指定されない
  • OBJECT-配列、変数等のデータオブジェクトに関連付けられている
  • FUNC-関数他実行可能なコードに関連付けられている
  • SECTION-セクションに関連付けられる。
    • 再配置の際に必要とされる
  • FILE-オブジェクトファイルに対応するソースファイルの名前。
    • この後にSECTIONタイプを続けるのが慣習
  • COMMON-基本的にはOBJECTと同じであるが、初期設定されていないブロックを表す。

2つのフィールドを合わせてInfoメンバを構成している事がわかって頂けましたか?

それでは次に、Otherメンバについて見てみましょう。

シンボルの可視性 Other

定数を見てみます。

   /* st_other */
    STV_DEFAULT   = 0 /* Default symbol visibility rules */
    STV_INTERNAL  = 1 /* Processor specific hidden class */
    STV_HIDDEN    = 2 /* Sym unavailable in other modules */
    STV_PROTECTED = 3 /* Not preemptible, not exported */

このメンバが保有しているのはシンボルがどの程度参照を許すかの情報です。
基本的にはコメントアウトの文章が的確に表現しているとおもいます。

カーネルモジュールを自作したりしない限りその他属性を気にする必要はないと思うので、
ここではDEFAULT属性だけを扱います。

  • DEFAULT-ST_TYPEで指定したものに準拠する
    • GLOBAL,WEAKは実行ファイル・Shared-Objectファイル外から参照可能。
    • LOCALは参照不可。

シンボルの実体 Value

実行形式においては シンボルの実体が格納された仮想アドレス
(再配置可能)オブジェクトファイルにおいては 所属セクションのオフセットが保持されています。

例えば.dataセクションにx,y,zの8bit整数を格納していれば、

x.Valueは0( .dataセクション先頭 )、
y.Valueは1( xの次のバイト)、
z.Valueは2を示すことになります。

C言語ではint型は4バイトなので、0,4,8のように値を取っています。


総評

シンボルテーブルについての概要を解説しました。
ネイティブコンパイラを自作する人にとってはとても大事な概念なので、
ゆっくり勉強していければと思います。