使用 git bisect run (2018) 在半小时内找到内核回归

2021-07-29 21:56:12

git bisect 命令可帮助您识别破坏某些内容的范围内的第一个提交。你给它一个好的提交和一个坏的提交,它会在两者之间做一个二分搜索来找到第一个坏提交。在每一步,您都可以根据它是否通过测试来选择 git bisect good 或 git bisect bad,这将使您在该范围内的剩余提交中途完成。有几个关于在 Linux 内核中使用 git bisect 的指南(例如,upstream、Gentoo 和 Ubuntu 都有一个)。不幸的是,它们是非常耗时的操作;他们都说了这样的话,“现在构建内核,重新启动它,并测试它,然后根据它是否有效输入 git bisect good 或 git bisect bad。”对于棘手的硬件兼容性错误,这可能是您唯一的选择。但是,如果您正在测试有关内核行为的某些内容,这将不必要地缓慢且手动,您可能会想做其他事情,例如读取提交日志。前几天在工作中,有人报告说某个应用程序在新虚拟机中不再工作。在使用 strace 进行一些初始调试后,我们确定该程序正在以 0 的积压调用 listen 系统调用:也就是说,它表示愿意接受最多零个连接。根据规范,它不应该工作——但它确实在他们的旧虚拟机上工作。新系统之间有一些不同之处,但值得注意的是新 VM 的内核为 4.9,旧的内核为 4.1。 (另一个是它部署在我的团队负责的新云环境中,有一些网络变化,所以我们想确保我们没有破坏任何东西!)我尝试通读 git log --grep listen v4.1。 .v4.9 net/,但内容太多了,我什么也找不到。所以我决定看看二分是否可以帮助我,使用 git bisect run,它可以实现全自动二等分。我对重新启动我的机器以对八个内核版本进行二进制搜索并不感到兴奋,但是如果我能让它以其他方式运行,我可以让它继续运行。对于普通程序,使用 git bisect run 非常容易,它只需要一个返回成功 (0) 或失败 (1) 的命令:您通常可以执行诸如 git bisect run make test 之类的操作。但是,对于内核回归,我们需要一个命令来引导内核并运行一些代码。为此我们可以使用 qemu 虚拟机软件,它有两个特性使它特别适合作为这样的命令:它可以直接引导 Linux 内核,而不是在硬盘上模拟引导加载程序,它可以运行临时 VM在单个命令行中,无需任何额外设置。我们将为自己构建一个很小的“initrd”(初始 RAM 磁盘),它通常用于加载足够的驱动程序以访问您的硬盘驱动器并完全启动您的系统。然而,我们的 initrd 将只包含我们的一个测试程序,它可能会打印一条成功消息,并关闭系统。我们无法从 qemu 中有意义地获得返回值,因此我们将仅 grep 其输出以获取成功消息。第一步是检查内核源代码,如果我们还没有它们,并构建一个内核:

也就是说,在标准输入/输出上使用 VM 的串行控制台以文本模式运行它,而不是尝试弹出图形窗口,并告诉内核使用串行端口进行控制台输出。如果您的系统支持它,您可以添加 -enable-kvm 以使其更快,尽管由于我们希望在运行测试后立即关闭 VM,因此它不会产生太大差异(2 秒对 4 秒)我的机器)。这会引起恐慌,因为我们既没有给内核一个根文件系统,也没有给它一个有效的 initrd。 (您可以通过按 Ctrl-A 和 X 来终止 VM。)所以让我们用单个二进制文件 init 编写一个 initrd。它需要关闭系统,所以我们回到我们的提示: $ mkdir initrd $ cd initrd $ cat > init.c << EOF #include <sys/reboot.h> #include <stdio.h> #include < unistd.h> int main(void) { printf("Hello world!\n");重启(RB_POWER_OFF); EOF(是的,关闭系统的系统调用名为“rebo​​ot”,因为系统调用已经使用了“shutdown”这个名称来关闭套接字。我猜早期的 UNIX 计算机不支持启动硬件断电来自软件,所以关机命令只会停止所有进程,同步和卸载磁盘,并打印一条消息,要求操作员切断电源。)静态编译这个程序,所以它是一个二进制文件,把它放在一个特定的形式中initrd(一个压缩的 cpio 档案,一种古老但非常简单的格式,带有一个奇怪的命令行工具)并确保它被命名为 init,然后我们可以用 qemu 启动它: $ cd initrd $ cc -static -o init init。 c $ 回声初始化 | cpio -H newc -o | gzip > initrd.gz 1621 块 $ cd .. $ qemu-system-x86_64 -nographic -append console =ttyS0 -kernel arch/x86/boot/bzImage -initrd initrd/initrd.gz... [ 0.502593 ] ALSA 设备列表: [0.502889]未找到声卡。 [0.503554]释放未使用的内核存储器:1088K(ffffffff81f2f000 - ffffffff8203f000)[0.504262]写保护内核只读数据:14336k [0.505004]释放未使用的内核存储器:1680K(ffff88000185c000 - ffff880001a00000)[0.505855]释放未使用的内核内存:1340K (ffff880001cb1000 - ffff880001e00000) 世界你好! [1.089618] 输入:ImExPS/2 通用资源管理器鼠标作为 /devices/platform/i8042/serio1/input/input3 [1.092997] ACPI:准备进入系统睡眠状态 S5 [1.094083] 重启:断电很棒。我们已经构建了自己的内核,向它传递了一个测试二进制文件来运行,并在退出的 qemu 命令中启动它。这变成了我们可以传递给 git bisect run 的东西。现在是编写实际测试的时候了。这是我最终追踪我的错误的结果:

#include <sys/types.h> #include <sys/socket.h> #include <sys/time.h> #include <sys/reboot.h> #include <sys/ioctl.h> #include <net/ if.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> int main ( void ) { / * 我正在追踪的问题只有在禁用 syncookies 的情况下才能重现。虽然 initrd 被解压到一个可写的临时文件系统中,但什么都不存在,所以如果我需要 /proc,我需要自己创建和挂载它。 */ if ( getpid () == 1 ) { mkdir ("/proc"); mount ( "proc" , "/proc" , "proc" , 0 , NULL ); char buf [] = "0 \n" ; int fd = open ( "/proc/sys/net/ipv4/tcp_syncookies" , O_WRONLY );写 ( fd , buf , 2 );关闭 ( fd ); } int server = socket ( AF_INET , SOCK_STREAM , IPPROTO_TCP ); /* 此外,虽然存在环回以太网设备,但它并未启用,因此网络测试将不起作用。此代码等效于 `ifconfig lo up`。 */ struct ifreq ifreq = { . ifr_name = "lo" , }; ioctl ( server , SIOCGIFFLAGS , & ifreq ); if ( ! ( ifreq . ifr_flags & IFF_UP )) { ifreq . ifr_flags |= IFF_UP ; ioctl ( 服务器 , SIOCSIFFLAGS , & ifreq ); } struct sockaddr_in addr = { . sin_family = AF_INET , . sin_port = htons (54321), . sin_addr = { htonl ( INADDR_LOOPBACK )}, }; bind ( server , ( struct sockaddr * ) & addr , sizeof ( addr ));听(服务器,0); int client = socket ( AF_INET , SOCK_STREAM , IPPROTO_TCP );结构时间超时 = { 3 , 0 }; setockopt ( 客户端 , SOL_SOCKET , SO_SNDTIMEO , & timeout , sizeof ( timeout )); if ( connect ( client , ( struct sockaddr * ) & addr , sizeof ( addr )) == 0 ) { printf ( "Success \n "); } else { perror(“连接”); } if ( getpid () == 1 ) { 重启( RB_POWER_OFF ); } 返回 0 ;大部分内容特定于我尝试测试的内容,但您可能还需要代码来创建和挂载 /proc 或启用 lo。此外,我在 getpid() == 1 上设置了一些条件,以便我可以安全地在我的主机系统上测试程序,在那里它不是以 root 身份运行,并且我不希望它关闭任何电源。 (我在 strace 下运行了几次以确保它按照我的预期运行,而且我不想费心在我的 initrd 中获取 strace。)所以我首先确保这可以在股票内核上重现本身,与我的工作场所可能添加的任何配置隔离: $ qemu-system-x86_64 -nographic -append console =ttyS0 -kernel arch/x86/boot/bzImage -initrd initrd/initrd.gz | grep ^Success $ git checkout v4.1 $ make defconfig && make -j8 $ qemu-system-x86_64 -nographic -append console =ttyS0 -kernel arch/x86/boot/bzImage -initrd initrd/initrd.gz | grep ^SuccessSuccess 酷,这绝对是这些版本之间的回归。 (配置选项集从内核版本到内核版本不同,因此在如此广泛的范围内,最简单的方法是获取当前内核的默认配置 - 如果您需要自定义配置选项,您可能需要在之后编辑 .config运行 make defconfig 或其他东西。)现在让 git bisect run 做它的事情: $ git bisect start $ git bisect bad v4.9 $ git bisect good v4.1 $ git bisect run sh -c 'make defconfig && make -j8 && qemu-system-x86_64 -nographic -append console=ttyS0 -kernel arch/x86/boot/bzImage -initrd initrd/initrd.gz | grep ^Success' 它开始打印一堆构建日志,然后我开始做其他事情。大约半小时后(我预计需要更长时间!),它打印出来:

ef547f2ac16bd9d77a780a0e7c70857e69e8f23f是第一坏commitcommit ef547f2ac16bd9d77a780a0e7c70857e69e8f23fAuthor:埃里克Dumazet <[email protected]>日期:星期五10月2日11点43分37秒2015 -0700 TCP:删除max_qlen_log该控制变量设定为第一听(FD,积压)呼叫,但如果应用程序试图增加或减少积压,则不会更新。当时监听器有一个不可调整大小的哈希表是有道理的。此外,四舍五入到 2 的幂也不是很友好。签字人:Eric Dumazet <[email protected]> 签字人:David S. Miller <[email protected]>$ git describe --contains ef547f2ac16bd9d77a780a0e7c70857e69e8f23fv4.412^1^1 2~2 看起来非常相关——这意味着他们之前已经完成了积压工作。查看提交,我们可以看到发生了什么:在内核 4.4 之前,backlog 参数的上限总是至少为 8,并且还会四舍五入到下一个 2 的幂。因此,在较旧的内核上,listen(fd, 0) 变成了 listen(fd, 8),尽管使用了错误的 listen(),但该程序之前仍能正常工作。这个提交实际上在我试图阅读的 git 日志中的某个地方,但我一定已经滚动过了它。 git reflog 显示 git bisect 在解决这个问题之前经历了 16 次提交:它在第 11 次尝试时找到了这个,然后又花了 5 次提交确认在这个提交之前的所有提交都是好的。所以我很高兴 git bisect run 找到了这个提交,我特别高兴它在无人看管的半小时内找到了它,而我不必手动编译和测试 16 个内核。