GNU_IFUNCとは何か

GNU_IFUNC(GNU indirect function)とは同一の関数の複数の実装から、ロード時に最適な実装を選択する仕組みです1。通常、同一の関数の複数の実装が存在した場合、動的リンカ(ld-linux.so)が最初に見つけた実装が選択されますが、その選択するための基準を開発者が決定できるところに、この機能の意義があります。

GNU_IFUNCを使った簡単な例を以下に示します。resolve_foofoo の実装が選択されていることがわかります。

> cat foo.c 
#include <stdio.h>

extern void foo();
void foo_default() { printf("foo_default\n"); }
void foo_1() { printf("foo_1\n"); }
void foo_2() { printf("foo_2\n"); }

void foo() __attribute__((ifunc("resolve_foo")));

static void *resolve_foo(void) {
    if (0)
        return foo_1;
    else if (42 == 41 + 1)
        return foo_2;
    else
        return foo_default;
}
> cat main.c 
void foo();

int main() {
    foo();
    return 0;
}
> gcc -shared -fpic -fPIC foo.c -o libfoo.so
> gcc -o main main.c libfoo.so
> ./main
foo_2

glibcでは memmove, memset, memcpy, strcmp, strstr などの関数でCPUの機能に応じて高度に最適化された実装をロード時に選択するために使われています2。例えば x86-64memcpy の場合はAVX512等のSIMD拡張が使えるかどうかを判定し、最適な実装を選択するコードが入っています。3

/*  glibc/sysdeps/x86_64/multiarch/ifunc-impl-list.c  */
 IFUNC_IMPL (i, name, __memcpy_chk,
          IFUNC_IMPL_ADD (array, i, __memcpy_chk,
                  CPU_FEATURE_USABLE (AVX512F),
                  __memcpy_chk_avx512_no_vzeroupper)
          IFUNC_IMPL_ADD (array, i, __memcpy_chk,
                  CPU_FEATURE_USABLE (AVX512VL),
                  __memcpy_chk_avx512_unaligned)
          IFUNC_IMPL_ADD (array, i, __memcpy_chk,
                  CPU_FEATURE_USABLE (AVX512VL),
                  __memcpy_chk_avx512_unaligned_erms)
          IFUNC_IMPL_ADD (array, i, __memcpy_chk,
                  CPU_FEATURE_USABLE (AVX),
                  __memcpy_chk_avx_unaligned)

最後にGNU_IFUNCの実装選択関数を悪用してみます。glibcGNU_IFUNCに関する再配置情報を処理する部分を見ると、ロード時に実装を選択するための関数が呼ばれていることがわかります4

/* glibc/sysdeps/x86_64/dl-machine.h */
else if (__glibc_unlikely (r_type == R_X86_64_IRELATIVE))
    {
      ElfW(Addr) value = map->l_addr + reloc->r_addend;
      if (__glibc_likely (!skip_ifunc))
    value = ((ElfW(Addr) (*) (void)) value) ();
      *reloc_addr = value;
    }

つまり、__attribute__(constructor)5 と同様に main() の前に関数を呼び出す手段として使えそうです。やってみましょう。

> cat foo.c 
#include <stdio.h>

extern void foo();
void foo_default() { printf("foo_default\n"); }
void foo_1() { printf("foo_1\n"); }
void foo_2() { printf("foo_2\n"); }

void foo() __attribute__((ifunc("resolve_foo")));

static void *resolve_foo(void) {
    printf("resolve_foo\n");
    if (0)
        return foo_1;
    else if (42 == 41 + 1)
        return foo_2;
    else
        return foo_default;
}
> cat main.c 
#include <stdio.h>

void foo();

int main() {
    printf("beginning of main()\n");
    foo();
    return 0;
}
> gcc -shared -fpic -fPIC foo.c -o libfoo.so
> gcc -o main main.c libfoo.so
> LD_BIND_NOW=1
> ./main
resolve_foo
beginning of main()
foo_2

R_X86_64_IRELATIVEmain() 起動後に遅延して解決されるのを防ぐために LD_BIND_NOW=1 が必要でしたが、無事 main() の前に resolve_foo を呼び出せたことがわかります。