6

この質問からです。

 gcc -c test.s
 objcopy -O binary test.o test.bin

test.oとはどう違いtest.binますか?

.text
    call start
    str:
        .string "test\n"
    start:
    movl    $4, %eax
    movl    $1, %ebx
    pop     %ecx
    movl    $5, %edx
    int     $0x80
    ret

上記は何をしているのですか?

4

2 に答える 2

11

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...jaeorobjdump"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

于 2011-04-04T12:12:52.710 に答える
0

.o はプレリンカ、.bin はポストリンカです。リンカーに関するウィキペディアの記事は次のとおり です

于 2011-04-03T12:47:23.997 に答える