前回は、jスクリプトを独自の形式のファイルにコンパイルすることに限定していました。これには特別なローダーが必要でした。 さらに、隣接するコマンドの分析が必要な実行可能コードの最適化がいくつかあると考えました。
さらに投稿:
- のぞき穴の最適化
- 標準機能
- ELFへの出力
- どのように機能しますか?
- どうしたの?
のぞき穴の最適化
計画された最適化を実装するために、最初に、一般的なベクトルコードを各コマンドの生成されたマシンコードの小さなベクトルに分割し、最後にそれらを結合します。
次に、2回のパスでは不十分です。最初のパスでは各コマンドのマシンコードを生成し、2回目では最適化を実行し、3回目では「研磨」してジャンプと行のオフセットを埋めます。 ジャンプオフセットの計算と同時に、可能であれば、
近いジャンプを短いジャンプに置き換えます。これにより、3回目のパスでコードがさらに削減され、さらに1回のパスが必要になります。 その上で、コードの削減により変更されたジャンプオフセットを再び修正します。
生成された
POP
の数を各チームに保存する必要があります。マシンコードだけでは、最後のバイトが
POP
命令であるか、単に値で一致するかを理解するのが難しいからです。 生成された
PUSH
の数を保存する必要はありません。マシンコードの最初のバイトは明確に解読されます。
struct commandn {
標準機能
前回は、標準関数
input,echoi,echos
の実装がブートローダーにありました。 今回はブートローダーはありません。 機能はどこにありますか?
原則として、生成された各バイナリにコードを埋め込むことができます。 しかし、それはいです。 代わりに、それらを個別の
.o
ファイルにコンパイルし、ファイルにリンクします。 このアプローチの利点は、プラットフォーム依存性の分離です:コンパイラーの観点からは、関数の呼び出しの方法にあるx86とx64のすべての違いは、プラットフォーム依存ライブラリに隠されており、プラットフォームに依存しないコードを生成します。
標準関数が
ESI
パラメーターを受け入れ、結果を
EAX
返すと便利です。 やってみましょう。 x64の実装は次のとおりです。
.global input,echoi,echos .text fd: .asciz "%d" fs: .asciz "%s" input: push %rax lea fd, %edi xor %eax, %eax mov %rsp, %rsi call scanf pop %rax ret echoi: lea fd, %edi echo: xor %eax, %eax jmp printf echos: movslq %esi, %rsi add %rbp, %rsi lea fs, %edi jmp echo
また、x86の場合は次のとおりです。
.global input,echoi,echos .text fd: .asciz "%d" fs: .asciz "%s" input: push %eax push %esp push $fd call scanf pop %eax pop2: pop %eax pop %eax ret echoi: push %esi push $fd echo: call printf jmp pop2 echos: add %ebp, %esi push %esi push $fs jmp echo
ELFへの出力
実行可能コードはほぼ準備ができています。 OSがリンクして実行できるように、「パック」するだけです。
メモリでは、コードは以前と同じ部分で構成されます:コード、データ、標準関数を呼び出すための通信領域。
.o
ファイル形式と直接実行可能ファイルの違いは、
.o
ではメモリ内のプログラム部分(
セクション )のアライメントが不明であることです。 したがって、コンパイラは、あるセクションから別のセクションのアドレスへのリンクを生成できません。 代わりに、コンパイラはリンカに指示を生成します-リンク時に計算するアドレスとその計算方法を示す
再配置 。 したがって、
ELF形式仕様の .o
ファイルは
、 再配置可能と呼ばれます。
データを2つのセクションに分割することが可能です。個別に初期化され、不変(通信エリアとライン)、個別に初期化されず、可変(注がれたレジスターのセル)。 しかし、x86では、両方のデータセクションのベースアドレスを常に保存するのに十分なレジスタがありません。つまり、呼び出しごとに再配置を生成しなければなりません。 あなたの人生を簡素化し、データの1つのセクションでうまくいきます。
同じ理由で、通信エリアを使用して標準関数を呼び出します。直接呼び出すには、呼び出しごとに再配置を作成する必要があります。 x86とx64で通信エリアを24バイトにしましょう-繰り返しますが、シンプルさとクロスプラットフォームです。 x64では、3つのポインター(
input,echoi,echos
へ)の配列になります。 x86では、各ポインターの後に4バイトのスタブが続きます。
常に4つの再配置があることがわかります。通信の分野における標準機能の3つのアドレス、およびプログラムの最初のコマンド
RBP/EBP
データのベースアドレスの
RBP/EBP
へのロード。 その結果、「再配置可能な」コードの生成は、前回の「ソリッドピース」の生成とほとんど変わりません。 いくつかの点で、それはさらに単純化されます:行がコードとは別に保存されるようになったため、解析段階でオフセットを記憶できます。 このようにして、コンパイルされたコードの出力の前に、「一時行識別子」とそれらのバインディングの段階を完全に取り除きます。 さらに、標準関数を呼び出すメソッドはプラットフォームに依存しなくなりました。
typedef std::map<std::string,int> stringmap; stringmap strings; int laststr = 24;
ELFの再配置には、
rel
または
rela
2つの形式があります。
rel
形式はよりコンパクトですが、リンカーは
rela
方が簡単です。 そのため、x64プラットフォームへの移行時に、
rel
deprecatedと宣言
されました 。 それでも、私のバージョンの
ld
は64ビットコードの
rel
をサポートしているため、
rel
正確に生成します。
8つの標準セクションを生成します:empty、
.shstrtab, .strtab, .symtab, .rel.text, .rel.data, .text, .data
。 ELFヘッダーと最初の6つのセクションには、事前定義されたコンテンツがあります。 生成されたコードに応じて、
.text
と
.data
のみ
.data
入力されます。
どのように機能しますか?
生成されたコードに追加する不変のヘッダーで、不明瞭な数字の束。 それらはすべてどういう意味ですか?
順番に行きましょう。
-
ELFMAG0,ELFMAG1,ELFMAG2,ELFMAG3
:ELFファイルを開始する4つの「マジックバイト」。 -
ELF_CLASS,ELF_DATA,EV_CURRENT
:「ビットネス」(ワード幅)、ワード内のバイト順、およびELF形式バージョンの識別子。 これらの識別子に応じて、ヘッダーの残りの部分が復号化されます(32ビット版と64ビット版では、フィールドサイズが異なります)。 -
ET_REL
:ELFファイルのタイプ(「再配置可能」、「実行可能」、「動的ライブラリ」); -
ELF_ARCH,EV_CURRENT
:プロセッサ識別子と形式のバージョン。 -
0,0
:エントリポイントアドレスとセグメントテーブルオフセット。 再配置されたファイルにはどちらもありません。 -
sizeof(elfhdr)
:セクションテーブルオフセット。 私たちと一緒に、彼女は見出しのすぐ後に行きます。 -
0
:x86 / x64で定義されていないフラグ。 -
sizeof(elfhdr)
:ヘッダーサイズ。 -
0,0
:セグメントテーブルのエントリのサイズと数。 このテーブルはありませんが、持っていません。 -
sizeof(Elf_Shdr),8
:セクションテーブルのエントリのサイズと数。 -
1
:すべてのセクションの名前を含むセクション番号。 -
"\0.shstrtab\0.strtab\0.symtab\0.rel.text\0.rel.data\0.text\0.data"
:すべてのセクションの名前、次々。 ゼロオフセットが空の名前を示すように、文字列の最初の文字は\0
なければなりません。 -
{0, SHT_NULL}
:標準セクション番号0は空でなければなりません。 -
1
:セクション名( shstrtab
内のオフセット); -
SHT_STRTAB
:セクションタイプ(この場合、「行テーブル」); -
0
:セクションのメモリ保護を設定するフラグ(読み取り、書き込み、実行)。 名前テーブルなどのサービスセクションの場合、これは必要ありません。 -
0
:メモリ内のセクションアドレス。 再配置されたファイルでは定義されていません。 -
(char*)&elf.shstrtab-(char*)&elf,sizeof(elf.shstrtab)
:ファイル内のセクションオフセットとそのサイズ。 -
0,0
:2つの追加フィールド。その解釈はセクションのタイプに依存します。 テーブルには行が定義されていません。 -
1
:アライメントブロックサイズ。 文字列の場合-1バイト、つまり アライメントなし; -
0
:セクション内のレコードサイズ。 長さ不定の行テーブルのレコード。 -
{11,SHT_STRTAB,0,0,(char*)&elf.strtab-(char*)&elf,sizeof(elf.strtab),0,0,1,0}
:別の行テーブル。 名前(11— .strtab
)のみが異なります。 -
{19,SHT_SYMTAB,0,0,(char*)&elf.symtab-(char*)&elf,sizeof(elf.symtab),2,2,8,sizeof(Elf_Sym)}
:セクション「文字テーブル」タイプ。 追加フィールドの内容:最初の.strtab
文字の名前が付いたセクションの番号(前のもの、つまり.strtab
)、2番目の.strtab
最初のグローバルシンボルの番号。 -
{27,SHT_REL,0,0,(char*)&elf.reltext-(char*)&elf,sizeof(elf.reltext),3,6,8,sizeof(Elf_Rel)}
:タイプ「再配置テーブル」のセクション。 追加フィールドの内容:シンボルテーブル(前のセクション、3 .text
でもあり.text
)および再配置されたセクション( .text
、6 .text
でもあり.text
)へのリンク。 -
{37,SHT_REL,0,0,(char*)&elf.reldata-(char*)&elf,sizeof(elf.reldata),3,7,8,sizeof(Elf_Rel)}
:別の再配置テーブル。 同じシンボルテーブル(No. 3)と.data
セクション(No. 7)を参照します。 -
{47,SHT_PROGBITS,SHF_ALLOC|SHF_EXECINSTR,0,sizeof(elf),offset,0,0,4,0}
:データセクション( SHT_PROGBITS
)実行可能( SHF_EXECINSTR
)サイズoffset
; -
{53,SHT_PROGBITS,SHF_ALLOC|SHF_WRITE,0,sizeof(elf)+offset,laststr+lastspill*4,0,0,4,0}
:変更可能なデータセクション( SHF_WRITE
)、文字列と4バイトを含む注ぐためのセル; -
"\0main\0input\0echoi\0echos"
:セクション名と同じ形式のシンボル名(インポートおよびエクスポート)。 -
{}
:文字番号0は空白のままにしてください。 -
{0, 0, 0, STT_SECTION, 0, 7}
:セクション( STT_SECTION
)No. 7を示す名前のない(0)ローカル文字 .data
; -
{1, 0, 0, ST_GLOBAL_NOTYPE, 0, 6}
: main
(名前オフセット1)という名前のグローバル(ST_GLOBAL_NOTYPE)文字は、セクションNo. 6のオフセット0を示します。 .text
; -
{6, ST_GLOBAL_NOTYPE, 0, 0, 0, 0}
:どのセクションにも属さないinput
(name offset 6)という名前のグローバル文字。 インポートされた; - インポートされた他の2つの文字
echoi
とechoi
も同様に定義されています。
プログラマを混乱させるために、
Elf32_Sym
および
Elf64_Sym
同じフィールドで定義されていますが、順序は異なります。 コードの2つのバージョンを記述し、
#ifdef
を
#ifdef
してそのうちの1つを選択する以外に方法はありません。
rel
タイプの再配置は、「再配置されたフィールド、シンボル番号、バインディングタイプコードのオフセット」のトリプルで構成されます。 最初の再配置(
{7, ELF_R_INFO(1,R_32)}
)-プロローグ内、コードの先頭からのオフセット7
EBP/RBP
ロードされたデータのベースアドレスを設定します。 この再配置は、シンボルNo. 1、つまり
.data
セクションへ。 どのアーキテクチャでも、サイズは32ビットです。 バインディングタイプのコードは異なります
R_386_32=1
、x64
R_X86_64_32=10
です。
データには他の3つの再配置があり、インポートされた3つの関数のアドレスをオフセット0.8.16で取得します。 これらの3つの再配置は、インポートされた文字(3、4、5)を参照し、常にマシンワード(
R_386_32,R_X86_64_64
)と等しいアンカーコード1を使用します。
完全なコンパイラコード:
tyomitch.net.ru/jsk.y.elf.html
バイナリを手動で収集することに興味がある場合は、実用的なトリックからクレイジーなハックまで
、ELFファイルのサイズを削減する方法を検討することをお勧めします。 そして同時に
完全に縮小したプログラムの例 。
結果のファイルのサイズは45バイトです。アセンブラーの5倍、Cの50倍です。 ファイルからできることはすべて捨てました。 そして、捨てられなかったものを、2つまたは3つの目的に同時に使用します。
このファイルの値の約半分は、何らかの形でELF標準に違反しています。 通常のプログラマーは、そのようなプログラムが彼の手によるものであることを認めて恥ずかしいでしょう。 驚くべきことに、Linuxはこの悪夢にPIDを割り当てることに同意します。
一方、このファイルの各バイトについて、なぜ必要なのかを説明できます。 多くの場合、コンパイルしたファイルについて同じことを言うことができますか?
どうしたの?
これで、個別にコンパイルできる2つの独立したコンポーネントから最終的なバイナリが取得されます。
[tyomitch@home ~]$ as jskstd.s -o jskstd.o
[tyomitch@home ~]$
[tyomitch@home ~]$ bison jsk.y
[tyomitch@home ~]$ c++ jsk.tab.c lex.yy.c -o jskc
[tyomitch@home ~]$
[tyomitch@home ~]$ ./jskc < test.jsk > code.o
[tyomitch@home ~]$ cc jskstd.o code.o
[tyomitch@home ~]$ ./a.out
0 1000,
500? (1=, 2=, 3=) 1
249? (1=, 2=, 3=) 3
! !
[tyomitch@home ~]$ objdump -d code.o
code.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <main>: 0: 55 push %rbp 1: 57 push %rdi 2: 56 push %rsi 3: 53 push %rbx 4: 48 c7 c5 00 00 00 00 mov $0x0,%rbp b: 33 c0 xor %eax,%eax d: b9 e8 03 00 00 mov $0x3e8,%ecx 12: 50 push %rax 13: 51 push %rcx 14: be 18 00 00 00 mov $0x18,%esi 19: ff 55 10 callq *0x10(%rbp) 1c: 59 pop %rcx 1d: 58 pop %rax 1e: 50 push %rax 1f: 51 push %rcx 20: be 00 00 00 00 mov $0x0,%esi 25: ff 55 08 callq *0x8(%rbp) 28: be 38 00 00 00 mov $0x38,%esi 2d: ff 55 10 callq *0x10(%rbp) 30: 59 pop %rcx 31: 51 push %rcx 32: be e8 03 00 00 mov $0x3e8,%esi 37: ff 55 08 callq *0x8(%rbp) 3a: be 3f 00 00 00 mov $0x3f,%esi 3f: ff 55 10 callq *0x10(%rbp) 42: 59 pop %rcx 43: 58 pop %rax 44: 3b c1 cmp %ecx,%eax 46: 0f 8f 6c 00 00 00 jg b8 <main+0xb8> 4c: 8d 14 01 lea (%rcx,%rax,1),%edx 4f: d1 fa sar %edx 51: 50 push %rax 52: 51 push %rcx 53: 52 push %rdx 54: be 64 00 00 00 mov $0x64,%esi 59: ff 55 10 callq *0x10(%rbp) 5c: 5a pop %rdx 5d: 52 push %rdx 5e: 8b f2 mov %edx,%esi 60: ff 55 08 callq *0x8(%rbp) 63: be 6c 00 00 00 mov $0x6c,%esi 68: ff 55 10 callq *0x10(%rbp) 6b: ff 55 00 callq *0x0(%rbp) 6e: 93 xchg %eax,%ebx 6f: 5a pop %rdx 70: 59 pop %rcx 71: 58 pop %rax 72: 89 85 04 01 00 00 mov %eax,0x104(%rbp) 78: 83 fb 01 cmp $0x1,%ebx 7b: 75 0b jne 88 <main+0x88> 7d: 8b 85 04 01 00 00 mov 0x104(%rbp),%eax 83: 8d 4a ff lea 0xffffffffffffffff(%rdx),%ecx 86: eb bc jmp 44 <main+0x44> 88: 83 fb 02 cmp $0x2,%ebx 8b: 75 05 jne 92 <main+0x92> 8d: 8d 42 01 lea 0x1(%rdx),%eax 90: eb b2 jmp 44 <main+0x44> 92: 8b 85 04 01 00 00 mov 0x104(%rbp),%eax 98: 83 fb 03 cmp $0x3,%ebx 9b: 75 0d jne aa <main+0xaa> 9d: be 9f 00 00 00 mov $0x9f,%esi a2: ff 55 10 callq *0x10(%rbp) a5: 5b pop %rbx a6: 5e pop %rsi a7: 5f pop %rdi a8: 5d pop %rbp a9: c3 retq aa: 50 push %rax ab: 51 push %rcx ac: be bb 00 00 00 mov $0xbb,%esi b1: ff 55 10 callq *0x10(%rbp) b4: 59 pop %rcx b5: 58 pop %rax b6: eb 8c jmp 44 <main+0x44> b8: be dd 00 00 00 mov $0xdd,%esi bd: ff 55 10 callq *0x10(%rbp) c0: 5b pop %rbx c1: 5e pop %rsi c2: 5f pop %rdi c3: 5d pop %rbp c4: c3 retq
, , , llvm. — .
[tyomitch@home ~]$ as jskstd.s -o jskstd.o
[tyomitch@home ~]$
[tyomitch@home ~]$ bison jsk.y
[tyomitch@home ~]$ c++ jsk.tab.c lex.yy.c -o jskc
[tyomitch@home ~]$
[tyomitch@home ~]$ ./jskc < test.jsk > code.o
[tyomitch@home ~]$ cc jskstd.o code.o
[tyomitch@home ~]$ ./a.out
0 1000,
500? (1=, 2=, 3=) 1
249? (1=, 2=, 3=) 3
! !
[tyomitch@home ~]$ objdump -d code.o
code.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <main>: 0: 55 push %rbp 1: 57 push %rdi 2: 56 push %rsi 3: 53 push %rbx 4: 48 c7 c5 00 00 00 00 mov $0x0,%rbp b: 33 c0 xor %eax,%eax d: b9 e8 03 00 00 mov $0x3e8,%ecx 12: 50 push %rax 13: 51 push %rcx 14: be 18 00 00 00 mov $0x18,%esi 19: ff 55 10 callq *0x10(%rbp) 1c: 59 pop %rcx 1d: 58 pop %rax 1e: 50 push %rax 1f: 51 push %rcx 20: be 00 00 00 00 mov $0x0,%esi 25: ff 55 08 callq *0x8(%rbp) 28: be 38 00 00 00 mov $0x38,%esi 2d: ff 55 10 callq *0x10(%rbp) 30: 59 pop %rcx 31: 51 push %rcx 32: be e8 03 00 00 mov $0x3e8,%esi 37: ff 55 08 callq *0x8(%rbp) 3a: be 3f 00 00 00 mov $0x3f,%esi 3f: ff 55 10 callq *0x10(%rbp) 42: 59 pop %rcx 43: 58 pop %rax 44: 3b c1 cmp %ecx,%eax 46: 0f 8f 6c 00 00 00 jg b8 <main+0xb8> 4c: 8d 14 01 lea (%rcx,%rax,1),%edx 4f: d1 fa sar %edx 51: 50 push %rax 52: 51 push %rcx 53: 52 push %rdx 54: be 64 00 00 00 mov $0x64,%esi 59: ff 55 10 callq *0x10(%rbp) 5c: 5a pop %rdx 5d: 52 push %rdx 5e: 8b f2 mov %edx,%esi 60: ff 55 08 callq *0x8(%rbp) 63: be 6c 00 00 00 mov $0x6c,%esi 68: ff 55 10 callq *0x10(%rbp) 6b: ff 55 00 callq *0x0(%rbp) 6e: 93 xchg %eax,%ebx 6f: 5a pop %rdx 70: 59 pop %rcx 71: 58 pop %rax 72: 89 85 04 01 00 00 mov %eax,0x104(%rbp) 78: 83 fb 01 cmp $0x1,%ebx 7b: 75 0b jne 88 <main+0x88> 7d: 8b 85 04 01 00 00 mov 0x104(%rbp),%eax 83: 8d 4a ff lea 0xffffffffffffffff(%rdx),%ecx 86: eb bc jmp 44 <main+0x44> 88: 83 fb 02 cmp $0x2,%ebx 8b: 75 05 jne 92 <main+0x92> 8d: 8d 42 01 lea 0x1(%rdx),%eax 90: eb b2 jmp 44 <main+0x44> 92: 8b 85 04 01 00 00 mov 0x104(%rbp),%eax 98: 83 fb 03 cmp $0x3,%ebx 9b: 75 0d jne aa <main+0xaa> 9d: be 9f 00 00 00 mov $0x9f,%esi a2: ff 55 10 callq *0x10(%rbp) a5: 5b pop %rbx a6: 5e pop %rsi a7: 5f pop %rdi a8: 5d pop %rbp a9: c3 retq aa: 50 push %rax ab: 51 push %rcx ac: be bb 00 00 00 mov $0xbb,%esi b1: ff 55 10 callq *0x10(%rbp) b4: 59 pop %rcx b5: 58 pop %rax b6: eb 8c jmp 44 <main+0x44> b8: be dd 00 00 00 mov $0xdd,%esi bd: ff 55 10 callq *0x10(%rbp) c0: 5b pop %rbx c1: 5e pop %rsi c2: 5f pop %rdi c3: 5d pop %rbp c4: c3 retq
, , , llvm. — .