システムプログラマーの成長計画-機械のように考える(二)


転載する時、出典と作者の連絡先の文章の出典を明記してください.http://www.limodev.cn/blog 作者の連絡先:李先静
誰がcallで私の-backtraceの実現の原理
関数呼び出し関係の表示(backtrace/callstack)は、gdbでbtコマンドでbacktraceを表示するなど、デバッガに必要な機能の1つです.プログラムがクラッシュしたとき、関数呼び出し関係は問題の根源を迅速に特定し、その実現原理を理解し、自分の知識面を拡張することができ、デバッガがない場合でも、自分のbacktraceを実現することができる.さらに重要なのはbacktraceの実現原理を分析することが興味深いことだ.一緒に検討してみましょう
glibcはbacktrace関数を提供しています.この関数は現在の関数のbacktraceを取得するのに役立ちます.まずその使用方法を見てから、それを真似て書きます.
#include <stdio.h>
#include <stdlib.h>
#include <execinfo.h>

#define MAX_LEVEL 4

static void test2()
{
int i = 0;
void* buffer[MAX_LEVEL] = {0};

int size = backtrace(buffer, MAX_LEVEL);

for(i = 0; i < size; i++)
{
printf("called by %p/n", buffer[i]);
}

return;
}

static void test1()
{
int a=0x11111111;
int b=0x11111112;

test2();
a = b;

return;
}

static void test()
{
int a=0x10000000;
int b=0x10000002;

test1();
a = b;

return;
}

int main(int argc, char* argv[])
{
test();

return 0;
}

コンパイル実行:gcc-g-Wall bt_std.c -o bt_std ./bt_std
スクリーン印刷:called by 0×8048440 called by 0×804848a called by 0×80484ab called by 0×80484c9
上に印刷されているのは呼び出し者のアドレスで、プログラマーにとってはあまり直感的ではありません.glibcはもう一つの関数backtrace_を提供しています.Symbolsは、これらのアドレスをソースコードの位置(通常は関数名)に変換することができます.しかし、この関数はあまり使いにくく、特にデバッグ情報がない場合、ほとんど役に立たない情報が得られます.ここでは、アドレスからソースコードの位置への変換を実現するために、別のツールaddr 2 lineを使用します.
実行:./bt_std |awk ‘{print “addr2line “$3″ -e bt_std”}’>t.sh;. t.sh;rm -f t.sh
スクリーン印刷:/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:12/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:28/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:39/home/work/mine/sysprog/think-compway/backtrace/b t_std.48
backtraceはどのように実現されますか?x 86のマシンで、関数呼び出し時にスタック内のデータの構造は以下の通りです.
---------------------------------------------
N

3
2
1
---------------------------------------------
EIP ,
EBP EBP, EBP 。
---------------- EBP ---------------
1
2
3

5
---------------------------------------------

(説明:下が低いのはアドレスで、上が高いアドレスで、スタックが下に成長しています)
呼び出す時、まず変調関数のパラメータをスタックに押し込み、C言語のスタック方式は:まず最後のパラメータを押し込み、最後から最後の2番目のパラメータを押し込み、この順序でスタックに入り、最後に最初のパラメータを押し込む.
そしてEIPとEBPを押し込むと、EIPは今回の呼び出しが完了した次の命令のアドレスを指し、このアドレスは関数呼び出し者のアドレスと近似できる.EBPは呼び出し者と被呼び出し関数との境界線であり、境界線の上には呼び出し者の一時変数、被呼び出し関数数のパラメータ、関数戻りアドレス(EIP)である,前の階層関数のEBPと,境界線の下には被変調関数の一時変数がある.
最後に、変調関数に入り、一時変数を割り当てる空間に入る.gccの異なるバージョンの処理は異なり、古いバージョンのgcc(例えばgcc 3.4)では、最初の一時変数が最も高いアドレスに置かれ、2番目の次は、順次分布する.新しいバージョンのgcc(例えばgcc 4.3),一時変数の位置は逆である,すなわち最後の一時変数が最も高いアドレスで,最後から2番目に次いで順次分布する.
backtraceを実現するには:
1.現在の関数のEBPを取得します.2.EBPによって呼び出し元のEIPを取得します.3.EBPによって前のレベルのEBPを取得します.4.この処理を最後まで繰り返します.
アセンブリコードを埋め込むことで、現在の関数のEBPを得ることができますが、ここではアセンブリを用いず、現在の関数のEBPを一時変数のアドレスで得ることができます.gcc 3.4で生成されたコードについて、現在の関数の最初の一時変数の次の位置がEBPであることを知っています.gcc 4.3で生成されたコードについては、現在の関数の最後の一時変数の次の位置はEBPです.
これらの背景知識を持って、私たちは自分のbacktraceを実現します.
#ifdef NEW_GCC
#define OFFSET 4
#else
#define OFFSET 0
#endif/*NEW_GCC*/

int backtrace(void** buffer, int size)
{
int n = 0xfefefefe;
int* p = &n;
int i = 0;

int ebp = p[1 + OFFSET];
int eip = p[2 + OFFSET];

for(i = 0; i < size; i++)
{
buffer[i] = (void*)eip;
p = (int*)ebp;
ebp = p[0];
eip = p[1];
}

return size;
}

旧バージョンのgccではOFFSETは0と定義されており、このときp+1はEBPであり、p[1]は上位レベルのEBPであり、p[2]呼び出し元のEIPです.この関数にはintの一時変数が全部で5個ありますので、新バージョンgccについてはOFFSETが5と定義されています.このときp+5がEBPとなります.1サイクルで1層上のEBPとEIPを繰り返し取り、最終的にすべての呼び出し元のEIPを得てbacktraceを実現します.
完全なプログラムでテストします(bt.c):
#include <stdio.h> 

#define MAX_LEVEL 4
#ifdef NEW_GCC
#define OFFSET 4
#else
#define OFFSET 0
#endif/*NEW_GCC*/

int backtrace(void** buffer, int size)
{
int n = 0xfefefefe;
int* p = &n;
int i = 0;

int ebp = p[1 + OFFSET];
int eip = p[2 + OFFSET];

for(i = 0; i < size; i++)
{
buffer[i] = (void*)eip;
p = (int*)ebp;
ebp = p[0];
eip = p[1];
}

return size;
}

static void test2()
{
int i = 0;
void* buffer[MAX_LEVEL] = {0};

backtrace(buffer, MAX_LEVEL);

for(i = 0; i < MAX_LEVEL; i++)
{
printf("called by %p/n", buffer[i]);
}

return;
}

static void test1()
{
int a=0x11111111;
int b=0x11111112;

test2();
a = b;

return;
}

static void test()
{
int a=0x10000000;
int b=0x10000002;

test1();
a = b;

return;
}

int main(int argc, char* argv[])
{
test();

return 0;
}

簡単なMakefileを書きます.
CFLAGS=-g -Wall
all:
gcc34 $(CFLAGS) bt.c -o bt34
gcc $(CFLAGS) -DNEW_GCC bt.c -o bt
gcc $(CFLAGS) bt_std.c -o bt_std

clean:
rm -f bt bt34 bt_std

コンパイルして実行:make./bt|awk'{print"addr 2 line"$3"-ebt"}>t.sh;.t.sh;
スクリーン印刷:/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:37/home/work/sysprog/think-compway/backtrace/bt.c:51/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:62/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:71
実行可能ファイルの場合、この方法は正常に動作します.共有ライブラリの場合、addr 2 lineはこのアドレスに基づいて対応するソースコードの位置を見つけることができません.なぜなら、addr 2 lineはアドレスオフセット量でしか検索できませんが、印刷されたアドレスは絶対アドレスです.共有ライブラリがメモリにロードされる位置は不確定なため、アドレスオフセット量を計算するには、プロセスmapも必要ですsファイルのヘルプ:
プロセスのmapsファイル(/proc/プロセス番号/maps)を使用すると、共有ライブラリのロード先を見つけることができます.たとえば、00 c 5 d 000-00 c 5 e 000 r-xp 00000000 08:05 2129013/home/work/mine/sysprog/think-in-compway/backtrace/libct_so.so.00 c 5 f 000 rw-p 00000000 08:05 2129013/work/sine/sysprog/think-in-compway/backtrace/libbt_so.so...
libbt_so.soのコードセグメントを0にロード×00c5d000-0×00 c 5 e 000、backtraceで印刷されたアドレスは、called by 0 xc 5 d 4 eb called by 0 xc 5 d 535 called by 0 xc 5 d 556 called by 0×80484ca
ここでは、印刷されたアドレスからロードされたアドレスを減算してオフセット量を算出することができる.例えば、0 xc 5 d 4 ebからロードアドレス0を減算する×00 c 5 d 000、オフセット量0を得る×4 eb、そして0×4 ebはaddr 2 lineに転送されます.
addr2line 0×4eb -f -s -e ./libbt_so.so
スクリーン印刷:/home/work/mine/sysprog/think-in-compway/backtrace/bt_so.c:38
スタック内のデータはとても面白くて、前節では、スタック内のデータを分析することによって、変参関数の実現原理を理解しました.この節では、スタック内のデータを分析することによって、backtraceの実現原理を学びました.
Category:システムプログラマー成長計画