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バイナリのデバック情報を使って関数名に変換することができる。