Linux 内核崩溃消息中的“代码”是啥?

Posted

技术标签:

【中文标题】Linux 内核崩溃消息中的“代码”是啥?【英文标题】:What is "Code" in Linux Kernel crash messages?Linux 内核崩溃消息中的“代码”是什么? 【发布时间】:2019-12-03 23:48:25 【问题描述】:

Linux内核加载失败后我有以下堆栈跟踪和崩溃信息:

[    3.684670] ------------[ cut here ]------------
[    3.695507] Bad FPU state detected at fpu__clear+0x91/0xc2, reinitializing FPU registers.
[    3.695508] traps: No user code available.
[    3.704745] invalid opcode: 0000 [#1] PREEMPT
[    3.715304] CPU: 0 PID: 1 Comm: swapper Not tainted 4.19.50-android-x86-geeb7e76-dirty #1
[    3.724594] Hardware name: AAEON UP-APL01/UP-APL01, Bios UPA1AM21 09/01/2017
[    3.732622] EIP: ex_handler_fprestore+0x2e/0x65
[    3.737807] Code: 00 55 89 e5 57 8b 48 04 8d 44 08 04 89 42 30 80 3d e7 fb a0 c1 00 75 16 c6 05 e7 fb a0 c1 01 50 68 b4 38 87 c1 e8 98 ba 00 00 <0f> 0b 58 5a 90 8d 74 26 00 eb f
[    3.759027] EAX: 0000004d EBX: c103d6f9 ECX: c19a2a48 EDX: c19a2a48
[    3.766169] ESI: df4c7e04 EDI: 00000006 EBP: df4c7c6c ESP: df4c7c60
[    3.773316] DS: 007b ES: 007b FS: 0000 GS: 00e0 SS: 0068 EFLAGS: 00010292
[    3.781044] CR0: 80050033 CR2: c168c6b4 CR3: 1e902000 CR4: 001406d0
[    3.788184] Call Trace:
[    3.791026]  ? fpu__clear+0x91/0xc2
[    3.795037]  fixup_exception+0x61/0x6e
[    3.799348]  do_trap+0x35/0xe9
[    3.802864]  do_invalid_op+0xd9f/0x108a
[    3.807269]  ? atime_needs_update+0x68/0xf5
[    3.812058]  ? touch_atime+0x37/0xbd
[    3.816168]  ? __check_object_size+0x83/0x123
[    3.821153]  ? fpu__clear+0x8e/0xc2
[    3.825166]  ? generic_file_read_iter+0x28d/0x723
[    3.830544]  ? generic_file_read_iter+0x28d/0x723
[    3.835931]  ? __vfs_read+0xe9/0x11f
[    3.840043]  common_exception+0x105/0x10e
[    3.844634] EIP: fpu__clear+0x91/0xc2
[    3.848840] Code: eb 05 e8 b4 f2 fd ff ff 0d 98 a8 99 c1 74 3b 90 8d 74 26 00 eb 07 90 8d 74 26 00 eb 1c 83 c8 ff bf c0 8c a2 c1 89 c2 0f c7 1f <a1> f4 8b a2 c1 ff 0d 98 a8 99 1
[    3.870070] EAX: ffffffff EBX: df4c5900 ECX: 00000000 EDX: ffffffff
[    3.877210] ESI: df4c5900 EDI: c1a28cc0 EBP: df4c7e4c ESP: df4c7e40
[    3.884356] DS: 007b ES: 007b FS: 0000 GS: 00e0 SS: 0068 EFLAGS: 00010286
[    3.892085]  ? do_alignment_check+0x1a/0x1a
[    3.896878]  ? common_exception+0x105/0x10e
[    3.901674]  flush_thread+0x33/0x37
[    3.905684]  flush_old_exec+0x540/0x5f9
[    3.910085]  load_elf_binary+0x24b/0xec1
[    3.914584]  ? pick_next_task_fair+0xdf/0x13a
[    3.919575]  ? __schedule+0x4bb/0x63f
[    3.923780]  ? sched_debug_header+0x45/0x40a
[    3.928666]  ? preempt_schedule+0x2d/0x3c
[    3.933266]  search_binary_handler+0x89/0x1ac
[    3.938259]  load_script+0x184/0x19f
[    3.942366]  search_binary_handler+0x89/0x1ac
[    3.947354]  __do_execve_file+0x454/0x668
[    3.951954]  do_execve+0x1b/0x1d
[    3.955673]  run_init_process+0x31/0x36
[    3.960082]  ? rest_init+0x99/0x99
[    3.963992]  kernel_init+0x5e/0xdf
[    3.967905]  ret_from_fork+0x19/0x30
[    3.972014] Modules linked in:
[    3.975542] ---[ end trace 7d27fceeb3852a38 ]---
[    3.980823] EIP: ex_handler_fprestore+0x2e/0x65
[    3.986014] Code: 00 55 89 e5 57 8b 48 04 8d 44 08 04 89 42 30 80 3d e7 fb a0 c1 00 75 16 c6 05 e7 fb a0 c1 01 50 68 b4 38 87 c1 e8 98 ba 00 00 <0f> 0b 58 5a 90 8d 74 26 00 eb f
[    4.007247] EAX: 0000004d EBX: c103d6f9 ECX: c19a2a48 EDX: c19a2a48
[    4.014387] ESI: df4c7e04 EDI: 00000006 EBP: df4c7c6c ESP: c1afa3b0
[    4.021536] DS: 007b ES: 007b FS: 0000 GS: 00e0 SS: 0068 EFLAGS: 00010292
[    4.029265] CR0: 80050033 CR2: c168c6b4 CR3: 1e902000 CR4: 001406d0
[    4.036413] note: swapper[1] exited with preempt_count 1

Code 是什么意思?我还能知道导致内核崩溃的确切 x86 指令(不是 C 函数)吗?

编辑:更新了代码。我试图在虚拟化环境中运行 Linux。

【问题讨论】:

指令&lt;0f&gt; 0b是导致异常的指令。那就是UD2 指令。这通常表明代码不应该到达那个位置(可能检测到某种未定义的行为?很可能是它之前的 call 指令(字节 e8 98 ba 00 00)不打算返回,但它确实返回了. 没有看到这个功能很难说。 你是否碰巧在模拟器(如 KVM/QEMU)中运行它? 感谢您的帮助!是的,我们在虚拟化环境中运行。我已经更新了原来的帖子。因此,可能是其他原因导致了这条非法指令。 @PeterCordes :我实际上询问了虚拟化环境,因为在某些版本的 Linux 和某些虚拟机(包括 KVM)上存在一些已知的与 FPU 相关的错误。我怀疑这可能与实际崩溃的问题有关。我没有从调试的角度询问虚拟化环境。我怀疑这最终可能是一个 XY 问题,因为 OP 在评论中建议 我最初不明白 Linux 为何崩溃。 阅读我的 cmets 到彼得的答案。 【参考方案1】:

Code 是 x86 机器代码的十六进制转储(可能是传统 32 位内核的 32 位模式,因为它只转储 32 位寄存器内容)。

标有&lt;&gt;的字节是EIP指向的地方,所以是ex_handler_fprestore里面的错误指令

将其提供给反汇编程序,例如https://defuse.ca/online-x86-assembler.htm#disassembly2,或者Linux的crashdump解码脚本https://elixir.bootlin.com/linux/latest/source/scripts/decodecode


请记住,x86 机器代码使用无法明确向后解码的可变长度编码。但这是编译器生成的代码,所以至少我们可以假设不应该有重叠的指令或与代码混合的静态数据(因为 x86 对此没有好处)。如果我们在编译器生成的代码中找到函数的开头,那么其余的指令都是“正常的”。

00 字节看起来像是先前指令的一部分或函数之间的填充:从那里解码会给我们add BYTE PTR [ebp-0x77],dl,这是合理的,in eax,0x57 之后不是,对于非驱动程序函数。

0x89 字节更有可能是 MOV 指令的操作码。

如果我们删除00 字节并从55(即push ebp)开始,我们会得到一个正常的函数体,包括您期望的堆栈帧设置序言,如果使用-Os-fno-omit-frame-pointer 编译。

一般来说,您可以一次删除一个字节,直到您获得看起来正常的解码,该解码至少在错误指令上有一个指令边界。 (但“看起来很理智”需要一些经验;反汇编可能在开始错误后偶然同步。这对于 x86 机器代码并不少见。)

# skipped the 00 byte which would desync decoding
0:  55                      push   ebp
1:  89 e5                   mov    ebp,esp
3:  57                      push   edi
4:  8b 48 04                mov    ecx,DWORD PTR [eax+0x4]      # EAX = 1st function arg, ECX = tmp
7:  8d 44 08 04             lea    eax,[eax+ecx*1+0x4]
b:  89 42 30                mov    DWORD PTR [edx+0x30],eax     # EDX = 2rd function arg
e:  80 3d e7 fb a0 c1 00    cmp    BYTE PTR ds:0xc1a0fbe7,0x0
15: 75 16                   jne    0x2d
17: c6 05 e7 fb a0 c1 01    mov    BYTE PTR ds:0xc1a0fbe7,0x1
1e: 50                      push   eax
1f: 68 b4 38 87 c1          push   0xc18738b4
24: e8 98 ba 00 00          call   0xbac1
29: 0f 0b                   ud2                     ### <=== EIP points here

# stuff after this probably isn't real code; it's unreachable
2b: 58                      pop    eax
2c: 5a                      pop    edx
2d: 90                      nop
2e: 8d 74 26 00             lea    esi,[esi+eiz*1+0x0]
32: eb                      .byte 0xeb

所以这个函数实际上以调用带有堆栈参数的noreturn 函数结束。 (32 位 x86 Linux 内核是用 -mregparm=3 构建的,所以前 3 个参数按 EAX、EDX、ECX 的顺序排列,所以这个函数不是 regparm 或者它有超过 3 个参数。你可以看到这个函数使用EAX 和 EDX 作为传入参数:在写入之前读取它们。)

但由于某种原因,它不是jmp 尾声;也许对于异常回溯,它希望这个函数的堆栈帧在堆栈上。 (这可以解释push ebp / mov ebp,esp 即使这个内核是用-fomit-frame-pointer 作为-O2 的一部分构建的。)

您必须查看 ex_handler_fprestore 的 C 源代码才能猜出原因。

ud2 is an illegal instruction。编译器(或内联汇编?)把它放在那里,所以如果函数返回它会出错。这是一个明显的迹象,表明该执行路径应该是无法访问的,或者被标记为故意陷阱为assert() 类型的机制。 (在 Linux 中,查找 BUG_ON())。

【讨论】:

@PeterCordes 感谢您的解释。它帮助我理解了我们应该如何去寻找错误的指令。我已经更新了原始帖子,因为我最初不明白 Linux 崩溃的原因。 @0andriy:谢谢,很好的建议。我通常不调试内核崩溃转储,所以我只是出于好奇而接近这个问题,而不是拥有基线汇编器/反汇编器之外的现有工具的经验。 啊,忘了说ud2在99.9%的情况下意味着在代码中寻找BUG_ON() @0andriy:谢谢,不确定BUG_ON() 是否跳转到更详细的错误报告,或者只是ud2。另请注意,如果您使用 clang 或某些现代 GCC 版本进行编译,则某些类型的 C 未定义行为可能会编译为 ud2(而不是通过始终导致某种类型的函数执行正常的代码生成以获取执行路径编译器不想处理的UB。) 原来是XSAVES/XRSTORS 导致了UD 异常,因为虚拟化环境不支持这些功能。感谢您的帮助!

以上是关于Linux 内核崩溃消息中的“代码”是啥?的主要内容,如果未能解决你的问题,请参考以下文章

FAISS 搜索失败并出现模糊错误:“非法指令”或内核崩溃

如何调试 Linux 内核模块 `init()` 中的问题?

drawRect 中的崩溃 - 是啥原因导致的?

Linux内核:分析coredump文件 - 内核代码崩溃

本地patch是啥意思

Linux中的零拷贝技术