关键词: GDB调试 VPP AVF
1. 前言
年初的时候接了一个任务,研究一下VPP在SMP服务器架构下创建AVF接口出现错误的bug。当时刚刚完成CSAPP的第三个attack lab的实验,对gdb的使用也更为熟练。本文就是对整个debug过程的整理和记录。
2. 背景知识
2.1 服务器架构
从系统架构来看,市面上的商用服务器基本可以分为三类:
- SMP(Symmetric Multi-Processor)对称多处理器结构
- NUMA(Non-Uniform Memory Access)非一致存储访问结构
- MPP(Massive Parallel Processing)海量并行处理结构
SMP结构是指服务器中多个CPU对称工作,各个CPU之间的关系平等无差别。所有的CPU共享全部资源,包括总线、内存和I/O系统等。SMP结构的优点是共享,缺点也显而易见,其扩展能力非常有限。每一个共享的环节都有可能造成瓶颈。试想一下,当SMP结构中的多个CPU访问同一块内存时,内存访问就会发生冲突,CPU的资源发生浪费。为了解决上述问题,NUMA结构应运而生。NUMA结构最基本的特征就是由多个CPU模块构成,每个CPU模块都含有多个CPU,并具有本地独立的缓存、I/O槽等。各个CPU模块之间可通过互联模块(Crossbar Switch)来进行信息交互,所以每个CPU都可以访问到整个系统的内存,但是访问模块内的本地内存的速度要远远高于访问模块外的远地内存,这也是NUMA为什么叫做非一致存储的缘由。相较于NUMA,MPP采取了另一种系统扩展的方式。本质上,MPP结构相当于把多个SMP服务器组合在一起,将每个SMP服务器视作一个节点,每个SMP节点之间可以通过节点互联网络进行信息交互,但是每个SMP节点内的CPU不能够直接访问另一个SMP节点的资源,也就是不能够进行异地内存访问。
2.2 VPP AVF插件
VPP里的AVF插件给intel 710系列网卡提供自适应的Virtual Function(VF),其设计的初衷是给虚拟机提供通用的VF。AVF(Adaptive Virtual Function),从其英文名字就可以理解,其功能即意味着不需要随着网卡的更新而更新VF的驱动,这样子就可以让虚机无需更新软硬件的情况下跑在新的网卡上。VPP AVF插件的使用方法在链接中有具体说明。
3. 调试过程
3.1 系统环境
出现上述bug的机器对应的是Qualcomm Centriq 2400,采用的是ARMv8-A微架构的CPU。
1 | snowball@net-arm-c2400-02:~$ lscpu |
3.2 bug复现
按照2.2节中给出的链接完成VF的创建,用gdb起VPP debug镜像来创建VF接口,命令如下:
1 | sudo gdb ./build-root/build-vpp_debug-native/vpp/bin/vpp |
在gdb命令行中敲入以下命令起VPP CLI:
1 | run -c ~/startup_avf.conf |
startup_avf.conf
是VPP的配置文件,使用AVF接口的话就要把原始的startup.conf
文件中的dpdk
插件注释掉。
在VPP DBGvpp命令行中创建VPP AVF接口,命令如下:
1 | DBGvpp# create int avf 0000:01:02.0 |
上述命令的结果如下,bug复现。
1 | 0: /home/snowball/tasks/avf_plugin_test/vpp/src/vlib/buffer_funcs.h:165 (vlib_buffer_pool_get_default_for_numa) assertion `numa_node < VLIB_BUFFER_MAX_NUMA_NODES' fails |
3.3 调试bug
首先我用gdb中的backtrace
命令来回溯函数调用栈,结果如下所示:
1 | (gdb) bt |
可以看到,在调用异常处理函数之前,代码出错的地方在vlib_buffer_pool_get_default_for_numa (vm=0xfffff6fcd000 <vlib_global_main>, numa_node=4294967295)
。找到vlib_buffer_pool_get_default_for_numa
的定义,如下所示:
1 | always_inline u8 |
结合bug复现阶段看到的错误信息assertion numa_node < VLIB_BUFFER_MAX_NUMA_NODES' fails
,再看此时的numa_node
的值是4294967295
,对应的数据类型是u32
,而VLIB_BUFFER_MAX_NUMA_NODES
宏定义的值为32,判断代码在此处断言错误导致程序出错。而u32
类型的numa_node
的值为4294967295
,而这个值很有可能是int
类型的-1
隐式转换成u32
类型的结果。带着这个基本判断,接下来我来对相关函数设置断点进行调试。
我先将断点打在函数avf_create_command_fn
上进行单步调试。
1 | (gdb) b avf_create_command_fn |
敲入命令run -c ~/startup_avf.conf
重新启动VPP debug CLI。
1 | DBGvpp |
上述单步调试结果可以看到,在avf_create_command_fn
函数中,代码做的主要是对我在VPP debug CLI行中输入的命令参数的解析,解析完成后,调用avf_create_if
函数创建AVF接口。我用step
或s
命令进入avf_create_if
函数,继续单步调试,调试信息如下所示:
1 | avf_create_if (vm=0xfffff6fcd000 <vlib_global_main>, args=0xffffb8f90a30) |
在avf_create_if
函数里,有一行代码是获取numa_node
的值,ad->numa_node = vlib_pci_get_numa_node(vm, h)
。我在执行此行代码之前,先打印出此时的numa_node
的值为0。代码执行后,numa_node
的值变为4294967295
。可以得出结论,问题出在vlib_pci_get_numa_node
这个函数中。查看函数的定义,如下所示:
1 | u32 |
在vlib_pci_get_numa_node
函数设置断点,重新启动VPP debug CLI进行单步调试。
1 | Thread 1 "vpp_main" hit Breakpoint 2, vlib_pci_get_numa_node (vm=0xfffff6fcd000 <vlib_global_main>, h=0) |
vlib_pci_get_numa_node
函数中调用了linux_pci_get_device
函数,我在代码执行这条语句前,打印numa_node
的值,发现是个随机值。代码执行后,numa_node
又变成了对应的4294967295
。重新起VPP debug CLI,我单步调试进入linux_pci_get_device
探个究竟。linux_pci_get_device
函数定义如下所示:
1 | static linux_pci_device_t * |
函数linux_pci_get_devices
的单步调试结果如下所示:
1 | (gdb) s |
在单步调试过程中,我用ptype
指令查看对应变量的数据类型。可以判断的是,函数在linux_pci_main_t *lpm = &linux_pci_main;
完成了numa_node
的赋值。那么,linux_pci_main
是如何被初始化并完成成员结构体linux_pci_devices
中numa_node
的赋值,成为最终问题的聚焦点。
在函数linux_pci_get_device
中,函数返回的使用的是pool_elt_at_index
函数,对应的是返回VPP中pool
中第h
块的内存地址。顺着这条线索,我应该找到对linux_pci_devices
在pool
中分配内存的初始化代码,果然,搜索pool_get
函数,在init_device_from_registered
函数中发现了linux_pci_devices
初始化的代码:
1 | void |
同样,我对init_device_from_registered
函数设置断点,重新启动VPP debug CLI。
1 | Breakpoint 1, init_device_from_registered (vm=0xfffff6fcd000 <vlib_global_main>, di=0xffffb7c75244) |
init_device_from_registered
函数的调试信息说明对应的pci
设备还没有完成初始化。单步调试完init_device_from_registered
函数后,函数进入linux_pci_init
,继续单步调试,发现了问题所在:
1 | linux_pci_init (vm=0xfffff6fcd000 <vlib_global_main>) at /home/snowball/tasks/avf_plugin_test/vpp/src/vlib/linux/pci.c:1464 |
linux_pci_init
函数在执行d = vlib_pci_get_device_info (vm, addr, 0)
之前,numa_node
的值为一个随机值65535
,执行完语句后,numa_node
的值变为-1
,而且查看d
的数据类型,发现numa_node
作为其结构体成员变量的数据类型为int
,初步印证了我一开始的假设,bug出现的原因是int
类型的-1
隐式转换成unsigned
类型。将断点打在vlib_pci_get_device_info
函数,重新调试。
1 | (gdb) |
vlib_pci_get_device_info
函数的单步调试信息如上所示。可以看到,在对numa_node
赋值为-1
之前,numa_node
为0。系统读取/sys/bus/pci/devices/0000:00:00.0/numa_node
作为numa_node
的值,查看文件,其值为-1
,然而代码进入了err
分支,说明clib_sysfs_read
函数读取有问题,进入clib_sysfs_read
函数进行调试。
1 | 77 result = va_unformat (&input, fmt, &va); |
result
的值为0, 返回clib_error_return
,说明va_unformat
函数解析有问题,进入va_unformat
函数。
va_unformat
函数调用do_percent
函数,最终的错误原因是clib_sysfs_read
读取的格式是%u
,最后导致解析的函数是unformat_integer (input, va, 10, UNFORMAT_INTEGER_UNSIGNED, data_bytes)
,而文件解析得到的值为-1
,采用UNFORMAT_INTEGER_UNSIGNED
解析方式就会出错。初步的解决方法是将err = clib_sysfs_read ((char *) f, "%u", &di->numa_node)
中的%u
变为%d
。
4. 小结
写的有些凌乱,但大致的调试过程和思路都记录下来。这次调试也给我一个写代码的警示,在大型工程中变量的数据类型一定要统一规范,否则很容易出现错误。
5. 参考文献
- SMP、NUMA、MPP体系结构介绍 https://www.cnblogs.com/yubo/archive/2010/04/23/1718810.html
- AVF简介 https://www.sdnlab.com/21100.html