1. 项目概述为什么perf不是“高级top”而是函数级性能诊断的手术刀你有没有遇到过这样的场景系统负载突然飙到8top显示CPU使用率95%但%CPU列里排在前几位的进程加起来只占了30%或者某个服务响应变慢htop里看着一切正常可用户就是卡在某个接口上不动——日志没报错监控指标平滑连strace都抓不到明显阻塞点。这时候很多人会下意识去查磁盘IO、网络延迟、内存泄漏结果折腾半天问题依旧藏在代码最深处。我带过的几个团队里有三次典型故障最后都指向同一个被忽略的环节一个本该O(1)完成的哈希查找在特定输入下退化成了O(n²)一个看似无害的字符串拼接循环在高并发下吃掉了整颗CPU核心还有一个更隐蔽的——某次迭代后新增的日志格式化函数内部调用了未缓存的正则编译每次调用都触发JIT编译开销。这些都不是系统级瓶颈而是函数粒度的逻辑缺陷而perf就是唯一能直接切开函数调用栈、把这类问题钉在火焰图上的工具。这个标题里的关键词“Linux性能优化”“perf”“CPU使用率”“异常函数”其实暗含了一个关键认知断层绝大多数人把CPU使用率当成一个整体指标却忘了它本质是所有函数执行时间的累加和。perf的价值正在于它不满足于告诉你“CPU忙”而是能精确回答“CPU在哪个函数里忙、忙了多久、被谁调用、调用链有多深”。它不像top那样只看进程维度也不像pidstat那样只统计线程级消耗而是深入到汇编指令级别通过硬件性能计数器PMU采样程序计数器PC寄存器的值再结合符号表symbol table把地址映射回函数名。这种能力让perf成为Linux下唯一能实现“从CPU使用率反向定位到具体C/C/Rust函数”的原生工具。它不需要修改代码、不需要重启服务、不需要侵入式埋点只要二进制文件带调试符号或有分离的debuginfo包就能在生产环境实时“透视”函数行为。我见过最震撼的一次排查是某金融后台服务在交易高峰时偶发延迟抖动perf record -g -p pid -F 99 --call-graph dwarf跑完20秒生成的火焰图里一个平时占比不到0.1%的json_parse_value函数在抖动时刻突然膨胀成整个图谱的基座——点进去一看是JSON解析库在处理超长嵌套数组时递归深度超过栈限制触发了大量信号处理和栈展开开销。这个发现直接推动了上游库的递归改迭代方案落地。所以这不是一个关于“怎么用perf”的教程而是一份如何用CPU使用率这把钥匙打开函数级性能黑箱的操作手册。适合所有在Linux上写C/C/Go/Rust服务、需要直面真实生产性能问题的开发者、SRE和性能工程师。如果你还在靠猜、靠日志、靠重启来解决性能问题那接下来的内容就是你该补上的最后一块拼图。2. 核心思路拆解为什么必须用perf而不是其他工具组合2.1 perf vs top/htop进程级与函数级的维度鸿沟很多人觉得top已经够用了毕竟它能实时显示每个进程的CPU占用百分比。但这里存在一个根本性误解top显示的%CPU是内核调度器在采样周期内该进程所有线程在CPU上实际运行的时间占比。它是一个聚合统计值就像告诉你“这栋楼总耗电量是1000度”但没说清楚是哪层的空调、哪间的服务器、还是哪个插座的充电器在耗电。perf则不同它通过硬件PMUPerformance Monitoring Unit直接监听CPU的性能事件比如cycles时钟周期、instructions执行指令数、cache-misses缓存未命中。当perf record运行时它会在每个采样点记录下当前CPU的程序计数器PC值也就是“此刻CPU正在执行哪条机器指令”。随后perf report会将这些PC地址通过读取可执行文件的符号表.symtab段或动态链接库的/proc/pid/maps信息映射回具体的函数名、源码行号甚至汇编指令。这就实现了从“楼总耗电”到“三楼东侧第7个机柜第2U服务器CPU第3核上hash_lookup函数第42行的mov指令正在反复执行”的精准定位。我试过用top监控一个故意写成死循环的while(1) { hash_lookup(key); }程序top只能告诉你进程占了99% CPU而perf record -e cycles -g -p pid --call-graph dwarf跑10秒后perf report --no-children直接列出hash_lookup占了98.7%的采样点且调用链清晰显示它被main函数直接调用。这种粒度差异决定了top适合宏观监控perf才是微观诊断的刚需。2.2 perf vs strace/ltrace系统调用追踪与函数执行追踪的本质区别strace和ltrace是另一个常被拿来对比的工具。strace跟踪进程发起的所有系统调用如read,write,openltrace则跟踪动态链接库的函数调用如malloc,printf。它们的价值在于理解程序与内核、与外部库的交互边界。但问题在于绝大多数性能瓶颈并不发生在系统调用层面而是在纯用户态的计算逻辑中。比如一个排序算法实现有严重缺陷它可能一次qsort调用就吃掉100ms CPU但strace只会看到一个mmap分配内存和一个brk堆扩展完全无法反映内部循环的低效ltrace能看到qsort被调用但看不到qsort内部比较函数cmp_func里那个O(n²)的字符串比较逻辑。perf则无视这种边界它对所有用户态指令一视同仁。只要你的二进制文件有符号信息perf就能告诉你cmp_func里第15行的strcmp调用因为传入了两个超长且前缀相同的字符串导致每次比较都要遍历到末尾从而成为整个qsort耗时的90%。我在某次排查一个图像缩放服务时strace显示所有read/write都很顺畅ltrace看到libjpeg的jpeg_start_compress调用也很快但服务整体吞吐量只有预期的1/3。perf record -e cycles,instructions,cache-misses -g -p pid跑完火焰图里yuv_to_rgb_convert函数异常突出点进去发现其内部一个for循环的索引变量被错误声明为char类型在处理大图时频繁发生符号位扩展导致CPU流水线严重stall。这个bugstrace和ltrace永远不可能发现因为它纯粹发生在寄存器运算层面。2.3 perf vs eBPF工具如bcc内核态可见性与用户态函数精度的权衡近年来eBPF生态爆发bcc工具集如funccount,profile也能做函数级分析。但perf和eBPF是互补而非替代关系。eBPF的优势在于可以安全地在内核态注入探针监控内核函数、网络协议栈、文件系统等且开销极低。但它的用户态函数追踪能力有硬伤它依赖uprobe用户态探针而uprobe需要在目标函数的入口地址设置断点这要求你必须提前知道要监控哪些函数名并且对动态链接、JIT编译如Java/Python、内联优化后的代码支持不完善。perf则不同它基于采样无需预设目标对任何正在运行的代码都有效。更重要的是perf的--call-graph dwarf模式能完整捕获调用栈包括那些被编译器内联inline的函数——DWARF调试信息里保留了内联展开的线索perf能据此重建出原始的调用关系。而eBPF的uprobe在遇到内联函数时往往只能看到外层函数。我曾用bcc/profile分析一个Go程序由于Go的GC和goroutine调度高度内联火焰图里大量runtime.mallocgc调用被折叠成runtime.systemstack无法追溯到具体是哪个业务函数触发了分配换成perf record -g -F 99 --call-graph dwarf -p pid立刻清晰看到user_service.ProcessRequest-json.Unmarshal-runtime.mallocgc的完整链条问题直指JSON反序列化时的结构体字段过多。所以perf是用户态函数性能分析的“黄金标准”eBPF则是内核态和特定场景的强力补充二者配合使用效果最佳。2.4 为什么必须结合CPU使用率从“忙”到“为什么忙”的因果闭环标题里强调“利用CPU使用率定位”这绝非多余。perf本身能采集大量事件但如果没有CPU使用率这个宏观指标作为锚点perf数据就容易陷入“只见树木不见森林”的陷阱。举个例子perf record -e cache-misses -g -p pid可能发现database_query函数cache miss率高达80%但这未必是问题——如果这个函数本身只占CPU的0.5%那么即使优化它对整体性能提升也微乎其微。反之如果top显示某进程CPU持续95%而perf record -e cycles -g -p pid显示data_processing_loop占了92%的采样点那这个函数就是绝对的优化靶心。CPU使用率在这里扮演了“优先级过滤器”的角色它帮我们把perf海量的采样数据聚焦到真正影响用户体验的“热路径”上。我的实操心得是永远先用top或pidstat 1确认哪个PID是罪魁祸首再用perf record -g -p pid -F 99 --call-graph dwarf针对它采样最后用perf report --no-children按函数自顶向下排序。这个“CPU使用率→perf采样→函数排序”的三步闭环是我过去十年里解决90%以上性能问题的固定流程。它简单、直接、零误判比任何复杂的监控告警都可靠。3. 核心细节解析与实操要点从安装到读懂火焰图的每一个坑3.1 环境准备符号表、调试信息与内核版本的隐形门槛perf能工作的前提是它能将采样到的内存地址准确映射回函数名和源码行号。这依赖于三个关键要素可执行文件的符号表.symtab、调试信息DWARF、以及内核对perf_event_open系统调用的支持。很多新手第一次跑perf report看到的全是[unknown]或[kernel.kallsyms]就是因为这三个环节出了问题。首先符号表。编译时必须保留符号信息。对于GCC/Clang确保没有使用-sstrip symbols或-fvisibilityhidden过度隐藏符号。一个安全的编译命令是gcc -g -O2 -rdynamic your_app.c -o your_app。其中-g生成DWARF调试信息-rdynamic等价于--export-dynamic确保动态链接时符号对dlsym可见这对perf解析动态库调用链至关重要。我见过最典型的失败案例是某团队用-s打包生产环境二进制perf采样后report里全是十六进制地址花了整整一天才意识到是编译参数问题。其次调试信息。-g生成的DWARF信息默认包含在二进制里但有些发行版如CentOS/RHEL为了减小包体积会将调试信息抽离到单独的debuginfo包中。此时你需要安装对应的debuginfo包并确保perf能找到它。方法是sudo debuginfo-install glibcRHEL系或sudo apt-get install linux-image-$(uname -r)-dbgsymUbuntu。安装后perf report会自动从/usr/lib/debug等路径加载。一个快速验证方法是readelf -S your_app | grep debug如果有.debug_*段说明调试信息内嵌如果没有则需检查debuginfo包。最后内核版本。perf的--call-graph dwarf模式要求内核版本3.7且编译时启用了CONFIG_PERF_EVENTSy和CONFIG_DEBUG_INFO_DWARF4y。你可以用zcat /proc/config.gz | grep PERF或grep CONFIG_PERF_EVENTS /boot/config-$(uname -r)来确认。低于3.7的内核只能用--call-graph fp帧指针模式但它对编译器优化尤其是-O2及以上非常敏感容易丢失调用栈。我的建议是生产环境内核尽量4.15这是目前最稳定支持DWARF调用图的版本。另外perf工具本身也需要匹配内核最好用linux-tools-$(uname -r)包安装而不是系统自带的旧版perf。提示一个终极验证方法是用perf record -g -e cycles -a sleep 1全局采样1秒然后perf report --no-children | head -20。如果第一行是[kernel.kallsyms]且占比很高说明内核符号没问题如果看到libc-*.so或你的程序名说明用户态符号也OK如果全是[unknown]那就得回头检查编译参数和debuginfo了。3.2 perf record核心参数详解采样频率、事件选择与调用图模式perf record是整个流程的起点参数选错后面所有分析都是空中楼阁。下面逐个拆解最常用、也最容易踩坑的参数-e events指定采样事件。最常用的是cyclesCPU时钟周期它与CPU使用率直接相关因为CPU使用率本质就是单位时间内执行的周期数占比。instructions执行指令数也很有用可以计算IPCInstructions Per CycleIPC1通常意味着流水线stall严重。cache-misses、branch-misses则用于定位缓存和分支预测问题。关键技巧不要同时采样太多事件会显著增加开销。我的习惯是首次排查只用-e cycles确认热点函数后再针对性加-e cache-misses,instructions做深度分析。perf record -e cycles,instructions,cache-misses -g -p pid的开销可能比单事件高3倍。-F freq采样频率单位Hz。-F 99表示每秒采样99次这是平衡精度和开销的黄金值。频率太高如-F 999采样开销本身就会干扰被测程序甚至导致perf进程CPU飙升频率太低如-F 10可能错过短时爆发的热点。-F 99源自一个经验法则人类反应时间约100ms99Hz能保证在100ms内至少有一次采样足以捕捉大多数性能抖动。对于超低延迟场景可以尝试-F 199但务必监控perf自身开销。-g启用调用图call graph采样。这是perf的灵魂功能没有它你只能看到函数名看不到谁调用了它。但-g本身只是开关真正的调用图质量取决于后面的--call-graph模式。--call-graph mode调用图生成模式有fpframe pointer、dwarf、lbrLast Branch Record三种。fp最老依赖编译器生成帧指针-O2及以上常被优化掉不可靠lbr需要CPU硬件支持Intel HaswellAMD Zen精度最高但兼容性差dwarf是目前最推荐的模式它利用DWARF调试信息中的.debug_frame段来重建调用栈对编译器优化不敏感兼容性好。所以--call-graph dwarf是必加参数。注意dwarf模式需要libdw库支持Ubuntu/Debian上装libdw-devRHEL/CentOS上装elfutils-libelf-devel。-p pidvs-a-p针对单个进程精准-a全局采样适合找“谁在偷偷吃CPU”。但-a会产生海量数据perf report会很慢。我的原则是先top定位PID再-p精准打击。一个完整的、经过千锤百炼的命令是perf record -e cycles -g -F 99 --call-graph dwarf -p your_pid -- sleep 30这里-- sleep 30是关键技巧它让perf record在采样30秒后自动退出避免手动CtrlC导致采样不完整。sleep 30本身不消耗CPU不会污染数据。3.3 perf report与火焰图从文本报告到可视化洞察perf record生成的perf.data文件需要用perf report来解析。但perf report的默认输出是文本列表对复杂调用栈不友好。真正的威力在于将其转换为火焰图Flame Graph。首先perf report的基本用法perf report --no-children -n --sort comm,dso,symbol--no-children是核心它禁用“子函数合并”让你看到每个函数的真实采样占比而不是被父函数“吸收”掉。-n显示采样次数--sort按命令名、共享库、符号名排序方便定位。但文本终究抽象。火焰图是Brendan Gregg发明的可视化技术它将调用栈水平展开每个函数框的宽度代表其采样占比纵向堆叠表示调用关系。生成步骤如下导出折叠格式folded stackperf script -F comm,pid,tid,cpu,time,period,event,ip,sym,dso,trace | ./stackcollapse-perf.pl out.perf-folded这里./stackcollapse-perf.pl是FlameGraph工具集里的脚本需提前下载。perf script将perf.data转为可读文本stackcollapse-perf.pl将其压缩成“函数名;调用者;调用者...”的折叠格式。生成SVG火焰图./flamegraph.pl out.perf-folded perf.svgflamegraph.pl会生成一个交互式SVG文件支持鼠标悬停查看详细占比点击缩放。火焰图的阅读有门道顶部宽大的矩形这是最顶层的函数即直接消耗CPU的“罪魁祸首”。比如main、event_loop、worker_thread。向下延伸的窄条表示被调用的函数。越往下调用层级越深。颜色无意义只是为了视觉区分不表示好坏。关键洞察点找那些“又高又宽”的矩形它们是高耗时且调用频繁的函数找那些“异常凸起”的矩形比如一个本该很小的log_format函数在某个分支里突然变得和main一样宽说明那里有逻辑缺陷。我处理过一个最经典的火焰图案例一个Web服务的http_handler函数在图中占据顶部但它的子函数里json_encode_response异常宽大。点开json_encode_response发现它下面不是常见的malloc或memcpy而是层层嵌套的regex_compile。原来服务在每次HTTP响应时都动态编译一个正则表达式用于日志脱敏而正则编译是CPU密集型操作。火焰图一眼就暴露了这个“不该在热路径上”的操作。修复方案很简单将正则编译提到初始化阶段缓存regex_t对象。上线后http_handler的CPU占比从65%降到8%TP99延迟下降40%。注意火焰图是“采样快照”不是精确计时。它显示的是“在采样点CPU正在执行这个函数”的概率。所以一个执行时间很短但被高频调用的函数也可能在图中显得很宽。要结合perf report --no-children里的具体采样次数# samples来判断。3.4 定位“异常函数”的实操心法从占比、调用链到上下文找到一个高占比函数只是开始真正的挑战是判断它是否“异常”。我的经验是用三重证据链交叉验证横向对比占比阈值一个健康的函数其perf report占比通常应5%。如果一个业务逻辑函数如order_process占比30%或一个工具函数如string_replace占比10%基本可以判定为异常。但要注意排除[kernel]、[vdso]等内核和VDSO函数它们高占比往往是系统调用开销大需另查。纵向深挖调用链分析用perf report --no-children --call-graph聚焦在可疑函数上按Enter键展开其调用栈。重点看它被谁调用如果是main或event_loop直接调用说明是主逻辑热点如果被malloc或pthread_mutex_lock调用说明是内存或锁竞争问题。调用链是否“不合常理”比如image_resize调用jpeg_decode再调用cpu_accelerated_blit这合理但如果image_resize调用network_send再调用ssl_encrypt那就奇怪了——图像处理不该触发网络加密可能是日志或监控埋点逻辑混入了热路径。上下文印证源码与场景这是最关键的一步。拿到函数名后立刻去查源码函数复杂度是O(1)的哈希查找还是O(n²)的嵌套循环输入数据特征函数是否对特定输入敏感比如一个字符串分割函数在处理超长、含大量分隔符的字符串时是否会退化外部依赖是否调用了未缓存的系统调用如gettimeofday在某些内核版本下很慢或第三方库的昂贵函数我有个血泪教训曾定位到一个config_load函数占比奇高调用链显示它被http_handler频繁调用。查源码发现它每次都会fopen/fread/fclose一个配置文件。问题不在函数本身而在于调用方——http_handler每处理一个请求都调用它而配置文件内容其实几小时才变一次。解决方案不是优化config_load而是加一层LRU缓存将调用频次从QPS级别降到分钟级别。所以“异常函数”的定义永远要放在业务场景和调用频次的上下文中去理解不能孤立看待。4. 实操过程与核心环节实现一次完整的线上问题复现与解决4.1 模拟一个典型问题一个被忽视的字符串哈希退化为了演示全流程我们构建一个可控的、能复现“异常函数”的C程序。这个程序模拟一个简单的键值存储服务核心是hash_lookup函数它使用一个朴素的字符串哈希算法DJB2并用链表处理冲突。// perf_demo.c #include stdio.h #include stdlib.h #include string.h #include unistd.h #include time.h #define HASH_SIZE 1024 #define MAX_KEY_LEN 1024 typedef struct hash_node { char *key; char *value; struct hash_node *next; } hash_node_t; hash_node_t *hash_table[HASH_SIZE] {0}; // 一个有缺陷的哈希函数对特定输入如全a字符串会产生大量碰撞 unsigned int djb2_hash(const char *str) { unsigned int hash 5381; int c; while ((c *str)) { hash ((hash 5) hash) c; // hash * 33 c } return hash (HASH_SIZE - 1); } // 查找函数当哈希碰撞严重时链表遍历会变成O(n) char* hash_lookup(const char *key) { unsigned int idx djb2_hash(key); hash_node_t *node hash_table[idx]; while (node) { if (strcmp(node-key, key) 0) { return node-value; } node node-next; } return NULL; } // 插入函数用于构造测试数据 void hash_insert(const char *key, const char *value) { unsigned int idx djb2_hash(key); hash_node_t *node malloc(sizeof(hash_node_t)); node-key strdup(key); node-value strdup(value); node-next hash_table[idx]; hash_table[idx] node; } int main() { // 构造大量碰撞数据所有key都是aaaaaaaaaa...长度不同 for (int i 0; i 10000; i) { char key[MAX_KEY_LEN]; memset(key, a, i % 1000); // 长度从0到999 key[i % 1000] \0; hash_insert(key, dummy_value); } printf(Hash table built with 10000 keys.\n); printf(Starting lookup loop...\n); // 主循环不断查找制造CPU热点 while(1) { // 查找一个必然存在的、且会导致长链表遍历的key char search_key[1001]; memset(search_key, a, 1000); search_key[1000] \0; volatile char *result hash_lookup(search_key); // volatile防止编译器优化掉 // 模拟一点工作 usleep(100); } return 0; }编译命令务必带调试信息gcc -g -O2 -rdynamic perf_demo.c -o perf_demo4.2 第一步确认CPU使用率异常启动程序./perf_demo 用top观察PID USER PR NI VIRT RES SHR S %CPU %MEM TIME COMMAND 12345 user 20 0 102400 12000 8000 R 99.7 0.1 00:02.34 perf_demo%CPU稳定在99.7%确认是CPU密集型问题且PID为12345。4.3 第二步perf record精准采样运行采样命令30秒perf record -e cycles -g -F 99 --call-graph dwarf -p 12345 -- sleep 30等待30秒后perf.data生成。检查大小ls -lh perf.data应该在10MB左右说明采样成功。4.4 第三步perf report初筛热点perf report --no-children -n --sort comm,dso,symbol | head -20输出关键部分# Samples: 29,456 of event cycles # Event count (approx.): 123456789012 # # Children Self Samples Command Shared Object Symbol # ........ ........ ............ ....... ................. ...... # 99.85% 99.85% 29412 perf_dem perf_demo [.] hash_lookup 0.10% 0.10% 29 perf_dem perf_demo [.] main 0.02% 0.02% 6 perf_dem libc-2.31.so [.] __strcmp_sse2_unaligned 0.01% 0.01% 3 perf_dem perf_demo [.] djb2_hashhash_lookup占比99.85%Self自身消耗也是99.85%说明它几乎不调用其他函数所有时间都在自己内部循环。这就是最典型的“异常函数”——一个本该是O(1)的查找却吃掉了全部CPU。4.5 第四步火焰图深度分析调用链生成火焰图# 假设FlameGraph工具在~/FlameGraph目录 git clone https://github.com/brendangregg/FlameGraph.git ~/FlameGraph perf script -F comm,pid,tid,cpu,time,period,event,ip,sym,dso,trace | ~/FlameGraph/stackcollapse-perf.pl out.perf-folded ~/FlameGraph/flamegraph.pl out.perf-folded perf_demo.svg用浏览器打开perf_demo.svg你会看到一个极其简洁的火焰图顶部一个巨大的hash_lookup矩形下面紧贴着一个窄窄的__strcmp_sse2_unalignedstrcmp的优化版本再下面就是main。这清晰地表明hash_lookup的绝大部分时间都花在了strcmp的字符串比较上。4.6 第五步源码分析与根因确认回到hash_lookup函数char* hash_lookup(const char *key) { unsigned int idx djb2_hash(key); // 所有a...字符串哈希值相同 hash_node_t *node hash_table[idx]; // 全部挤在一个桶里 while (node) { // 只能顺序遍历整个10000节点的链表 if (strcmp(node-key, key) 0) { // 每次都调用strcmp且key很长 return node-value; } node node-next; } return NULL; }根因浮出水面djb2_hash对全a字符串的哈希值是固定的因为hash hash * 33 a初始值5381最终结果模1024后恒定导致10000个键全部映射到同一个哈希桶。hash_lookup被迫遍历一个长度为10000的链表。每次strcmp都要比较最长1000个字符最坏情况是10000 * 1000 1000万次字符比较CPU自然爆满。4.7 第六步验证与修复验证修改djb2_hash加入一个简单的随机扰动仅用于演示生产环境请用成熟哈希库如MurmurHashunsigned int djb2_hash(const char *str) { unsigned int hash 5381; int c, i 0; while ((c *str)) { hash ((hash 5) hash) c i; // 加入索引扰动 } return hash (HASH_SIZE - 1); }重新编译、运行、perf recordhash_lookup占比会降到5%火焰图里strcmp也会消失证明修复有效。生产修复方案立即将哈希函数替换为业界标准的murmur3_32它对输入分布不敏感。长期引入哈希表扩容机制当某个桶的链表长度阈值如8时自动rehash。监控在hash_lookup函数开头加一个if (list_length 10) log_warn(Hash collision storm!);作为线上预警。这个完整流程从top发现异常到perf定位函数再到火焰图看清调用最后源码确认根因就是我在生产环境每天重复的标准动作。它不依赖任何外部监控系统只需要一个Linux终端和几条命令就能直达问题核心。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 “perf report全是[unknown]”符号解析失败的七种可能这是新手遇到最多的问题。根据我的经验按发生概率排序问题原因快速诊断命令解决方案1. 二进制被strip过file your_app(看是否含not stripped)readelf -S your_app | grep .symtab(看是否有.symtab段)重新编译去掉-s参数或用objcopy --add-gnu-debuglinkyour_app.debug your_app关联debuginfo2. debuginfo包未安装ls /usr/lib/debug/usr/bin/your_app*(RHEL/CentOS)ls /usr/lib/debug/.build-id/*(Ubuntu)sudo debuginfo-install your_app或 sudo