Linux如何定位函数热点函数调用栈

Linux如何定位函数热点函数调用栈

Posted by Albert on April 21, 2021

Linux如何定位函数热点函数调用栈

一、前言

​ 最近再看一个问题,发现系统中的某一个进程CPU占用很高,达到百分之100的CPU占用,这个时候需要找到该进程的热点函数以及调用栈(由于涉及到公司的保密以及安全问题,所以我自己构造了一个程序来实战调试,以便于理解),从而去排查代码。我使用如下代码进行调试。这个程序是一个死循环程序,运行起来后,你会发现CPU的占用会达到百分之100。

#include <stdio.h>
int test_perf_top(int i, int j)
{
        int c = i+j;
        int tmp;
        while(1)
        {
                tmp++;
                if(tmp>c){
                        tmp=0;
                }
        }

}

int main(int argc, char *argv[])
{
        printf("start test\n");
        test_perf_top(1000, 2000);
        return 1;
}

使用如下命令进行编译:

gcc  main.c

二、使用perf top查看热点函数

​ 关于perf top的详细使用方法,这里不再阐述,详细用户请查看perf的man page.

​ 首先我们运行程序a.out, 然后用top先查看下进程状态:可以看到a.out这个进程的cpu占用是百分之100.

image-20210421141247213

​ 接下来我们用perf top看下这个进程到底在干什么事情:

perf top -p 2341 -g

image-20210421141622720

​ 由上图可以看到,这个a.out这个进程在test_perf_top这个函数花费了大部分时间,这个时候我们去查看这个函数的代码,找出有可能出现死循环的代码分析,从而解决问题。

三、使用GDB调试

​ 当然这个问题也可以用gdb来进行调试

gdb -p 2341 如果进程是多线程,那么请先用top命令查看出是哪一个线程占用cpu高。

image-20210421142351289

​ 执行bt可以查看当前进程的调用栈。从而推断进程在频繁执行test_perf_top函数

四、没有符号表,如何处理

​ 前面两种方法都是在有符号表的情况下进行的,但是假如可执行程序被strip掉符号表之后,那么无论是gdb还是perf top打印出来的都是一串16进制的字符串, 这不利于我们进一步排查问题,特别是操作系统开启了地址随机化、编译程序的时候将程序编译成位置无关。那么实际上加载到内核执行的程序的地址和二进制的可执行程序中的地址是对应不上的。这个时候难道就没有办法了吗?当然是有的。我们可以用gdb 打印出热点函数代码段前后20条汇编代码, 然后用objdump也反汇编出可执行程序(这个可执行程序最好重新编译,带上符号表)的汇编代码。然后用beyond compare这种软件进行对比汇编代码,基本上都可以定位到具体的函数。

​ 我们首先用file命令看下a.out是否带有符号表:

​ 由于我们前面使用gcc编译的时候并没有去符号表,所以显示是not stripedimage-20210421143530781

​ 我们用strip 命令将a.out的符号表去掉。strip a.out

image-20210421143822407

​ 现在符号表去掉了:我们重新运行下程序:进程号变为了2881

image-20210421144057542

perf topgdb来看一下

​ 首先是perf top -p 2881 -g:

​ 完全不知道是什么鬼。光看这个信息是看不出来的;

image-20210421144212370

​ 接着我用gdb看一下:

gdb -p 2881

image-20210421144430423

​ 从gdb的输出来看,我也看不出是什么鬼。符号看不出,还有汇编啊,像我这种懒惰分子,除非使用到汇编代码,否则一行一行去看汇编代码,我是不愿意的。

​ 第一步、 我先用gdb里面的 x命令打印出这个0x0000000000400556地址附近的代码(前后都最好打印一些汇编,这样beyond compare比较好匹配):

image-20210421145113939

注意看,我为了打印当前pc指针前后的几十条汇编,所以我故意把打印开始的地址变成了0x0000000000400536,而不是

0x0000000000400556, 将这些汇编复制到beyond compare的左边,等待比较。

​ 第二步、重新编译下原程序,不要去掉符号表,然后反汇编。

objdump -d a.out输出如下汇编:

image-20210421145914560

然后把输出的汇编代码拷贝到beyond compare的右边,比较:

image-20210421150102530

这样我们就看出了是在test_perf_top这个函数中执行了代码,于是就可以定位到出问题的代码。然后去分析这个函数就可以了。

五、strace的使用

​ 构造的这个例子并不适合使用strace进行调试,因为他没有系统调用,但是一般情况下我们写的一些网络服务都是使用select框架或者是epoll框架,然后调用read等系统调用去收发数据包, 这类进程,我们还可以通过strace 调试某一个进程,然后看出哪些系统调用在频繁调用,唤醒事件是否符合预期,来进一步调试问题。