蚊章汇 - 分析
type: channel
link: https://linuxcn.bwsl.wang/tag/%E5%88%86%E6%9E%90/
items: 动态连接的诀窍：使用 LD_PRELOAD 去欺骗、注入特性和研究程序
items: 使用火焰图分析CPU性能回退问题

动态连接的诀窍：使用 LD_PRELOAD 去欺骗、注入特性和研究程序
type: item
date: 2017-12-25T05:42:40+08:00
description: 本文假设你具备基本的 C 技能Linux 完全在你的控制之中。虽然从每个人的角度来看似乎并不总是这样，但是高级用户喜欢去控制它。我将向你展示一个基本的诀窍，在很大程度上你可以去影响大多数程序的行为，它并不仅是好玩，在有时候也很有用。一个让我们产生兴趣的示例让我们以一个简单的示例开始。先乐趣，后科学。random\_num.c：#include <stdio.h> #include <stdlib.h> #include <time.h> int main(){ srand(time(NULL)); int i = 10; while(i--) printf("%d\n",rand()%100); return 0; } 我相信，它足够简单吧。我不使用任何参数来编译它，如下所示：gcc random_num.c -o random_num 我希望它输出的结果是明确的：从 0-99 中选择的十个随机数字，希望每次你运行这个程序时它的输出都不相同。现在，让我们假装真的不知道这个可执行程序的出处。甚至将它的源文件删除，或者把它移动到别的地方 —— 我们已不再需要它了。我们将对这个程序的行为进行重大的修改，而你并不需要接触到它的源代码，也不需要重新编译它。因此，让我们来创建另外一个简单的 C 文件：unrandom.c：int rand(){ return 42; //the most random number in the universe } 我们将编译它进入一个共享库中。gcc -shared -fPIC unrandom.c -o unrandom.so 因此，现在我们已经有了一个可以输出一些随机数的应用程序，和一个定制的库，它使用一个常数值 42 实现了一个 rand() 函数。现在 …… 就像运行 random_num 一样，然后再观察结果：LD_PRELOAD=$PWD/unrandom.so ./random_nums 如果你想偷懒或者不想自动亲自动手（或者不知什么原因猜不出发生了什么），我来告诉你 —— 它输出了十次常数 42。如果先这样执行export LD_PRELOAD=$PWD/unrandom.so 然后再以正常方式运行这个程序，这个结果也许会更让你吃惊：一个未被改变过的应用程序在一个正常的运行方式中，看上去受到了我们做的一个极小的库的影响 ……等等，什么？刚刚发生了什么？是的，你说对了，我们的程序生成随机数失败了，因为它并没有使用 “真正的” rand()，而是使用了我们提供的的那个 —— 它每次都返回 42。但是，我们告诉过它去使用真实的那个。我们编程让它去使用真实的那个。另外，在创建那个程序的时候，假冒的 rand() 甚至并不存在！这句话并不完全正确。我们只能告诉它去使用 rand()，但是我们不能去选择哪个 rand() 是我们希望我们的程序去使用的。当我们的程序启动后，（为程序提供所需要的函数的）某些库被加载。我们可以使用 ldd 去学习它是怎么工作的：$ ldd random_nums linux-vdso.so.1 => (0x00007fff4bdfe000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f48c03ec000) /lib64/ld-linux-x86-64.so.2 (0x00007f48c07e3000) 正如你看到的输出那样，它列出了被程序 random_nums 所需要的库的列表。这个列表是构建进可执行程序中的，并且它是在编译时决定的。在你的机器上的具体的输出可能与示例有所不同，但是，一个 libc.so 肯定是有的 —— 这个文件提供了核心的 C 函数。它包含了 “真正的” rand()。我使用下列的命令可以得到一个全部的函数列表，我们看一看 libc 提供了哪些函数：nm -D /lib/libc.so.6 这个 nm 命令列出了在一个二进制文件中找到的符号。-D 标志告诉它去查找动态符号，因为 libc.so.6 是一个动态库。这个输出是很长的，但它确实在列出的很多标准函数中包括了 rand()。现在，在我们设置了环境变量 LD_PRELOAD 后发生了什么？这个变量 为一个程序强制加载一些库。在我们的案例中，它为 random_num 加载了 unrandom.so，尽管程序本身并没有这样去要求它。下列的命令可以看得出来：$ LD_PRELOAD=$PWD/unrandom.so ldd random_nums linux-vdso.so.1 => (0x00007fff369dc000) /some/path/to/unrandom.so (0x00007f262b439000) libc.so…

使用火焰图分析CPU性能回退问题
type: item
date: 2015-01-15T06:49:00+08:00
description: 你能快速定位CPU性能回退的问题么？ 如果你的工作环境非常复杂且变化快速，那么使用现有的工具是来定位这类问题是很具有挑战性的。当你花掉数周时间把根因找到时，代码已经又变更了好几轮，新的性能问题又冒了出来。幸亏有了CPU火焰图（flame graphs），CPU使用率的问题一般都比较好定位。但要处理性能回退问题，就要在修改前后的火焰图之间，不断切换对比，来找出问题所在，这感觉就是像在太阳系中搜寻冥王星。虽然，这种方法可以解决问题，但我觉得应该会有更好的办法。所以，下面就隆重介绍红/蓝差分火焰图（red/blue differential flame graphs）：上面是一副交互式SVG格式图片（链接）。图中使用了两种颜色来表示状态，红色表示增长，蓝色表示衰减。这张火焰图中各火焰的形状和大小都是和第二次抓取的profile文件对应的CPU火焰图是相同的。（其中，y轴表示栈的深度，x轴表示样本的总数，栈帧的宽度表示了profile文件中该函数出现的比例，最顶层表示正在运行的函数，再往下就是调用它的栈）在下面这个案例展示了，在系统升级后，一个工作载荷的CPU使用率上升了。 下面是对应的CPU火焰图（SVG格式）通常，在标准的火焰图中栈帧和栈塔的颜色是随机选择的。 而在红/蓝差分火焰图中，使用不同的颜色来表示两个profile文件中的差异部分。在第二个profile中deflate\_slow()函数以及它后续调用的函数运行的次数要比前一次更多，所以在上图中这个栈帧被标为了红色。可以看出问题的原因是ZFS的压缩功能被启用了，而在系统升级前这项功能是关闭的。这个例子过于简单，我甚至可以不用差分火焰图也能分析出来。但想象一下，如果是在分析一个微小的性能下降，比如说小于5%，而且代码也更加复杂的时候，问题就为那么好处理了。红/蓝差分火焰图这个事情我已经讨论了好几年了，最终我自己编写了一个我个人认为有价值的实现。它的工作原理是这样的：抓取修改前的堆栈profile1文件抓取修改后的堆栈profile2文件使用profile2来生成火焰图。（这样栈帧的宽度就是以profile2文件为基准的）使用“2 - 1”的差异来对火焰图重新上色。上色的原则是，如果栈帧在profile2中出现出现的次数更多，则标为红色，否则标为蓝色。色彩是根据修改前后的差异来填充的。这样做的目的是，同时使用了修改前后的profile文件进行对比，在进行功能验证测试或者评估代码修改对性能的影响时，会非常有用。新的火焰图是基于修改后的profile文件生成（所以栈帧的宽度仍然显示了当前的CPU消耗），通过颜色的对比，就可以了解到系统性能差异的原因。只有对性能产生直接影响的函数才会标注颜色（比如说，正在运行的函数），它所调用的子函数不会重复标注。生成红/蓝差分火焰图我已经把一个简单的代码实现推送到github上（见火焰图），其中新增了一个程序脚本，difffolded.pl。为了展示工具是如何工作的，用Linux perf\_events 来演示一下操作步骤。（你也可以使用其他profiler）抓取修改前的profile 1文件:# perf record -F 99 -a -g -- sleep 30 # perf script > out.stacks1 一段时间后 (或者程序代码修改后), 抓取profile 2文件:# perf record -F 99 -a -g -- sleep 30 # perf script > out.stacks2 现在将 profile 文件进行折叠（fold）, 再生成差分火焰图:$ git clone --depth 1 http://github.com/brendangregg/FlameGraph $ cd FlameGraph $ ./stackcollapse-perf.pl ../out.stacks1 > out.folded1 $ ./stackcollapse-perf.pl ../out.stacks2 > out.folded2 $ ./difffolded.pl out.folded1 out.folded2 | ./flamegraph.pl > diff2.svg difffolded.p只能对“折叠”过的堆栈profile文件进行操作，折叠操作是由前面的stackcollapse系列脚本完成的。（见链接火焰图）。 脚本共输出3列数据，其中一列代表折叠的调用栈，另两列为修改前后profile文件的统计数据。func_a;func_b;func_c 31 33 [...] 在上面的例子中"funca()->funcb()->func\_c()" 代表调用栈，这个调用栈在profile1文件中共出现了31次，在p…