objcopy -O binary
ソースファイルの内容をコピーします。これtest.o
は「再配置可能オブジェクト ファイル」です。これはコードであり、シンボル テーブルと再配置情報でもあり、ファイルを他のファイルとリンクして実行可能プログラムにすることができます。によって生成されるtest.bin
ファイルobjcopy
にはコードのみが含まれ、シンボル テーブルや再配置情報は含まれません。このような「生の」ファイルは、「通常の」プログラミングには役に立ちませんが、独自のローダーを持つコードには便利です。
32 ビット x86 システムで Linux を使用していると仮定します。ファイルのサイズは515test.o
バイトです。試してみると、オブジェクト ファイルobjdump -x test.o
の内容を説明する次のようになります。test.o
$ objdump -x test.o
test.o: file format elf32-i386
test.o
architecture: i386, flags 0x00000010:
HAS_SYMS
start address 0x00000000
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000001e 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000000 00000000 00000000 00000054 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000054 2**2
ALLOC
SYMBOL TABLE:
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
0000000b l .text 00000000 start
00000005 l .text 00000000 str
これにより、かなり多くの情報が得られます。特に、ファイルには、ファイル.text
内のオフセット 0x34 (10 進数で 52) から始まり、長さが 0x1e バイト (10 進数で 30) のセクションが含まれています。それを逆アセンブルして、オペコード自体を確認できます。
$ objdump -d test.o
test.o: file format elf32-i386
Disassembly of section .text:
00000000 <str-0x5>:
0: e8 06 00 00 00 call b <start>
00000005 <str>:
5: 74 65 je 6c <start+0x61>
7: 73 74 jae 7d <start+0x72>
9: 0a 00 or (%eax),%al
0000000b <start>:
b: b8 04 00 00 00 mov $0x4,%eax
10: bb 01 00 00 00 mov $0x1,%ebx
15: 59 pop %ecx
16: ba 05 00 00 00 mov $0x5,%edx
1b: cd 80 int $0x80
1d: c3 ret
これは多かれ少なかれ、あなたが始めたアセンブリです。、および中間のオペコードは偽物です。これはje
、リテラル文字列 ( 、結果としてバイト 0x74 0x65 0x73 0x64 0x0a 0x00) をオペコードとして解釈しようとしています。また、セクションで見つかった実際のバイト数、つまりオフセット 0x34 から始まるファイル内のバイト数も示します。最初のバイトは 0xe8 0x06 0x00...jae
or
objdump
"test\n"
objdump -d
.text
では、test.bin
ファイルを見てみましょう。長さは 30 バイトです。これらのバイトを 16 進数で見てみましょう。
$ hd test.bin
00000000 e8 06 00 00 00 74 65 73 74 0a 00 b8 04 00 00 00 |.....test.......|
00000010 bb 01 00 00 00 59 ba 05 00 00 00 cd 80 c3 |.....Y........|
ここでは、 の.text
セクションから正確に 30 バイトを認識していますtest.o
。ファイルの内容objcopy -O binary
、つまり空でない唯一のセクション、つまり生のオペコード自体を抽出し、他のすべて、特にシンボルテーブルと再配置情報を削除しました。
再配置とは、メモリ内の特定の場所に格納されたときに適切に実行されるように、特定のコードで何を変更する必要があるかということです。たとえば、コードが変数を使用し、その変数のアドレスを取得したい場合、再配置情報には、実際にコードをメモリに配置する人 (通常はリンカー) に次のように伝えるエントリが含まれます。変数が実際にどこにあるかがわかったら、変数のアドレスを書きます。」興味深いことに、あなたが示したコードは再配置を必要としません: 一連のバイトを任意のメモリ位置に書き込んで、そのまま実行することができます。
コードが何をするか見てみましょう。
call
オペコードはmov
、オフセット 0x0b の命令にジャンプします。また、これは であるためcall
、戻りアドレスをスタックにプッシュします。戻りアドレスは、呼び出しが完了した後、つまりret
オペコードに到達したときに実行を継続する場所です。call
これは、オペコードに続くバイトのアドレスです。ここで、そのアドレスは文字列リテラルの最初のバイトのアドレスです"test\n"
。
- 2 つの
movl
ロード%eax
と%ebx
には、それぞれ数値 4 と 1 が指定されています。
pop
オペコードは、スタックから最上位の要素を削除し、 に格納します%ecx
。このトップ要素は何ですか? これはまさに、オペコードによってスタックにプッシュされたcall
アドレス、つまりリテラル文字列の最初のバイトのアドレスです。
- 3 番目は数値 5 を
movl
ロードします。%edx
int $0x80
は 32 ビット x86 Linux のシステム コールです。これはカーネルを呼び出します。カーネルは、何をすべきかを知るためにレジスターを調べます。カーネルは、最初に を調べ%eax
て「システム コール番号」を取得します。32 ビット x86 では、「4」は__NR_write
、つまりwrite()
システム コールです。この呼び出しでは、レジスタ%ebx
、%ecx
、%edx
の順に 3 つのパラメータが必要です。これらは、宛先ファイル記述子 (ここでは 1: 標準出力)、書き込むデータへのポインター (ここではリテラル文字列)、および書き込むデータの長さ (ここでは 5、4 文字と改行に対応) です。キャラクター)。したがって、これは"test\n"
標準出力に書き込みます。
- 最後
ret
は呼び出し元に戻ります。ret
スタックから値をポップし、そのアドレスにジャンプします。これは、このコード チャンクがcall
オペコードで呼び出されたことを前提としています。
test
したがって、要約すると、コードは改行で出力されます。
カスタム ローダーで試してみましょう。
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
int
main(void)
{
void *p;
int f;
p = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
f = open("test.bin", O_RDONLY);
read(f, p, 30);
close(f);
mprotect(p, 30, PROT_READ | PROT_EXEC);
((void (*)(void))p)();
return 0;
}
(上記のコードは、戻り値のエラーをテストしていません。もちろん、これは非常に悪いことです。)
ここでは、メモリのページ (4096 バイト) を で割り当て、mmap()
読み書きできるページを要求します。p
そのチャンクを指します。次に、 と を使用してopen()
、read()
ファイルclose()
の内容test.bin
(30 バイト) をそのチャンクに読み込みます。
このmprotect()
呼び出しは、私のページのアクセス権を変更するようにカーネルに指示します。今のところ、これらのバイトを実行できるようにしたいと考えています。つまり、それらをマシン コードと見なします。チャンクに書き込む権利を放棄します (正確なカーネル構成によっては、書き込みと実行の両方が可能なページを持つことが禁止される場合があります)。
不可解な((void (*)(void))p)();
ことは次のように読みますp
。引数を取らず、何も返さない関数へのポインターとしてキャストします。その機能を呼び出します。call
これは、データのチャンクにa を作成するための C 構文です。
そのプログラムを実行すると、次のようになります。
$ ./blah
test
これは予想されたことです: のコードは標準出力にtest.bin
書き出されます。test