在项目软件代码开发中,当软件代码量多了后,由于各种疏忽和编码审查不严格,导致代码中存在缺陷,程序运行后总是有各种异常出现,严重的导致程序崩溃,这个时候就期望能够在程序崩溃时,记录异常点相关的堆栈信息,用于对异常的分析,常见的方法有在系统中配置程序崩溃生成coredump文件,后续用gdb分析,这种方法需要了解gdb常用指令,并且涉及到修改了系统的一些配置,同时release版本下,有时候用gdb分析不一定有效,因此本文通过软件代码实现,简单记录程序崩溃的信息,主要使用到的是backtrace、abi::__cxa_demangle、nm、addr2line等函数和指令。
0. 程序中需要用到的头文件信息如下:
#include#include #include #include #include #include #include #if defined (__linux__) #include
1. 注册异常处理函数:
// 信号处理的map集合 static const std::mapgsc_mp4Signals = { {SIGSEGV, "SIGSEGV"}, {SIGABRT, "SIGABRT"}, {SIGINT, "SIGINT"}, {SIGFPE, "SIGFPE"}, {SIGILL, "SIGILL"}, {SIGSYS, "SIGSYS"}, {SIGBUS, "SIGBUS"} // 可以添加其他信号 }; static void registerExceptionHandler() { // 需要配合nm,addr2line等指令使用 struct sigaction action; sigemptyset(&action.sa_mask); action.sa_sigaction = &pfnExceptionHandler_linux; action.sa_flags = SA_SIGINFO; for (const auto& signals: gsc_mp4Signals) { if (0 > sigaction(signals.first, &action, NULL)) { printf("Error: sigaction failed!n"); } } return; }
1.5. 实现异常处理函数之前,还需要实现几个辅助接口,获取进程名称,运行路径,格式化的时间,实现如下:
涉及到的宏定义如下:
#define FORMAT_TIME_MAX_LEN 64 // 格式化时间最长长度 #define APP_NAME_MAX_LEN 128 // APP名称最大长度 #define APP_MAX_LEN 1024 // 定义APP处理最长长度 #define RATIO_1000 1000 // 1000的进率 #define FOLDER_MAX_LEN 512 // 文件夹长度最大值
void getAppRunPath(char* pPath, unsigned int uiLen)
{
if (!pPath)
{
return;
}
memset(pPath, 0, uiLen);
std::string strPath = "";
char szAbsPath[APP_MAX_LEN] = { 0 };
ssize_t iPathLen = readlink("/proc/self/exe", szAbsPath, APP_MAX_LEN); // 获取可执行程序的绝对路径
if (0 < iPathLen && iPathLen < APP_MAX_LEN)
{
std::string strFullPath(szAbsPath);
size_t iPos = strFullPath.find_last_of('/');
if (iPos != std::string::npos)
{
strPath = strFullPath.substr(0, iPos); // return the directory without the file name
}
}
memcpy(pPath, strPath.data(), (uiLen < strPath.length() ? uiLen : strPath.length()));
return;
}
bool createDirectory(const char* pFolderPath)
{
if (!pFolderPath)
{
return false;
}
bool bRet = true;
// 创建目录,函数方式
if (0 != access(pFolderPath, 0))
{
// 返回0表示创建成功,-1表示失败
if (0 > mkdir(pFolderPath, S_IRUSR | S_IWUSR | S_IXUSR | S_IRWXG | S_IRWXO))
{
bRet = false;
}
}
return bRet;
}
void getAppName(char* pName, unsigned int uiLen)
{
if (!pName)
{
return;
}
memset(pName, 0, uiLen);
std::string strAppName = "unknown";
char szAbsPath[APP_MAX_LEN] = { 0 };
ssize_t iPathLen = readlink("/proc/self/exe", szAbsPath, APP_MAX_LEN); // 获取可执行程序的绝对路径
if (0 < iPathLen && iPathLen < APP_MAX_LEN)
{
std::string strFullPath(szAbsPath);
size_t iPos = strFullPath.find_last_of('/');
if (iPos != std::string::npos)
{
strAppName = strFullPath.substr(iPos + 1);
}
}
memcpy(pName, strAppName.data(), (uiLen < strAppName.length() ? uiLen : strAppName.length()));
return;
}
static std::string getFormatTime()
{
time_t stTimeNow;
time(&stTimeNow);
char szTmp[FORMAT_TIME_MAX_LEN] = { 0 };
strftime(szTmp, sizeof(szTmp), "%Y%m%d_%H%M%S", localtime(&stTimeNow));
int iMillsec = 0;
struct timeval tv;
gettimeofday(&tv, NULL);
iMillsec = tv.tv_usec / RATIO_1000;
char szTime[FORMAT_TIME_MAX_LEN] = { 0 };
sprintf(szTime, "%s_%03d", szTmp, iMillsec);
std::string strTime(szTime);
return strTime;
}
2. 实现异常处理接口:
static void pfnExceptionHandler_linux(int signum, siginfo_t* info, void* ctx)
{
signal(signum, SIG_DFL); // 还原默认的信号处理
// 创建dump日志文件夹
char szAppFolder[FOLDER_MAX_LEN] = { 0 };
getAppRunPath(szAppFolder, sizeof(szAppFolder));
std::string strFolder = std::string(szAppFolder) + "/dump/";
if (!createDirectory(strFolder.data()))
{
return;
}
// 写入异常自定义头部信息
std::ostringstream oss;
oss << "Stack Trace: " << std::endl;
oss << "Signal (" << signum << "), " << strsignal(signum) << std::endl;
// 读取堆栈信息
const size_t sFrameSize = 32; // 堆栈大小
void* pStackBuffer[sFrameSize] = { 0 };
int iSize = backtrace(pStackBuffer, sFrameSize);
//backtrace_symbols_fd(pStackBuffer, iSize, STDOUT_FILENO);
char** symbols = backtrace_symbols(pStackBuffer, iSize);
if (!symbols)
{
return;
}
// 解析函数符号信息
for (int i = 0; i < iSize; ++i)
{
char* mangleName = 0;
char* offsetBegin = 0;
char* offsetEnd = 0;
for (char* p = symbols[i]; *p; ++p)
{
if ('(' == *p)
{
mangleName = p;
}
else if ('+' == *p)
{
offsetBegin = p;
}
else if (')' == *p)
{
offsetEnd = p;
break;
}
}
if (mangleName && offsetBegin && offsetEnd && (mangleName < offsetBegin))
{
*mangleName++ = ' ';
*offsetBegin++ = ' ';
*offsetEnd++ = ' ';
int status = -4;
// 获取混淆解析后可读的函数符号信息
char* retRealName = abi::__cxa_demangle(mangleName, nullptr, nullptr, &status); // 解析符号,得到真正的函数名
if (0 == status && retRealName)
{
oss << "[Bt" << i << "] status(" << status << "), " << symbols[i] << ": " << retRealName << " + ";
}
else
{
oss << "[Bt" << i << "] status(" << status << "), " << symbols[i] << ": " << mangleName << " + ";
}
oss << offsetBegin << offsetEnd << std::endl;
if (retRealName)
{
free(retRealName);
retRealName = nullptr;
}
}
else
{
oss << "[Bt" << i << "] status(-4), " << symbols[i] << std::endl;
}
}
free(symbols);
oss << std::endl;
// 异常写入文件
char szAppName[APP_NAME_MAX_LEN] = { 0 };
getAppName(szAppName, sizeof(szAppName));
std::string strAppName(szAppName);
std::string strFilePath = strFolder + "core-" + strAppName + "-" + getFormatTime();
std::ofstream fout(strFilePath.data());
do
{
if (!(fout.is_open()))
{
break;
}
fout << oss.str().data();
fout.close();
} while (0);
exit(1);
return;
}
3. 在main函数中靠前的位置注册函数即可:
int main(int argc, char** argv)
{
registerExceptionHandler();
// TODO 添加业务代码
}
4. 编译业务程序,注:编译过程中,编译选项需要添加:-g和-rdynamic,否则异常日志是不可识别的。
例如:直接使用g++编译:
g++ -g -rdynamic local.h local.cpp exceptionhelper.h exceptionhelper.cpp main.cpp -o App
使用cmakelists中时,需添加:
set(CMAKE_C_FLAGS "-std=gnu99 -g -rdynamic") set(CMAKE_CXX_FLAGS "-std=c++11 -g -rdynamic")
使用qtcreator编译时,在pro中配置:
QMAKE_CFLAGS += -std=gnu++11 -g -rdynamic QMAKE_CXXFLAGS += -std=c++11 -g -rdynamic QMAKE_LFLAGS += -g -rdynamic
5. 结果测试,使用了异常程序和main函数同一个工程,以及使用导出动态库的方式分别进行测试,编译的应用名称为App,导出库名为libApple.so。分别设置的异常代码如下图所示:
生成的三个异常日志信息如下:
日志中可以看到记录了异常的函数入口,以及偏移地址。
6. 借助nm、addr2line分析,查看函数地址的指令(在应用和库同级目录下,启动终端(Terminal))如下:
nm -C [appName] | grep [functionName]
nm -C [libName] | grep [functionName]
查看行数的指令如下:
addr2line -e [appName] [address]
addr2line -e [libName] [address]
查询到Local::send()地址为0x71a0,加上偏移地址0x159,即0x71a0 + 0x159 = 0x72f9。
即看到和第5步中展示的程序代码行数一致的,其他两个如下:
表明了该方式的可行性。



