干货|Linux进程函数栈打印工具gstack源码解读、运用及扩展编程
2017-12-25 00:00
483 查看
点击上方“中兴开发者社区”,关注我们每天读一篇一线开发者原创好文
查看/usr/bin/gstack脚本源代码,发现它其实只是包装了gdb bt,并用sed对gdb bt的输出结果做了过滤而已。如下给出gstack脚本源代码的解读,该脚本分为五部分:
第一部分:检查是否提供一个入参,如果入参数量不是1,则打印用法提示,并退出脚本。
第二部分:检查入参必须是一个当前正在运行的程序的PID,如果不是,则退出脚本。
第三部分:判断内核是否支持gdb打印所有线程函数栈,如果不支持,则后续会将“bt”命令输入gdb中;如果支持,则后续会将“thread apply all bt”命令输入gdb中。
第四部分:执行gdb,通过“gdb [options] [executable-file] [process-id]”方式附着到指定PID的进程上,通过<<EOF方式为gdb传入多个命令,并将执行输出的结果通过管道“|”传给后续的sed命令。
第五部分:用sed去掉gdb输出的无效行,只提取含有线程信息、函数信息的行。
通过上述对gstack脚本源代码的分析可知,gstack只是gdb bt的简单封装,与我们的目标还有一定差距。看来需要自己编写一些扩展脚本或程序,才能进一步达成目标。首先,需要编写一个脚本,重复运行多次gstack,采集目标程序足够多次函数调用栈;其次,需要进一步净化数据,比如函数地址信息就需要过滤掉;还有,需要归并出不同的函数调用栈,找到不同的函数调用链,因为gstack输出的函数栈是用Thread行分隔的,可以编写一个程序来解析Thread行,将每个Thread块(多行)放到哈希桶中排重(即,排除重复项),从而得到唯一不同的函数调用链。
仍以redis-server为例,执行 make gstack_log PID=`pidof redis-server` NN=5,即可对redis-server连续运行5次gstack,并将结果保存到一个临时文件tmp_gstack_1353.txt中。在正式采集时,可以将NN设置为很大,比如NN=2000次,以采集到足够多的不同的函数调用栈信息。
然后,查看一下输出的临时文件的内容,即多次gstack输出结果的罗列。下一步需要将每个Thread行所分隔的块(多行),如块1、块2、块3、块4、、、进行净化和排重。
编写一个Node.js小程序gstack_data_format.js,用于对gstack输出结果净化并排重。程序读入gstack结果文件(如:tmp_gstack_1353.txt),一行一行地读入并累加到一个字符串变量中,遇到Thread行则停止累加,并将该字符串作为KEY添加到一个HASH桶中,因为HASH KEY天然不会重复,利用这个特点进行排重;遇到Thread行后,清空该字符串变量,重新开始累加;依次往复,直到读完整个文件。程序基本流程如下,具体源代码请见本文附录。
如下给出gstack_data_format.js的运行效果。该gstack结果文件为1186行,采集到237个函数栈,进行净化、排重后,得到2个唯一不同的函数调用栈。
var async = require ('async');
var g_iLineMinLen = 4;
var g_strFilename = 'tmp_gstack.txt';
if (process.argv.length <= 2) {
console.log ("HELP: ", process.argv[0], process.argv[1], "<GSTACK_FILENAME>");
process.exit(0);
return;
}
g_strFilename = process.argv[2];
console.log ("INPUT GSTACK_FILENAME: <<", g_strFilename, ">>");
var g_strFileContent = fs.readFileSync(g_strFilename, 'utf-8');
var g_strLineAry = new Array();
g_strLineAry = g_strFileContent.split("\n");
g_strFileContent = null;
var g_iAllLineCount = g_strLineAry.length;
console.log ('GOT', g_iAllLineCount, 'LINES FROM <<', g_strFilename, '>>');
var g_iNotNullCounter = 0;
var g_iThreadCounter = 0;
var g_strRecord = "";
var g_strRecordLineCount = 0;
var g_iIsRecordOK = 1;
var g_Hash = new Array();
async.forEachSeries (g_strLineAry, funcGetOneLine, funcGotAllLines);
function funcGetOneLine(strLine, callback) {
if (!strLine) {
callback();
return;
}
var strLeft = strLine.substr (0, 7);
if (strLeft == 'Thread ') {
if (g_iThreadCounter !== 0){
if (g_strRecord && g_iIsRecordOK){
g_Hash[g_strRecord] = g_strRecordLineCount;
}
}
g_iThreadCounter ++;
g_strRecord = " ";
g_strRecordLineCount = 0;
g_iIsRecordOK = 1;
} else {
var strNew = strLine.replace (/\(.*\)/g, " ");
strNew = strNew.replace (/0x.*? in /g, "");
if (strNew.length <= g_iLineMinLen) {
g_strRecord = null;
g_iIsRecordOK = 0;
} else {
g_strRecord += strNew + "\n ";
g_strRecordLineCount ++;
}
}
process.nextTick (function () {
g_iNotNullCounter ++;
callback();
});
}
function funcGotAllLines(err){
if (err) {
throw err;
}
console.log (' DONE TOTAL LINE :', g_iAllLineCount);
console.log (' DONE NOT NULL LINE:', g_iNotNullCounter);
console.log (' DONE THREAD LINE :', g_iThreadCounter);
var iFuncKeyCount = 0;
for (strFuncKey in g_Hash) {
iFuncKeyCount ++;
}
console.log (' DONE RECORD COUNT :', iFuncKeyCount);
console.log ();
console.log ('DETAILED RECORD INFORMATION:');
iFuncKeyCount = 0;
for (strFuncKey in g_Hash) {
iFuncKeyCount ++;
console.log ('FUNCTION CALLING STACK: NO.'+iFuncKeyCount);
console.log (strFuncKey);
}
process.exit(0);
return;
}
拓展阅读
干货|白话SSL/TLS默认重协商漏洞原理与安全重协商对抗机制
干货|公钥密码学在勒索病毒中的应用原理图解
一.需求场景
近期在工作中需要分析一个Linux服务器程序长期运行时的处理流程,想知道程序都执行了哪些函数调用链?比如,假设程序中有数千个函数,有时会触发func1() -> func2() -> func3()调用链;有时会触发func5() -> func2() -> func9()调用链;有时会触发func107() -> func999() -> fun3() -> func557() -> func 123()调用链;等等。通过分析函数调用链,有助于加深理解程序运行流程,便于重点分明地分析和走查源代码,提高工作效率,好处诸多。二.思路分析
我们知道gdb的bt(backtrace)可以打印函数调用栈,但需要手动敲命令执行,不能批量多次运行,似乎不太方便。有没有更好的工具和方法搞定这个需求呢?有的,gstack就是一款用于方便查看函数调用栈的工具。gstack的用途是“print a stack trace of a running process”,即打印一个正在运行的进程的函数调用栈。下面以一个正在运行的redis-server进程为例,执行gstack `pidof redis-server`即可看到该进程当前正在运行的3个线程各自的函数调用栈。这其实与gdb bt看到的差不多,而且我们的目标是只需要函数名,而不需要地址信息,那么gstack有没有什么参数可以去掉每行的地址,以精简打印呢?如下所示,gstack竟然没有帮助,这不像是一个正常的程序啊。用file `which gstack`查看,果然,它只是一个脚本,并不是一个正常的程序。查看/usr/bin/gstack脚本源代码,发现它其实只是包装了gdb bt,并用sed对gdb bt的输出结果做了过滤而已。如下给出gstack脚本源代码的解读,该脚本分为五部分:
第一部分:检查是否提供一个入参,如果入参数量不是1,则打印用法提示,并退出脚本。
第二部分:检查入参必须是一个当前正在运行的程序的PID,如果不是,则退出脚本。
第三部分:判断内核是否支持gdb打印所有线程函数栈,如果不支持,则后续会将“bt”命令输入gdb中;如果支持,则后续会将“thread apply all bt”命令输入gdb中。
第四部分:执行gdb,通过“gdb [options] [executable-file] [process-id]”方式附着到指定PID的进程上,通过<<EOF方式为gdb传入多个命令,并将执行输出的结果通过管道“|”传给后续的sed命令。
第五部分:用sed去掉gdb输出的无效行,只提取含有线程信息、函数信息的行。
通过上述对gstack脚本源代码的分析可知,gstack只是gdb bt的简单封装,与我们的目标还有一定差距。看来需要自己编写一些扩展脚本或程序,才能进一步达成目标。首先,需要编写一个脚本,重复运行多次gstack,采集目标程序足够多次函数调用栈;其次,需要进一步净化数据,比如函数地址信息就需要过滤掉;还有,需要归并出不同的函数调用栈,找到不同的函数调用链,因为gstack输出的函数栈是用Thread行分隔的,可以编写一个程序来解析Thread行,将每个Thread块(多行)放到哈希桶中排重(即,排除重复项),从而得到唯一不同的函数调用链。
三.扩展编程
首先,编写一个makefile脚本,用shell for循环不断调用gstack,将输出结果追加到临时文本文件中。仍以redis-server为例,执行 make gstack_log PID=`pidof redis-server` NN=5,即可对redis-server连续运行5次gstack,并将结果保存到一个临时文件tmp_gstack_1353.txt中。在正式采集时,可以将NN设置为很大,比如NN=2000次,以采集到足够多的不同的函数调用栈信息。
然后,查看一下输出的临时文件的内容,即多次gstack输出结果的罗列。下一步需要将每个Thread行所分隔的块(多行),如块1、块2、块3、块4、、、进行净化和排重。
编写一个Node.js小程序gstack_data_format.js,用于对gstack输出结果净化并排重。程序读入gstack结果文件(如:tmp_gstack_1353.txt),一行一行地读入并累加到一个字符串变量中,遇到Thread行则停止累加,并将该字符串作为KEY添加到一个HASH桶中,因为HASH KEY天然不会重复,利用这个特点进行排重;遇到Thread行后,清空该字符串变量,重新开始累加;依次往复,直到读完整个文件。程序基本流程如下,具体源代码请见本文附录。
如下给出gstack_data_format.js的运行效果。该gstack结果文件为1186行,采集到237个函数栈,进行净化、排重后,得到2个唯一不同的函数调用栈。
四.总结
正如本文我们一步一步所做,对运行的目标进程,执行多次gstack,获得大量函数调用栈,使用Node.js程序净化并排重,准确获得少量的唯一不同的函数调用栈。继而只需要依照这少量函数栈的信息,走查相关源代码。这个方法可以帮助我们快速理清程序的主要流程、便于我们快速走读走查源代码,提高学习和工作的效率。附:gstack_data_format.js源代码(用于对gstack结果净化排重)
var fs = require ('fs');var async = require ('async');
var g_iLineMinLen = 4;
var g_strFilename = 'tmp_gstack.txt';
if (process.argv.length <= 2) {
console.log ("HELP: ", process.argv[0], process.argv[1], "<GSTACK_FILENAME>");
process.exit(0);
return;
}
g_strFilename = process.argv[2];
console.log ("INPUT GSTACK_FILENAME: <<", g_strFilename, ">>");
var g_strFileContent = fs.readFileSync(g_strFilename, 'utf-8');
var g_strLineAry = new Array();
g_strLineAry = g_strFileContent.split("\n");
g_strFileContent = null;
var g_iAllLineCount = g_strLineAry.length;
console.log ('GOT', g_iAllLineCount, 'LINES FROM <<', g_strFilename, '>>');
var g_iNotNullCounter = 0;
var g_iThreadCounter = 0;
var g_strRecord = "";
var g_strRecordLineCount = 0;
var g_iIsRecordOK = 1;
var g_Hash = new Array();
async.forEachSeries (g_strLineAry, funcGetOneLine, funcGotAllLines);
function funcGetOneLine(strLine, callback) {
if (!strLine) {
callback();
return;
}
var strLeft = strLine.substr (0, 7);
if (strLeft == 'Thread ') {
if (g_iThreadCounter !== 0){
if (g_strRecord && g_iIsRecordOK){
g_Hash[g_strRecord] = g_strRecordLineCount;
}
}
g_iThreadCounter ++;
g_strRecord = " ";
g_strRecordLineCount = 0;
g_iIsRecordOK = 1;
} else {
var strNew = strLine.replace (/\(.*\)/g, " ");
strNew = strNew.replace (/0x.*? in /g, "");
if (strNew.length <= g_iLineMinLen) {
g_strRecord = null;
g_iIsRecordOK = 0;
} else {
g_strRecord += strNew + "\n ";
g_strRecordLineCount ++;
}
}
process.nextTick (function () {
g_iNotNullCounter ++;
callback();
});
}
function funcGotAllLines(err){
if (err) {
throw err;
}
console.log (' DONE TOTAL LINE :', g_iAllLineCount);
console.log (' DONE NOT NULL LINE:', g_iNotNullCounter);
console.log (' DONE THREAD LINE :', g_iThreadCounter);
var iFuncKeyCount = 0;
for (strFuncKey in g_Hash) {
iFuncKeyCount ++;
}
console.log (' DONE RECORD COUNT :', iFuncKeyCount);
console.log ();
console.log ('DETAILED RECORD INFORMATION:');
iFuncKeyCount = 0;
for (strFuncKey in g_Hash) {
iFuncKeyCount ++;
console.log ('FUNCTION CALLING STACK: NO.'+iFuncKeyCount);
console.log (strFuncKey);
}
process.exit(0);
return;
}
拓展阅读
干货|白话SSL/TLS默认重协商漏洞原理与安全重协商对抗机制
干货|公钥密码学在勒索病毒中的应用原理图解
相关文章推荐
- Redux源码解读--(1)知识准备之函数式编程
- 【深入Java编程】JVM源码分析之堆外内存完全解读
- dotNet源码解读--HashTable目录扩展的奥秘
- (转)Java并发编程总结---Hadoop核心源码实例解读
- RxJava运用技巧-RxAndroid部分源码解读
- 适配器模式及SDK源码中的运用(附:分别面向接口、类、对象的适配器扩展)
- Apache Beam WordCount编程实战及源码解读
- TX2平台下can总线收发功能的实现(二)——Linux下can总线编程模型和源码解读
- CI框架源码解读之利用Hook.php文件完成功能扩展的方法
- Spring源码解读 Spring初始化Bean时扩展
- CI框架源码解读之利用Hook.php文件完成功能扩展的方法
- <寒江独钓>Windows内核安全编程__Ramdisk源码解读
- Dubbo 源码解读 —— 可支持序列化及自定义扩展
- JAVA源码解读---HashMap目录扩展的奥秘
- 第6课:Spark Streaming源码解读之Job动态生成和深度思考
- 【转】Lua 为什么在游戏编程领域被广泛运用?
- 第16课:Spark Streaming源码解读之数据清理内幕彻底解密
- Tomcat源码解读系列三Tomcat对HTTP请求处理的整体流程
- vim扩展功能(高效率编程)使用学习笔记
- jpcsp源码解读12:本地码管理器与Compiler.xml