th0x4c 備忘録

[Oracle] V$session, V$active_session_history の階層問合せによる待機のブロッキング・セッションの特定

目的

v$session, v$active_session_history の blocking_session 列で待機をブロックしているセッションが分かるが、階層問合せによって待機をツリー状に表示して辿ることで最終的にブロックしているセッションを特定する。

環境

  • OS: Oracle Enterprise Linux 5.8
  • DB: Oracle Database 11g Release 2 (11.2.0.3)

前提知識

v$session.blocking_session 列、v$active_session_history.blocking_session 列

v$session, v$active_session_history の blocking_session 列でそのセッションをブロックしているセッションの SID が分かる。 (例えばそのセッションが要求しているロックを保持しているセッションの SID)

階層問合せ

階層問合せ(もしくは 再帰クエリ)によって、親子関係があるような階層構造、ツリー構造を持つデータの問合せができる。 Oracle Database では独自の CONNECT BY 句を使った問合せを行う。 (11gR2 からは業界標準 SQL99 の再帰 with 句での問合せも可能)

例えば EMP 表には各従業員の EMPNO 列と、その上位マネージャの EMPNO である MGR 列があるが、以下の階層問合せでマネージャと部下の親子関係を表示できる。 (以下は Wikipedia 再帰クエリ から抜粋)

1
2
3
4
SELECT LEVEL, LPAD (' ', 2 * (LEVEL - 1)) || ename employee, empno, mgr
FROM emp
START WITH mgr IS NULL
CONNECT BY PRIOR empno = mgr;
 LEVEL EMPLOYEE         EMPNO    MGR
------ --------------- ------ ------
     1 KING              7839
     2   JONES           7566   7839
     3     SCOTT         7788   7566
     4       ADAMS       7876   7788
     3     FORD          7902   7566
     4       SMITH       7369   7902
     2   BLAKE           7698   7839
     3     ALLEN         7499   7698
     3     WARD          7521   7698
     3     MARTIN        7654   7698
     3     TURNER        7844   7698
     3     JAMES         7900   7698
     2   CLARK           7782   7839
     3     MILLER        7934   7782

v$session の階層問合せによる待機のブロッキング・セッションの特定

実際に複数セッションで待機状態を作って試す。

  • セッション1:

    • UPDATE emp SET sal = sal WHERE empno = 7369;
  • セッション2:

    • UPDATE emp SET sal = sal WHERE empno = 7499;
    • UPDATE emp SET sal = sal WHERE empno = 7369; (セッション1を待機)
  • セッション3:

    • UPDATE emp SET sal = sal WHERE empno = 7521;
    • UPDATE emp SET sal = sal WHERE empno = 7499; (セッション2を待機)
  • セッション4:

    • UPDATE emp SET sal = sal WHERE empno = 7566;
    • UPDATE emp SET sal = sal WHERE empno = 7369; (セッション1を待機)
  • セッション5:

    • UPDATE emp SET sal = sal WHERE empno = 7521; (セッション3を待機)
  • セッション6:

    • UPDATE emp SET sal = sal WHERE empno = 7499; (セッション2を待機)
  • セッション7:

    • UPDATE emp SET sal = sal WHERE empno = 7566; (セッション4を待機)

階層問合せを行い、待機状態を確認する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
set pages 1000
set lines 200
col level     for 9999
col hierarchy for a20
col path      for a20
col sid       for 9999
col event     for a32

SELECT
  LEVEL,
  LPAD(' ', 2 * (LEVEL - 1)) || sid hierarchy,
  SYS_CONNECT_BY_PATH(sid, '<-') path,
  sid,
  blocking_session,
  event
FROM v$session
START WITH blocking_session IS NULL
CONNECT BY PRIOR sid = blocking_session
ORDER SIBLINGS BY sid
/
SQL> SELECT
       LEVEL,
       LPAD(' ', 2 * (LEVEL - 1)) || sid hierarchy,
       SYS_CONNECT_BY_PATH(sid, '<-') path,
       sid,
       blocking_session,
       event
     FROM v$session
     START WITH blocking_session IS NULL
     CONNECT BY PRIOR sid = blocking_session
     ORDER SIBLINGS BY sid
     /

LEVEL HIERARCHY            PATH                   SID BLOCKING_SESSION EVENT
----- -------------------- -------------------- ----- ---------------- --------------------------------
    1 1                    <-1                      1                  pmon timer
    1 2                    <-2                      2                  VKTM Logical Idle Wait
    1 3                    <-3                      3                  DIAG idle wait

...(*snip*)

    1 141                  <-141                  141                  SQL*Net message from client
    2   10                 <-141<-10               10              141 enq: TX - row lock contention
    3     20               <-141<-10<-20           20               10 enq: TX - row lock contention
    3     136              <-141<-10<-136         136               10 enq: TX - row lock contention
    4       143            <-141<-10<-136<-143    143              136 enq: TX - row lock contention
    2   19                 <-141<-19               19              141 enq: TX - row lock contention
    3     142              <-141<-19<-142         142               19 enq: TX - row lock contention
  • セッション1(SID=141) を セッション2(SID=10) と セッション4(SID=19) が待機して、
  • セッション2(SID=10) を セッション3(SID=136) と セッション6(SID=20) が待機して、
  • セッション3(SID=136) を セッション5(SID=143) が待機して、
  • セッション4(SID=19) を セッション7(SID=142) が待機している

つまり、セッション1(SID=141)が最終的にブロックしている起源(ルート)であることがわかる。

なお、LEVEL 疑似列により階層のレベルが分かる。また、SYS_CONNECT_BY_PATH により階層のパスを表すことができる。

v$active_session_history の階層問合せによる待機のブロッキング・セッションの特定

v$active_session_history では、アイドル以外の待機状態もしくは CPU 使用中のセッションしか現れない。 上述の例のように最終的に待機させていたセッション(セッション1:SID=141)がアイドル状態だと、ルートのセッションが現れないので単純には階層表示できない。 従って、blocking_session がすべて現れるように工夫する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
set pages 1000
set lines 200
col level         for 9999
col hierarchy     for a20
col path          for a20
col session_id    for 9999
col session_state for a13
col event         for a32

SELECT MAX(sample_id) FROM v$active_session_history;

define sample_id = &sample_id

WITH x AS
(
  SELECT session_id, blocking_session, session_state, event
  FROM v$active_session_history
  WHERE sample_id = &sample_id
  UNION ALL
  SELECT blocking_session, null, null, 'IDLE?'
  FROM v$active_session_history
  WHERE sample_id = &sample_id
  AND blocking_session NOT IN (SELECT session_id
                               FROM v$active_session_history
                               WHERE sample_id = &sample_id)
  GROUP BY blocking_session
)
SELECT
  LEVEL,
  LPAD(' ', 2 * (LEVEL - 1)) || session_id hierarchy,
  SYS_CONNECT_BY_PATH(session_id, '<-') path,
  session_id,
  blocking_session,
  session_state,
  event
FROM x
START WITH blocking_session IS NULL
CONNECT BY PRIOR session_id = blocking_session
ORDER SIBLINGS BY session_id
/

undefine sample_id
SQL> SELECT MAX(sample_id) FROM v$active_session_history;

MAX(SAMPLE_ID)
--------------
        244679

SQL> define sample_id = &sample_id
Enter value for sample_id: 244679

SQL> WITH x AS
     (
       SELECT session_id, blocking_session, session_state, event
       FROM v$active_session_history
       WHERE sample_id = &sample_id
       UNION ALL
       SELECT blocking_session, null, null, 'IDLE?'
       FROM v$active_session_history
       WHERE sample_id = &sample_id
       AND blocking_session NOT IN (SELECT session_id
                                    FROM v$active_session_history
                                    WHERE sample_id = &sample_id)
       GROUP BY blocking_session
     )
     SELECT
       LEVEL,
       LPAD(' ', 2 * (LEVEL - 1)) || session_id hierarchy,
       SYS_CONNECT_BY_PATH(session_id, '<-') path,
       session_id,
       blocking_session,
       session_state,
       event
     FROM x
     START WITH blocking_session IS NULL
     CONNECT BY PRIOR session_id = blocking_session
     ORDER SIBLINGS BY session_id
     /

LEVEL HIERARCHY            PATH                 SESSION_ID BLOCKING_SESSION SESSION_STATE EVENT
----- -------------------- -------------------- ---------- ---------------- ------------- --------------------------------
    1 141                  <-141                       141                                IDLE?
    2   10                 <-141<-10                    10              141 WAITING       enq: TX - row lock contention
    3     20               <-141<-10<-20                20               10 WAITING       enq: TX - row lock contention
    3     136              <-141<-10<-136              136               10 WAITING       enq: TX - row lock contention
    4       143            <-141<-10<-136<-143         143              136 WAITING       enq: TX - row lock contention
    2   19                 <-141<-19                    19              141 WAITING       enq: TX - row lock contention
    3     142              <-141<-19<-142              142               19 WAITING       enq: TX - row lock contention

アイドル状態のために現れないセッションを UNION ALL で仮想的に v$active_session_history に追加したインラインビュー x を使用した。 (もっといいやり方があるかもしれない。)

参考

[Debug] Ptrace によるデバッグ

目的

システムコール ptrace(2) を使用してデバッグする。 ptrace を使用して簡易的なデバッガを作成することで、GDB などの汎用デバッガより細かい制御を行うことも可能になる。

環境

  • OS: CentOS 5.5
  • Kernel: 2.6.18-194.el5 x86_64
  • CPU: Intel® Xeon® CPU E5345 (Virtual Machine on VMware)

前提知識

システムコール ptrace(2)

別のプロセスにアタッチしたり、別のプロセスのメモリやレジスタを参照、書込み等を行うことができるシステムコール。 デバッガを実装するために使用される。

1
2
3
4
#include <sys/ptrace.h>

long ptrace(enum __ptrace_request request, pid_t pid,
            void *addr, void *data);

request にアタッチしたプロセスに何を行うかを指定する。 例えば、以下が指定できる。(参考: man ptrace)

  • PTRACE_ATTACH

    pid で指定したプロセスにアタッチする。アタッチしたプロセスは子プロセスとしてトレースできるようにする。(引数 addrdata は無視される。)

  • PTRACE_PEEKTEXT, PTRACE_PEEKDATA

    メモリの addr の位置を参照する。(引数 data は無視される。)

  • PTRACE_PEEKUSR

    USER 領域のオフセット addr の位置を参照する。(引数 data は無視される。)

  • PTRACE_POKETEXT, PTRACE_POKEDATA

    data をメモリの addr の位置に書込む。

  • PTRACE_POKEUSR

    data を USER 領域のオフセット addr の位置に書き込む。

  • PTRACE_GETREGS

    レジスタの値を data にコピーする。data は user_regs_struct 構造体(<linux/user.h>)。(引数 addr は無視される。)

  • PTRACE_SETREGS

    data をレジスタにコピーする。data は user_regs_struct 構造体(<linux/user.h>)。(引数 addr は無視される。)

インストラクション INT 3

INT X(X は 0~255 の数値) は x86, x86-64 プロセッサのインストラクションで、ソフトウェア割り込みを発生させる。

特に INT 3(オペコード: 0xCC) により SIGTRAP シグナルが発生し、処理が中断される。これは、デバッガがブレイクポイントを設定するために使用される。

レジスタ IP

x86-64 プロセッサのレジスタ RIP (インストラクションポインタ)には次に実行される命令のアドレスが格納されている。(プログラムカウンタとも呼ばれる。)

ptrace によるブレイクポイントの設定

ptrace では、アタッチしたプロセスのメモリやレジスタを参照したり、書込むといった原始的なことしかできない。

したがって、ptrace によってブレイクポイントを設定してデバッグするには、以下のような手順が必要。

  1. PTRACE_ATTACH でターゲットのプロセスにアタッチ。

  2. PTRACE_PEEKTEXT でブレイクポイントを設定したいアドレスの命令を一時的に保存しておく。

  3. PTRACE_POKETEXT で 2. のアドレスの命令を INT 3 (0xCC)に書換える。

  4. PTRACE_CONT でプロセスを再開させる。

  5. waitpid(2) でブレイクするのを待つ。

  6. プロセスが INT 3 で書換えたアドレスでブレイクする。

  7. 次に実行する命令のアドレスを示すレジスタ IP は INT 3 で書換えたアドレス(ブレイクポイントを設定したアドレス)の次の位置(+1バイトの位置)を指しているので、PTRACE_GETREGS/PTRACE_SETREGS で IP をブレイクポイントを設定したアドレスに書換えて戻す。

  8. PTRACE_POKETEXT で 3. で書換えた命令を 2. で保存していたオリジナルに戻す。(ブレイクポイントを設定したアドレスのメモリを元の命令に戻す。)

  9. これでブレイクしたい位置で中断されているので、レジスタを見たり、メモリを参照したりして任意のデバッグを行う。

  10. 再度ブレイクさせたかったら 2. に戻る。 ただし、同じ命令のアドレスでブレイクさせたかったら、PTRACE_SINGLESTEP で 1 命令だけ進めてから、2. に戻る。(そうでないと現在の IP 位置を INT 3 で書換えてしまい、すぐにブレイクして処理が進まなくなってしまう。)

  11. デバッグが終わったら、PTRACE_DETACH でプロセスからデタッチする。

具体例

上述のような方法でブレイクポイントを設定してデバッグする簡易的なデバッガを作成する。

簡易デバッガ

引数としてアタッチしたいプロセスの PID と、ブレイクするアドレスを指定して、ブレイクポイントでレジスタ RDI の値を表示する簡易的なデバッガ。

ブレイクするアドレスとして関数の先頭アドレスを指定すれば、レジスタ RDI には第 1 引数が入っているはずなので、このプログラムを使って対象の関数が実行される度に第 1 引数を表示することができる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#include <stdio.h>      /* printf */
#include <string.h>     /* memset */
#include <sys/ptrace.h> /* ptrace */
#include <sys/reg.h>    /* RIP */
#include <sys/types.h>  /* waitpid, stat */
#include <sys/wait.h>   /* waitpid */
#include <stdlib.h>     /* atoi, strtol, exit */
#include <linux/user.h> /* user_regs_struct */
#include <sys/stat.h>   /* stat */
#include <unistd.h>     /* stat */

void my_command(pid_t pid)
{
  struct user_regs_struct regs;

  memset(&regs, 0, sizeof(regs));

  ptrace(PTRACE_GETREGS, pid, 0, &regs);
  printf("arg 1: %ld\n", regs.rdi);
}

void p_attach(pid_t pid)
{
  int status;

  ptrace(PTRACE_ATTACH, pid, NULL, NULL);
  waitpid(pid, &status, 0);
}

long p_break(pid_t pid, void *addr)
{
  long original_text;

  original_text = ptrace(PTRACE_PEEKTEXT, pid, addr, NULL);
  ptrace(PTRACE_POKETEXT, pid, addr, ((original_text & 0xFFFFFFFFFFFFFF00) | 0xCC));
  printf("Breakpoint at %p.\n", addr);

  return original_text;
}

void p_delete(pid_t pid, void *addr, long original_text)
{
  ptrace(PTRACE_POKEUSER, pid, sizeof(long) * RIP, addr);
  ptrace(PTRACE_POKETEXT, pid, addr, original_text);
}

void p_continue(pid_t pid)
{
  int status;

  ptrace(PTRACE_CONT, pid, NULL, NULL);
  printf("Continuing.\n");

  waitpid(pid, &status, 0);

  if (WIFEXITED(status))
  {
    printf("Program exited normally.\n");
    exit(0);
  }

  if (WIFSTOPPED(status))
    printf("Breakpoint.\n");
  else
    exit(1);
}

void p_stepi(pid_t pid)
{
  int status;

  ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL);
  waitpid(pid, &status, 0);
}

void p_quit(pid_t pid)
{
  ptrace(PTRACE_DETACH, pid, NULL, NULL);
}

int main(int argc, char *argv[])
{
  pid_t pid;
  void *addr;
  long original_text;
  struct stat buf;

  if (argc < 2)
  {
    printf("Usage: ptrace_demo <pid> <addr>\n");
    printf("Example: ptrace_demo 1234 0xabcdef0123456789\n");
    exit(1);
  }

  pid = atoi(argv[1]);
  addr = (void *)strtol(argv[2], NULL, 0);

  p_attach(pid);
  original_text = p_break(pid, addr);
  while (1)
  {
    p_continue(pid);
    p_delete(pid, addr, original_text);

    if (stat("./quit", &buf) == 0)
      break;

    /* do stuff */
    my_command(pid);

    p_stepi(pid);
    original_text = p_break(pid, addr);
  }

  p_quit(pid);

  return 0;
}

なお、レジスタの書換えは以下のように PTRACE_GETREGS/PTRACE_SETREGS でなくて、PTRACE_POKEUSER でも行けた。(43行目)

1
  ptrace(PTRACE_POKEUSER, pid, sizeof(long) * RIP, addr);

もちろん、PTRACE_GETREGS/PTRACE_SETREGS を使って以下のようにしてもよい。

1
2
3
4
5
  struct user_regs_struct regs;

  ptrace(PTRACE_GETREGS, pid, 0, &regs);
  regs.rip = (unsigned long) addr;
  ptrace(PTRACE_SETREGS, pid, 0, &regs);

あと、デバッグを止めるときはカレントディレクトリに quit というファイルを置くことにした。($ touch ./quit とかで作ればよい。)

コンパイルしておく。

$ gcc -o ptrace_demo ptrace_demo.c

デバッグ対象プログラム

デバッグ対象として以下の簡単なプログラムを用意。1秒毎に func 関数が呼ばれている。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <unistd.h>

void func(int x);

void func(int x)
{
  printf("hello, world: %d\n", x);
}

int main(int argc, char *argv[])
{
  int i = 0;

  for (i = 1; i <= 100; i++)
  {
    sleep(1);
    func(i);
  }
  return 0;
}

コンパイルしておく

$ gcc -o sample sample.c

デバッグ例

上記の func 関数が呼ばれる度に第 1 引数(レジスタ RDI) の値を出力するデバッグを行う。

まず、func のアドレスを確認。

$ nm sample | grep func
00000000004004d8 T func

func のアドレスは 0x00000000004004d8 。

プログラムを実行。

$ ./sample
hello, world: 1
hello, world: 2
hello, world: 3
hello, world: 4
hello, world: 5
hello, world: 6
...<略>

PID, アドレスを指定してこの簡易デバッガを実行してみる。

$ ps -ef | grep sample
hashi    21250 32254  0 17:33 pts/2    00:00:00 ./sample

$ ./ptrace_demo 21250 0x00000000004004d8
Breakpoint at 0x4004d8.
Continuing.
Breakpoint.
arg 1: 27
Breakpoint at 0x4004d8.
Continuing.
Breakpoint.
arg 1: 28
Breakpoint at 0x4004d8.
Continuing.
Breakpoint.
arg 1: 29
Breakpoint at 0x4004d8.
Continuing.
Breakpoint.
...<略>

func でブレイクして、第 1 引数(レジスタ RDI) の値が出力できている。

止めるときは同じディレクトリに quit という名前のファイルを作れば、デタッチしてデバッグ終了する。

$ touch quit

GDB で実行した場合

上述の方法で GDB で以下のようにしたときと同じようにデバッグすることができた。

$ gdb ./sample 21250
(gdb) b func
Breakpoint 1 at 0x4004dc
(gdb) command
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just "end".
>p $rdi
>c
>end
(gdb) c
Continuing.

Breakpoint 1, 0x00000000004004dc in func ()
$1 = 27

Breakpoint 1, 0x00000000004004dc in func ()
$2 = 28

Breakpoint 1, 0x00000000004004dc in func ()
$3 = 29

...<略>

ptrace を使用したデバッガを作るとで GDB より柔軟に情報採取をすることも可能になる。

参考

[Debug] LD_PRELOAD, Dlsym, GCC拡張機能によって共有ライブラリの関数の呼出し前後で任意の処理を実行する

目的

LD_PRELOAD, dlsym, GCC拡張機能によって共有ライブラリの関数の呼出し前後で任意の処理を実行する。

環境

  • OS: CentOS 5.5
  • Kernel: 2.6.18-194.el5 x86_64
  • GCC: gcc 4.1.2 20080704

使用する機能

LD_PRELOAD

環境変数 LD_PRELOAD に共有ライブラリを指定すると、そのライブラリがすべてのライブラリに先立ってロードされる。 これを利用して通常ロードしている共有ライブラリ内の関数を置き換えることができる。(参考: man ld.so)

dlsym

dlsym(3) は、シンボル名の文字列を引数に取り、そのシンボルのアドレスを返す。 これを利用して、関数のアドレスを得ることができる。(参考: man dlsym)

GCC 拡張 __attribute__((constructor)), __attribute__((deconstructor))

GCC 拡張で __attribute__ キーワードと共に関数の属性(attribute)を指定することができる。(参考: info gcc –> “C Extensions” –> “Function Attributes”)

constructor 属性が指定された関数は、main() 関数が呼ばれる前に実行される。 deconstructor 属性が指定された関数は、main() 関数が完了するか exit() が呼ばれた後で実行される。

具体例

実際に LD_PRELOAD, dlsym, GCC拡張機能により共有ライブラリの関数の呼出し前後で任意の処理を実行してみる。

準備

今回使用するのは共有ライブラリを使用する以下のプログラム。

  • foo.so 共有ライブラリ foo.h
1
int foo(int x, int y, char *z);
  • foo.so 共有ライブラリ foo.c
1
2
3
4
5
6
7
8
9
10
11
12
/* $ gcc -g -Wall -fPIC -shared -o libfoo.so foo.c */

#include "foo.h"
#include <stdio.h>

int foo(int x, int y, char *z)
{
  printf("[foo ] hello\n");
  printf("[foo ] request: %s\n", z);
  printf("[foo ] bye\n");
  return x + y;
}
  • main 関数(foo.so の関数を利用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* $ gcc -g -Wall -L. -lfoo main.c */

#include "foo.h"
#include <stdio.h>

int main(int argc, char *argv[])
{
  int r = 0;

  printf("[main] hello\n");

  r = foo(10, 20, "ten plus twenty");
  printf("[main] return from foo: %d\n", r);

  printf("[main] bye\n");
  return 0;
}

コンパイル方法と実行結果は以下。main 関数から共有ライブラリ foo.so 内の関数 foo() を呼んでいる。

$ gcc -g -Wall -fPIC -shared -o libfoo.so foo.c
$ gcc -g -Wall -L. -lfoo main.c
$ ./a.out
[main] hello
[foo ] hello
[foo ] request: ten plus twenty
[foo ] bye
[main] return from foo: 30
[main] bye

dlsym, GCC 拡張を使用した共有ライブラリの作成

上記の共有ライブラリ foo.so 内の関数 foo() の前後で処理をするために以下の bar.c から共有ライブラリ bar.so を作成する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*
 * $ gcc -g -Wall -D_GNU_SOURCE -fPIC -shared -o libbar.so bar.c -ldl
 * $ env LD_PRELOAD=./libbar.so ./a.out
 */

#include <stdio.h>  /* printf */
#include <dlfcn.h>  /* dlsym RTLD_NEXT */
#include <unistd.h> /* _exit */

static void init() __attribute__((constructor));
static void fini() __attribute__((destructor));

static long (*original_foo)(long arg1, long arg2, void *arg3);

static void init()
{
  printf("[bar ] init\n");

  original_foo = dlsym(RTLD_NEXT, "foo");
  printf("[bar ] orignal_foo: %p\n", original_foo);

  if (original_foo == NULL)
    _exit(1);
}

static void fini()
{
  printf("[bar ] fini\n");
}

long foo(long arg1, long arg2, void *arg3)
{
  long ret = 0;

  printf("[bar ] before foo arg1: %ld, arg2: %ld, arg3: %p(\"%s\")\n",
         arg1, arg2, arg3, (char *)arg3);

  ret = (*original_foo)(arg1, arg2, arg3);

  printf("[bar ] after foo ret: %ld\n", ret);

  return ret;
}

以下、コードの解説。

1
2
static void init() __attribute__((constructor));
static void fini() __attribute__((destructor));

GCC 拡張機能により、関数 init() に constructor 属性を設定し、main() 関数が呼ばれる前に実行されるようにしている。 また、関数 fini() に destructor 属性を設定し、プログラム終了直前に実行されるようにしている。

1
2
3
4
5
6
7
8
9
10
11
12
static long (*original_foo)(long arg1, long arg2, void *arg3);

static void init()
{
  printf("[bar ] init\n");

  original_foo = dlsym(RTLD_NEXT, "foo");
  printf("[bar ] orignal_foo: %p\n", original_foo);

  if (original_foo == NULL)
    _exit(1);
}

main() 関数が呼ばれる前に実行される init() 関数内で、オリジナルの foo() 関数(のアドレス)をグローバル変数 original_foo に格納している。dlsym() の引数に RTLD_NEXT を指定することで現在のライブラリ(この例では bar.so)以降で最初に関数が現れるところを探す。この機能により別の共有ライブラリ(この例では foo.so)の関数へのラッパーを提供することができる。

1
2
3
4
5
6
7
8
9
10
11
12
13
long foo(long arg1, long arg2, void *arg3)
{
  long ret = 0;

  printf("[bar ] before foo arg1: %ld, arg2: %ld, arg3: %p(\"%s\")\n",
         arg1, arg2, arg3, (char *)arg3);

  ret = (*original_foo)(arg1, arg2, arg3);

  printf("[bar ] after foo ret: %ld\n", ret);

  return ret;
}

オリジナルの foo() のラッパー関数として同一関数名を定義し、先ほど格納したオリジナルの関数を呼んでいる。その前後で任意の処理を実行している。(この例では引数や返値を出力している)

なお、この例のようにオリジナルの関数の引数の数は合わせたが、引数や返値の型が一致していなくても longvoid * などオリジナルの型が入るような型であれば暗黙的に型変換してうまく動いてくれるようだ。(クローズドソースの共有ライブラリとかオリジナルの関数の引数の型が分からないようなケースでも推測してある程度合わせればOKということ。)

コンパイルは以下のように実施して libbar.so を作成する。RTLD_NEXT マクロを使用するため、-D_GNU_SOURCE を加えていることと、dlsym() を使用するために -ldl を加えていることに注意。

$ gcc -g -Wall -D_GNU_SOURCE -fPIC -shared -o libbar.so bar.c -ldl

LD_PRELOAD の使用

あとは、作成した libbar.so を LD_PRELOAD で指定してロードされるようにすればよい。 元の実行ファイルや共有ライブラリは再コンパイル、リリンクすることなしに foo() 関数を置き換えて、前後に処理を実行できている。 これを利用すれば、既存の共有ライブラリの関数の引数を出力するなど、デバックが容易にできる。

$ env LD_PRELOAD=./libbar.so ./a.out
[bar ] init
[bar ] orignal_foo: 0x2b5fdd50d55c
[main] hello
[bar ] before foo arg1: 10, arg2: 20, arg3: 0x400785("ten plus twenty")
[foo ] hello
[foo ] request: ten plus twenty
[foo ] bye
[bar ] after foo ret: 30
[main] return from foo: 30
[main] bye
[bar ] fini

LD_PRELOAD しなかったときの出力と比較して、foo() 関数の前後で引数情報などが出力できている。 また、main 関数前後でも処理が実行されていることが分かる。

参考