あなたのコードは位置独立ではありません。たとえそうであったとしても、それを任意の位置に移動するための正しい再配置がありません。printf
(または他の関数)への呼び出しは、pc 相対アドレス指定で行われます (PLT を使用しますが、それはここでのポイント以外です)。これは、printf を呼び出すために生成された命令が静的アドレスへの呼び出しではなく、「現在の命令ポインターから X バイトの関数を呼び出す」ことを意味します。コードを移動したため、呼び出しは不適切なアドレスに行われます。(ここでは i386 または amd64 を想定していますが、一般的には安全な想定です。奇妙なプラットフォームを使用している人は通常、それについて言及しています)。
具体的には、x86 には関数呼び出し用の 2 つの異なる命令があります。1 つは、現在の命令ポインターに値を追加することによって関数呼び出しの宛先を決定する、命令ポインターに関連する呼び出しです。これは、最も一般的に使用される関数呼び出しです。2 番目の命令は、レジスタまたはメモリ位置内のポインタへの呼び出しです。これは、より多くのメモリの間接化を必要とし、パイプラインを停止させるため、コンパイラではあまり一般的ではありません。共有ライブラリの実装方法 (への呼び出しprintf
実際には共有ライブラリに移動します) は、独自のコードの外で行うすべての関数呼び出しに対して、コンパイラがコードの近くに偽の関数を挿入することです (これは上記の PLT です)。あなたのコードは、この偽の関数に対して通常の pc 相対呼び出しを行い、偽の関数は実際のアドレスを見つけてprintf
それを呼び出します。しかし、それは本当に問題ではありません。ほとんどすべての通常の関数呼び出しは pc 相対であり、失敗します。このようなコードでの唯一の希望は、関数ポインターです。
また、executable に関するいくつかの制限に遭遇する場合もありますmprotect
。の戻り値を確認してmprotect
ください。私のシステムでは、もう 1 つの理由でコードが機能しませんmprotect
。おそらく、 のバックエンド メモリ アロケータにmalloc
は、そのメモリの実行可能な保護を妨げる追加の制限があるためです。これが次のポイントにつながります。
mprotect
自分が管理していないメモリを呼び出すと、物事が壊れます。これには、 から取得したメモリが含まれますmalloc
。mprotect
カーネルから自分で取得したものだけを使用する必要があります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 相対アドレッシングがないため、機能します。