Network Tracing Hands on

语言: CN / TW / HK

eBPF

提问:以下哪个内核可以运行 ebpf?

  • ZStack C74
  • ZStack C76
  • CentOS 7.7
  • ZStack experimental repo 中的内核

(分别对应了 3.10.0-693 3.10.0-957 3.10.0-1062 4.18.0-240)

答案:有三个内核可以支持

C74 是不支持的,C76 有有限的支持,C77 支持程度更好一点,experimental kernel 里是 4.18.0,对 ebpf 支持已经很完善(当然了,ebpf 还处于快速发展阶段,因此还是版本越高越完善)

https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html-single/7.6_release_notes/index#technology_previews_kernel

https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/7.7_release_notes/new_features#enhancement_kernel

检查自己安装的内核:

grubby --info=ALL

在 ZStack 管理的计算节点上安装来自 experimental 的 4.18 内核:

yum --disablerepo=\* --enablerepo=zstack-experimental-mn install kernel-4.18.0

在 ZStack 管理节点上安装来自 experimental 的 4.18 内核:

yum --disablerepo=\* --enablerepo=zstack-experimental-mn install kernel-4.18.0

查看内核编译选项

[[email protected] ~]# cat /boot/config-3.10.0-1062.18.1.el7.x86\_64 | grep -i bpf
CONFIG\_BPF=y
CONFIG\_BPF\_SYSCALL=y
CONFIG\_BPF\_JIT\_ALWAYS\_ON=y
CONFIG\_NETFILTER\_XT\_MATCH\_BPF=m
CONFIG\_NET\_CLS\_BPF=m
CONFIG\_BPF\_JIT=y
CONFIG\_HAVE\_EBPF\_JIT=y
CONFIG\_BPF\_EVENTS=y
CONFIG\_BPF\_KPROBE\_OVERRIDE=y

安装 bcc,跑一个 ebpf demo

参考:https://github.com/iovisor/bcc/blob/master/INSTALL.md#rhel—binary

yum install bcc-tools

下载 bcc 源码:

git clone <https://github.com/iovisor/bcc.git>

运行 hello world(跟踪系统调用 sys_clone):

python examples/hello\_world.py

在 hello world 上稍作修改,跟踪 ipt_do_table 调用

修改 hello world,改成调用 ipt_do_table(进入 iptables 的时候)的时候打印 hello world

[[email protected] bcc]# cat examples/hello\_world.1.py
#!/usr/bin/python
from bcc import BPF
BPF(text='int kprobe\_\_ipt\_do\_table(void \*ctx) { bpf\_trace\_printk("Hello, World!\\n"); return 0; }').trace\_print()

此时会发现脚本会不断打印 hello world

下面将 iptables 模块移除:

# **保存一下之前的 iptables 内容:**
[[email protected] ~]# iptables-save > ww.1.ipt

# **准备一个空的 iptables 配置**
[[email protected] ~]# cat ww.empty.ipt
# Generated by iptables-save v1.4.21 on Thu Sep 23 10:24:35 2021
\*raw
:PREROUTING ACCEPT [0:0]
:OUTPUT ACCEPT [69467:48787226]
COMMIT
# Completed on Thu Sep 23 10:24:35 2021
# Generated by iptables-save v1.4.21 on Thu Sep 23 10:24:35 2021
\*filter
:INPUT ACCEPT [81:6753]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [69467:48787226]
COMMIT
# Completed on Thu Sep 23 10:24:35 2021
# Generated by iptables-save v1.4.21 on Thu Sep 23 10:24:35 2021
\*mangle
:PREROUTING ACCEPT [76482:29150585]
:INPUT ACCEPT [73927:27411897]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [69467:48787226]
:POSTROUTING ACCEPT [69467:48787226]
COMMIT
# Completed on Thu Sep 23 10:24:35 2021

# **停掉 zsn,将空配置导入**

[[email protected] ~]# systemctl stop zstack-network-agent
[[email protected] ~]# cat ww.empty.ipt | iptables-restore

[[email protected] ~]# lsmod | grep ipt
ipt\_REJECT 16384 0
nf\_reject\_ipv4 16384 1 ipt\_REJECT
iptable\_raw 16384 0
iptable\_filter 16384 0
iptable\_mangle 16384 0
ip\_tables 28672 3 iptable\_filter,iptable\_raw,iptable\_mangle`

# **卸载模块**
[[email protected] ~]# modprobe -r iptable\_raw
[[email protected] ~]# modprobe -r iptable\_filter`
[[email protected] ~]# modprobe -r iptable\_mangle
[[email protected] ~]# modprobe -r ipt\_REJECT

[[email protected] ~]# lsmod | grep ipt
[[email protected] ~]#

发现不会再打印任何 hello world

类似的,可以跟踪其他函数,例如网络路径中的 iprcv、br_nf_forward 等等

复杂的例子:skbtracer

首先下载代码(原来的代码有一些问题,例如没有 KBUILD_MODNAME,dropstack 存在 bug 无法使用等,我做了一些修复)

git clone [https://github.com/MatheMatrix/skbtracer](https://github.com/MatheMatrix/skbtracer/blob/main/skbtracer.c)

可以简单看下命令介绍

python skbtracer.py --help

可以从 10.0.54.172 发个包文看下收包的路径,skbtracer 可以指定协议、IP 等信息做过滤

python skbtracer.py --proto=icmp -H 10.0.54.172

因为我的这个环境里,地址配在了网桥上,我们可以看下地址配置网卡的收包过程

ip link add link eth0 name eth0.999 type vlan id 999
ip link set dev eth0.999 up 
ip a add 192.168.99.14/24 dev eth0.999

此外,我们可以在发包的虚拟机配一个路由,例如

ip r add 192.168.100.10 via 192.168.99.14

然后往这个假的 192.168.100.10 地址发包,让 192.168.99.14 起到一个路由的作用

skbtracer 能否同时 trace 函数和 netfilter?

此外,可以把 iptable 的 trace 打开:

如果我通过 iptables INPUT 将报文 drop,可以看到 request 最后一步会显示 INPUT drop:

如果我用 tc 来 drop 报文呢?

首先是准备工作

yum --enablerepo=zstack-experimental-mn install kernel-modules-extra
tc qdisc add dev eth0.999 root netem loss 50%

然后发四个报文,其中 seq=2 被丢掉了

查看 trace,发现相比正常 trace 痕迹,这里少了一个 __dev_queue_xmit 把报文从 eth0.999 送到 eth0 的过程,进而也少了 eth0 packet_rcv 的过程,因此可以确认是 eth0.999 把包丢掉了

但如果是在 eth0 上丢包呢?

[[email protected] opensource]# tc qdisc add dev eth0 root netem loss 33%
[[email protected] opensource]#
[[email protected] opensource]# ping 10.0.54.172 -c 4
PING 10.0.54.172 (10.0.54.172) 56(84) bytes of data.
64 bytes from 10.0.54.172: icmp\_seq=1 ttl=64 time=0.346 ms
64 bytes from 10.0.54.172: icmp\_seq=2 ttl=64 time=0.445 ms
64 bytes from 10.0.54.172: icmp\_seq=4 ttl=64 time=0.470 ms
--- 10.0.54.172 ping statistics ---
4 packets transmitted, 3 received, 25% packet loss, time 3052ms
rtt min/avg/max/mdev = 0.346/0.420/0.470/0.056 ms

发送四个报文,seq=3被丢弃了,但是从 trace 上看确实发出去了,没有收到回包,说明我们的 trace 还不够细致,需要改造 skbtracer

如何改造 skbtracer 追踪 tc?

通过对 tc 的原理进行分析可以知道 tc 本质上是在 qdisc 上做文章,通过 perf 可以查到 qdisc 也有相应的 tracepoint,因此我们可以通过在 skbtracer 增加 qdisc 的 tracepoint 来达到目的

[[email protected] skbtracer]# git diff
diff --git a/skbtracer.c b/skbtracer.c
index e62b542..e49c613 100644
--- a/skbtracer.c
+++ b/skbtracer.c
@@ -1,3 +1,5 @@
+#define KBUILD\_MODNAME "skbtracer"
+
#include <bcc/proto.h>
#include <uapi/linux/ip.h>
#include <uapi/linux/ipv6.h>
**@@ -589,6 +591,10 @@ int kprobe\_\_ip\_finish\_output(struct pt\_regs \*ctx, struct net \*net, struct sock \***
**return do\_trace(ctx, skb, \_\_func\_\_+8, NULL);**
**}**
**+TRACEPOINT\_PROBE(qdisc, qdisc\_dequeue) {**
**+ return do\_trace(args, (struct sk\_buff\*)args->skbaddr, \_\_func\_\_, NULL);**
**+}**
**+**
**#endif**
#if \_\_BCC\_iptable
@@ -671,7 +677,7 @@ int kprobe\_\_\_\_kfree\_skb(struct pt\_regs \*ctx, struct sk\_buff \*skb)
event.start\_ns = bpf\_ktime\_get\_ns();
bpf\_strncpy(event.func\_name, \_\_func\_\_+8, FUNCNAME\_MAX\_LEN);
get\_stack(ctx, &event);
- route\_event.perf\_submit(ctx, event, sizeof(\*event));
+ route\_event.perf\_submit(ctx, &event, sizeof(event));
return 0;
}
#endif

为了方便,并没有做什么开关之类的,而是直接加到了 trace 里,让我们来再试一下

[[email protected] opensource]# ping 10.0.54.172 -c 4
PING 10.0.54.172 (10.0.54.172) 56(84) bytes of data.
64 bytes from 10.0.54.172: icmp\_seq=1 ttl=64 time=0.757 ms
64 bytes from 10.0.54.172: icmp\_seq=2 ttl=64 time=0.455 ms
64 bytes from 10.0.54.172: icmp\_seq=3 ttl=64 time=0.388 ms
--- 10.0.54.172 ping statistics ---
4 packets transmitted, 3 received, 25% packet loss, time 3098ms
rtt min/avg/max/mdev = 0.388/0.533/0.757/0.161 ms

这里就很容易看到了,之前的报文都会经过 tracepoint__qdisc__qdisc_dequeue,而最后一个报文却没有到达 tracepoint__qdisc__qdisc_dequeue,也就是在 qdisc dequeue 之前被丢弃了,因此我们的排查方向就可以往 qdisc 的方向排查

对于更新的 kernel 版本,还提供了 qdisc_enqueue 的 tracepoint,就可以更明确的展示出是不是 qdisc 这块搞鬼了

此外,如果你已经逐步的缩小了范围,那么可以通过检查 /proc/kallsyms 里的导出函数或者 perf list 中的 tracepoint 进一步 trace,缩小调查范围

思考题

目前 skbtracer 只做了 ip 层以下大部分和 iptables,如果想要跟踪 ebtables 该怎么改造?

拓展文章

iptables trace target

简介

iptables 有海量的现成的 target extension,参考: https://ipset.netfilter.org/iptables-extensions.man.html 有很多 extension 其实很小众但很有意思,过去的网络(设备)公司很多报文的修改等花活都是在 netfilter 这个框架下加 extension 实现的,当然随着设备公司转向用户态协议栈等方案,netfilter 插件技术也逐渐成文屠龙技了,但还是有很多很实用的可以多探索,例如 trace。

modprobe nf\_log\_ipv4
sysctl -w net.netfilter.nf\_log.2=nf\_log\_ipv4
iptables -t raw -I PREROUTING -p icmp -j TRACE # 根据需求修改过滤条件,这里是针对收包的,maybe 你需要针对入包的

思考题

还能结合什么其他 target extension 来实现 跟踪报文在 iptables 的“流转”过程吗?

dropwatch

简介

dropwatch 通过跟踪 skb_free 来跟踪丢包,不过功能相对有限,在高版本内核中,dropwatch 完全没有 ebpf 来的灵活好用,但好处是 3.10.X 内核你就可以通过 dropwatch 简单排查,有一个小 tip 是利用大量发报文定位到对应的 dropwatch。例如 iptables 丢包:

iptables -I INPUT -s 10.0.54.172 -j DROP

可以看到是 nf_hook_slow,其实根据函数名就能大致判断是 iptables 丢包,当然也可以进入到对应版本内核代码查看

用 perf 取代 dropwatch 获取更详细的信息

前面有提到 dropwatch 本质上是 trace skb_free 的函数,那么其实无论是 systemtap、ebpf、perf 等各种 trace 工具都可以做到类似的事情,下面我们演示用 perf 获取发生 skb free 的更详细的调用栈

[[email protected] skbtracer]# perf record -g -a -e skb:kfree_skb
Lowering default frequency rate to 2000.
Please consider tweaking /proc/sys/kernel/perf_event_max_sample_rate.
^C[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.404 MB perf.data (453 samples) ]

[[email protected] skbtracer]# perf script
swapper     0 [000] 100201.435009: skb:kfree_skb: skbaddr=0xffff9b364e848b00 protocol=2048 location=0xffffffffb75ae9f7
        ffffffffb7524bd3 kfree_skb+0x73 ([kernel.kallsyms])
        ffffffffb7524bd3 kfree_skb+0x73 ([kernel.kallsyms])
        ffffffffb75ae9f7 nf_hook_slow+0xa7 ([kernel.kallsyms])
        ffffffffb75bb29c ip_local_deliver+0xcc ([kernel.kallsyms])
        ffffffffc0b98191 ip_sabotage_in+0x41 ([kernel.kallsyms])
        ffffffffb75ae994 nf_hook_slow+0x44 ([kernel.kallsyms])
        ffffffffb75bb5bc ip_rcv+0x30c ([kernel.kallsyms])
        ffffffffb753ebf7 __netif_receive_skb_core+0xb27 ([kernel.kallsyms])
        ffffffffb753ed8d netif_receive_skb_internal+0x3d ([kernel.kallsyms])
        ffffffffc0b6cf64 br_pass_frame_up+0xc4 ([kernel.kallsyms])
        ffffffffc0b6d154 br_handle_frame_finish+0x164 ([kernel.kallsyms])
        ffffffffc0b9907b br_nf_hook_thresh+0xdb ([kernel.kallsyms])
        ffffffffc0b99a09 br_nf_pre_routing_finish+0x129 ([kernel.kallsyms])
        ffffffffc0b99f91 br_nf_pre_routing+0x341 ([kernel.kallsyms])
        ffffffffb75ae994 nf_hook_slow+0x44 ([kernel.kallsyms])
        ffffffffc0b6d621 br_handle_frame+0x1f1 ([kernel.kallsyms])
        ffffffffb753e767 __netif_receive_skb_core+0x697 ([kernel.kallsyms])
        ffffffffb753ed8d netif_receive_skb_internal+0x3d ([kernel.kallsyms])
        ffffffffb753f68a napi_gro_receive+0xba ([kernel.kallsyms])
        ffffffffc031091e receive_buf+0x17e ([kernel.kallsyms])
        ffffffffc0311413 virtnet_poll+0x153 ([kernel.kallsyms])
        ffffffffb753fb89 net_rx_action+0x149 ([kernel.kallsyms])
        ffffffffb7a000e4 __softirqentry_text_start+0xe4 ([kernel.kallsyms])
        ffffffffb6ebc357 irq_exit+0xf7 ([kernel.kallsyms])
        ffffffffb7801f9f do_IRQ+0x7f ([kernel.kallsyms])
        ffffffffb7800a8f ret_from_intr+0x0 ([kernel.kallsyms])
        ffffffffb76d832e native_safe_halt+0xe ([kernel.kallsyms])
        ffffffffb76d7f9c __cpuidle_text_start+0x1c ([kernel.kallsyms])
        ffffffffb6eebd37 do_idle+0x207 ([kernel.kallsyms])
        ffffffffb6eebf8f cpu_startup_entry+0x6f ([kernel.kallsyms])
        ffffffffb88a01e5 start_kernel+0x51e ([kernel.kallsyms])
        ffffffffb6e000e7 secondary_startup_64+0xb7 ([kernel.kallsyms])

swapper     0 [000] 100201.438796: skb:kfree_skb: skbaddr=0xffff9b364e848b00 protocol=2048 location=0xffffffffb75bacad
        ffffffffb7524bd3 kfree_skb+0x73 ([kernel.kallsyms])
        ffffffffb7524bd3 kfree_skb+0x73 ([kernel.kallsyms])
        ffffffffb75bacad ip_rcv_finish+0x20d ([kernel.kallsyms])
        ffffffffc0b98191 ip_sabotage_in+0x41 ([kernel.kallsyms])
        ffffffffb75ae994 nf_hook_slow+0x44 ([kernel.kallsyms])
        ffffffffb75bb5bc ip_rcv+0x30c ([kernel.kallsyms])
        ffffffffb753ebf7 __netif_receive_skb_core+0xb27 ([kernel.kallsyms])
        ffffffffb753ed8d netif_receive_skb_internal+0x3d ([kernel.kallsyms])
        ffffffffc0b6cf64 br_pass_frame_up+0xc4 ([kernel.kallsyms])
        ffffffffc0b6d154 br_handle_frame_finish+0x164 ([kernel.kallsyms])
        ffffffffc0b9907b br_nf_hook_thresh+0xdb ([kernel.kallsyms])
        ffffffffc0b99a09 br_nf_pre_routing_finish+0x129 ([kernel.kallsyms])
        ffffffffc0b99f91 br_nf_pre_routing+0x341 ([kernel.kallsyms])
        ffffffffb75ae994 nf_hook_slow+0x44 ([kernel.kallsyms])
        ffffffffc0b6d621 br_handle_frame+0x1f1 ([kernel.kallsyms])
        ffffffffb753e767 __netif_receive_skb_core+0x697 ([kernel.kallsyms])
        ffffffffb753ed8d netif_receive_skb_internal+0x3d ([kernel.kallsyms])
        ffffffffb753f68a napi_gro_receive+0xba ([kernel.kallsyms])
        ffffffffc031091e receive_buf+0x17e ([kernel.kallsyms])
        ffffffffc0311413 virtnet_poll+0x153 ([kernel.kallsyms])
        ffffffffb753fb89 net_rx_action+0x149 ([kernel.kallsyms])
        ffffffffb7a000e4 __softirqentry_text_start+0xe4 ([kernel.kallsyms])
        ffffffffb6ebc357 irq_exit+0xf7 ([kernel.kallsyms])
        ffffffffb7801f9f do_IRQ+0x7f ([kernel.kallsyms])
        ffffffffb7800a8f ret_from_intr+0x0 ([kernel.kallsyms])
        ffffffffb76d832e native_safe_halt+0xe ([kernel.kallsyms])
        ffffffffb76d7f9c __cpuidle_text_start+0x1c ([kernel.kallsyms])
        ffffffffb6eebd37 do_idle+0x207 ([kernel.kallsyms])
        ffffffffb6eebf8f cpu_startup_entry+0x6f ([kernel.kallsyms])
        ffffffffb88a01e5 start_kernel+0x51e ([kernel.kallsyms])
        ffffffffb6e000e7 secondary_startup_64+0xb7 ([kernel.kallsyms])

swapper     0 [000] 100201.441461: skb:kfree_skb: skbaddr=0xffff9b364e848b00 protocol=2048 location=0xffffffffb75bb348
        ffffffffb7524bd3 kfree_skb+0x73 ([kernel.kallsyms])
        ffffffffb7524bd3 kfree_skb+0x73 ([kernel.kallsyms])
        ffffffffb75bb348 ip_rcv+0x98 ([kernel.kallsyms])
        ffffffffb753ebf7 __netif_receive_skb_core+0xb27 ([kernel.kallsyms])
        ffffffffb753ed8d netif_receive_skb_internal+0x3d ([kernel.kallsyms])
        ffffffffc0b6cf64 br_pass_frame_up+0xc4 ([kernel.kallsyms])
        ffffffffc0b6d154 br_handle_frame_finish+0x164 ([kernel.kallsyms])
        ffffffffc0b9907b br_nf_hook_thresh+0xdb ([kernel.kallsyms])
        ffffffffc0b99a09 br_nf_pre_routing_finish+0x129 ([kernel.kallsyms])
        ffffffffc0b99f91 br_nf_pre_routing+0x341 ([kernel.kallsyms])
        ffffffffb75ae994 nf_hook_slow+0x44 ([kernel.kallsyms])
        ffffffffc0b6d621 br_handle_frame+0x1f1 ([kernel.kallsyms])
        ffffffffb753e34b __netif_receive_skb_core+0x27b ([kernel.kallsyms])
        ffffffffb753ed8d netif_receive_skb_internal+0x3d ([kernel.kallsyms])
        ffffffffb753f68a napi_gro_receive+0xba ([kernel.kallsyms])
        ffffffffc031091e receive_buf+0x17e ([kernel.kallsyms])
        ffffffffc0311413 virtnet_poll+0x153 ([kernel.kallsyms])
        ffffffffb753fb89 net_rx_action+0x149 ([kernel.kallsyms])
        ffffffffb7a000e4 __softirqentry_text_start+0xe4 ([kernel.kallsyms])
        ffffffffb6ebc357 irq_exit+0xf7 ([kernel.kallsyms])
        ffffffffb7801f9f do_IRQ+0x7f ([kernel.kallsyms])
        ffffffffb7800a8f ret_from_intr+0x0 ([kernel.kallsyms])
        ffffffffb76d832e native_safe_halt+0xe ([kernel.kallsyms])
        ffffffffb76d7f9c __cpuidle_text_start+0x1c ([kernel.kallsyms])
        ffffffffb6eebd37 do_idle+0x207 ([kernel.kallsyms])
        ffffffffb6eebf8f cpu_startup_entry+0x6f ([kernel.kallsyms])
        ffffffffb88a01e5 start_kernel+0x51e ([kernel.kallsyms])
        ffffffffb6e000e7 secondary_startup_64+0xb7 ([kernel.kallsyms])

swapper     0 [000] 100201.445009: skb:kfree_skb: skbaddr=0xffff9b364e848b00 protocol=2054 location=0xffffffffc0b6d143
        ffffffffb7524bd3 kfree_skb+0x73 ([kernel.kallsyms])
        ffffffffb7524bd3 kfree_skb+0x73 ([kernel.kallsyms])
        ffffffffc0b6d143 br_handle_frame_finish+0x153 ([kernel.kallsyms])
        ffffffffc0b6d584 br_handle_frame+0x154 ([kernel.kallsyms])
        ffffffffb753e767 __netif_receive_skb_core+0x697 ([kernel.kallsyms])
        ffffffffb753ed8d netif_receive_skb_internal+0x3d ([kernel.kallsyms])
        ffffffffb753f68a napi_gro_receive+0xba ([kernel.kallsyms])
        ffffffffc031091e receive_buf+0x17e ([kernel.kallsyms])
        ffffffffc0311413 virtnet_poll+0x153 ([kernel.kallsyms])
        ffffffffb753fb89 net_rx_action+0x149 ([kernel.kallsyms])
        ffffffffb7a000e4 __softirqentry_text_start+0xe4 ([kernel.kallsyms])
        ffffffffb6ebc357 irq_exit+0xf7 ([kernel.kallsyms])
        ffffffffb7801f9f do_IRQ+0x7f ([kernel.kallsyms])
        ffffffffb7800a8f ret_from_intr+0x0 ([kernel.kallsyms])
        ffffffffb76d832e native_safe_halt+0xe ([kernel.kallsyms])
        ffffffffb76d7f9c __cpuidle_text_start+0x1c ([kernel.kallsyms])
        ffffffffb6eebd37 do_idle+0x207 ([kernel.kallsyms])
        ffffffffb6eebf8f cpu_startup_entry+0x6f ([kernel.kallsyms])
        ffffffffb88a01e5 start_kernel+0x51e ([kernel.kallsyms])
        ffffffffb6e000e7 secondary_startup_64+0xb7 ([kernel.kallsyms])

这里是先用 perf record 记录了 kfree_skb 的事件,同时用 -g 记录了 call graph 方便我们追溯,最后用 perf script 自动处理记录的信息。

第一个报文,有一个 protocol 是 2048,可以在 https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml 查到就是 IP 报文,常用的,还有 2054 ARP

可以看到这个报文在 nf_hook_slow 之后就被调用了 kfree_skb,无疑就是我们上面分析的那种情况了

后面的几个报文,nf_hook_slow 已经过去了,分别在 ip_rcv_finish、ip_rcv、br_handle_frame_finish 被丢弃,都是些个大函数,而且这些函数往往是通过 goto DROP 的方式丢包,里面丢包的点很多,只能逐个分支分析情况了.

你可能能会想,这里不是给出了偏移量了吗?能否定位到某一行代码?这个留作思考题。

思考题

dropwatch 和 perf 给出来的偏移量能否定位都某一行代码?(提示:分两种情况,如果有对应版本的 debuginfo,和没有对应版本的 debuginfo)

拓展文章

「其他文章」