ELFファイルを作る part4
以前にELFファイルを作るpart3までで、値を直接指定してのELFファイル作成を行った。また、簡易brainf*ckコンパイラを作る part3では命令部分のコードを外部から指定し、それを実行出来る実行形式ファイルの出力を行った。
これらはいずれも外部ファイルとのリンクは行わず、入出力といった操作もlibcではなくシステムコールを使うことでまかなっていた。
さて、最初の一歩として単純なものを作るにはそれでいいが、そこから先へと進み、多少なりとも複雑なものを作るとなると外部ライブラリとのリンクは必須となってくる。
そこで、まずはlibcとのリンクを目論むことに。だが、このリンクを一つしただけで、一気にELFファイルは複雑になる・・・。
ということで、まずはそのELFファイルをあれこれといじってみて、構造を把握してみようかと。
そのために考えた流れとしては
- libcを組み込んであるELFファイルをsection等に分割し、値を変えたりsection自体を削除してみたりして再構築して最低限必要なものや挙動を読み取ろう
- そのためにはELFファイルを分解するプログラム、また、その分解したものから再生成するプログラムが必要
- つまりはsectionのバイナリ値を元に、ELFファイルを生成するプログラムがあればよい。(それに分解して得たsectionを入れれば再生成できるはず)
- まずは極めて簡単なsectionから成る構成を考え、それが動くELFファイルを作ろう
ということに至る。
目標
ELFファイルを作るpart3で作成したELFファイルを、.textセクションの指定のみから全体を作り上げる
何をどう作ればいいのか?
実装するコード
実装するプログラム本体である.textセクションの内容は、先に作ったELFファイルより
_start: mov eax, 60 mov edi, 42 syscall
であり、16進数に直すと
0xb8, 0x3c, 0x00, 0x00, 0x00 # mov eax, 60 0xbf, 0x2a, 0x00, 0x00, 0x00 # mov edi, 42 0x0f, 0x05 # syscall
という対応になる。
このコードが動作するよう、構築していく
ELFの構成
ELFの構成については余所でも触れられている所が沢山あるので、ここで細かくは触れない。
一応基本的な所だけ触れておくと、ELFファイルは主に
- ELFヘッダー
- プログラムヘッダー
- データ部分
- セクションヘッダー
の4つから成る。
データ部分は、プログラムヘッダーに管理される場合はセグメント。セクションヘッダーに管理される場合はセクションという領域毎に管理される。
これらの内、ELFヘッダーとデータ本体であるbody部分は必要だが、プログラムヘッダーとセクションヘッダーについては状態によっては必要では無いケースもある。この辺りはプログラムヘッダーはロードに使われ、セクションヘッダーはリンクに使われる事による。
実行ファイルが実際に実行されるまでには、要所として
- オブジェクトファイルの作成
- リンク
- 実行ファイルをロード
という流れがあるわけで、流れとしてはセクションの作成→セグメントの作成という順序になる。
今回の場合は.textセクション分のデータのみが決定していることになるので、これを元にセグメントの作成、プログラムヘッダーの作成、ELFヘッダーの作成を行っていくことになる。
プログラムヘッダーについてはリンク時に、ロードできるファイルを作成する過程で作成される事になるので、このプログラムヘッダーを自分で作成するということは、自力でリンクするとも言えなくもないかな?
Section Header
今回実装する.textセクション以外にも、sectionヘッダーには記述する必要のあるものが存在する。
以前にnasmで作成したELFファイルにもあったように、nullセクションヘッダーとshstrtblセクションがそれらである。nullセクションヘッダーは単純にsectionヘッダーの全要素が0x00で埋められているセクションというだけで、sectionヘッダー部の先頭にあればよい。
shstrtblは各セクションの名前を保持している訳だが、一見「必要」ではなさそうなこれらは、後でセグメントを作成する際にリンカスクリプトで使われることになる。
shstrtblのデータ部はbody部分の後ろに配置されることが多いようだ。但し、この領域は最終的にロードされる部分には含まれる必要もなく、その為、このセクションはアドレスの指定等を持たない。
Program Header
プログラムヘッダーとセグメントの作成は、リンカスクリプトによって制御される。
手元の環境では、リンカスクリプトは /usr/lib64/ldscripts/elf_x86_64.x にあった。これにより、どのセクションをどのような順序でセグメントとして配置していくかが決定される。
今回作成する内容では、作成対象となるセグメントはLOADくらいのもので、あとはそれと別にプログラムヘッダーが置いてある領域自体のセグメント(PHDR)があるくらい。
これらのセグメントの対応を表すと、こんな感じ
見ての通り、セグメントは重複する範囲を持つことがある。この場合、PHDRセグメントはLOADセグメントに内包されることになる。
実装
現状ではとりあえず動いたって言う程度なので、コードの汚さ等はご愛敬。
今後直して行きます故に。
sectionの作成
sectionを作成するにあたってsectionの持つ要素を考えてみると
- sectionヘッダー
- sectionデータ
- section名
がある。
ということで、これらを保持するsectionクラスを作成し、また、複数のsectionクラスを管理するSectionControllerクラスを作成した。また、sectionヘッダについての設定等もやりやすくするため、コレについてもShクラスを作成。
使ってみた所はこんな感じ
name = '.text' byteList = [0xb8, 0x3c, 0x00, 0x00, 0x00] byteList += [0xbf, 0x2a, 0x00, 0x00, 0x00] byteList += [0x0f, 0x05] sh = Sh() sh.set('type', 1) sh.set('flag', 6) sh.set('size', len(byteList)) sh.set('address_align', 1) sh.set('entry_table_size', 0) sctCtrl = SectionController() sctCtrl.append(Section(byteList, name, sh))
ひとまずは.textセクション一つだけだが、複数セクション必要な場合は同様にsctCtrlへ追加していく。
更に、ここでsctCtrl.append()したsection以外に、特殊なsectionとしてnullセクションとshstrtblセクションがあるのは先に述べたとおり。
nullセクションはいいとして、このappendが終わった段階でshstrtblセクションは作成することが出来る。よって
sctNull, sctList, sctStr = sctCtrl.getSectionList()
これで必要なセクションは確保出来た。
segmentの作成
先に取得したセクションからsegmentを作成する。
さて、いざ作成しようとしたときに、ここで一つ疑問が。sectionをどういう順序でどういう属性で、どういうセグメントのタイプになるようにすればいいのか?
具体的にはsectionの持つタイプと属性を元に、そのsectionが配置されるsegmentのタイプと属性が決まればいいはず(多分)。先の例で
sh.set('type', 1) sh.set('flag', 6)
と、していたことから、今回作成した.textのsectionはPROGBITSタイプでAX属性を持つことが読み取れる。
さぁ、この型と属性は一体どういったsegmentの型と属性に割り当てられればいいのか?
これまた先に述べたように、通常はこの処理を行うということはリンクするという事になるわけで、この対応付けはリンカスクリプトの指示によって行われる。
手元の環境では、リンカスクリプトのパスは /usr/lib64/ldscripts/elf_x86_64.x にあった。
本当はこれを読み解けばsegmentを作成できるのだろうが、いまいちまだちゃんと把握しきれていない・・・。だが、これに書かれているsection名の順にsegmentは構成されるようである。
とりあえず、.textセクションはLOADタイプに入れられるのは間違いない訳だし、属性についてはAX(=Allocate, Execute)が許可されているということでsegmentの方ではreadとexecuteが使えれば問題ないだろう。
ここの対応付けについてはoracleのこのページを読めば書いてありそうな気がするが、ちょっと手が回ってないので後回し。
といった決め打ちも含めつつ、segmentの作成を実行する。
segCtrl = SegmentController() sctList = segCtrl.makeSegment(sctList) phSeg, segList = segCtrl.getSegments()
この段階で配置するべきアドレスやオフセットが判明するので、それを元にそれぞれのsectionヘッダーが持つべきアドレスとオフセット値を書き込んでいたりする。
これが終わればあとはELFヘッダーを適宜作成し、つなぎ合わせれば完成だ。
実行形式ELFの作成
最後はこんな感じ
we = WriteElf() we.setSection(sctNull, sctList, sctStr) we.setSegment(phSeg, segList) we.setStartAddr(segCtrl.getStartAddr()) we.make()
ここまでに作成したsection, segmentを総動員。
実行されているWriteElfの中ではELFヘッダーを自作したりしている。
そして最後は
result = eh.output() + ph + body + sh p = (c_ubyte * len(result))() p[:] = result with open('write.out', 'wb') as fp: fp.write(p)
こんな感じ。
実行結果
出来上がったwrite.outを実行してみる
$ ./write.out $ echo $? 42
無事に指定したプログラムが動いているようだ。
課題
次は複数セクションから成るELF実行ファイルの作成を行うと共に、リンカスクリプト周りを一度しっかり抑えておく必要がありそう。
section -> segment時の型の対応を決定する方法はしっかり把握しないと、今後何も出来なそうだ。まずはそちらが優先か?