深入探索Linux内核二进制差异检测技术

本文详细介绍了在Linux内核开发过程中如何检测源代码修改是否导致二进制差异的方法,包括使用diffoscope工具和objdump命令进行精确的二进制比较分析,确保代码修改不会引入意外的执行变化。

发现二进制差异

作为持续替换Linux内核中单元素数组工作的一部分,展示源代码更改没有导致可执行代码差异非常有用。例如,如果你从以下代码开始:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct foo {
    unsigned long flags;
    u32 length;
    u32 data[1];
};

void foo_init(int count)
{
    struct foo *instance;
    size_t bytes = sizeof(*instance) + sizeof(u32) * (count - 1);
    ...
    instance = kmalloc(bytes, GFP_KERNEL);
    ...
};

而你只修改了结构体定义:

1
2
-    u32 data[1];
+    u32 data[];

字节计算将会不正确,因为它仍然从所需计数中减去1个元素的空间。(我们暂时忽略这里可能最终出现算术上溢/下溢的开放式计算;这可以通过使用struct_size()辅助函数或size_mul()、size_add()等辅助函数系列单独解决。)

在这个例子中,大小计算中遗漏的调整相对容易发现,但有时结构大小如何编织到代码中就不那么明显了。我一直在使用出色的diffoscope工具来检查问题。如果你在比较构建时没有记住可重现构建所解决的问题,它可能会产生很多噪音,还有一些额外的注意事项。我准备构建时禁用了"已知会破坏代码布局"的选项,但启用了调试信息:

1
2
3
4
5
6
7
$ KBF="KBUILD_BUILD_TIMESTAMP=1980-01-01 KBUILD_BUILD_USER=user KBUILD_BUILD_HOST=host KBUILD_BUILD_VERSION=1"
$ OUT=gcc
$ make $KBF O=$OUT allmodconfig
$ ./scripts/config --file $OUT/.config \
        -d GCOV_KERNEL -d KCOV -d GCC_PLUGINS -d IKHEADERS -d KASAN -d UBSAN \
        -d DEBUG_INFO_NONE -e DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT
$ make $KBF O=$OUT olddefconfig

然后我构建一个标准目标,将输出保存在"before"中。在这个例子中,我正在检查drivers/scsi/megaraid/:

1
2
3
$ make -jN $KBF O=$OUT drivers/scsi/megaraid/
$ mkdir -p $OUT/before
$ cp $OUT/drivers/scsi/megaraid/*.o $OUT/before/

然后我打补丁并构建修改后的目标,将输出保存在"after"中:

1
2
3
4
$ vi the/source/code.c
$ make -jN $KBF O=$OUT drivers/scsi/megaraid/
$ mkdir -p $OUT/after
$ cp $OUT/drivers/scsi/megaraid/*.o $OUT/after/

然后运行diffoscope:

1
$ diffoscope $OUT/before/ $OUT/after/

如果diffoscope输出报告没有内容,那么我们就完成了。🥳

不过,通常当源代码行移动时,其他内容也会移位(例如WARN宏依赖于行号,因此错误表的内容可能会有所变化等),diffoscope输出会看起来很嘈杂。为了仅检查可执行代码,diffoscope使用的命令会在输出中报告,我们可以直接运行它,但可能不会报告移位后的行号。即运行没有–line-numbers的objdump:

1
2
3
4
5
6
$ ARGS="--disassemble --demangle --reloc --no-show-raw-insn --section=.text"
$ for i in $(cd $OUT/before && echo *.o); do
        echo $i
        diff -u <(objdump $ARGS $OUT/before/$i | sed "0,/^Disassembly/d") \
                <(objdump $ARGS $OUT/after/$i  | sed "0,/^Disassembly/d")
done

如果我看到意外的差异,例如:

1
2
-    c120:      movq   $0x0,0x800(%rbx)
+    c120:      movq   $0x0,0x7f8(%rbx)

那么我会在objdump输出中添加行号来搜索模式:

1
$ vi <(objdump --line-numbers $ARGS $OUT/after/megaraid_sas_fp.o)

我会搜索"0x0,0x7f8",找到上面的源文件和行号,在该位置打开源文件,并查看哪里计算错误:

1
$ vi drivers/scsi/megaraid/megaraid_sas_fp.c +329

一旦追踪到问题,我会从上面的"打补丁并构建修改后的目标"步骤重新开始,重复直到没有差异。例如,在起始示例中,我还需要做这个更改:

1
2
-    size_t bytes = sizeof(*instance) + sizeof(u32) * (count - 1);
+    size_t bytes = sizeof(*instance) + sizeof(u32) * count;

不过,如前所述,更好的做法是:

1
2
-    size_t bytes = sizeof(*instance) + sizeof(u32) * (count - 1);
+    size_t bytes = struct_size(instance, data, count);

但有时添加辅助函数使用会增加二进制输出差异,因为它们执行可能饱和于SIZE_MAX的溢出检查。为了帮助提高补丁的清晰度,这些更改可以与修复数组声明分开进行。

© 2022 - 2023, Kees Cook。本作品采用知识共享署名-相同方式共享4.0国际许可协议进行许可。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计