发现二进制差异
作为持续替换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国际许可协议进行许可。