zoukankan      html  css  js  c++  java
  • 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:

    五、总结

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

  • 相关阅读:
    vue2.0 移动端,下拉刷新,上拉加载更多插件,修改版
    修改maven产生missing artifact错误
    程序的态度-一生想靠近的完美
    计算机中的存储
    Vue-router结合transition实现app前进后退动画切换效果
    js获取input上传图片装换为base64格式图片
    mongoose的基本操作
    vue路由跳转时判断用户是否登录功能
    Node.js学习笔记之文件上传
    推荐一个基于Vue2.0的的一款移动端开发的UI框架,特别好用。。。
  • 原文地址:https://www.cnblogs.com/tsecer/p/14310166.html
Copyright © 2011-2022 走看看