標籤:

如何快速地在每個函數入口處加入相同的語句?

最近在看一個c語言項目源碼,函數很多,對函數調用關係不是很明確,想在每個函數體入口處加入一些調試宏或者print 語句,一個一個函數的添加,太慢了,有什麼好的辦法嗎


若你熟悉Clang,你可以利用Clang來做source-to-source轉換,也就是去訪問每一個函數的定義,然後插入你的代碼。

但是,如 @陳碩 所言,對於function trace,我們不需要這麼麻煩,因為編譯器其實有幫你做這件事情,也就是-finstrument-functions。有了這個編譯選項後,編譯器會在每一個函數開始處插入__cyg_profile_func_enter,退出的地方插入__cyg_profile_func_exit,你所需要做的就是實現這兩個函數,而不用動代碼的其它地方。

我舉一個簡單的例子,首先我們有用於跟蹤函數的func_trace.c

#include &

static FILE *fp_trace;

void __attribute__((constructor)) traceBegin(void) {
fp_trace = fopen("func_trace.out", "w");
}

void __attribute__((destructor)) traceEnd(void) {
if (fp_trace != NULL) {
fclose(fp_trace);
}
}

void __cyg_profile_func_enter(void *func, void *caller) {
if (fp_trace != NULL) {
fprintf(fp_trace, "entry %p %p
", func, caller);
}
}

void __cyg_profile_func_exit(void *func, void *caller) {
if (fp_trace != NULL) {
fprintf(fp_trace, "exit %p %p
", func, caller);
}
}

使用 gcc func_trace.c -c 產生目標文件

隨後我們編寫一個簡單的測試代碼main.c

#include &

int foo(void) {
return 2;
}

int bar(void) {
zoo();
return 1;
}

void zoo(void) {
foo();
}

int main(int argc, char **argv) {
bar();
}

隨後將main.c與func_trace.o一起編譯,並且記得加上-finstrument-functions, 即:

gcc main.c func_trace.o -finstrument-functions

然後運行./a.out,就會產生func_trace.out,裡面的文件內容會類似這樣:

entry 0x4006d6 0x7f60c11a7ec5

entry 0x400666 0x4006fb

entry 0x4006a9 0x40068a

entry 0x40062d 0x4006c3

exit 0x40062d 0x4006c3

exit 0x4006a9 0x40068a

exit 0x400666 0x4006fb

exit 0x4006d6 0x7f60c11a7ec5

那麼接下來就是處理這個跟蹤文件的數據了,你可以利用addr2line達到這一點: addr2line -f -e $EXE $ADDR 即可以顯示出來函數名字了,所以我們可以寫一個小的Shell腳本來達到目的:

#!/bin/bash
EXECUTABLE="$1"
TRACELOG="$2"
while read TRACEFLAG FADDR CADDR; do
FNAME="$(addr2line -f -e ${EXECUTABLE} ${FADDR}|head -1)"
if test "${TRACEFLAG}" = "entry"
then
CNAME="$(addr2line -f -e ${EXECUTABLE} ${CADDR}|head -1)"
CLINE="$(addr2line -s -e ${EXECUTABLE} ${CADDR})"
echo "Enter ${FNAME} called from ${CNAME} (${CLINE})"
fi
if test "${TRACEFLAG}" = "exit"
then
echo "Exit ${FNAME}"
fi
done &< "${TRACELOG}"

然後使用方法很簡單 ./func_trace.sh a.out func_trace.out

就會出現函數的調用關係,而 main called from ?? 的 ?? 是 C 庫的東西。而後面的 ?? : ? 則是代表沒有debug信息,如果你想查看的話,那麼你需要編譯的時候加上 -g,如下所示:

其實,若有了這樣的數據,可以繼續往下做下去,如可以利用graphviz建立可視化調用關係圖,其實就是 IDE 的函數調用關係圖,你不如考慮直接使用IDE?:-)


Google 去年開源了 XRay 並且現在已經整合進 LLVM 了,如果只是需要做一個輕量的 logging 的話更好用——不需要自己寫 handler (默認的 handler 可以自動記錄 trace)和分析 log 結果(XRay 已經提供了一些基礎的分析工具,包括製圖),只要開啟 -fxray-instrument 然後用 llvm-xray 分析 trace 就行,性能據說也非常好。

XRay Instrumentation

不幸的是,似乎目前只在 SVN trunk 里,要等到下個 release (5.0?)才會被包含。


-finstrument-functions


使用 Understand,不要用 SourceInsight,強太多了,什麼圖都可以給你畫出來,注意用最新的版本,一年前的老版本對大項目經常來個崩潰,新版本穩定很多了。

我用 4.0 Build 872,網上有破解。


開Visual Studio生成個圖就好了,搞那麼麻煩幹什麼。


靜態分析:靜態分析工具、flex/clang定製靜態分析、IDA Pro 反彙編調用圖

運行時分析:src轉譯過程插樁、obj插樁、調試器批量斷點、system trace

都比改源碼好。非要改的話,學一下IDE里project/workspace全局正則表達式替換或者乾脆上 sed 。

不過,看大型項目源碼,耐下性子,備好紙筆記錄可能效果更好。


看了大家的回答基本是在編譯階段或通過靜態分析來實現加入埋點的。我提供一個運行時的思路:

solaris下的DTrace和linux下的systemtap是非常強大的動態跟蹤工具,它們可以在不修改代碼的情況下對程序的各個時刻的調用堆棧做快照。從而給你一個運行時的真實的調用關係圖。

再配合Brendan Gregg發明的火焰圖工具,可以更加直觀的以可視化的方式提供一個調用全景。比如,下圖是一個mysql的火焰圖:

為了看清細節,它是可以zoom in的:

http://www.brendangregg.com/FlameGraphs/cpu-mysql-updated.svg


我建議你通過調試和代碼分析工具來閱讀代碼。


正確的做法難道不是打斷點然後看調用棧嗎


火焰圖


佔個坑,這個東西我寫過

回去翻翻電腦,看能不能找到。

============================

找到了,如下。正則表達式,自己用的,還有一點bug,不過懶得改了,夠用。只有一小部分不匹配。

可以用python遍歷文件,然後每個函數都擼一遍就好了。

獲取函數名稱的正則表達式:

(?&<= )s*?(?!#)((?:(?:[a-zA-Z0-9_]+)(?:s*?*)?(?&解釋:

(?&<= ) #新函數必須新起一行,但匹配時不把回車符匹配上

s*? #開頭可以有空格,換行,TAB等,但盡量少的匹配

(?!#) #最開頭不允許為#等宏定義

( #此括弧內為第一個分組$1,匹配函數名稱前面的返回類型和限制條件

(?:(?:[a-zA-Z0-9_]+) #匹配返回類型以及限制條件,比如int,long. volatile, const 等

(?:s*?*)? #可能會出現返回值為指針類型,如:const int* hell(void)

(?& s+ #類型後面有空格,如:const int ×××

)+ #函數名前面可以有一個或者多個限制條件,類型,例如:volatile const long long * ××××()

)

(?!=) #類型和函數名稱之間是不可能有等號『=』的

([a-zA-Z_][a-zA-Z0-9_]*) #第二個分組$2,匹配函數名稱,函數名首字母不能為數字,之後可以字母數字均可

s* #函數名稱後面可以有空格

(([^)]*?)) #第三個分組$3,匹配函數的參數,兩個括弧之間可以是很多字元,『)』除外,這個可以後期制定更詳細的正則匹配

s* #函數參數之後可以有空格、換行、製表符等等。當然,應該要包括連接符『』,但是,考慮到一般不這樣用,而且可能與宏定義匹配到,在此處暫時不考慮,後期可以考慮添加進去

{ #函數的大括弧

( #第四個分組$4,匹配函數體

[^{}]* #函數體在有{}之前,還有部分函數內容

(?: #匹配可能有的{。。{。。。}。。。函數內容

(?:(?"brace"{)[^{}]*)+ #碰到一個{,則將brace壓入堆棧,加1,另外(?"brace"{)無法去掉分組,以後看是否可以解決。第五個分組$5,無意義

(?:(?"-brace"})[^{}]*)+ #碰到一個},則將brace彈出堆棧,減1,同樣(?"brace"})無法去掉分組。第六個分組$6,無意義

)* #一個或者多個,貪婪匹配

(?(brace)(?!)) #最後,判斷{}是否閉合,即判斷brace計數是否為零。如果不閉合,則不識別。這裡有個問題,注釋裡面的{}會干擾,造成函數識別錯誤。以後版本更改

)

} #函數的大括弧

================================

python示例,太丑了,客官可以略過。

def AddFuntionHead():

cwd = os.getcwd()

#cwd = "%userprofile%\Desktop"

tree = get_recursive_file_list(cwd)

#print tree

function_rule = r"(?&<= )s*?(?!#)((?:(?:[a-zA-Z0-9_]+)(?:s*?*)*?(?& #function_all = r"(?&<= )s*?(?!#)((?:(?:[a-zA-Z0-9_]+)(?:s*?*)?(?& file_name_rule = re.compile(r"w:\.*\([wd]*).c")

#outfile = open( cwd + "\output_funtion.txt" , "w+b")

#inputStr = "
"///////////////////////////////////"
"

#inputStr = "
printf("--file:%s function:%s line:%d\n",__FILE__,__func__,__LINE__);
"

inputStr = "
printf("--time=%s\t\tfunction=%s\n",__TIME__,__func__);
"

#used to replace current file content

outputStr = ""

#if you want to add just some file,you can add the absolute file path in the follow list,for example:

#tree = ["D:\testTemp\test.c","D:\testTemp\test2.c","D:\testTemp\test3.c"]

#tree = ["D:\testTemp\test.c","D:\testTemp\test2.c"]

for fileName in tree:

if fileName[-2:] == ".c":

print "============================================"

print fileName + " is Working."

print "============================================"

f = open(fileName,"r+U")

fileContent = f.read()

#print fileContent

#print "============================================"

#print "============================================"

rule = re.compile(function_rule)

matchStr = rule.finditer(fileContent)

currentEnd = 0

beforeEnd = 0

outputStr = ""

for matchArray in matchStr:

#print "==========="

#print matchArray.group() + "
"

#print matchArray.span()

currentEnd = matchArray.end()

outputStr += fileContent[beforeEnd:currentEnd] + inputStr

beforeEnd = currentEnd

outputStr += fileContent[beforeEnd:]

print outputStr

f.close()

f = open(fileName,"w")

f.write(outputStr)

f.close()

def ClearFunctionHead():

cwd = os.getcwd()

#cwd = "%userprofile%\Desktop"

tree = get_recursive_file_list(cwd)

#print tree

file_name_rule = re.compile(r"w:\.*\([wd]*).c")

#function_rule = r"s{2}printf(.{3}file:.*function:.*line:.*__FILE__,__func__,__LINE__);s"

function_rule = r"s{2}printf.*?--.*?__TIME__,__func__);s"

#used to replace current file content

outputStr = ""

#if you want to add just some file,you can add the absolute file path in the follow list,for example:

#tree = ["D:\testTemp\test.c","D:\testTemp\test2.c","D:\testTemp\test3.c"]

#tree = ["D:\testTemp\test.c","D:\testTemp\test2.c"]

for fileName in tree:

if fileName[-2:] == ".c":

print "============================================"

print fileName + " is Working."

print "============================================"

f = open(fileName,"r+U")

fileContent = f.read()

#print fileContent

#print "============================================"

#print "============================================"

rule = re.compile(function_rule)

matchStr = rule.finditer(fileContent)

currentStart = 0

beforeStart = 0

beforeEnd = 0

outputStr = ""

for matchArray in matchStr:

#print "==========="

#print matchArray.group() + "
"

#print matchArray.span()

currentStart = matchArray.start()

outputStr += fileContent[beforeEnd:currentStart]

beforeEnd = matchArray.end()

outputStr += fileContent[beforeEnd:]

print outputStr

f.close()

f = open(fileName,"w")

f.write(outputStr)

f.close()

=================

知乎沒有代碼編輯器嗎?

格式亂了


建議初學者應該用SourceInsight從main函數開始,一點點來推邏輯,再根據工具提供的函數調用圖來輔助,同時結合日誌列印信息就能很好的分析代碼了。


單純為了理清函數調用關係 用gdb調試 使用bt命令可以知道 我之前分析XServer代碼 就是一樣搞得


函數插樁


vim加上ctags


codereview中使用ctrl +h替換左花括弧為左花括弧+列印語句的宏定義+行尾分號。


喜歡


本來想說用aop的,結果一看是c語言。別說用不了spring了,代理模式都不知道怎麼用……


Dotnet 可以用postsharp 解決這類crosscutting concern


我三年前研究samba源碼時候遇到過類似問題,詳見:http://m.blog.csdn.net/article/details?id=12676131


剛工作的時候,自己寫了一個,很粗鄙,勉強可以用。那時候也不知道找找現成的工具。。


麻煩但是靈活的方法,寫個編譯器模塊,比如clang/llvm的 然後在每次遇到函數的時候處理一下,

具體如下圖。

書是 The Definitive ANTLR 4 Reference


推薦閱讀:

自學python,目標是web開發,請問我現在應該怎樣學習最合理?
什麼是體素渲染,如何從頭編寫一個體素渲染器?
free一塊修改過的malloc指針會發生什麼?
為什麼基礎很好的程序員代碼依舊寫的很爛?
有哪些適合編程的筆記本電腦值得推薦?

TAG:編程 |