オブジェクトファイルのシンボルテーブルを最低限理解する。
目次
注意
readelf 実装
で検索すると未だに一番上に出てきてしまっているので注意.
これはelfについて全然詳しくない時期に書いたものです.
一応入門的内容についてまとめたものがあるので,
よろしければそちらを.
概要
お久しぶりです。
最近やりたい事が沢山あってブログをかけずにいましたが、
唐突にブログ書かなくなったのは書きたくなくなったからでも知見をひた隠しにしたくなったからでもなく、
— Drumato (@drumato) May 14, 2019
単純にもっとやりたい事が他に出来ているからです
に述べている通りです。
今日はELFバイナリに埋め込まれるシンボルテーブルについて軽く解説しようと思います。
自作コンパイラとかやってる人には ほぼ必須の知識ですし、
バイナリ解析( Exploitation勢含む)が好きな人には楽しいお話だと思います。
今回の記事を作成する上で、
こちらのサイトを参考にしました。
ご存知Oracle社のドキュメントなので安心出来ますね。
解説中に登場するソースコードは基本的にGolangで記述されています。
対応する構造体等は/usr/include/elf.h
に記載されているので参考にしてください。
対象読者
本題
ELFフォーマット復習
まず前提として、ELFバイナリの形式を確認しましょう。
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することでその存在を確認できます。
この表現方法は後述するシンボル名も採用しているため解説が必要でしょう。
文字列表現は、
(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
の実行結果を示します。
ここでいう0x201
はセクションのサイズを示し、
0x1628
がセクション列先頭からの位置を示します。
シンボルの情報を格納する info
まずはreadelfコマンドを使って、
実際にシンボルテーブルの内容を見てみます。
なにやらいろいろと情報が出てきています。
少し複雑に見えますね。
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
として生成したオブジェクトファイルのシンボルテーブルを見てみましょう。
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_WEAK
とSTB_GLOBAL
の違いについて、
こちらのドキュメントから引用します。
リンカーがいくつかの再配置可能オブジェクトファイルを結合する場合は、同じ名前を持つ複数の STB_GLOBAL シンボルは定義できません。ただし、定義された大域シンボルが存在している場合、同じ名前のウィークシンボルが現れてもエラーは発生しません。リンカーは大域定義を使用し、ウィーク定義を無視します。 同様に、共有シンボルが存在している場合にそれと同じ名前のウィークシンボルが現れても、エラーは発生しません。リンカーは共通定義を使用し、ウィーク定義を無視します。共通シンボルは、SHN_COMMON を保持する st_shndx フィールドを持ちます。「シンボル解決」を参照してください。 リンカーがアーカイブライブラリを検索すると、未定義または一時的な大域シンボル定義が存在するアーカイブメンバーが抽出されます。メンバーの定義は、大域シンボルまたはウィークシンボルになります。 リンカーはデフォルトでは、未定義のウィークシンボルを解決するためのアーカイブメンバーを抽出しません。解決されていないウィークシンボルは、値 0 を持ちます。-z weakextract を使用すると、このデフォルトの動作をオーバーライドします。このオプションを使用すると、ウィーク参照がアーカイブメンバーを抽出できます。
まとめると、
- 複数ファイルに同名の
STB_GLOBAL
バインドを持つシンボルは存在できないSTB_GLOBAL
とSTB_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
のように値を取っています。
総評
シンボルテーブルについての概要を解説しました。
ネイティブコンパイラを自作する人にとってはとても大事な概念なので、
ゆっくり勉強していければと思います。