Home / __builtin_return_address の調査

2014-07-17 日記より
関数の戻りアドレスやフレームアドレスを得るには
GCC::__builtin_return_address
以前使った時は問題無かったのだが、改めて使ってみるとハングしてしまった orz
printf 系 %p に渡したのだがそれが悪かったのか…
便利なのでその内調べよう。

(2014-11-08)上のリンク先には
スタックの頂点に達したときは、この関数は 0 を返す。
なんて書かれているが鵜呑みに出来ない。丸で嘘とは云わんが…

実用性皆無だが不具合を起こす簡単な例:

#include <stdio.h>
#include <stdint.h>

static const void* func1()
{
    return __builtin_return_address(2);
}

int main(int argc, char** argv)
{
    const void* p = func1();
    printf("%p\n", p);
    return 0;
}

これを実行すると Segmentation fault (コアダンプ)となる。(Ubuntu 12.04 LTS、gcc 4.6.3)

汎用的調査

/var/log に何か残っていないか…有りました。

$ less  /var/log/kern.log
... xxx kernel: [  640.645969] show_signal_msg: 24 callbacks suppressed
... xxx kernel: [  640.646002] a.out[2547]: segfault at 4 ip 080483ec sp bfd7af88 error 4 in a.out[8048000+1000]

segfault at 4 と有り、違法なアクセス。error 4 はユーザーでのエラー。
では軽く nm でバイナリを見る。

$ nm ./a.out
...
080483e4 t _ZL5func1v
080483f1 T main
         U printf@@GLIBC_2.0
...

ip 080483ec なので func1 の中で有っているけど、実際のアプリだとロードアドレスはずれたりしないんだろうか…

もう少し正確に見るには -S でアセンブリコードを見ても良いがアドレス付ける方法が判らないので objdump -d a.out でディスアセンブルして見る。

080483e4 <_ZL5func1v>:
 80483e4: 55         push   %ebp
 80483e5: 89 e5      mov    %esp,%ebp
 80483e7: 8b 45 00   mov    0x0(%ebp),%eax
 80483ea: 8b 00      mov    (%eax),%eax
 80483ec: 8b 40 04   mov    0x4(%eax),%eax
 80483ef: 5d         pop    %ebp
 80483f0: c3         ret

ぴったり一致した。0x4(%eax)が犯人だね!(白々しい)
フレーム構造を指定回数だけ辿って戻り番地を返している訳だが、main の戻り先関数の戻り先には辿り着けないんだよね、きっと。(eax=0)
と言う訳で segfault at 4。

無邪気に __builtin_return_address へ 0 以外を指定出来ない。
スタックの頂点に達したときは、この関数は 0 を返す。
等とよく言えたものだ…

お遊びで擬似的対策コード

異常なフレームを参照しない事が肝要だが、そこは ebp が 0 かどうか判断するとして、まずはビルトイン関数の代替コードを考える。
が、生憎インラインアセンブリに詳しくないので、BINARY HACK を見て ebp を参照し、__builtin_return_address(2) と互換な Cコードを書いてみた。

register uint32_t ebp asm ("%ebp");

static const void* func1()
{
    return (const void* )
        (
            (const uint32_t* )(
                *(
                    (const uint32_t* )(
                        *(
                            (const uint32_t* )ebp
                        )
                    )
                )
            )
        )[1];
}

完全等価とはいかなかったが、上出来だろう。

080483e4 <_ZL5func1v>:
 80483e4: 55         push   %ebp
 80483e5: 89 e5      mov    %esp,%ebp
 80483e7: 89 e8      mov    %ebp,%eax
 80483e9: 8b 00      mov    (%eax),%eax
 80483eb: 8b 00      mov    (%eax),%eax
 80483ed: 83 c0 04   add    $0x4,%eax
 80483f0: 8b 00      mov    (%eax),%eax
 80483f2: 5d         pop    %ebp
 80483f3: c3         ret

バックトレースしたい訳じゃないので、回数毎に幾つかマクロを用意すれば実用性は有る気がする。
尤も x86 に固定とかの条件ならばインラインを勉強して書いた方が早いかも知れない。宿題だ…

以下、テスト&書き掛け

#include < stdio.h >
#include < stdint.h >

register const uint32_t *ebp asm ("%ebp");
static const void *gp;

#define PTR(reg)    ((const uint32_t *)(reg))


#define BLT_RETURN_ADDRESS_0()      \
           PTR(ebp)[1]

#define BLT_RETURN_ADDRESS_1()      \
               *ebp                 \
    ?      PTR(*ebp)[1]             \
    :      PTR( ebp)[1]             \

#define BLT_RETURN_ADDRESS_2()      \
               *ebp                 \
    ?     *PTR(*ebp)                \
    ? PTR(*PTR(*ebp))[1]            \
    :      PTR(*ebp)[1]             \
    :      PTR( ebp)[1]

static const void* func1()
{
    gp = __builtin_return_address(2);
    return (const void* )(BLT_RETURN_ADDRESS_2());
}

static const void* func2()
{
    return func1();
}

static const void* func3()
{
    return func2();
}

int main(int argc, char** argv)
{
    const void* p = func2();
    printf("%p\n%p\n", p, gp);
    return 0;
}

補足

strip -s xxx でシンボル除去すると nm は勿論、objdump でも関数名は不明になる。


Home / __builtin_return_address の調査

© 2008 usskim    http://usskim.web.fc2.com/
inserted by FC2 system