読者です 読者をやめる 読者になる 読者になる

__builtin_return_addressについて

debug compiler

__builtin_return_address関数の紹介。簡単にいうとある関数が
どこから呼び出されたか知ることができる関数です。厳密には
該当の関数を終えたときにどの番地に戻るかということなの
ですが、たいてい call命令などの次の命令を示すので、呼び出した
場所の特定も容易にできてしまいます。
私はカーネルデバッグ時に多用します。


ユーザランドであればデバッガを使うことが容易ですが、
カーネルだと若干面倒です。カーネルでもメジャーなアーキテクチャ
カーネルデバッガが安心して使えるんですが、マイナーなアーキテクチャだと
カーネルデバッガ用のコードが誤っている(経験あり)ということがあるので、
どうしてもprintデバッグに頼ってしまいます。


__builtin_return_addressではアドレスしか知ることができないので,
objdumpや nmの併用が基本となります。


変数の値とともに流れも知ることができるので理解も深まります。
すべてが単純な関数呼び出しであれば、GNU global等で済みますが、
関数ポインタを多用されるような場合では、それだけでは辛いです。
そんなときにも使うと便利です。

利用できるコンパイラ

gcc、clang、pccで使えることを確認しました。

デバッグにあたり

デバッグを行う場合、作り上げているものについては printfデバッグ
全然問題ないのですが、リリースしたものなどをデバッグする場合は、
printfデバッグを行うべきでない場合があります。あからさまなバグなら
いいんですが、タイミングによるバグ等、センシティブなものについては
同じ(1byteも違わない)バイナリでデバッグを行うということが基本と
いうか絶対になります。


ICE、JTAGがあるならそれらを優先して使うようにしましょう。
それがないならデバッガ使うなりしてください。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

enum {
    PROTO_TCP = 0,
    PROTO_UDP,
    PROTO_END,
};

typedef int (*send_t)(char *);

static int tcp_send(char *msg)
{
    printf("[%s] comefrom=%p\n", __func__, __builtin_return_address(0));
    return printf("[TCP] send => %s\n", msg);
}

static int udp_send(char *msg)
{
    printf("[%s] comefrom=%p\n", __func__, __builtin_return_address(0));
    return printf("[UDP] send => %s\n", msg);
}

static send_t proto_table[] = {
    [PROTO_TCP] = tcp_send,
    [PROTO_UDP] = udp_send,
};

static int send(int protocol, char *msg)
{
    return (*proto_table[protocol])(msg);
}

int main (int argc, char *argv[])
{
    char *protocol;
    int protocol_id;
    int send_count;

    if (argc < 3) {
        fprintf(stderr, "Usage: %s protocol message\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    protocol = argv[1];
    if (strncmp(protocol, "tcp", strlen("tcp")) == 0) {
        protocol_id = PROTO_TCP;
    } else if(strncmp(protocol, "udp", strlen("udp")) == 0) {
        protocol_id = PROTO_UDP;
    } else {
        fprintf(stderr, "Invalid protocol: %s (TCP or UDP)", protocol);
        exit(EXIT_FAILURE);
    }

    send_count = send(protocol_id, argv[2]);
    printf("send %d characters\n", send_count);

    return 0;
}

send関数で関数ポインタ経由の関数コールが行われています。
カーネルでの抽象化はだいたいこんなコードで、ソースだけ
見るだけでは実際どの関数が呼ばれるかがわかりません。


__builtin_return_addressの引数で呼び出し元を
どんどんと遡れるとなっているのですが、これはアーキテクチャ
依存で 0(最新の return address)しか指定できないものが
多いです。


このコードを以下のように動作させます。

  % gcc -std=c99 builtin_return_address.c
  % ./a.out tcp message
  [tcp_send] comefrom=0x4006c3 # アドレスは環境により異なる
  [TCP] send => message
  send 22 characters

tcp_sendが抜けると 0x4006c3に戻ることがわかります。
アセンブルしてその確認してみます。
(lessにつないで, 上記のアドレスで検索をかければよいでしょう)

  % objdump -d a.out | less
  ...
000000000040069e <send>:
  40069e:       55                      push   %rbp
  40069f:       48 89 e5                mov    %rsp,%rbp
  4006a2:       48 83 ec 10             sub    $0x10,%rsp
  4006a6:       89 7d fc                mov    %edi,-0x4(%rbp)
  4006a9:       48 89 75 f0             mov    %rsi,-0x10(%rbp)
  4006ad:       8b 45 fc                mov    -0x4(%rbp),%eax
  4006b0:       48 98                   cltq   
  4006b2:       48 8b 14 c5 40 10 60    mov    0x601040(,%rax,8),%rdx
  4006b9:       00 
  4006ba:       48 8b 45 f0             mov    -0x10(%rbp),%rax
  4006be:       48 89 c7                mov    %rax,%rdi
  4006c1:       ff d2                   callq  *%rdx
  4006c3:       c9                      leaveq <= ココの番地に戻る
  4006c4:       c3                      retq  
  ...

send関数に該当のアドレスがあることがわかります。
一つ前の命令は callqなんでここから tcp_sendが呼ばれた
こともわかります。

最後に

__buildin_return_addressの紹介をしました。
printデバッグは誰でもやるかと思うのですが、単純にここ通りました
っていう情報だけじゃなく、どこに戻るか(どこからきたか)っていう
情報も合わせて出力すると、どういうフローで呼ばれたときに問題が
起こるかということもわかり、デバッグのヒントになると思います。