查看堆栈信息

查看堆栈信息有三种方法:

  • 使用 gdb attach 到进程上。

  • 使用 gstack 导出栈信息。

  • 使用 pstack 到处栈信息。

ASan

TL;DR

  1. 添加 编译链接 参数:-O1 -g -fsanitize=address -fno-omit-frame-pointer

  2. 添加环境变量

    export ASAN_SYMBOLIZER_PATH=/path/to/llvm-symbolizer
    export ASAN_OPTIONS=detect_leaks=1
  3. 执行程序

如果在 macos 上执行程序时出现了 [1]

malloc: nano zone abandoned due to inability to reserve vm space.

则可以通过添加环境变量 MallocNanoZone=0 的方式解决。

使用 ASan 构建程序

ASan(AddressSanitizer) 是一个内存错误检测工具。

使用方式为:

  1. 添加编译和链接参数:-O1 -g -fsanitize=address -fno-omit-frame-pointer

  2. 设置环境变量 ASAN_SYMBOLIZER_PATH 以指向 llvm-symbolizer 文件

  3. 执行程序

如果希望发现错误后仍然运行,则需要:

  1. 添加编译链接参数:-fsanitize-recover=address

  2. 添加环境变量 ASAN_OPTIONS=halt_on_error=0

ASan 有一些运行程序前设置的 Flags

ASan 的一些错误解释可以在 C/C++ 擦除器 | 用于运行时 bug 检测的检测代码 中找到。

LeakSanitizer

LeakSanitizer 默认集成到了 ASan 中。可用于检测内存泄漏。要使用 LeakSanitizer,需要添加运行时参数:

  • ASAN_OPTIONS=detect_leaks=1

由于 LLVM 自身的问题,可能会对某些函数进行误报。这是可以通过设置 LSAN 的参数来忽略某些函数。例如:

# suppr.txt
leak:fetchInitializingClassList
leak:cache_t::insert

然后添加运行参数:

export LSAN_OPTIONS=suppressions=`pwd`/suppr.txt

即可。

ThreadSanitizer

TSan(ThreadSanitizer) [2] 用于检测程序中的条件竞争。要使用 TSan,需要:

  • 添加编译参数 -O2 -g -fsanitize=thread

TSan 和 ASan 是互斥的

TSan 同样有一些运行时设置的 Flags

TSan 不支持 libc/libc++ 静态链接

MemorySanitizer

MSan 用于检测内存未初始化的问题。要使用 MSan,需要:

  • 添加编译和链接参数:-fsanitize=memory -fPIE -pie -fno-omit-frame-pointer -fsanitize-memory-track-origins

  • 设置环境变量 MSAN_SYMBOLIZER_PATH 以指向 llvm-symbolizer 文件

MSan 要求使用 MSan 构建程序中的所有代码

realloc 导致的内容错误

在进行 benchmark 时碰到了下面的错误:

bench_big_file-48da6c631385e586: malloc.c:2617: sysmalloc: Assertion `(old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0)' failed.

benchmark 默认是打开 asan 了的,但是依然碰到这个问题。说明内存读写没有出现问题。经过调试,发现是由于 array 在 grow 过程中使用 realloc 分配内存,但是 capacity 并没有更新。从而导致了这个问题。

UBSan

UBSan 用于探测未定义行为。最简单的形式是添加编译和链接参数 -fsanitize=undefined

对于 Rust 而言,启用的方式为: [3]

CFLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all" cargo build

TIPS: UBSan 可以和 ASan 一同启用。

对于更复杂的 UBSan,参见 UndefinedBehaviorSanitizer

sysprof

sysprof 是 一个系统性能监控工具,可以用来分析进程的详细信息。sysprof 是一个图形应用,但是也提供了 sysprof-cli 在终端使用。

缺点:

  • 需要超级用户权限

valgrind

valgrind 包含了一系列用于优化的工具。

callgrind

valgrind --tool=callgrind --separate-threads=yes ./dsmaster --terminal -l trace
gprof2dot -f callgrind -n10 -s callgrind.out.146758-02 > 1.dot
dot -Tpng valgrind.dot -o valgrind.png

kcachegrind

gprof

gprof 是 gcc 自带的性能分析工具。其通过在代码中插入代码片段完成对性能的分析。

缺点:

  • 进程需要能够正常结束。因而无法对 deamon 服务进行分析。

  • 进程需要能够容忍 SIGPROF 信号

  • gprof 只能分析用户态时间

  • 不能用于多线程

  • 不能分析函数指针

gprof 的使用方式为:

  1. 添加编译参数 -pg

  2. 添加链接参数 -pg

  3. 执行程序使其正常结束以生成 gmon.out 文件

  4. 生成图形化结果:

    gprof /path/to/execute | gprof2dot -n0 -e0 | dot -Tpng -o output.png

gperftools

gperftools 和 gprof 优缺点相似。使用方式为:

  1. 链接 profiler 库

  2. 执行 CPUPROFILE=/tmp/profile ./myprogram

  3. 获取结果:

    pprof ./test_capture test_capture.prof --pdf > prof.pdf

TIPS: gperftools 中的 pprof 工具可以读取 perf 数据文件,因此可以将它们一起使用。

perf

如果 perf 出现 Rust 调用栈错误,可能的解决方式为添加 --call-graph dwarf 编译参数。

Bytehound

Bytehound 是一个内存分析工具。其通过替换 malloc 的方式对程序进行分析。能够用来分析堆栈使用和内存泄露信息。

使用 GCC 分析堆栈使用

GCC 提供了以下编译标志用来协助分析堆栈使用:

标志作用

-fstack-usage

生成一个后缀名为 .su 的文件用来打印堆栈使用情况

编译器优化

SROA(Scalar Replacement of Aggregates)

SROA 优化用于在简单的情况下对结构体进行优化。当结构体仅用作局部变量时,SROA 会尝试将部分字段使用常量或者局部变量替代,而结构体中的此字段被删除。

MacOS

在 MacOS 上没有 perf 命令,可以通过两个方式解决:

  1. 使用 instruments 指令。例如:

    instruments -t "Time Profiler"  ./stm32flash  -m 8n1 -b 115200  -w xx.hex     -S 0x000c00:9000 -v   /dev/tty.usbserial-14520

    然后直接打开文件即可:

    open instrumentscli0.trace
  2. 使用 dtrace 命令

    # 捕获进程 pid == 12345 的信息。
    dtrace -x ustackframes=100 -n 'profile-97 /pid == 12345/ { @[ustack()] = count(); } tick-60s { exit(0); }' -o out.user_stacks
    stackcollapse.pl out.kern_stacks > out.kern_folded
    # 生成火焰图
    flamegraph.pl out.kern_folded > kernel.svg

分离调试信息

对于 GCC 和 Clang 而言 [4],通过添加 -gsplit-dwarf 编译选项可以将 dwarf 调试信息分离出去。此外,也可以借助 objcopy 手动分离: [5]

split debuginfo
$ gcc -g -shared -o libtest.so libtest.c (1)
$ objcopy --only-keep-debug libtest.so libtest.debug (2)
$ objcopy --add-gnu-debuglink=libtest.debug libtest.so (3)
$ strip libtest.so
  1. 通过 -g 选项保留 debuginfo。

  2. 将 libtest.so 中的调试信息拷贝到 libtest.debug 中。

  3. 告诉 gdb 调试时从 libtest.debug 中获取调试信息。

build id

如果链接时添加参数 -build-id,则 ld 会在文件中添加一个 note.gnu.build-id,通过 objdump 可以获取它的值:

buildid
$ objdump -s -j .note.gnu.build-id /bin/ls

/bin/ls:     file format elf32-i386

Contents of section .note.gnu.build-id:
 8048168 04000000 14000000 03000000 474e5500  ............GNU.
 8048178 c6fd8024 2a11673c 7c6a5af6 2c65b1b5  ...$*.g<|jZ.,e..
 8048188 d7e13fd4

gdb 会安装下面的顺序查找 debuginfo:

  1. 查找 [debug-file-directory]/.build-id/xx/yyyyyy.debug

    • debug-file-directory 是 gdb 的环境变量。可以通过 show debug-file-directory 命令获取。

    • xx 是十六进制 hash 的前两位,yyyyy 是剩余部分。例如对于上面的例子而言,xx 是 c6,而 yyyyy 是 fd80242a11673c7c6a5af62c65b1b5d7e13fd4。最后总的路径为 /usr/lib/debug/.build-id/c6/fd80242a11673c7c6a5af62c65b1b5d7e13fd4.debug

  2. 在当前路径下查找 debuglink 指向的文件。

  3. 在 ./.debug 路径下查找 debuglink 指向的文件。

  4. 在 [debug-file-directory]/[file-path] 中查找文件。例如 /usr/lib/libfoo.so 对应的路径为 /usr/lib/debug/usr/lib/libfoo.so

添加自定义内容到 elf

使用 objcopy 可以将自定义内容添加到 elf 中。例如将 commit 保留下来:

commit id
build:
    echo $(shell git rev-parse HEAD) > .version
    objcopy --add-section .foo.commit_id=.version libfoo.so

评估软件缓存性能

部分软件(例如 slab 分配器)可能需要考虑缓存问题,频繁的内存失效会极大程度上影响程序的性能。欲评估软件缓存性能,可以使用 perf 或者 cachegrind。

使用 perf 评估缓存性能[1]

如要大致了解程序使用系统缓存的情况,可以使用下面的命令:

# perf stat -e task-clock,cycles,instructions,cache-references,cache-misses  ./a.out

程序结束后,perf 将会生成类似下面的输出:

Performance counter stats for './a.out':

       229.872935 task-clock                #    0.996 CPUs utilized
      626,676,991 cycles                    #    2.726 GHz
      525,543,766 instructions              #    0.84  insns per cycle
       18,587,219 cache-references          #   80.859 M/sec
        6,605,955 cache-misses              #   35.540 % of all cache refs

      0.230761764 seconds time elapsed

上述的程序相对较短:只执行了 525,543,766 条指令,一共需要 626,676,991 个处理器周期。

\(\frac{缓存未命令中数}{指令数}\) 的值越低越好。本示例中的值为 \(\frac{626,676,991}{525,543,766}=1.25\%\)。由于 RAM 内存和缓存访问成本差异很大,因此此比例的细微变动也能够显著影响性能。如果未命中比率超过 5%,则需要进一步调查。

当 dwarf 信息可以访问时,可以获取缓存未命中的代码位置,可以使用 perf record 而不是 perf state 来收集这些信息:

$ perf record -e cache-misses ./a.out

命令执行完毕后,信息被收集到 perf.data 中:

[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.048 MB perf.data (~2093 samples) ]

使用 perf report 可以查看详细信息:

$ perf report --stdio
# ========
# captured on: Wed Oct  2 14:38:59 2013
# hostname : dhcp129-131.rdu.redhat.com
# os release : 3.10.0-31.el7.x86_64
# perf version : 3.10.0-31.el7.x86_64.debug
# arch : x86_64
# nrcpus online : 8
# nrcpus avail : 8
# cpudesc : Intel(R) Core(TM) i7-3740QM CPU @ 2.70GHz
# cpuid : GenuineIntel,6,58,9
# total memory : 7859524 kB
# cmdline : /usr/bin/perf record -e cache-misses ./a.out
# event : name = cache-misses, type = 0, config = 0x3, config1 = 0x0, config2 =
# HEADER_CPU_TOPOLOGY info available, use -I to display
# HEADER_NUMA_TOPOLOGY info available, use -I to display
# pmu mappings: cpu = 4, software = 1, tracepoint = 2, uncore_cbox_0 = 6, uncore
# ========
#
# Samples: 888  of event 'cache-misses'
# Event count (approx.): 6576993
#
# Overhead       Command      Shared Object                        Symbol
# ........  ............  .................  ............................
#
    85.19%  a.out  a.out              [.] main
    11.20%  a.out  [kernel.kallsyms]  [k] clear_page_c_e
     2.23%  a.out  a.out              [.] checkSTREAMresults
     0.62%  a.out  [kernel.kallsyms]  [k] _cond_resched
     0.20%  a.out  [kernel.kallsyms]  [k] trigger_load_balance
     0.13%  a.out  [kernel.kallsyms]  [k] task_tick_fair
     0.13%  a.out  [kernel.kallsyms]  [k] free_pages_prepare
     0.11%  a.out  [kernel.kallsyms]  [k] perf_pmu_disable
     0.09%  a.out  [kernel.kallsyms]  [k] tick_do_update_jiffies64
     0.09%  a.out  [kernel.kallsyms]  [k] __acct_update_integrals
     0.00%  a.out  [kernel.kallsyms]  [k] flush_signal_handlers
     0.00%  a.out  [kernel.kallsyms]  [k] perf_event_comm_output

#
# (For a higher level overview, try: perf report --sort comm,dso)
#

如果需要更详细的信息,可以使用 perf annotate 命令:

$ perf annotate

main
       │       movsd  %xmm0,(%rsp)                                             ▒
       │       xchg   %ax,%ax                                                  ▒
       │     #ifdef TUNED                                                      ▒
       │             tuned_STREAM_Add();                                       ▒
       │     #else                                                             ▒
       │     #pragma omp parallel for                                          ▒
       │             for (j=0; j<N; j++)                                       ▒
       │                 c[j] = a[j]+b[j];                                     ▒
  1.96 │2a0:   movsd  0x24868e0(%rax),%xmm0                                    ▒
 11.17 │       add    $0x8,%rax                                                ▒
  2.21 │       addsd  0x15444d8(%rax),%xmm0                                    ▒
 13.74 │       movsd  %xmm0,0x6020d8(%rax)                                     ▒
       │             times[2][k] = mysecond();                                 ◆
       │     #ifdef TUNED                                                      ▒
       │             tuned_STREAM_Add();                                       ▒
       │     #else                                                             ▒
       │     #pragma omp parallel for                                          ▒
       │             for (j=0; j<N; j++)                                       ▒
  3.56 │       cmp    $0xf42400,%rax                                           ▒
       │     ↑ jne    2a0                                                      ▒
       │                 c[j] = a[j]+b[j];                                     ▒
       │     #endif                                                            ▒
Press 'h' for help on key bindings

使用 Cachegrind 评估缓存性能[2]

要使用此工具,需要在 Valgrind 命令行上指定 --tool=cachegrind

$ valgrind --tool=cachegrind ls -l

执行完成后,Cachegrind 会类似下面的统计信息:

==31751== I   refs:      27,742,716
==31751== I1  misses:           276
==31751== L2  misses:           275
==31751== I1  miss rate:        0.0%
==31751== L2i miss rate:        0.0%
==31751==
==31751== D   refs:      15,430,290  (10,955,517 rd + 4,474,773 wr)
==31751== D1  misses:        41,185  (    21,905 rd +    19,280 wr)
==31751== L2  misses:        23,085  (     3,987 rd +    19,098 wr)
==31751== D1  miss rate:        0.2% (       0.1%   +       0.4%)
==31751== L2d miss rate:        0.1% (       0.0%   +       0.4%)
==31751==
==31751== L2 misses:         23,360  (     4,262 rd +    19,098 wr)
==31751== L2 miss rate:         0.0% (       0.0%   +       0.4%)

除了摘要信息外,Cachegrind 还会将信息以人类可读的形式保存到 cachegrind.out.pid 文件中。使用附带的 cg_annotate 工具可以更好地分析此文件。

使用 mallopt 探测内存问题

GLIBC 提供的 malloc 可以通过环境变量或者 mallopt 来更改部分行为。具体值如下:

mallopt环境变量用途

M_CHECK_ACTION

MALLOC_CHECK_

含意

0

忽略错误。

1

打印错误消息。

2

中断程序。

3

打印错误消息、堆栈、内存映射并中断程序。

5

打印一个简单的错误消息。

7

打印简单的错误消息、堆栈和内存映射并中断程序。

M_PERTURB

MALLOC_PERTURB_

使用指定值初始化分配的内存(使用 memset)

Last moify: 2022-12-04 15:11:33
Build time:2025-07-18 09:41:42
Powered By asphinx