所以我想分析我的应用程序,我特别想记录从程序开始到程序内调用的每个函数(DLL中的ingnoring函数)进入和退出的时间,即我想要一个简单的表格,看起来像这样:
THREAD_ID FUNCTION_ADDRESS TIME EVENT_TYPE
5520 0xFF435360 0 ENTERED
5520 0xFF435ED3 25 ENTERED
5520 0xFF433550 40 ENTERED
5520 0xFF433550 50 EXITED
5520 0xFF433550 60 ENTERED
5520 0xFF433550 70 EXITED
5520 0xFF435ED3 82 EXITED
5520 0xFF435360 90 EXITED
对于看起来像这样忽略编译器优化的程序:
void test1(void)
{
int a = 0;
++a;
}
void test(void)
{
test1();
test1();
}
void main(void)
{
test();
}
我找不到任何现成的解决方案,我能找到的最接近的是微软的VSPerfReport,但它只是输出每个函数花费的时间,而不是进入和退出的时间。
所以我开始研究用一个简单的函数钩住我所有的函数,该函数产生一个缓冲区,我可以从中生成上面的表格。 为了做到这一点,我只是想创建一个在 main 开始时调用的函数,该函数可以通过整个 exe 修改 CALL 指令以调用我的钩子函数。
像MinHook 等这样的库对我来说似乎都有点 OTT,并且可能无法工作,因为它是一个 x64 应用程序,我并没有试图挂钩 DLL 函数。
因此,我想只修改每个CALL指令中的JMP指令,即该程序:
void main(void)
{
...asm prologue
test();
002375C9 call test (235037h)
}
...asm epilogue
此处的调用转到JMP的表格:
@ILT+40(__set_errno):
0023502D jmp _set_errno (243D80h)
@ILT+45(___crtGetEnvironmentStringsA):
00235032 jmp __crtGetEnvironmentStringsA (239B10h)
test:
00235037 jmp test (237170h)
@ILT+55(_wcstoul):
0023503C jmp wcstoul (27C5D0h)
@ILT+60(__vsnprintf_s_l):
我想浏览此表,将与应用程序.exe中的函数相关的所有 JMP 重新路由到包含时序代码的钩子函数,然后返回到调用函数。
那么ILT代表什么,我假设一些查找表,我将如何掌握它?
这是否可能我听说过 IAT 挂钩,但在我看来,这仅在钩接 DLL 时。 同样在这里,我忽略了退出,尽管另一个JMP代替RET指令可能会有所帮助?
感谢您的任何帮助
你研究过谷歌的分析工具吗?您可能会发现修改比制作自己的更容易一些。它确实执行代码插入以执行其分析,因此至少,它们的注入框架对您有益。
但是,对于这样的事情,您主要希望避免时序开销,因此我建议按地址跟踪,然后在分析完成后,将地址转换为符号名称。钩子本身也可能是一项艰巨的任务,我建议制作一个多合一的包装器,它不会改变函数入口或退出,而是重定向调用站点。
那么ILT代表什么,我假设一些查找表,我将如何掌握它?
导入查找表,如果您还计划分析内部函数,它将没有多大用处。掌握它需要探索平台模块格式(PE,ELF,MACH-O)的内部结构。
gcc 有一个选项来生成对函数进入和退出的钩子的调用。使用 -finstrument-functions
进行编译,编译器生成对 __cyg_profile_func_enter
和 __cyg_profile_func_exit
的调用。 您可以在 gcc 文档 http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html 中阅读更多信息。 这里有一篇很好的文章,其中包含如何使用它的示例 http://www.ibm.com/developerworks/library/l-graphvis/。
在 Linux 上,您可以使用gprof(1)
来获取该数据。但请接受宾利在他的"编程珍珠"中对性能的看法。第二部分是他的"编写高效程序"(可惜已绝版)的提炼,非常详细地讨论了如何(更重要的是,何时)优化代码。
struct my_time_t;
my_time_t get_current_time(); // may be asm
struct timestamp;
struct timer_buffer {
std::unique_ptr<timestamp[]> big_buffer;
size_t buffer_size;
size_t current_index;
size_t written;
buffer( size_t size ): big_buffer( new timestamp[size] ), buffer_size(size), current_index(0), written(0) {}
void append( timestamp const& t ) {
big_buffer[current_index] = t;
++current_index;
++written;
current_index = current_index % buffer_size;
}
};
struct timestamp {
static timer_buffer* buff;
timestamp const* loc;
my_time_t time;
const char* filename;
size_t linenum;
timestamp( my_time_t t, const char* f=nullptr, size_t l = 0 ):
loc(this), time(t), f(filename), l(linenum)
{
go();
}
void go() {
buff->append(*this);
}
};
struct scoped_timestamp:timestamp {
scoped_timestamp( my_time_t t, const char* f=nullptr, size_t l = 0 ):
timestamp(t, f, l)
{}
~scoped_timestamp() {
go();
}
};
#define TIMESTAMP_SCOPE( NAME ) scoped_timestamp NAME(get_current_time(), __FILE__, __LINE__);
#define TIMESTAMP_SPOT() do{timestamp _(get_current_time(), __FILE__, __LINE__);}while(false)
在某处创建timestamp::buff
。 使buff
足够大。 写一个快速高效的get_current_time()
。
你认为有问题的函数的开头插入TIMESTAMP_SCOPE(_)
。
认为需要时间的位置之间插入TIMESTAMP_SPOT();
。
在关机之前添加一些timer_buffer
的后处理 - 将其写出到磁盘或其他什么。 请注意是否written
> current_index
,在这种情况下,您包装了缓冲区。 请注意,上面的代码都不包含任何分支,因此它应该相对性能友好(除了不断将buff
拥有的数组移动到缓存中)。
该loc
存在,因此您可以相对轻松地找到创建/销毁对(因为它的二进制值跟踪堆栈的值!),因此您可以在事后分析缓冲区,哪些函数调用花费了太长时间。 将可视化工具放在一起并不是那么难,我已经看到了与上面非常相似的东西,用于检测视频流驱动程序代码中的毫秒级时序故障和打嗝。
从current_index
开始分析并向后工作,寻找货币对,直到你点击0
(或者,如果written
!= current_index
,直到你绕到current_index+1
)。 恢复调用图应该不难(如果需要)。
去除上述大部分内容,并简单地为每个timestamp
使用唯一标签,可以减小缓冲区的大小,但使重建调用图变得更加困难。
是的,这不是自动检测。 但是,代码中行为缓慢的部分将是其中相对较小的部分。 因此,开始使用类似上述内容进行检测,我猜您会比弄乱反汇编编译器的二进制输出和弄乱跳转表更快地得到答案。