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時の型の対応を決定する方法はしっかり把握しないと、今後何も出来なそうだ。まずはそちらが優先か?
簡易brainf*ckコンパイラを作る part3
ここまでで触れていたもので、準備は整った。本題に戻って、brainf*ckのコードをプログラムに読ませ、そのコードの実行ファイルを生成する。
具体的にどうするかを攫っておくと、
- pythonでJIT(64bit版Linux環境) part3を元にして、読み込んだコードをネイティブコードに変換する
- 簡易brainf*ckコンパイラを作る part2で生成していたELFのメインコード部分を上で作ったコードで置き換える
- これらに応じて各ヘッダ部の値を整える
を行う事になる。
目標
brainf*ckのコードをプログラムで読み込み、そのコードについての実行可能ファイルを生成する
コード
コードはpythonで記述してあります。
とりあえず動作することを目的に書いてあるので、最適化やコードの汚さは目をつぶっておいてください・・・
基本的にはJITでやってた頃のコードを使い回した形になっています。
異なる点としては、JITと異なりその場で実行する必要が無いのでその辺りが省かれているところが。
処理の流れとしては、248行目~で実行時に引数として渡されたbrainf*ckコードを読み込み、253行目でそのコードをネイティブコードへ変換したものの配列を取得。255行目でそのネイティブコードを包含するELF形式のバイナリ値を持った配列を作成している。
以降はそれをファイルに出力しているが、ここはstruct.pack()を使ってファイルに書いてもよかったが、JITの時にデバッグ用として出力していたいのがあったのでそれをそのまま利用。
残りの部分は地道にバイナリ値を設定しているだけなので特に注釈を入れるような所は無いかと。
注意としては、translate関数内の処理について、JITの時はpythonコードの実行処理内にbrainf*ckのコードが関数として実行されるような状況であったため、最初にいくつかのレジスタをpushし、最後にはpopした上でret命令で終わるようになっている。
今回の場合は動作させる処理はbrainf*ckのコード以外には無く、同時に、処理を戻すべき場所も無いため、コードの最初にpushは不要かつ終了時はexitシステムコールで終わらせる必要がある。
実行
hello world
$ cat hello.bf >+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-]<.>+++++++++++[<+++++>-]<.>++++++++[<+++>-]<.+++.------.--------.[-]>++++++++[<++++>-]<+.[-]++++++++++. $ python bf_compiler.py hello.bf $ ./elf.out Hello World!
echo
$ cat echo.bf >>++++++++[->++++++++<]>>>>+++++++++[->++++++++++<]>[<<,[->+<<+<<+>>>]<<<[->>>+<<<]>>>>>[->+>>+<<<]>[<<[->+>>+<<<]>>>[-<<<+>>>]<<[[-]<->]>-]>>[-<<<+>>>]<<<<<<<[-<+<<+>>>]<[>>[-<+<<+>>>]<<<[->>>+<<<]>>[[-]>-<]<-]<<[->>>+<<<]>>>>><[[-]>++++++++++++++++++++++++++++++++>[[-]<-------------------------------->]<<]>>[-]<.>>] $ python bf_compiler.py echo.bf $ ./elf.out A A
conclusion
この他、いくつかbrainf*ckのコードを余所様より参照して試してみたけど、無事に動作してくれている模様。
非常に簡易なものではあるが、brainf*ckのコードから実行可能ファイルの生成、つまりはbrainf*ckコンパイラを作ることができた。
元々これを作り始めたのは丁度一年ほど前、コンパイラ実装会という勉強会に参加させていただいた事に端を発します。あれこれ寄り道してたせいもあって一年もかかってしまいましたが、一つの里程標まではなんとか辿り着けました。
例えば実行ファイルの生成一つとってもライブラリとのリンク等々、他にも課題が山積ではあるが、まずは今後のスタート地点までは来れた事が嬉しい。
簡易brainf*ckコンパイラを作る part2
part1に引き続き、もう一点必要なものがある。
brainf*ckはそれぞれの命令以外に、少なくともサイズ30000のバイトの配列を持つ必要があり、これは今までgasの記法を主に使っている所で見られた
.comm mem, 30000
に相当する。
この領域をELFファイルに直接持たせ、使用する必要がある。
目標
ELFファイルにbss領域を持たせ、その領域で値の読み書きをする。
コード
今までにあったELFとの違いは
- プログラムヘッダが2つある
- 実行コード部分でbss領域に書き込む処理を追加
- sectionヘッダーにbss領域についての記述を追加
といったところになる。
処理本体のコードではまず、bss領域のアドレスをrbxレジスタへ格納し(46行目)、その先頭アドレスへAsciiコードで"H"を表す0x48を格納している。
その後のwriteシステムコールを呼ぶ箇所で、出力対象文字列へのアドレス指定へbss領域を指定。最後に後始末としてexitシステムコールをよんで終了させている。
ここでrbxレジスタへ指定しているbss領域のアドレスは0x08248000で、この値はbss領域用のsectionヘッダーにて、sh_addrの値を$$ + 200000としている事により、orgディレクティブで指定している0x8048000から200000だけ進んだ位置がbss領域として確保されている為にこの値となる。
前述したように、bss領域についてsectionヘッダーを追加しており、また、bss領域はセグメントなのでprogramヘッダーも追加している。
ここでbss領域についてprogramヘッダーを定義する際にそれぞれの値をどう指定するかは、bss領域を使う単純なプログラムを書いて、そこから得られる実行ファイルのヘッダー情報を読み取って真似た。
例えば
$ cat bss.c int foo[200]; void _start(){ }
というようなbss.cを記述し
$ gcc -nostdlib bss.c $ readelf -all a.out
とすることで
Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000001e8 0x00000000000001e8 R E 200000 LOAD 0x0000000000001000 0x0000000000601000 0x0000000000601000 0x0000000000000000 0x0000000000000320 RW 200000 NOTE 0x0000000000000158 0x0000000000400158 0x0000000000400158 0x0000000000000024 0x0000000000000024 R 4 GNU_EH_FRAME 0x0000000000000198 0x0000000000400198 0x0000000000400198 0x0000000000000014 0x0000000000000014 R 4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 8
といった結果を得られる。同時に得られるsectionヘッダーの情報
[ 5] .bss NOBITS 0000000000601000 00001000 0000000000000320 0000000000000000 WA 0 0 32
より、bss領域のプログラムヘッダーは
LOAD 0x0000000000001000 0x0000000000601000 0x0000000000601000 0x0000000000000000 0x0000000000000320 RW 200000
であることが読み取れる。アドレスの開始位置とサイズが同じなので。
これらを参考に、ELFヘッダーの調整、programヘッダーの追加、sectionヘッダーの追加をすることでbss領域を確保することができる。
実行
$ nasm -f bin -o nasm.out elf64-bss.asm $ ./nasm.out H
期待する結果を得られ、また
$ readelf -all nasm.out ... Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .shstrtbl STRTAB 0000000000000000 000000e6 0000000000000016 0000000000000000 0 0 1 [ 2] .text PROGBITS 00000000080480b0 000000b0 0000000000000036 0000000000000000 AX 0 0 4 [ 3] .bss NOBITS 0000000008078d40 00001000 0000000000007530 0000000000000000 WA 0 0 16 ... Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000008048000 0x0000000008048000 0x00000000000001fc 0x00000000000001fc R E 200000 LOAD 0x0000000000001000 0x0000000008248000 0x0000000008248000 0x0000000000000000 0x0000000000007530 RW 200000 ...
期待されるセグメントがbss領域として動作していると思われます
簡易brainf*ckコンパイラを作る part1
pythonでJIT(64bit版Linux環境) part3でも軽く触れていたが、ここまでで作成していたELFファイルや、JITでやっていた事をまとめると、brainf*ckのコードを読み込ませてELFの実行ファイルを作成させる事ができる。要はbrainf*ckコンパイラを作成できる。
ただ、その前にちょっとだけ、ここまででは不足している要素を補足する。
目標
「ELFファイルを作る」で作成していたELFでは外部ライブラリとのリンクを考慮していない。だが、brainf*ckの仕様として、文字の出力と入力が出来る必要がある。それ故、これらの実現の為に両者をシステムコールで直接実行出来るようにする。
コード
※先ほどまではnasmで書いていましたが、これらについてはgasの記法になっています
文字の出力
出力についてはwriteシステムコールを呼び出すことで実現できます。x86-64環境下でのシステムコールの呼び出し方は、raxレジスタへ呼び出したいシステムコールの番号を格納し、更に、その際に渡したい引数を特定のレジスタへ格納します。
システムコールの番号については、自分の環境下では /usr/include/asm-x86/unistd_64.h に記述があり、writeシステムコールは
#define __NR_write 1
より、1であることが読み取れます。
また、このシステムコールが必要とする引数はmanコマンドでwriteシステムコールについて見ると
$ man 2 write ... SYNOPSYS # include <unistd.h> ssize_t write(int fd, const void *buf, size_t count); ...
とあるので、順に、ファイルディスクリプタ指定、出力対象文字のアドレス、出力する文字数を渡すことになります。
コード中のコメントにもあるように、第一引数はrdi、第二引数はrsi、第三引数はrdxへ格納します。ediやedxを使っているのは単に、あとで生成するネイティブコードのバイト数を減らすためです。
ファイルディスクリプタの値については、シェルをいじっている時にもよくリダイレクト先として指定するあれらの値なので
stdin | 0 |
stdout | 1 |
stderr | 2 |
となります。
さて、コードを見てみると、6行目でまずbss領域に確保されているメモリのアドレスをrbxへ格納し、次行でその先頭位置に0x41(=A)を格納しています。
それ以降はシステムコール呼び出しの為の処理となり、内容はコメントに記述してあります。そして、最後にsyscall命令を実行することで、これらの指定に応じたシステムコールが呼び出される事になります。尚、syscall命令はx86-64での命令で、32bit環境下では別の命令を使うことになります。
文字の入力
先ほどのwriteシステムコールでは文字を出力して終わりでしたが、今回は読み込みの為、正しく読み込めているかの確認のためにreadした値をwriteするといったコードになっています。
入力についてはreadシステムコールを使うことで実現できます。勿論、システムコールの実行手順等はwriteシステムコールの場合と同様なので、その辺りは省略。システムコール番号は
#define __NR_read 0
より、0であることがわかります。また、manコマンドより、コールに必要な引数を見ると
$ man 2 read ... SYNOPSIS #include <unistd.h> ssize_t read(int fd, void *buf, size_t count); ...
とあるので、この辺りもほぼwriteと同様ですね。ファイルディスクリプタの指定がstdin(=0)になっているくらい。
実行
write.sの実行
$ gcc write.s $ ./a.out A
echo.sの実行
$ gcc echo.s $ ./a.out F F
ということで、目的の結果を得られました。
ELFファイルを作る part3
part2では非常に単純化したELFファイルを作成しましたが、この状態から作成されるファイルに対してobjdump等を実行してもメインの実行コードについての情報が出てきません。
なので、今後進めていった際、不具合に遭遇した場合の問題解決を容易にするためにも、これにsectionヘッダーを追加してそれらの情報が出るようにします。
目標
part2で作成したELFファイルにsectionヘッダーを追加し、objdumpでメインの実行コード(_start配下に記載される箇所)を検出できるようにする
実装
part2でのELFをベースとし、sectionヘッダー部を追加。それに会わせてELFヘッダー部も調整する。
コード
sectionヘッダーの各バイトへ格納する値を示す構造体定義は、programヘッダーと同様に /usr/include/linux/elf.h へ記述があります。
typedef struct elf64_shdr { Elf64_Word sh_name; /* Section name, index in string tbl */ Elf64_Word sh_type; /* Type of section */ Elf64_Xword sh_flags; /* Miscellaneous section attributes */ Elf64_Addr sh_addr; /* Section virtual addr at execution */ Elf64_Off sh_offset; /* Section file offset */ Elf64_Xword sh_size; /* Size of section in bytes */ Elf64_Word sh_link; /* Index of another section */ Elf64_Word sh_info; /* Additional section information */ Elf64_Xword sh_addralign; /* Section alignment */ Elf64_Xword sh_entsize; /* Entry size if section holds table */ } Elf64_Shdr;
これらの値についての説明は仕様書を見るなり、他のサイトを参照して貰った方が既に纏まってるので割愛。
個人的なメモとしては
- sh_name:このsectionの名前がstringテーブル(前述のelf64-section.asmでshstrtblとラベルが設定されている箇所)でどこに指定されているかを表すインデックス値(=stringテーブル先頭からのオフセット値)。|
- sh_addr:プロセスメモリ中に、このsectionヘッダが示す内容が保持される際の最初のアドレス。プロセスメモリ中へは現れないなら0を入れる。
- sh_offset:このsectionヘッダが何について書かれているか、その対象がどこにあるかを指すオフセット値。
といったように、基本的にsectionヘッダはどこかの領域に対しての説明を持つ事になり、オフセットやアドレス、サイズといった値の指定はその対象の領域についてを示す事になる。
sectionヘッダーは複数記述することができ、これらを記述する際、最初のsectionヘッダーは全て0で埋める事が決められている。
http://downloads.openwatcom.org/ftp/devel/docs/elf-64-gen.pdf (PDFファイル)より、
The first entry in the section header table (with an index of 0) is reserved, and must contain all zeroes.
elf64-section.asmでは、「各sectionヘッダーの名前を保持するテーブル」についてのsectionヘッダーは、sectionヘッダーの列挙内では2番目=indexは1の位置に記述されているので、ELFヘッダーのe_shstrndxに対応する箇所が1になっている。また、e_shnumも予約として0で埋められているsectionヘッダーを含めた数の3となっている。
実行
$ nasm -f bin -o nasm.out elf64-sectino.asm $ ./nasm.out $ echo $? 42
処理自体は期待される値を得られ、
$ objdump -S -M intel nasm.out nasm.out: file format elf64-x86-64 Disassembly of section .text: 0000000008048078 <.text>: 8048078: b8 3c 00 00 00 mov eax,0x3c 804807d: bf 2a 00 00 00 mov edi,0x2a 8048082: 0f 05 syscall
objdumpから実行されているコードの解釈についても情報を得られた。
ELFファイルを作る part2
part1では単純なELFファイルを作る際に埋め込む為の、極めて小さいアセンブラのコードを作りました。
なので、次はそのコードを入れる為の、ELFに則ったバイナリを作成する。
目標
ELFに則ったバイナリを作成し、その中にpart1で作成したコードを入れ、動作させてみる
実装
基本的にはこれも先の参考元と同様なコードを書く。
違いは64bit用にいくつかの値を変更してるだけ。
コード
db, dw, dd, dqは疑似命令(pseudo-instruction)で、それぞれが指定するバイト数だけの領域を確保してオペランドで指定した値を初期値として格納することになります。この辺りはこちらを参照
ELFヘッダーが各バイトへ格納するべき値を示す構造体の定義は/usr/include/linux/elf.hに記述されており、自分の環境では以下の通り。
typedef struct elf64_hdr { unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */ Elf64_Half e_type; Elf64_Half e_machine; Elf64_Word e_version; Elf64_Addr e_entry; /* Entry point virtual address */ Elf64_Off e_phoff; /* Program header table file offset */ Elf64_Off e_shoff; /* Section header table file offset */ Elf64_Word e_flags; Elf64_Half e_ehsize; Elf64_Half e_phentsize; Elf64_Half e_phnum; Elf64_Half e_shentsize; Elf64_Half e_shnum; Elf64_Half e_shstrndx; } Elf64_Ehdr;
コードの方にもコメントで対応が書いてあるので、特に説明は不要かと。
参考元と異なる所としては、e_machineの値がx86-64を示す0x3eになっているくらい。
セクションヘッダは無いのでe_shnumは0が入っており、後のコードにもセクションヘッダについての記述は無い。
プログラムヘッダの構造体についても同ファイルに記述がある。ELFヘッダは32bitと64bitでニーモニックの順番は変わらなかったけど、プログラムヘッダは順番が変わってる箇所があるので注意。
これらのヘッダに続いてpart1で作成したコードを挿入し、filesizeの値を指定して完了。
実行
$ nasm -f bin -o a.out elf64-base.s $ chmod +x a.out $ ./a.out $ echo $? 42
期待された値を得られた。
ELFファイルを作る part1
池袋バイナリ勉強会(7)に行ってきました。前回にも触れたように、先にJITで動作させていたバイナリ値をELFの中に組み込めればbfコンパイラが出来上がる。
というわけで、次のステップとしてELFファイルの作成へ。
目標
64bit用ELFの挙動を知るため、必要最小限のELFと何か命令を作成し、実行する。
実行する命令が長いとELFについて集中出来ないので、まずは出来る限り短いバイナリから成る命令文を作成してみる。
導入
さて、実際の所、ELFと言ったところで名前を知っている程度で内容についてはサッパリ。
適当に簡単なコードを書いてみてa.outを出力し、objdumpしてみたりもするけど、いきなりやるには容量が大きすぎてどう手を付ければいいのか・・・。
そこでアドバイスをいただき、こちらを参考に最小限のELFファイルを作成してみることにした。ちなみにこのサイトで注意する点は、基本的に32bitでの内容であることと、途中から基本的なELFを学ぶだけにはそぐわないトリッキーな内容に突入していくこと。
sample 1
tiny1.cの方は単純に42を返して終了するだけの、極めて単純なCの記述。これを実行して結果を取得してみる。
$ gcc -Wall tiny1.c $ ./a.out ; echo $? 42
参考にしたサイトと全く同様ですね。
echo $?は終了ステータスの値を取得します。ここでは42が返されていたので、その値が。
さて、続いて上記のtiny1.sのコード。こっちもほぼ同じだけど、最初のBITSで指定される値を32から64に参考元のコードを変更。実行してみる。
$ nasm -f elf64 tiny1.s $ gcc -Wall -s tiny1.o $ ./a.out $ echo $? 42
参考元のコマンドのまま、nasm -f elf tiny1.sとすると、32bitで解釈されてしまう。
自分の環境下では32bitの開発環境を入れてなかったのでエラーが発生した。ともあれこれも無事に動作してくれた。
sample 2
さて、これもほぼ参考元と同じ。
mainインターフェースを使う箇所を省き、自分で_startルーチンを記述することにより、出来上がるELFファイルを単純にしている。勿論これに伴って、ビルドするコマンドもtiny1.sの時とは変わってくる。
ここで参考元のコードとtiny2.sが異なる点としては、最後から二行目のmov命令を実行しているところ。32bitの場合はコールする関数へ引数を渡す場合はpush命令でスタックしていくが、64bitの場合は特定のレジスタへ値をmovして格納しておく事で指定する。
rdiではなくediを使っているのは、単にビルドして出来上がるバイナリの長さを無駄に長くしないため。
これを実行してみる。
$ nasm -f elf64 tiny2.s $ gcc -Wall -s -nostartfiles tiny2.o $ ./a.out $ echo $? 42
見込み通りに42を得られた。
_startルーチンを独自に実装してることになるので、gccの実行時に-nostartfilesオプションが加えられている。
sample 3
exitは結局の所ライブラリ関数なので、このコールはlibcと関連付く事になってしまう。その為、sample2までは出来上がるバイナリにリンクのための、今回欲しくは無い箇所が含まれてしまった。
ということで、ここでシステムコールを直接呼び出す事に。
参照元ではexitシステムコール番号が1となっているが、手元の環境では60となっていた。システムコール一覧については /usr/include/asm/unistd_64.h を参照。
#define __NR_exit 60 __SYSCALL(__NR_exit, sys_exit)
との記述がある。
また、システムコール呼び出しを実行する為に32bitではint 0x80という命令を実行させているが、64bit環境ではsyscallという命令になる。
そして、引数を渡すレジスタも32bitではebxとなっているが、64bitではsample2と同様にedi(又はrdi)。この引数を渡すレジスタ指定についてはこちらに詳しい。
さて、実行してみよう。
$ nasm -f elf64 tiny3.s $ gcc -Wall -s -nostdlib tiny3.o $ ./a.out $ echo $? 42
参考元にあるように、この時点でgccで無ければならない事はなくなっているので、直接リンカを呼び出してもOK
$ nasm -f elf64 tiny3.s $ ld -s tiny3.o $ ./a.out $ echo $? 42
同様の結果を得られる。
conclusion
ここまでで外部呼び出しや無駄なリンクを除いた、非常にシンプルな命令文が得られた。
これをELFに従って組み込んでやれば、非常に単純なELFファイルが出来上がる。