15

ここでルールとバッファを少し調整してmallocから、関数をバッファにコピーしようとしています。

バッファリングされた関数の呼び出しは機能しますが、関数内で別の関数を呼び出そうとすると、セグメンテーション エラーがスローされます。

なぜ何か考えはありますか?

#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdlib.h>

int foo(int x)
{
    printf("%d\n", x);
}

int bar(int x)
{
}

int main()
{
    int foo_size = bar - foo;

    void* buf_ptr;

    buf_ptr = malloc(1024);

    memcpy(buf_ptr, foo, foo_size);

    mprotect((void*)(((int)buf_ptr) & ~(sysconf(_SC_PAGE_SIZE) - 1)),
             sysconf(_SC_PAGE_SIZE),
             PROT_READ|PROT_WRITE|PROT_EXEC);

    int (*ptr)(int) = buf_ptr;

    printf("%d\n", ptr(3));

    return 0;
}

foo関数を次のように変更しない限り、このコードは segfault をスローします。

int foo(int x)
{
    //Anything but calling another function.
    x = 4;
    return x;
}

ノート:

コードはバッファに正常にコピーfooされます。いくつかの仮定を行ったことは知っていますが、私のプラットフォームでは問題ありません。

4

3 に答える 3

38

あなたのコードは位置独立ではありません。たとえそうであったとしても、それを任意の位置に移動するための正しい再配置がありません。printf(または他の関数)への呼び出しは、pc 相対アドレス指定で行われます (PLT を使用しますが、それはここでのポイント以外です)。これは、printf を呼び出すために生成された命令が静的アドレスへの呼び出しではなく、「現在の命令ポインターから X バイトの関数を呼び出す」ことを意味します。コードを移動したため、呼び出しは不適切なアドレスに行われます。(ここでは i386 または amd64 を想定していますが、一般的には安全な想定です。奇妙なプラットフォームを使用している人は通常、それについて言及しています)。

具体的には、x86 には関数呼び出し用の 2 つの異なる命令があります。1 つは、現在の命令ポインターに値を追加することによって関数呼び出しの宛先を決定する、命令ポインターに関連する呼び出しです。これは、最も一般的に使用される関数呼び出しです。2 番目の命令は、レジスタまたはメモリ位置内のポインタへの呼び出しです。これは、より多くのメモリの間接化を必要とし、パイプラインを停止させるため、コンパイラではあまり一般的ではありません。共有ライブラリの実装方法 (への呼び出しprintf実際には共有ライブラリに移動します) は、独自のコードの外で行うすべての関数呼び出しに対して、コンパイラがコードの近くに偽の関数を挿入することです (これは上記の PLT です)。あなたのコードは、この偽の関数に対して通常の pc 相対呼び出しを行い、偽の関数は実際のアドレスを見つけてprintfそれを呼び出します。しかし、それは本当に問題ではありません。ほとんどすべての通常の関数呼び出しは pc 相対であり、失敗します。このようなコードでの唯一の希望は、関数ポインターです。

また、executable に関するいくつかの制限に遭遇する場合もありますmprotect。の戻り値を確認してmprotectください。私のシステムでは、もう 1 つの理由でコードが機能しませんmprotect。おそらく、 のバックエンド メモリ アロケータにmallocは、そのメモリの実行可能な保護を妨げる追加の制限があるためです。これが次のポイントにつながります。

mprotect自分が管理していないメモリを呼び出すと、物事が壊れます。これには、 から取得したメモリが含まれますmallocmprotectカーネルから自分で取得したものだけを使用する必要がありますmmap

(私のシステムで)これを機能させる方法を示すバージョンは次のとおりです。

#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <err.h>

int
foo(int x, int (*fn)(const char *, ...))
{
        fn("%d\n", x);
        return 42;
}

int
bar(int x)
{
        return 0;
}

int
main(int argc, char **argv)
{
        size_t foo_size = (char *)bar - (char *)foo;
        int ps = getpagesize();

        void *buf_ptr = mmap(NULL, ps, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_PRIVATE, -1, 0);

        if (buf_ptr == MAP_FAILED)
                err(1, "mmap");

        memcpy(buf_ptr, foo, foo_size);

        int (*ptr)(int, int (*)(const char *, ...)) = buf_ptr;

        printf("%d\n", ptr(3, printf));

        return 0;
}

ここでは、コンパイラが関数呼び出しのコードを生成する方法に関する知識を悪用しています。関数ポインターを使用することで、PC 相対ではない呼び出し命令を強制的に生成します。また、メモリ割り当てを自分で管理して、最初から適切なアクセス許可を取得し、制限に遭遇しないようにしbrkています。おまけとして、この実験の最初のバージョンで実際にバグを見つけるのに役立つエラー処理を行いました。また、他のマイナーなバグ (インクルードの欠落など) も修正しました。これにより、コンパイラで警告を有効にして、別の潜在的な問題を検出することができました。

これをさらに深く掘り下げたい場合は、次のようなことができます。関数の 2 つのバージョンを追加しました。

int
oldfoo(int x)
{
        printf("%d\n", x);
        return 42;
}

int
foo(int x, int (*fn)(const char *, ...))
{
        fn("%d\n", x);
        return 42;
}

全体をコンパイルして逆アセンブルします。

$ cc -Wall -o foo foo.c
$ objdump -S foo | less

生成された 2 つの関数を見てみましょう。

0000000000400680 <oldfoo>:
  400680:       55                      push   %rbp
  400681:       48 89 e5                mov    %rsp,%rbp
  400684:       48 83 ec 10             sub    $0x10,%rsp
  400688:       89 7d fc                mov    %edi,-0x4(%rbp)
  40068b:       8b 45 fc                mov    -0x4(%rbp),%eax
  40068e:       89 c6                   mov    %eax,%esi
  400690:       bf 30 08 40 00          mov    $0x400830,%edi
  400695:       b8 00 00 00 00          mov    $0x0,%eax
  40069a:       e8 91 fe ff ff          callq  400530 <printf@plt>
  40069f:       b8 2a 00 00 00          mov    $0x2a,%eax
  4006a4:       c9                      leaveq
  4006a5:       c3                      retq

00000000004006a6 <foo>:
  4006a6:       55                      push   %rbp
  4006a7:       48 89 e5                mov    %rsp,%rbp
  4006aa:       48 83 ec 10             sub    $0x10,%rsp
  4006ae:       89 7d fc                mov    %edi,-0x4(%rbp)
  4006b1:       48 89 75 f0             mov    %rsi,-0x10(%rbp)
  4006b5:       8b 45 fc                mov    -0x4(%rbp),%eax
  4006b8:       48 8b 55 f0             mov    -0x10(%rbp),%rdx
  4006bc:       89 c6                   mov    %eax,%esi
  4006be:       bf 30 08 40 00          mov    $0x400830,%edi
  4006c3:       b8 00 00 00 00          mov    $0x0,%eax
  4006c8:       ff d2                   callq  *%rdx
  4006ca:       b8 2a 00 00 00          mov    $0x2a,%eax
  4006cf:       c9                      leaveq
  4006d0:       c3                      retq

この場合の関数呼び出しの命令printfは「e8 91 fe ff ff」です。これは pc 相対関数呼び出しです。命令ポインターの前に 0xfffffe91 バイト。これは符号付き 32 ビット値として扱われ、計算に使用される命令ポインタは次の命令のアドレスです。したがって、0x40069f (次の命令) - 0x16f (先頭の 0xfffffe91 は、符号付き演算の後ろに 0x16f バイトあります) からアドレス 0x400530 が得られ、逆アセンブルされたコードを見ると、次のアドレスが見つかります。

0000000000400530 <printf@plt>:
  400530:       ff 25 ea 0a 20 00       jmpq   *0x200aea(%rip)        # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
  400536:       68 01 00 00 00          pushq  $0x1
  40053b:       e9 d0 ff ff ff          jmpq   400510 <_init+0x28>

これが先に述べた魔法の「疑似機能」です。これがどのように機能するかについては触れません。共有ライブラリが機能するためには必要であり、今のところ知っておく必要があるのはそれだけです。

2 番目の関数は、関数呼び出し命令「ff d2」を生成します。これは、「rdxレジスタ内に格納されたアドレスで関数を呼び出す」ことを意味します。PC 相対アドレッシングがないため、機能します。

于 2016-05-11T07:56:30.747 に答える
3

コンパイラーは、観察可能な結果が正しい (あたかもルールのように) 場合、自由にコードを生成できます。したがって、あなたが行うことは、未定義の動作の呼び出しにすぎません。

Visual Studio はリレーを使用することがあります。つまり、関数のアドレスは相対ジャンプを指すだけです。as is ルールのため、標準では完全に許可されていますが、そのような構造は確実に壊れます。もう 1 つの可能性は、ローカル内部関数を相対ジャンプで呼び出すことですが、関数自体の外部にあります。その場合、コードはそれらをコピーせず、相対呼び出しはランダム メモリを指すだけです。つまり、異なるコンパイラ (または同じコンパイラの異なるコンパイル オプション) を使用すると、期待される結果が得られたり、クラッシュしたり、エラーなしでプログラムが直接終了したりする可能性があります。これはまさに UB です。

于 2016-05-11T08:06:46.773 に答える
1

少し説明できると思います。まず第一に、両方の関数に return ステートメントが含まれていない場合、標準の §6.9.1/12 に従って未定義の動作が呼び出されます。第二に、これは多くのプラットフォームで最も一般的であり、明らかにあなたのプラットフォームでも次のとおりです。関数の相対アドレスは、関数のバイナリ コードにハードコードされています。つまり、「foo」内で「printf」の呼び出しがあり、別の場所から移動 (実行など) すると、「printf」が呼び出されるはずのアドレスが無効になります。

于 2016-05-11T08:00:03.843 に答える