乾貨|Linux程式函式棧列印工具gstack原始碼解讀、運用及擴充套件程式設計
點選上方“中興開發者社群”,關注我們
每天讀一篇一線開發者原創好文
一.需求場景
近期在工作中需要分析一個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預設重協商漏洞原理與安全重協商對抗機制
相關文章
- 用Shell擴充套件實現原始碼統計程式套件原始碼
- 寫擴充套件性好的程式碼:函式套件函式
- PostgreSQL 原始碼解讀(216)- 實現簡單的擴充套件函式SQL原始碼套件函式
- 乾貨 | 把Flutter擴充套件到微信小程式端的探索Flutter套件微信小程式
- kotlin 擴充套件(擴充套件函式和擴充套件屬性)Kotlin套件函式
- 乾貨丨如何水平擴充套件和垂直擴充套件DolphinDB叢集?套件
- 【Kotlin】擴充套件屬性、擴充套件函式Kotlin套件函式
- 擴充套件Linux網路棧套件Linux
- ajax 原始碼解讀之如何擴充套件 ajax 的功能原始碼套件
- Kotlin擴充套件函式Kotlin套件函式
- Z 函式(擴充套件KMP)函式套件KMP
- PostgreSQL 原始碼解讀(248)- HTAB動態擴充套件圖解#2SQL原始碼套件圖解
- PostgreSQL 原始碼解讀(247)- HTAB動態擴充套件圖解#1SQL原始碼套件圖解
- CheckBoxList擴充套件方法程式碼套件
- linux下php實現C/C++擴充套件程式設計LinuxPHPC++套件程式設計
- 用SQL Server寫指令碼和程式設計實現SSIS包的擴充套件SQLServer指令碼程式設計套件
- 使用高階函式實現類的擴充套件設計函式套件
- 使用Kotlin擴充套件函式擴充套件Spring Data案例Kotlin套件函式Spring
- Kotlin實戰:用實戰程式碼更深入地理解預定義擴充套件函式Kotlin套件函式
- es6-函式擴充套件函式套件
- ReactiveUI是.NET的Reactive程式設計擴充套件框架ReactUI程式設計套件框架
- ASP.NET MVC 5 Web程式設計3 -- Controller的應用及擴充套件ASP.NETMVCWeb程式設計Controller套件
- 擴充套件工具套件
- 設計師對可擴充套件設計工具的探索套件
- 乾淨的程式碼: 編寫可讀的函式函式
- Kotlin-常用擴充套件函式Kotlin套件函式
- HIVE自定義函式的擴充套件Hive函式套件
- Kotlin基礎 — 擴充套件函式Kotlin套件函式
- [譯]AppExtension程式設計指南:擴充套件基礎4APP程式設計套件
- [譯]AppExtension程式設計指南:擴充套件基礎1APP程式設計套件
- [譯]AppExtension程式設計指南:擴充套件基礎2APP程式設計套件
- [譯]AppExtension程式設計指南:擴充套件基礎3APP程式設計套件
- 讀 zepto 原始碼之工具函式原始碼函式
- 【趣解程式設計】函式程式設計函式
- 使用解構賦值與擴充套件運算子,讓你的程式碼更優雅賦值套件
- 怎樣用 Bash 程式設計:邏輯操作符和 shell 擴充套件程式設計套件
- 學習PHP中統計擴充套件函式的使用PHP套件函式
- chrome擴充套件程式開發Chrome套件