Hatena::ブログ(Diary)

お前の血は何色だ!! 4 このページをアンテナに追加 RSSフィード Twitter

2018-05-11

C#の例外メッセージの書き方はクソだ

C#の例外メッセージの書き方はクソだ。

あの例外メッセージの書き方は、例外の悪い書き方でしかない。

例えばこんなのだ。

System.NullReferenceException Object reference not set to an instance of an object.

このどこが悪いのか?

それは、理由の情報が書いてないことだ。

NullReferenceException ぬるぽが起きたらしいね。

なるほど、オブジェクトがnullだったらしい。

Nullだったということはわかる。

そうだね。nullらしいね。それじゃあ修正しよう。

では、どのオブジェクトがNULLだったのかい?

・・・・

・・・・・・・・

わかりません。

まったくわかりません。

スタックトレースに書いてある関数のどこかです。

ふさげるなっ!!



デバッグビルドだとIDEが場所で止まってくれるし、情報も吐き出される。

しかし、このダメな例外メッセージの書き方で問題になるのは、リリースビルドのときだ。

スタックトレースには、pdbがないと行番号は出力されないし、あってもなぜか出力されないこともある。

(世界は広いのでそういう変な環境もあるらしい)

よって、私たちが得られる情報は、直前に実行されていた関数内のどこかのオブジェクトがnullだったということだけだ。

どこだよそれは。

変数名はコンパイルの過程で消えるから出せない?

そうだね。確かに消えて出せない。でも、変数の型ならだせるだろう?

どの型が問題を起こしたのか?これだけでも候補を絞り込むことができる。

なんでこんな簡単なことをやらないの?


他にもダメなのがいっぱいある。

これを作った奴らはおかしい。

System.ArgumentOutOfRangeException Index was out of range.
System.Runtime.InteropServices.ExternalException A generic error occurred in GDI .

どれもこれも理由が明記されていない。

範囲外アクセスしたのはわかる。でもそれはどの型のオブジェクトで、何番目にアクセスしたから範囲外なのかといった基本的な情報が欠落している。

これはダメな例外メッセージの典型例だ。

意味が分からない例外メッセージはないと同じだ。 throw new ArgumentOutOfRangeException("fuck you"); と書いているのとほぼ同じだ。

いったいMSの開発者は何を考えていたんだ。

これを書いたMSの開発者は後で私のオフィスに来るように。


もし、このクソ仕様を受け入れるならば、関数をできるだけ小さい単位に細分化する必要がある。

1つの関数が1つのメソッドしか呼ばない程度にな。

そんな面倒なことはしてられないけど。


C#ネイティヴの例外は、起きたことはわかるんだけど、誰がなぜ起こしたのかわからない。

ダメな例外メッセージの書き方そのものだ。

C#の例外メッセージの書き方を真似しなければ、いいプログラムが書けるだろう。

C++だと黒魔術を利用して、例外ハンドラーそのものに割り込んで、もっとましな例外メッセージを追加したり、情報を多く取れるだろう。

しかし、C#の世界ではそれは不可能だ。ランタイムライブラリに割り込むことは不可能だろう。たぶん。

もし、もっとましな方法があるなら教えてほしい。

いったいどうやるのがベストプラクティスなんだ。

2018-03-09

死後の世界は存在するのか?[思考実験]

死後の世界は存在するのか?

死後の世界は、いろいろな宗教において、大切なテーマになっている。

今回はこれについて考えてみよう。

死後の世界は存在するのか?

俺様は、存在しないと思う。

理由は以下の通り。

例えば、生き物は死ねばあの世に行くと仮定する。

と、いうことは、ウイルス細菌はどうなんだ?

ウイルスも死ねばあの世に行くんだろうか?

行くとすれば、インフルエンザウイルスや、エボラウイルスエイズウイルスなどの強烈なウイルスや、

天然痘などの人類が絶滅させたウイルスもあの世にいるはずだ。

あの世は恐ろしいウイルスまみれということになるだろう。

害虫とかもあの世に行くんだろうか?

殺虫剤やバルサン農薬などで私達は毎年のように大量の害虫を殺している。

そしたら、あの世は恐ろしい害虫まみれということになる。

ミジンコ単細胞生物はあの世に行くんだろうか?

そしたら、あの世は恐ろしいミジンコまみれということになる。

・・・・

そんなのありえないだろう。

だから、死後の世界は存在しない。

それとも、あの世に行けるのは、人間だけということなんだろうか?

ネコや犬はいけないの?

でも、化け猫などのおばけの話はあるよね。

と、いうことは、哺乳類はいけるのかな?

でも、鳥類の化物の話も世界中にある。

と、いうことは鳥類も行けるのかな?

両生類は?魚類は?

・・・・

こちらの考え方でも理論が破綻する。


よって、死後の世界は存在しないと考えるのが一番合理的だ。

説得力がある。

だから、私は、死後の世界は存在しないと思っている。

2017-07-03

C#で var_dump

C#で var_dump するいい例がなかったので作った。

public static String var_dump(object obj)
{
    if (obj == null
        || obj is uint || obj is int
        || obj is ushort || obj is short
        || obj is byte || obj is byte
        || obj is float || obj is double || obj is bool
        || obj is UInt16 || obj is Int16
        || obj is UInt32 || obj is Int32
        || obj is UInt64 || obj is Int64
        ) 
    {
        return obj.ToString();
    }
    if (obj is string)
    {
        return "\"" + obj.ToString() + "\"";
    }
    string ret ;
    IEnumerable ienum = obj as IEnumerable;
    if (ienum != null)
    {
        ret = "[";
        foreach (object o in ienum)
        {
            ret += var_dump(o) + ",";
        }
        return ret + "]";
    }

    ret = "{";
    const BindingFlags FINDS_FLAG = BindingFlags.Public | BindingFlags.NonPublic |
            BindingFlags.Instance | BindingFlags.Static |
            BindingFlags.DeclaredOnly;
    FieldInfo[] infoArray = obj.GetType().GetFields(FINDS_FLAG);
    foreach (FieldInfo info in infoArray)
    {
        object o = info.GetValue(obj);
        ret += info.Name + ": " + var_dump( o ) + ",";
    }
    return ret + "}";
}

2017-06-16

ELF形式からシンボルデータを読み取って、アドレスから関数を求めよう。

ELF形式からシンボルデータを読み取って、アドレスから関数を求めよう。

バイナリのあるアドレスから、そのアドレスにある関数名を求めるにはどうすればいいだろうか?

たとえば、クラッシュして落ちたときに、クラッシュしたアドレスのログを残したい場合だ。

アドレスだけではわけがわからないので、関数名も一緒にログに書きたいのだ。

libbfd

libbfdを使えれば、bfd_openr や bfd_read_minisymbols といろいろ使って、アドレスから関数名を求めることができる。

ただ、もとまるのかもしれないが、libbfdはGPLである。

残念ながら LGPLではない。

ライセンスを気にする人は利用できないだろう。

Binary File Descriptor library

https://en.wikipedia.org/wiki/Binary_File_Descriptor_library

License: GNU General Public License


addr2line

では、代わりに addr2lineコマンドを利用して、

addr2line -p -C -e `readlink /proc/self/exe` 0x123 とかを呼び出すか?

それもよい。

でも、addr2lineはOSディフォルトでインストールされるわけではない。

インストールされていない環境もあるだろう。


ないならつくろう

では、どうするか?

自前で、elf形式を解析して、symbol情報を参照し、addrから関数を求めればいいのだ。

行数も出せればいいのだが、計算式がよくわからないので省略する。

とりあえず、アドレスが関数名になるだけでもかなりのヒントだろう。

ELF形式は、他の実行形式に比べて、比較的資料があるが、

日本語でシンボル情報参照まで書いている人があまりいなかったので、ここに解説したいと思う。


ソースコード

スニペットではなく動くものが見たい人は、ここらへん参照。

https://github.com/rti7743/naichichi2/blob/master/naichichi2/haikuwoyome.h#L123


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

#include <cxxabi.h> //でまんぐる

#include <elf.h>    //山田エルフ先生

//ELF形式は、64bitと32bitでわかれるらしいので、typedefして切り分ける。
#ifdef __x86_64__  
typedef uint64_t Elf_Addr;
typedef Elf64_Ehdr Elf_Ehdr;
typedef Elf64_Shdr Elf_Shdr;
typedef Elf64_Sym  Elf_Sym;
#else
typedef uint32_t Elf_Addr;
typedef Elf32_Ehdr Elf_Ehdr;
typedef Elf32_Shdr Elf_Shdr;
typedef Elf32_Sym  Elf_Sym;
#endif //__x86_64__


//C++のデマングル
static std::string demangle(const std::string& name)
{
	char buf[256];
	int status;
	size_t length = 256;
	if ( abi::__cxa_demangle(name.c_str(),buf,&length,&status))
	{
		return buf;
	}
	return name;
}

//elf形式の実行ファイルのシンボル情報からアドレス位置にある関数名の取得を行う.
static bool ElfToSymbol(const std::string& filename,Elf_Addr addr,std::string* outSymbol)
{
	int fd = open(filename.c_str(),O_RDONLY);
	if (fd < 0)
	{//ファイルを開けない.
		return false;
	}

	Elf_Ehdr ehdr;
	Elf_Shdr shdr;
	Elf_Shdr shdr_linksecsion;
	Elf_Sym  sym;
	int r = read(fd,&ehdr,sizeof(ehdr));
	if (r < 0)
	{//ファイル先頭のELFヘッダを読み込めない。
		close(fd);
		return false;
	}
	if ( memcmp(ehdr.e_ident,ELFMAG,SELFMAG) != 0 )
	{//ELF文字の確認。
		close(fd);
		return false;
	}

	//find SHDRテーブルの探索.
	for(int i = 0 ; i < ehdr.e_shnum ; i++ )
	{
		lseek(fd,ehdr.e_shoff + (i * sizeof(shdr)),SEEK_SET);
		r = read(fd,&shdr,sizeof(shdr));
		if ( r < sizeof(shdr) )
		{
			continue;
		}
		if ( ! (shdr.sh_type == SHT_SYMTAB || shdr.sh_type == SHT_DYNSYM) )
		{//シンボルが書かれているテーブルではないっぽい.
			continue;
		}

		//sh_link番目にあるデータに文字列テーブルがあるらしい.
		lseek(fd,ehdr.e_shoff + (shdr.sh_link * sizeof(shdr)),SEEK_SET);
		r = read(fd,&shdr_linksecsion,sizeof(shdr_linksecsion));
		if ( r < sizeof(shdr_linksecsion) )
		{
			continue;
		}

		//現在のSHDRテーブルを読む
		const unsigned int nloop_count = shdr.sh_size / sizeof(sym);
		for(int n = 0 ; n < nloop_count; n++ )
		{
			lseek(fd,shdr.sh_offset + (n*sizeof(sym)),SEEK_SET);
			r = read(fd,&sym,sizeof(sym));
			if ( r < sizeof(sym) )
			{
				continue;
			}

			if (addr < sym.st_value || addr >= sym.st_value  + sym.st_size )
			{//探しているアドレスではない
				continue;
			}
			//found.
			char buf[256];
			if (sym.st_name != 0)
			{
				//名前がある場合、[sh_link].sh_offset + sym.st_name に名前がある. 0終端
				lseek(fd,shdr_linksecsion.sh_offset + sym.st_name,SEEK_SET);
				r = read(fd,buf,255);
				if ( r < 0 )
				{
					continue;
				}
				buf[r] = 0; //終端

				*outSymbol = demangle(buf);
			}
			close(fd);
			return true;
		}
	}

	close(fd);
	return false;
}

解説

基本的にコードに書いてある通りだが、

とりあえず、冗長なエラー処理を除いて、簡単に解説していこう。

ELF形式の構造は64ビット、32ビットで変わるので typedef しておくと吉。

これらは、 elf.h ( /usr/include/elf.h ) に記載されている。

#include <elf.h>    //山田エルフ先生

//ELF形式は、64bitと32bitでわかれるらしいので、typedefして切り分ける。
#ifdef __x86_64__  
typedef uint64_t Elf_Addr;
typedef Elf64_Ehdr Elf_Ehdr;
typedef Elf64_Shdr Elf_Shdr;
typedef Elf64_Sym  Elf_Sym;
#else
typedef uint32_t Elf_Addr;
typedef Elf32_Ehdr Elf_Ehdr;
typedef Elf32_Shdr Elf_Shdr;
typedef Elf32_Sym  Elf_Sym;
#endif //__x86_64__

実行ファイルの 先頭から ELFヘッダはスタートする。

int fd = open(filename.c_str(),O_RDONLY);

Elf_Ehdr ehdr;
int r = read(fd,&ehdr,sizeof(ehdr));

if ( memcmp(ehdr.e_ident,ELFMAG,SELFMAG) != 0 )
{//ELF文字の確認。
	close(fd);
	return false;
}

そして、

ファイルのELFヘッダ ehdr.e_shoff バイト目から、 ehdr.e_shnum個の セクションヘッダテーブル struct Elf_Shdrのデータが並んでいる。

struct Elf_Shdrファイルの下の方に配置される。

struct Elf_Shdrは必須ではない。

stripして消されている可能性、つまり、ない可能性もある。

struct Elf_Shdrを順次読んでいき、 シンボル情報が書かれている可能性がある SHT_SYMTAB と SHT_DYNSYM を探り当てる。

for(int i = 0 ; i < ehdr.e_shnum ; i++ )
{
	lseek(fd,ehdr.e_shoff + (i * sizeof(shdr)),SEEK_SET);
	r = read(fd,&shdr,sizeof(shdr));
	
	if ( ! (shdr.sh_type == SHT_SYMTAB || shdr.sh_type == SHT_DYNSYM) )
	{//シンボルが書かれているテーブルではないっぽい.
		continue;
	}
...
}

SHT_SYMTAB と SHT_DYNSYMにはアドレスとそのシンボル情報が文字列テーブルの何番目に記録されているか書かている。

文字列テーブルは、shdr.sh_link番目の struct Elf_Shdr に書かれている。

なので、文字列テーブルの struct Elf_Shdr を、別途参照する必要がある。

ここでは、struct Elf_Shdr shdr_linksecsion; として読み込む.

//sh_link番目にあるデータに文字列テーブルがあるらしい.
lseek(fd,ehdr.e_shoff + (shdr.sh_link * sizeof(shdr)),SEEK_SET);
r = read(fd,&shdr_linksecsion,sizeof(shdr_linksecsion));

さて、文字列テーブルに寄り道をしたが、アドレスの話に戻る。

ファイルの shdr.sh_offset からデータが始まる。

データは、struct Elf_Sym である。

データは、hdr.sh_sizeバイト存在することになる。

//現在のSHDRテーブルを読む

const unsigned int nloop_count = shdr.sh_size / sizeof(sym);
for(int n = 0 ; n < nloop_count; n++ )
{
	lseek(fd,shdr.sh_offset + (n*sizeof(sym)),SEEK_SET);
	r = read(fd,&sym,sizeof(sym));

	if (addr < sym.st_value || addr >= sym.st_value  + sym.st_size )
	{//探しているアドレスではない
		continue;
	}
	...
}

struct Elf_Sym には、

その関数の開始アドレス sym.st_value

その関数の長さ sym.st_size

その関数の名前 sym.st_name

などがある。


探しているアドレスがどこにマッチするのかを探索し、

探しているアドレスが見つかれば名前を取得する。


関数の名前を取得するには、

sym.sh_link番目の struct Elf_Shdr を参照する。

先ほど、shdr_linksecsion として、ロードしたものだ。

shdr_linksecsion.sh_offset + sym.st_name が、望む文字列である。

	//found.
	char buf[256];
	if (sym.st_name != 0)
	{
		//名前がある場合、[sh_link].sh_offset + sym.st_name に名前がある. 0終端
		lseek(fd,shdr_linksecsion.sh_offset + sym.st_name,SEEK_SET);
		r = read(fd,buf,255);
		if ( r < 0 )
		{
			continue;
		}
		buf[r] = 0; //終端

		*outSymbol = demangle(buf);
	}

取得したシンボル名は、C++のマングル表記されているので、demangleする。

そのままでも読めはするが、わかりにくいので、demangleしたものを利用する。

demangleには複数の方法があるが、今回は abi::__cxa_demangle を利用した。

//C++のデマングル
static std::string demangle(const std::string& name)
{
	char buf[256];
	int status;
	size_t length = 256;
	if ( abi::__cxa_demangle(name.c_str(),buf,&length,&status))
	{
		return buf;
	}
	return name;
}

以上で、アドレスからELFバイナリのデバック情報を使って関数名に変換することができる。

2017-06-15

アイエエー 例外? なんで例外?

C++で未キャッチ例外が発生したときに、スタックトレース(バックトレース)を作成するライブラリです。

https://github.com/rti7743/naichichi2/blob/master/naichichi2/haikuwoyome.h

特徴

ヘッダ単体で使えます。

windows MSVC と Linux gccで動きます。

C++11が使えると、例外の中身も表示されます。

NYSLなので自由に使えます。コピペしてOK

名前がとてもかっこいい。

遊び方

#include "haikuwoyome.h"

int main()
{
	//これ以降で、未キャッチ例外が発生したら、スタックトレースを生成します.
	HaikuWoYome::KaisyakuShiteYaru(); //ハイクを詠め。介錯してやる。

	f();
	
	return 0;
}


#include "haikuwoyome.h"

void ff()
{
	*(int*)0 = 0;
}


void f()
{
	ff();
}

int main()
{
	//これ以降で、未キャッチ例外が発生したら、スタックトレースを生成します.
	HaikuWoYome::KaisyakuShiteYaru(); //ハイクを詠め。介錯してやる。

	f();

	return 0;
}

コンパイル C++11 が使える場合

g++  -std=c++11  -Wl,--no-as-needed -ldl  a.cpp

出力例

=HAIKU=============================================
backtrace:
HaikuWoYome::BackTrace() ./a.out() [0x402798]
HaikuWoYome::OnSignalFunction(int) ./a.out() [0x402c08]
/lib/x86_64-linux-gnu/libc.so.6(+0x354b0) [0x7fc5dbe484b0]
ff() ./a.out() [0x402ed0]
f() ./a.out() [0x402ee2]
main ./a.out() [0x402ef3]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7fc5dbe33830]
_start ./a.out() [0x401849]

signal: 11 (SIGSEGV)
process_id: 29259 thread_id:29259
===================================================


C++11がない場合でも、同一です。

g++   -Wl,--no-as-needed -ldl  a.cpp
=HAIKU=============================================
backtrace:
HaikuWoYome::BackTrace() ./a.out() [0x40204e]
HaikuWoYome::OnSignalFunction(int) ./a.out() [0x4024be]
/lib/x86_64-linux-gnu/libc.so.6(+0x354b0) [0x7f766a3d84b0]
ff() ./a.out() [0x402786]
f() ./a.out() [0x402798]
main ./a.out() [0x4027a9]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f766a3c3830]
_start ./a.out() [0x4014c9]

signal: 11 (SIGSEGV)
process_id: 29277 thread_id:29277
===================================================




#include "haikuwoyome.h"

void ff()
{
	throw std::runtime_error("さよならー");
}


void f()
{
	ff();
}

int main()
{
	//これ以降で、未キャッチ例外が発生したら、スタックトレースを生成します.
	HaikuWoYome::KaisyakuShiteYaru(); //ハイクを詠め。介錯してやる。

	f();

	return 0;
}

コンパイル C++11 が使える場合

g++  -std=c++11  -Wl,--no-as-needed -ldl  a.cpp

出力例

=HAIKU=============================================
backtrace:
__cxa_throw ./a.out(__cxa_throw+0x47) [0x402a28]
ff() ./a.out() [0x40306a]
f() ./a.out() [0x403089]
main ./a.out() [0x40309a]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f4b4ac47830]
_start ./a.out() [0x4019b9]

exception: std::runtime_error さよならー
process_id: 29693 thread_id:29693
===================================================

C++11がない場合は、例外の中身を取得できません.

g++   -Wl,--no-as-needed -ldl  a.cpp

=HAIKU=============================================
backtrace:
__cxa_throw ./a.out(__cxa_throw+0x47) [0x40235e]
ff() ./a.out() [0x4029ee]
f() ./a.out() [0x402a2a]
main ./a.out() [0x402a3b]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7fca4fb5c830]
_start ./a.out() [0x4016b9]

process_id: 29685 thread_id:29685
===================================================

標準は stderrに出力するだけですが、カスタムすこともできます。

#include <stdio.h>

//私のロガーです
void mylogger(const char* msg)
{
        puts("HELLO");
        puts(msg);
        puts("/HELLO");
}

//私のロガーを使います.
#define HAIKU_WO_YOME_OUTPUT_STDERR(MSG) mylogger(MSG)
#include "haikuwoyome.h"



void ff()
{
        throw std::runtime_error("さよならー");
}

void f()
{
        ff();
}

int main()
{
        //これ以降で、未キャッチ例外が発生したら、スタックトレースを生成します.
        HaikuWoYome::KaisyakuShiteYaru(); //ハイクを詠め。介錯してやる。

        f();

        return 0;
}
HELLO
=HAIKU=============================================
backtrace:
__cxa_throw ./a.out(__cxa_throw+0x47) [0x402a17]
ff() ./a.out() [0x403045]
f() ./a.out() [0x403064]
main ./a.out() [0x403075]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7ff44e551830]
_start ./a.out() [0x401979]

exception: std::runtime_error さよならー
process_id: 28341 thread_id:28341
===================================================

/HELLO



std::exceptionを基底にもたない例外を使いたい場合は、以下のようにします。

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

class MyException
{
        std::string msg;
public:
        MyException() {}
        MyException(const std::string& msg) { this->msg = msg; }
        virtual ~MyException(){}

        std::string what() const{ return this->msg; }
        //
        //または、
        //const char* what() const{ return this->msg.c_str() ; }
};

//私のロガーです
void mylogger(const char* msg)
{
        puts(msg);
}

//私のロガーを使います.
#define HAIKU_WO_YOME_OUTPUT_STDERR(MSG) mylogger(MSG)
//私の作った例外を使います.
#define HAIKU_WO_YOME_YOUR_EXECPTION_CLASS MyException
#include "haikuwoyome.h"

void ff()
{
        throw MyException("さよならー");
}


void f()
{
        ff();
}

int main()
{
        //これ以降で、未キャッチ例外が発生したら、スタックトレースを生成します.
        HaikuWoYome::KaisyakuShiteYaru(); //ハイクを詠め。介錯してやる。

        f();

        return 0;
}

=HAIKU=============================================
backtrace:
__cxa_throw ./a.out(__cxa_throw+0x47) [0x402a7f]
ff() ./a.out() [0x4030fb]
f() ./a.out() [0x403137]
main ./a.out() [0x403148]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f5ca9c6e830]
_start ./a.out() [0x401999]

exception: MyException さよならー
process_id: 28812 thread_id:28812
===================================================

当然ですが、例外をキャッチすると何も表示されません。

#include "haikuwoyome.h"

void ff()
{
	throw std::runtime_error("うぼあー");
}


void f()
{
	ff();
}

int main()
{
	//これ以降で、未キャッチ例外が発生したら、スタックトレースを生成します.
	HaikuWoYome::KaisyakuShiteYaru(); //ハイクを詠め。介錯してやる。

	try
	{
		f();
	}
	catch(std::exception& e)
	{
	}
	return 0;
}

出力例…当然何もでない.



			

この例外を投げたのは誰だー

この例外を投げたのは誰だーから6年。やっと解決したっぽい。

この例外を投げたのは誰だー

http://d.hatena.ne.jp/rti7743/20110109/1294605380