LD_AUDITとGlobal Offset Table

GNU製のLinux向け動的リンカld-linux.so には環境変数 LD_AUDIT 経由で使える監視APIがあります。このAPIを使えば、ld-linux.soの様々な挙動にフックして、その挙動を監視・干渉することができます。また、興味深いことにLD_AUDITを有効にするとGOT(Global Offset Table)周りの挙動も変化します。

PLT(Procedure Linkage Table)やGOTについて知らない場合はこちらの記事が分かりやすいです。この記事のglibcはすべてコミット 137ed5ac44を参照しています。

動作例

LD_AUDITを使ってltrace(1)まがいのものを作ってみます。以下の3つのファイルを作ります。なお、このサンプルはLinux glibcで、LD_AUDIT機能による関数トレースを参考にしました。

audit.c

#define _GNU_SOURCE
#include <link.h>
#include <stdio.h>

unsigned int la_version(unsigned int version) { return LAV_CURRENT; }

unsigned int la_objopen(struct link_map* map, Lmid_t lmid, uintptr_t* cookie) {
    printf("[LD_AUDIT] %s loaded\n", map->l_name);
    return LA_FLG_BINDTO | LA_FLG_BINDFROM;
}

ElfW(Addr) la_x86_64_gnu_pltenter(ElfW(Sym) * sym, unsigned int ndx,
                                  uintptr_t* refcook, uintptr_t* defcook,
                                  La_x86_64_regs* regs, unsigned int* flags,
                                  const char* symname, long* framesizep) {
    printf("[LD_AUDIT] %s entering\n", symname);
    return sym->st_value;
}

hello.c

#include <stdio.h>

int main() {
    puts("Hello World!");
    puts("Hello World!");
    puts("Hello World!");
    return 0;
}

run.sh

#! /bin/bash -eux

gcc -o libaudit.so -shared -fpic -fPIC -Wl,-soname,libaudit.so audit.c
gcc -o hello hello.c

run.sh を実行すると以下のような結果が得られて、ltraceっぽいことができているのがわかります。

> ./run.sh
+ gcc -o libaudit.so -shared -fpic -fPIC -Wl,-soname,libaudit.so audit.c
+ gcc -o hello hello.c
+ LD_AUDIT=libaudit.so
+ ./hello
[LD_AUDIT]  loaded
[LD_AUDIT] /lib64/ld-linux-x86-64.so.2 loaded
[LD_AUDIT] /lib/x86_64-linux-gnu/libc.so.6 loaded
[LD_AUDIT] _dl_find_dso_for_object entering
[LD_AUDIT] __tunable_get_val entering
[LD_AUDIT] __tunable_get_val entering
[LD_AUDIT] __tunable_get_val entering
[LD_AUDIT] __tunable_get_val entering
[LD_AUDIT] __tunable_get_val entering
[LD_AUDIT] __tunable_get_val entering
[LD_AUDIT] __tunable_get_val entering
[LD_AUDIT] __tunable_get_val entering
[LD_AUDIT] __tunable_get_val entering
[LD_AUDIT] __tunable_get_val entering
[LD_AUDIT] __tunable_get_val entering
[LD_AUDIT] __tunable_get_val entering
[LD_AUDIT] puts entering
Hello World!
[LD_AUDIT] puts entering
Hello World!
[LD_AUDIT] puts entering
Hello World!
+ LD_BIND_NOW=1
+ LD_AUDIT=libaudit.so

解説

la_versionLD_AUDITを使うために必ず必要な関数、la_objopenは新しいshared objectがロードされたときに呼び出される関数であり、今回は深入りしません。

la_x86_64_gnu_pltenterは PLT(Procedure Linkage Table)がGOT(Global Offset Table)を埋めるときに呼び出される関数であり、具体的にはここで呼び出されます。

この実行結果で興味深いのは [LD_AUDIT] puts entering が複数回出力されていることです。一度PLTのエントリがGOTに正しいアドレスを埋めた後は、putsの呼び出しはlibcのアドレスを埋める部分を通らず、libc中のputs本体に直接jmpしla_x86_64_gnu_pltenterを呼び出すとことはできないはずです。

ここから考えるとLD_AUDITが設定されているときはGOTにアドレスを埋める処理が行われない、と推測できます。glibcの実装を確認してみましょう。

また、dl-machine.hにはprofiling extensionが使用されている場合、GOTを書き換えない旨のコメントがあります。

/ The got[2] entry contains the address of a function which gets called to get the address of a so far unresolved function and jump to it. The profiling extension of the dynamic linker allows to intercept the calls to collect information. In this case we don't store the address in the GOT so that all future calls also end in this function. /

参考