__builtin_return_addressについて
__builtin_return_address関数の紹介。簡単にいうとある関数が
どこから呼び出されたか知ることができる関数です。厳密には
該当の関数を終えたときにどの番地に戻るかということなの
ですが、たいてい call命令などの次の命令を示すので、呼び出した
場所の特定も容易にできてしまいます。
私はカーネルデバッグ時に多用します。
ユーザランドであればデバッガを使うことが容易ですが、
カーネルだと若干面倒です。カーネルでもメジャーなアーキテクチャ
カーネルデバッガが安心して使えるんですが、マイナーなアーキテクチャだと
カーネルデバッガ用のコードが誤っている(経験あり)ということがあるので、
どうしてもprintデバッグに頼ってしまいます。
__builtin_return_addressではアドレスしか知ることができないので,
objdumpや nmの併用が基本となります。
変数の値とともに流れも知ることができるので理解も深まります。
すべてが単純な関数呼び出しであれば、GNU global等で済みますが、
関数ポインタを多用されるような場合では、それだけでは辛いです。
そんなときにも使うと便利です。
デバッグにあたり
デバッグを行う場合、作り上げているものについては 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が呼ばれた
こともわかります。