gcc内置profile功能的实现、应用及限制

一、问题的背景

在某些情况下,我们希望对C中的特定函数执行时间进行统计,当然比较简单的方法就是在这个需要进行profile的函数添加一个函数局部变量,并在构造和析构函数的时候的时间差来作为整个函数的执行时间。如果这种需要统计的函数比较多,那么这种手动添加起来就比较麻烦,此时就考虑到了gcc编译器提供的profile选项,也即是-pg选项。

二、gcc对该选项的处理

1、选项的配置文件

gcc对于编译选项是通过专门的配置文件配置出来的,所以只是通过源代码并不能看到这些选项的解析。
gcc-4.8.2gcccommon.opt
p
Common Var(profile_flag)
Enable function profiling
构建过程中生成的options.c文件中的内容
{ "-o",
"-o <file> Place output into <file>",
"missing filename after %qs",
0,
NULL, NULL, N_OPTS, N_OPTS, 1, -1,
CL_C | CL_CXX | CL_Fortran | CL_Go | CL_ObjC | CL_ObjCXX | CL_COMMON | CL_DRIVER | CL_JOINED | CL_SEPARATE,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
offsetof (struct gcc_options, x_asm_file_name), 0, CLVC_STRING, 0 },
{ "-p",
"Enable function profiling",
0,
0,
NULL, NULL, N_OPTS, N_OPTS, 1, -1,
CL_COMMON,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
offsetof (struct gcc_options, x_profile_flag), 0, CLVC_BOOLEAN, 0 },
{ "-pass-exit-codes",
0,
0,
0,
NULL, NULL, N_OPTS, N_OPTS, 15, -1,
CL_DRIVER,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
offsetof (struct gcc_options, x_pass_exit_codes), 0, CLVC_BOOLEAN, 0 },
options.h头文件中定义
#ifdef GENERATOR_FILE
extern int profile_flag;
#else
int x_profile_flag;
#define profile_flag global_options.x_profile_flag
#endif

2、选项的使用

gcc-4.8.2gccfunction.c
void
expand_function_start (tree subr)
{
/* Make sure volatile mem refs aren't considered
valid operands of arithmetic insns. */
init_recog_no_volatile ();

crtl->profile
= (profile_flag
&& ! DECL_NO_INSTRUMENT_FUNCTION_ENTRY_EXIT (subr));

gcc-4.8.2gccfunction.h
/* Accessor to RTL datastructures. We keep them statically allocated now since
we never keep multiple functions. For threaded compiler we might however
want to do differently. */
#define crtl (&x_rtl)

3、代码的生成

由于x86_64中定义了NO_PROFILE_COUNTERS宏,所以生成的对mcount函数的调用时没有额外参数的。
gcc-4.8.2gccfinal.c
rtx
final_scan_insn (rtx insn, FILE *file, int optimize_p ATTRIBUTE_UNUSED,
int nopeepholes ATTRIBUTE_UNUSED, int *seen)
{
……
case NOTE_INSN_PROLOGUE_END:
targetm.asm_out.function_end_prologue (file);
profile_after_prologue (file);
……
}

static void
profile_after_prologue (FILE *file ATTRIBUTE_UNUSED)
{
if (!targetm.profile_before_prologue () && crtl->profile)
profile_function (file);
}

gcc-4.8.2gccconfigi386i386.c
/* Output assembler code to FILE to increment profiler label # LABELNO
for profiling a function entry. */
void
x86_function_profiler (FILE *file, int labelno ATTRIBUTE_UNUSED)
{
const char *mcount_name = (flag_fentry ? MCOUNT_NAME_BEFORE_PROLOGUE
: MCOUNT_NAME);

if (TARGET_64BIT)
{
#ifndef NO_PROFILE_COUNTERS
fprintf (file, " leaq %sP%d(%%rip),%%r11 ", LPREFIX, labelno);
#endif

if (DEFAULT_ABI == SYSV_ABI && flag_pic)
fprintf (file, " call *%s@GOTPCREL(%%rip) ", mcount_name);
else
fprintf (file, " call %s ", mcount_name);
}
else if (flag_pic)
{
#ifndef NO_PROFILE_COUNTERS
fprintf (file, " leal %sP%d@GOTOFF(%%ebx),%%" PROFILE_COUNT_REGISTER " ",
LPREFIX, labelno);
#endif
fprintf (file, " call *%s@GOT(%%ebx) ", mcount_name);
}
else
{
#ifndef NO_PROFILE_COUNTERS
fprintf (file, " movl $%sP%d,%%" PROFILE_COUNT_REGISTER " ",
LPREFIX, labelno);
#endif
fprintf (file, " call %s ", mcount_name);
}
}

4、C库对于mcount函数的定义

glibc-2.17sysdepsx86_64\_mcount.S
#include <sysdep.h>

.globl C_SYMBOL_NAME(_mcount)
.type C_SYMBOL_NAME(_mcount), @function
.align ALIGNARG(4)
C_LABEL(_mcount)
/* Allocate space for 7 registers. */
subq $56,%rsp
movq %rax,(%rsp)
movq %rcx,8(%rsp)
movq %rdx,16(%rsp)
movq %rsi,24(%rsp)
movq %rdi,32(%rsp)
movq %r8,40(%rsp)
movq %r9,48(%rsp)

/* Setup parameter for __mcount_internal. */
/* selfpc is the return address on the stack. */
movq 56(%rsp),%rsi
/* Get frompc via the frame pointer. */
movq 8(%rbp),%rdi

三、自定义mcount_internal

为了避免保存、恢复寄存器状态,可以考虑自定义调用的外部函数
extern void mcount_internal (u_long frompc, u_long selfpc) internal_function;
tsecer@harry: cat main.cpp
int main(int argc, const char *argv[])
{
return 0;
}
tsecer@harry: cat mcount.hook.cpp
#include <stdio.h>

using u_long = unsigned long;
extern "C" void __mcount_internal (u_long frompc, u_long selfpc)
{
printf("call from %lu ", frompc);
}

tsecer@harry: cat Makefile
a.out: mcount.hook.cpp main.cpp
g++ -g -std=c++11 -c -fpic mcount.hook.cpp
g++ -g -std=c++11 -c -pg main.cpp
g++ main.o mcount.hook.o
tsecer@harry: make -B
g++ -g -std=c++11 -c -fpic mcount.hook.cpp
g++ -g -std=c++11 -c -pg main.cpp
g++ main.o mcount.hook.o
tsecer@harry: ./a.out
tsecer@harry:

从反汇编来看,这个地方使用的mcount是在编译时已经确定(callq fa440指令,通过相对地址)而不是动态查询的,所以在外部定义这个符号并不能进行正确的函数hook
00000000000fb180 <_mcount>:
fb180: 48 83 ec 38 sub $0x38,%rsp
fb184: 48 89 04 24 mov %rax,(%rsp)
fb188: 48 89 4c 24 08 mov %rcx,0x8(%rsp)
fb18d: 48 89 54 24 10 mov %rdx,0x10(%rsp)
fb192: 48 89 74 24 18 mov %rsi,0x18(%rsp)
fb197: 48 89 7c 24 20 mov %rdi,0x20(%rsp)
fb19c: 4c 89 44 24 28 mov %r8,0x28(%rsp)
fb1a1: 4c 89 4c 24 30 mov %r9,0x30(%rsp)
fb1a6: 48 8b 74 24 38 mov 0x38(%rsp),%rsi
fb1ab: 48 8b 7d 08 mov 0x8(%rbp),%rdi
fb1af: e8 8c f2 ff ff callq fa440 <__mcount_internal>
fb1b4: 4c 8b 4c 24 30 mov 0x30(%rsp),%r9
fb1b9: 4c 8b 44 24 28 mov 0x28(%rsp),%r8
fb1be: 48 8b 7c 24 20 mov 0x20(%rsp),%rdi
fb1c3: 48 8b 74 24 18 mov 0x18(%rsp),%rsi
fb1c8: 48 8b 54 24 10 mov 0x10(%rsp),%rdx
fb1cd: 48 8b 4c 24 08 mov 0x8(%rsp),%rcx
fb1d2: 48 8b 04 24 mov (%rsp),%rax
fb1d6: 48 83 c4 38 add $0x38,%rsp
fb1da: c3 retq

四、自定义mcount函数

1、通过栈变量获得返回地址

在X86的ABI中,前四个整数类型的参数是通过寄存器获得的,所以这种直接通过栈变量取到返回地址的做法是不行的。
tsecer@harry: cat mcount.hook.cpp
#include <stdio.h>

using u_long = unsigned long;
extern "C" void mcount(u_long frompc)
{
printf("call from %lu ", frompc);
}

tsecer@harry:

2、gcc扩展的内联汇编及内置函数

可以通过gcc提供的内联汇编或者内置函数获得调用该函数的返回值。
tsecer@harry: cat main.cpp
int main(int argc, const char *argv[])
{
return 0;
}
tsecer@harry: cat mcount.hook.cpp
#include <stdio.h>

using u_long = unsigned long;
extern "C" void mcount()
{
u_long frompc, retaddr = (u_long)__builtin_return_address(0);

asm ("movq 8(%%rbp), %0 " : "=r"(frompc));
printf("call from %lx retaddr %lx ", frompc, retaddr);
}

tsecer@harry: cat Makefile
a.out: mcount.hook.cpp main.cpp
g++ -g -std=c++11 -c -fpic mcount.hook.cpp
g++ -g -std=c++11 -c -pg main.cpp
g++ main.o mcount.hook.o
tsecer@harry: make -B
g++ -g -std=c++11 -c -fpic mcount.hook.cpp
g++ -g -std=c++11 -c -pg main.cpp
g++ main.o mcount.hook.o
tsecer@harry: ./a.out
call from 400574 retaddr 400574
tsecer@harry: addr2line 400574
/home/harry/study/gcc-gp/main.cpp:2
tsecer@harry:

五、总结

其实该功能是看起来很美,名字也很有吸引力,但事实上功能还是很有限的。最重要的限制在于这个是以函数调用的形式执行的。比方说开始说的希望通过定义局部变量,并通过构造和析构函数获得函数执行时间的方法并不可行。所以它通常只能找到函数被执行的次数、调用关系,但是不能统计函数执行时长。

原文地址:https://www.cnblogs.com/tsecer/p/14310166.html