C/C++语言在编译以后,函数的名字会被编译器修改,改成编译器内部的名字,这个名字会在链接的时候用到。
将C++源程序标识符(original C++ source identifier)转换成C++ ABI标识符(C++ ABI identifier)的过程称为mangle;相反的过程称为demangle。
1 ABI
ABI是Application Binary Interface的简称。
C/C++发展的过程中,二进制兼容一直是个问题。不同编译器厂商编译的二进制代码之间兼容性不好,甚至同一个编译器的不同版本之间兼容性也不好。
之后,C拥有了统一的ABI,而C++由于其特性的复杂性以及ABI标准推进不力,一直没有自己的ABI。
这就涉及到标识符的mangle问题。比如,C++源码中的同一个函数名,不同的编译器或不同的编译器版本,编译后的名称可能会有不同。
每个编译器都有一套自己内部的名字,比如对于linux下g++而言。以下是基本的方法:
每个方法都是以_Z开头,对于嵌套的名字(比如名字空间中的名字或者是类中间的名字,比如Class::Func)后面紧跟N , 然后是各个名字空间和类的名字,每个名字前是名字字符的长度,再以E结尾。(如果不是嵌套名字则不需要以E结尾)。
比如对于_Z3foov 就是函数foo() , v 表示参数类型为void。又如N:C:Func 经过修饰后就是 _ZN1N1C4FuncE,这个函数名后面跟参数类型。 如果跟一个整型,那就是_ZN1N1C4FuncEi。
2 RTTI与type_info
C++在编译时开启RTTI(Run-Time Type Identification,通过运行时类型识别)特性时,可以在代码中使用typeid操作符(当然还需要包含<typeinfo>),此符号可以对一个变量或者一个类名使用,返回一个type_info对象的引用。编译时会为每种使用到RTTI的特性的C++类都建立一个唯一的type_info对象,并且会包含继承关系,dynamic_cast便是根据这个对象来判断某个基类对象的指针能否向下转换成子类对象的指针。下面为一个使用typeid的例子:
#include <iostream> #include <string> #include <typeinfo> using namespace std; int main() { string s; if(typeid(s) == typeid(string)) { cout<<"same type"<<endl; } else { cout<<"different type"<<endl; } return 0; }
3 mangle
但是我们今天关注的不是RTTI,而是关注与通过type_info获取到的名称信息,type_info有一个name()的方法,返回const char*,但是这个name到底是什么在C++规范中没有限定,因此不同编译器返回的结果不同,例如下面的代码:
cout<<typeid(std::string)<<endl;
如果使用vc编译器进行编译,将返回:
class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >
而g++编译执行时返回的却是:
Ss
后者很难理解,因为这是mangle后的符号名,而VC编译器返回的是demangle后的结果。使用C++的类与函数编译成obj文件后,都是使用mangle后的符号名。例如:假如我们编译了某个linux静态库,可以用nm工具查看其内部包含接口的符号(windows下可以使用dumpbin):
nm libmyfunc.a
其会返回许多mangle后的符号名,它们其实就是我们在库中编写的函数接口。
4 demangle
将C++ ABI标识符(C++ ABI identifier)转换成C++源程序标识符(original C++ source identifier)的过程称为demangle。更简单的说,识别C++编译以后的函数名的过程,就叫demangle。
在libstdc++里关于abi命名空间的文档中(https://gcc.gnu.org/onlinedocs/libstdc++/latest-doxygen/namespaces.html),介绍了GCC所使用的跨厂商(cross-vendor) C++ ABI,其中暴露的一个函数abi::__cxa_demangle就是用于demangling。
像C++ filt,连接器(linker)及其他的工具,都具有解析C++ ABI名称的能力,比如,使用C++filt命令行工具来demangle名字:
robin@centos7:~/blog$ c++filt St13bad_exception
std::bad_exception
现在你也可以做到(这个函数本身可能使用不同的demanglers,但是它所提供的抽象接口让你不用关心具体的实现)。
在如下情况你会关注demangle:
- 运行时阶段你想查看RTTI中的typeid字符串;
- 当你在处理runtime-support异常类时。
比如下面的示例:
#include <exception> #include <iostream> #include <cxxabi.h> struct empty { }; template <typename T, int N> struct bar { }; int main() { int status; char *realname; // exception classes not in <stdexcept>, thrown by the implementation // instead of the user std::bad_exception e; realname = abi::__cxa_demangle(e.what(), 0, 0, &status); std::cout << e.what() << "\t=> " << realname << "\t: " << status << '\n'; free(realname); // typeid bar<empty,17> u; const std::type_info &ti = typeid(u); realname = abi::__cxa_demangle(ti.name(), 0, 0, &status); std::cout << ti.name() << "\t=> " << realname << "\t: " << status << '\n'; free(realname); return 0; }
程序输出如下:
St13bad_exception => std::bad_exception : 0
3barI5emptyLi17EE => bar<empty, 17> : 0
本节的连接中的文档中介绍了demangler接口。它是用C写的,所以想demangle C++时,可以直接使用,而不需要再写一个C++的版本。
需要注意的是,需要手动释放返回的字符数组。比如,可以使用如下类似代码来自动管理返回的字符数组:
#include <cxxabi.h> // needed for abi::__cxa_demangle std::shared_ptr<char> cppDemangle(const char *abiName) { int status; char *ret = abi::__cxa_demangle(abiName, 0, 0, &status); /* NOTE: must free() the returned char when done with it! */ std::shared_ptr<char> retval; retval.reset( (char *)ret, [](char *mem) { if (mem) free((void*)mem); } ); return retval; }
上面代码使用了一个std::shared_ptr并附带一个定制的lambda ‘delete’函数来释放返回的内存。下面定义一个宏来访问demangled类型名:
#define CLASS_NAME(somePointer) ((const char *)cppDemangle(typeid(*somePointer).name()).get() )
在C++程序中这样来使用:
printf("I am inside of a %s\n",CLASS_NAME(this));
5 借助backtrace和demangle实现异常类Exception
实现原理是,首先获取栈痕迹,然后demangle符号名称。
5.1 backtrace相关函数
C++的异常类是没有栈痕迹的,如果需要获取栈痕迹,需要使用以下函数:
#include <execinfo.h>
int backtrace(void **buffer, int size);
char **backtrace_symbols(void *const *buffer, int size);
void backtrace_symbols_fd(void *const *buffer, int size, int fd);
(1)int backtrace (void **buffer, int size);
该函数用来获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针数组。参数size用来指定buffer中可以保存多少个void* 元素。函数返回值是实际获取的指针个数,最大不超过size大小在buffer中的指针实际是从堆栈中获取的返回地址,每一个堆栈框架有一个返回地址。注意某些编译器的优化选项对获取正确的调用堆栈有干扰,另外内联函数没有堆栈框架;删除框架指针也会使无法正确解析堆栈内容。
(2)char **backtrace_symbols (void *const *buffer, int size);
该函数将从backtrace函数获取的信息转化为一个字符串数组。参数buffer是从backtrace函数获取的数组指针,size是该数组中的元素个数(backtrace的返回值),函数返回值是一个指向字符串数组的指针,它的大小同buffer相同。每个字符串包含了一个相对于buffer中对应元素的可打印信息。它包括函数名,函数的偏移地址和实际的返回地址。backtrace_symbols生成的字符串都是malloc出来的,但是不要最后一个一个的free,因为backtrace_symbols会根据backtrace给出的callstack层数,一次性的将malloc出来一块内存释放,所以,只需要在最后free返回指针就OK了。
(3)void backtrace_symbols_fd (void *const *buffer, int size, int fd);
该函数与backtrace_symbols函数具有相同的功能,不同的是它不会给调用者返回字符串数组,而是将结果写入文件描述符为fd的文件中,每个函数对应一行。它不需要调用malloc函数,因此适用于有可能调用该函数会失败的情况。
backtrace将当前程序的调用信息存储在buffer中,backtrace_symbols则是将buffer翻译为字符串。后者用到了malloc,所以需要手工释放内存。
man手册中提供了如下的代码:
#include <execinfo.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> void myfunc3(void) { int j, nptrs; #define SIZE 100 void *buffer[100]; char **strings; nptrs = backtrace(buffer, SIZE); printf("backtrace() returned %d addresses\n", nptrs); /* The call backtrace_symbols_fd(buffer, nptrs, STDOUT_FILENO) would produce similar output to the following: */ strings = backtrace_symbols(buffer, nptrs); if (strings == NULL) { perror("backtrace_symbols"); exit(EXIT_FAILURE); } for (j = 0; j < nptrs; j++) printf("%s\n", strings[j]); free(strings); }
/* "static" means don't export the symbol... */ static void myfunc2(void) { myfunc3(); } void myfunc(int ncalls) { if (ncalls > 1) myfunc(ncalls - 1); else myfunc2(); } int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "%s num-calls\n", argv[0]); exit(EXIT_FAILURE); } myfunc(atoi(argv[1])); exit(EXIT_SUCCESS); }
编译并执行:
$ cc -rdynamic prog.c -o prog
$ ./prog 3
输出如下:
backtrace() returned 8 addresses
./prog(myfunc3+0x1f) [0x8048783]
./prog() [0x8048810]
./prog(myfunc+0x21) [0x8048833]
./prog(myfunc+0x1a) [0x804882c]
./prog(myfunc+0x1a) [0x804882c]
./prog(main+0x52) [0x8048887]
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0xb76174d3]
./prog() [0x80486d1]
5.2 实现异常类
因此我写出以下的异常类,注意上面的打印结果经过了名字改编,所以我们使用abi::__cxa_demangle将名字还原出来。
Exception.h代码如下:
#Exception.h #ifndef EXCEPTION_H_ #define EXCEPTION_H_ #include <string> #include <exception> class Exception : public std::exception { public: explicit Exception(const char* what); explicit Exception(const std::string& what); virtual ~Exception() throw(); virtual const char* what() const throw(); const char* stackTrace() const throw(); private: void fillStackTrace(); //填充栈痕迹 std::string demangle(const char* symbol); //反名字改编 std::string message_; //异常信息 std::string stack_; //栈trace }; #endif // EXCEPTION_H_
Exception.cpp代码如下:
#include "Exception.h" #include <cxxabi.h> #include <execinfo.h> #include <stdlib.h> #include <stdio.h> using namespace std; Exception::Exception(const char* msg) : message_(msg) { fillStackTrace(); } Exception::Exception(const string& msg) : message_(msg) { fillStackTrace(); } Exception::~Exception() throw () { } const char* Exception::what() const throw() { return message_.c_str(); } const char* Exception::stackTrace() const throw() { return stack_.c_str(); } //填充栈痕迹 void Exception::fillStackTrace() { const int len = 200; void* buffer[len]; int nptrs = ::backtrace(buffer, len); //列出当前函数调用关系 //将从backtrace函数获取的信息转化为一个字符串数组 char** strings = ::backtrace_symbols(buffer, nptrs); if (strings) { for (int i = 0; i < nptrs; ++i) { // TODO demangle funcion name with abi::__cxa_demangle //strings[i]代表某一层的调用痕迹 stack_.append(demangle(strings[i])); stack_.push_back('\n'); } free(strings); } } //反名字改编 string Exception::demangle(const char* symbol) { size_t size; int status; char temp[128]; char* demangled; //first, try to demangle a c++ name if (1 == sscanf(symbol, "%*[^(]%*[^_]%127[^)+]", temp)) { if (NULL != (demangled = abi::__cxa_demangle(temp, NULL, &size, &status))) { string result(demangled);
free(demangled); return result; } } //if that didn't work, try to get a regular c symbol if (1 == sscanf(symbol, "%127s", temp)) { return temp; } //if all else fails, just return the symbol return symbol; }
5.3 测试异常类
测试代码如下:
#include "Exception.h" #include <stdio.h> using namespace std; class Bar { public: void test() { throw Exception("oops"); } }; void foo() { Bar b; b.test(); } int main() { try { foo(); } catch (const Exception& ex) { printf("reason: %s\n", ex.what()); printf("stack trace: %s\n", ex.stackTrace()); } }
打印结果如下:
eason: oops
stack trace: Exception::fillStackTrace()
Exception::Exception(char const*)
Bar::test()
foo()
./a.out(main+0xf)
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3)
./a.out()
注意编译的时候,加上-rdynamic选项
有了这个类,我们可以在程序中这样处理异常:
try { // } catch (const Exception& ex) { fprintf(stderr, "reason: %s\n", ex.what()); fprintf(stderr, "stack trace: %s\n", ex.stackTrace()); abort(); } catch (const std::exception& ex) { fprintf(stderr, "reason: %s\n", ex.what()); abort(); } catch (...) { fprintf(stderr, "unknown exception caught \n"); throw; // rethrow }
参考文档:
http://gcc.gnu.org/onlinedocs/libstdc++/manual/ext_demangling.html
http://www.bagualu.net/wordpress/archives/2312
http://blog.csdn.net/icefireelf/article/details/6591298
https://www.cnblogs.com/inevermore/p/4005489.html
https://stackoverflow.com/questions/4939636/function-to-mangle-demangle-functions
https://www.cnblogs.com/inevermore/p/4005489.html