Linux 内核 UBSAN 未定义行为检测器:从编译插桩到运行时报告的使用指南
Linux 内核 UBSAN 未定义行为检测器从编译插桩到运行时报告的使用指南【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux导读本文以 Linux 内核源码树中的 Documentation/dev-tools/ubsan.rst 为主线结合 lib/Kconfig.ubsan、scripts/Makefile.ubsan 与 lib/ubsan.c 等源码系统讲解内核 UBSANUndefined Behavior Sanitizer的工作原理、启用方式、细粒度检查项、文件级排除技巧以及报告解读方法。读完本文你将掌握如何在开发/调试内核时开启 UBSAN 以捕获整数溢出、数组越界、移位越界等未定义行为并学会利用测试模块验证检测效果。UBSAN 是什么基于编译期插桩的运行时未定义行为检查器UBSAN 是内核提供的一种运行时未定义行为UB检查器。它不依赖运行时虚拟机或动态二进制翻译而是利用编译器的插桩能力编译时编译器会在可能触发未定义行为的操作如移位、除法、数组下标访问、整数溢出之前自动插入检查代码当检查失败、即检测到 UB 时被插入的代码会调用形如__ubsan_handle_*的函数来打印错误信息。GCC 自 4.9.x 起便提供该能力对应-fsanitizeundefined选项及其子选项GCC 5.x 起实现了更多检查器Clang/LLVM 同样通过其 UndefinedBehaviorSanitizer 提供类似支持。在内核中这些编译选项并非直接由用户手工传入而是通过 Kconfig 配置项映射到具体编译标志由 scripts/Makefile.ubsan 统一拼装。一次真实报告的结构解读原文档给出了一个经典的 UBSAN 报告样例发生于include/linux/bitops.h:110的 32 位类型左移 32 位的错误 UBSAN: Undefined behaviour in ../include/linux/bitops.h:110:33 shift exponent 32 is to large for 32-bit type unsigned int CPU: 0 PID: 0 Comm: swapper Not tainted 4.4.0-rc1 #26 0000000000000000 ffffffff82403cc8 ffffffff815e6cd6 0000000000000001 ... Call Trace: [ffffffff815e6cd6] dump_stack0x45/0x5f [ffffffff8163a5ed] ubsan_epilogue0xd/0x40 [ffffffff8163ac2b] __ubsan_handle_shift_out_of_bounds0xeb/0x130 ... 从源码实现看这份报告由 lib/ubsan.c 中的ubsan_prologue()与ubsan_epilogue()两段流程产生ubsan_prologue()lib/ubsan.c#L219-L229先递增当前任务的in_ubsan计数然后以pr_warn(CUT_HERE)打印分隔线再以pr_err(UBSAN: %s in %s:%d:%d\n, ...)输出违规原因、源文件、行号与列号同时调用kunit_fail_current_test()让运行中的 KUnit 测试能够感知本次违规ubsan_epilogue()lib/ubsan.c#L231-L239调用dump_stack()输出调用栈并以check_panic_on_warn(UBSAN)支持panic_on_warn等系统策略。因此一份完整报告包含三块信息违规位置文件:行:列、违规详情如shift exponent 32 is to large for 32-bit type unsigned int、以及触发路径的调用栈——其中栈顶通常是负责打印的__ubsan_handle_*函数往下即可追溯真正出错的业务代码。启用 UBSAN最小配置与内核构建流程在内核配置中启用 UBSAN 只需CONFIG_UBSANy该选项对应 lib/Kconfig.ubsan 中的menuconfig UBSAN依赖架构支持位ARCH_HAS_UBSAN。开启后scripts/Makefile.ubsan 会把各子配置项翻译为编译参数例如UBSAN_SHIFT→-fsanitizeshift、UBSAN_DIV_ZERO→-fsanitizeinteger-divide-by-zero、UBSAN_BOUNDS视编译器分别使用-fsanitizebounds-strictGCC或-fsanitizearray-boundsClang等最终通过export CFLAGS_UBSAN注入到内核对象的编译命令行中。从menuconfig UBSAN的帮助文本可以看到官方说明启用后编译器会在运行时检测多种未定义行为详细机制见Documentation/dev-tools/ubsan.rst——这正是本文所讲解的文档。细粒度检查项Kconfig 各子选项详解在CONFIG_UBSANy基础上lib/Kconfig.ubsan 提供了多个可独立开关的子检查项配置项默认值对应编译标志检测内容UBSAN_BOUNDS随UBSAN开启GCC:-fsanitizebounds-strictClang:-fsanitizearray-boundstrap 模式下还可叠加-fsanitizelocal-bounds编译期已知大小的数组的直接下标越界访问UBSAN_SHIFT关-fsanitizeshift左移溢出向左溢出、有符号类型的负移位指数UBSAN_DIV_ZERO关-fsanitizeinteger-divide-by-zero整数除零内核已有异常处理兜底此项主要用于提供更丰富的调试信息注意当前配置声明依赖!CC_IS_CLANG即 Clang 下不可用UBSAN_UNREACHABLE关-fsanitizeunreachable控制流到达本应不可达的位置UBSAN_BOOL随UBSAN开启-fsanitizebool加载到布尔值中的值既非 0 也非 1UBSAN_ENUM随UBSAN开启-fsanitizeenum加载到枚举中的值超出该枚举定义范围UBSAN_ALIGNMENT架构无高效非对齐访问时为开-fsanitizealignment非对齐内存访问UBSAN_TRAP关-fsanitize-trapundefined或-fsanitize-undefined-trap-on-error不打印报告直接触发 trapUBSAN_INTEGER_WRAP关depends on BROKEN实验性signed/unsigned-integer-overflow、implicit-*-integer-truncation等有/无符号整数加减乘溢出、隐式整数截断UBSAN_KVM_EL2关—仅 ARM64为运行在 EL2 的 KVM 代码nvhe/hvhe/protected 等分离模式启用 UBSAN几点从 Kconfig 注释中可确认的实现细节UBSAN_BOUNDS只针对编译期即可确定大小的数组的直接下标越界它并不能防护通过strcpy()/memcpy()等函数族造成的数组溢出那部分由CONFIG_FORTIFY_SOURCE负责。二者是互补关系。编译器能力探测通过cc-option完成CC_HAS_UBSAN_BOUNDS_STRICT检查 GCC 的-fsanitizebounds-strict其更严格地处理包含柔性数组成员的数组与 Clang 普通-fsanitizebounds相当CC_HAS_UBSAN_ARRAY_BOUNDS检查 Clang 的-fsanitizearray-bounds。因为 Clang 的-fsanitizebounds实际由array-bounds与local-bounds组成而local-bounds只能在 trap 模式使用故内核按UBSAN_TRAP是否开启来组合选项。UBSAN_UNREACHABLE依赖!(OBJTOOL (STACK_VALIDATION || UNWINDER_ORC || HAVE_UACCESS_VALIDATION))注释明确指出objtool 已负责不可达代码检查且会因在不可达位置看到 UBSan 插桩而报错所以该检查项与上述配置互斥。文件级与目录级控制UBSAN_SANITIZE 的三种用法UBSAN 允许在构建系统中按文件、按目录精确控制插桩范围避免对整个内核全量插桩带来的体积与性能开销# 1. 排除单个文件不插桩 UBSAN_SANITIZE_main.o : n # 2. 排除某个目录下的所有目标文件 UBSAN_SANITIZE : n # 3. 目录整体禁用后再单独恢复某个文件 UBSAN_SANITIZE_main.o : y这三种用法与 KASAN/KCSAN 等 sanitizer 的构建控制风格一致。其生效位置在 scripts/Makefile.lib#L77-L80构建系统根据目标文件拼接$(UBSAN_SANITIZE_$(target-stem).o)与$(UBSAN_SANITIZE)判断是否为可插桩的内核对象进而决定是否附加$(CFLAGS_UBSAN)整数回绕检测还额外受$(UBSAN_INTEGER_WRAP_...)/$(UBSAN_INTEGER_WRAP)控制。因此当你希望某个已知问题文件不被误报、或某个性能敏感目录不引入额外指令时可在该目录的 Makefile 中按上述语法书写。Trap 模式更小的内核、更简短的报告lib/Kconfig.ubsan 的UBSAN_TRAP帮助文本详细说明了取舍带 sanitizer 的内核因在错误路径上附带大量调试文本体积通常增加约 5%开启UBSAN_TRAP后插桩代码不再调用报告函数而是直接发出 trap 指令从而显著减小内核体积但代价是所有告警包括潜在无害条件都会变成中止当前内核代码执行的异常——无论当前上下文、锁持有情况如何可能使系统不稳定。同时要注意trap 模式下发生违规时内核将以illegal instruction错误直接 Oops且不再输出任何细节例外是 arm64 与 x86它们会报告是哪个 sanitizer 触发失败。这会给区分是否由 UBSAN 引起的 Oops以及定位具体违规细节带来困难也降低了内核日志对 bug 报告的价值。因此 trap 模式适合对体积敏感、且能接受违规即中止语义的构建场景日常调试仍推荐默认的报告模式。从 lib/ubsan.c#L22-L100 可以看到当CONFIG_UBSAN_TRAP或CONFIG_UBSAN_KVM_EL2开启时内核通过report_ubsan_failure()把 Clang 的 trap 码映射为可读字符串如UBSAN: array index out of bounds、UBSAN: shift out of bounds、UBSAN: divide/remainder overflow、UBSAN: unreachable code、UBSAN: loading invalid value、UBSAN: alignment assumption等这正是 arm64/x86 在 trap 模式下仍能给出提示的原因。且该函数只编译进实际启用的检查项以控制代码体积。非对齐访问检测UBSAN_ALIGNMENT 的单独开关非对齐访问的检测通过独立的CONFIG_UBSAN_ALIGNMENT控制。它在支持高效非对齐访问的架构即CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESSy上默认关闭因为这类架构上非对齐访问本身合法强行开启会产生大量 UBSAN 报告Kconfig 注释称之为false positives。如果确实需要仍可手动在配置中开启但需要做好日志洪泛的心理准备。此外该选项还依赖!UBSAN_TRAP !COMPILE_TEST——即不能与 trap 模式共存也不参与COMPILE_TEST场景。验证检测能力内核自带 UBSAN 测试模块内核在 lib/Kconfig.ubsan#L163-L168 提供了TEST_UBSAN测试模块tristate依赖m对应源码为 lib/test_ubsan.c。该模块主动触发各类未定义行为以验证对应检查项是否生效涵盖整数加减乘与取负溢出如test_ubsan_add_overflow()对INT_MAX执行val 2、test_ubsan_sub_overflow()对INT_MIN减 2、test_ubsan_mul_overflow()、test_ubsan_negate_overflow()依赖CONFIG_UBSAN_INTEGER_WRAP除零test_ubsan_divrem_overflow()以 0 为除数依赖CONFIG_UBSAN_DIV_ZERO隐式截断test_ubsan_truncate_signed()将LONG_MAX赋给int依赖CONFIG_UBSAN_INTEGER_WRAP移位越界test_ubsan_shift_out_of_bounds()覆盖负移位指数与左移溢出依赖CONFIG_UBSAN_SHIFT数组越界test_ubsan_out_of_bounds()以保护字节包围数组分别测试越界上方与下方访问依赖CONFIG_UBSAN_BOUNDS非法 bool/enum 值test_ubsan_load_invalid_value()向bool变量写入0xff、向枚举写入越界值依赖CONFIG_UBSAN_BOOL/CONFIG_UBSAN_ENUM。每个用例通过UBSAN_TEST(config, ...)宏在触发前打印当前配置是否开启方便对照预期。装上模块观察 dmesg 输出即可快速确认自己编译的内核各 UBSAN 检查项是否真正生效。报告抑制机制与实现要点lib/ubsan.c 中有两个值得注意的实现细节去重与递归抑制suppress_report()lib/ubsan.c#L131-L134会在两种情况跳过报告——当前任务正处于 UBSAN 报告流程中current-in_ubsan或该源码位置已被报告过was_reported()通过test_and_set_bit置位source_location-reported的REPORTED_BIT。前者防止报告自身触发 UBSAN 造成递归后者避免同一位置反复刷屏。类型描述符解析报告打印数值前内核通过type_descriptor中的type_info解码类型位宽与符号性type_is_signed()、type_bit_width()、get_signed_val()等并支持 128 位整数依赖CONFIG_ARCH_SUPPORTS_INT128的十六进制输出最终由handle_overflow()/__ubsan_handle_*系列函数以lhs rhs cannot be represented in type ...的格式呈现。这些处理函数__ubsan_handle_add_overflow、__ubsan_handle_sub_overflow、__ubsan_handle_mul_overflow、__ubsan_handle_negate_overflow、__ubsan_handle_implicit_conversion、__ubsan_handle_shift_out_of_bounds等通过EXPORT_SYMBOL导出是编译器插桩代码与内核报告逻辑之间的契约接口。使用建议与注意事项小结常规调试直接开启CONFIG_UBSANy保留默认的报告模式配合各子检查项按需裁剪用TEST_UBSAN模块快速验证。体积敏感构建可考虑CONFIG_UBSAN_TRAPy但要接受违规即 Oops、日志信息少的代价且它会使UBSAN_ALIGNMENT与UBSAN_UNREACHABLE等选项受限。范围控制利用UBSAN_SANITIZE/UBSAN_SANITIZE_xxx.o在 Makefile 中按文件、按目录精确排除或恢复插桩。与其它防御机制分工UBSAN 解决未定义行为检测问题数组函数族溢出防护请配合CONFIG_FORTIFY_SOURCEobjtool 的不可达代码检查与UBSAN_UNREACHABLE互斥。非对齐访问在支持高效非对齐访问的架构上保持CONFIG_UBSAN_ALIGNMENT默认关闭避免误报洪泛。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考