有时候我们会遇到,去研究一个比较陌生的项目,很多的cpp,c,文件。几万行甚至更多的源代码。
很多时候在什么工具都没有的情况下是比较烦的。计算你从main函数开始一行行的去读,都要花费很多的时间。尤其是那些么有注释的代码。
当然,你可以说,用某些工具去阅读(比如 SourceInsight)生成调用图。
这里还是有很多的问题,比如thread关系,多进程下的关系等等。还是相当的复杂,而且,阅读别人的代码本身就是一个枯燥的事情,需要花费大量时间去和作者的思路同步。
有没有简单一些的方法呢?
比如,程序运行一次,我就自动生成调用关系?
当然,现在有一些可以提供此类辅助的工具,比如valgrand(千万别把它理解成一个只是能找内存泄漏的工具,它的最大用途是生成调用时序图,以及函数间时间消耗列表)。不过用过的伙伴们都知道,这货会造成你的进程比实际进程慢上10倍。在大并发的正式环境,几乎无法上线,测试环境还可以马马虎虎跑跑。
那么,我们来想想,如果我们什么工具都没有的情况下,有没有办法去快速了解一个陌生的工程呢?
是的,方法是有的,通过以下方法,你可以最快速的获得函数间的调用关系,执行时间,顺序等等信息。甚至,学完这个方法,你甚至可以大致了解valgrand的内部机制了,其实它并不神秘。
只是,更多的时候,我们不需要这么详尽的列表,我们只需要一个简单的调用时序图。
首先,想要快速理解一个有成千上万个类和函数的程序,确实是一件很困难的事情。
如果要你回答这个问题,你可能会说,最简单的就是在所有的函数入口和出口追加一段代码输出,就可以知道函数间的调用关系了,但是,这个执行难度太大了,你需要修改成千上万的函数,类,宏,等等等等。这个根本就不可行。真的不可行吗?其实,很多时候,我们的编译器已经帮助我们做了很多工作,我们完全可以利用它的一些特性,在不改变项目代码的前提下,完成这个功能。
那就是gcc g++下的 -finstrument-functions 编译选项。
可见,我们头疼的问题,那些大神们也很头痛,以至于编译器开发的时候就想到了,只是这个秘密藏的有点深,不是朋友我根本不告诉它。
它能干什么神奇的事情,我们来看看。
众所周知,编译器在编译你的工程的时候,会通读一遍你的代码,生成代码树,根据优化选项对一些调用进行优化。这就是它的作用,我们可以在代码编译的时候,让编译器在代码树上添加一些我们想要的东东。这就是原理。(比如函数的头和尾)
就会出现函数的调用关系,而 main called from ?? 的 ?? 是 C 库的东西。而后面的 ?? : ? 则是代表没有debug信息,如果你想查看的话,那么你需要编译的时候加上 -g,如下所示:
你看,我们通过这个编译选项和回调函数,在不改变任何源代码的情况下,执行一遍函数,生成了调用关系树。
当然,你可以在这个回调函数里增加很多你希望增加的东西,比如函数执行时间,堆栈监控,堆监控,等等等等。(这不就是valgrand干的事情么)
当然,我们更多的时候,只希望看到调用关系就够了,其他的东西暂时不是我们关心的事情,先要了解事情的顺序。
这个方法还可用在,当你的程序莫名的崩溃,gdb展开core文件,你只能看到一堆内核崩溃输出的时候的无奈。通过这个方法,你可以知道,你的程序最后进入了那个函数,从而为错误的判定增加一些调试的砝码。(方法可以很灵活)
这里说说C程序的一些"坏习惯",比如公认的不眠不休的重复造轮子。
要知道,C程序有时候开发环境是比较严苛的,虽然有很多开源和前辈的东西,但是一样会有很多的坑。由于C面向的东西都比较底层,造成了严重的自我危机感和不信任感,这是一个行业的特征。其实是挺可怜的,"与其死在别人手上不如死在自己手上"是很多C程序的座右铭。有些时候,这不是一种自负,而是一种无奈。
在增长自我知识的同时,更有效的去学习别人的知识,更是让你真正成长的原因。所以,有时候我们需要用到一些方法,去达成我们的目的,尤其是在一个纷繁复杂的环境中,学会最优的生存方法。
很多时候在什么工具都没有的情况下是比较烦的。计算你从main函数开始一行行的去读,都要花费很多的时间。尤其是那些么有注释的代码。
当然,你可以说,用某些工具去阅读(比如 SourceInsight)生成调用图。
这里还是有很多的问题,比如thread关系,多进程下的关系等等。还是相当的复杂,而且,阅读别人的代码本身就是一个枯燥的事情,需要花费大量时间去和作者的思路同步。
有没有简单一些的方法呢?
比如,程序运行一次,我就自动生成调用关系?
当然,现在有一些可以提供此类辅助的工具,比如valgrand(千万别把它理解成一个只是能找内存泄漏的工具,它的最大用途是生成调用时序图,以及函数间时间消耗列表)。不过用过的伙伴们都知道,这货会造成你的进程比实际进程慢上10倍。在大并发的正式环境,几乎无法上线,测试环境还可以马马虎虎跑跑。
那么,我们来想想,如果我们什么工具都没有的情况下,有没有办法去快速了解一个陌生的工程呢?
是的,方法是有的,通过以下方法,你可以最快速的获得函数间的调用关系,执行时间,顺序等等信息。甚至,学完这个方法,你甚至可以大致了解valgrand的内部机制了,其实它并不神秘。
只是,更多的时候,我们不需要这么详尽的列表,我们只需要一个简单的调用时序图。
首先,想要快速理解一个有成千上万个类和函数的程序,确实是一件很困难的事情。
如果要你回答这个问题,你可能会说,最简单的就是在所有的函数入口和出口追加一段代码输出,就可以知道函数间的调用关系了,但是,这个执行难度太大了,你需要修改成千上万的函数,类,宏,等等等等。这个根本就不可行。真的不可行吗?其实,很多时候,我们的编译器已经帮助我们做了很多工作,我们完全可以利用它的一些特性,在不改变项目代码的前提下,完成这个功能。
那就是gcc g++下的 -finstrument-functions 编译选项。
可见,我们头疼的问题,那些大神们也很头痛,以至于编译器开发的时候就想到了,只是这个秘密藏的有点深,不是朋友我根本不告诉它。
它能干什么神奇的事情,我们来看看。
众所周知,编译器在编译你的工程的时候,会通读一遍你的代码,生成代码树,根据优化选项对一些调用进行优化。这就是它的作用,我们可以在代码编译的时候,让编译器在代码树上添加一些我们想要的东东。这就是原理。(比如函数的头和尾)
我举一个简单的例子,首先我们有用于跟踪函数的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\n", func, caller);
}
}
void __cyg_profile_func_exit(void *func, void *caller) {
if (fp_trace != NULL) {
fprintf(fp_trace, "exit %p %p\n", 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
#!/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
&amp;lt;img src="https://pic3.zhimg.com/v2-4a363286541430a1d99c6a9547490bfa_b.png" data-rawwidth="1412" data-rawheight="360" class="origin_image zh-lightbox-thumb" width="1412" data-original="https://pic3.zhimg.com/v2-4a363286541430a1d99c6a9547490bfa_r.png"&amp;gt;你看,我们通过这个编译选项和回调函数,在不改变任何源代码的情况下,执行一遍函数,生成了调用关系树。
当然,你可以在这个回调函数里增加很多你希望增加的东西,比如函数执行时间,堆栈监控,堆监控,等等等等。(这不就是valgrand干的事情么)
当然,我们更多的时候,只希望看到调用关系就够了,其他的东西暂时不是我们关心的事情,先要了解事情的顺序。
这个方法还可用在,当你的程序莫名的崩溃,gdb展开core文件,你只能看到一堆内核崩溃输出的时候的无奈。通过这个方法,你可以知道,你的程序最后进入了那个函数,从而为错误的判定增加一些调试的砝码。(方法可以很灵活)
这里说说C程序的一些"坏习惯",比如公认的不眠不休的重复造轮子。
要知道,C程序有时候开发环境是比较严苛的,虽然有很多开源和前辈的东西,但是一样会有很多的坑。由于C面向的东西都比较底层,造成了严重的自我危机感和不信任感,这是一个行业的特征。其实是挺可怜的,"与其死在别人手上不如死在自己手上"是很多C程序的座右铭。有些时候,这不是一种自负,而是一种无奈。
在增长自我知识的同时,更有效的去学习别人的知识,更是让你真正成长的原因。所以,有时候我们需要用到一些方法,去达成我们的目的,尤其是在一个纷繁复杂的环境中,学会最优的生存方法。