【问题标题】:Why am I able to perform floating point operations inside a Linux kernel module?为什么我能够在 Linux 内核模块中执行浮点运算?
【发布时间】:2013-03-30 19:42:18
【问题描述】:

我在 x86 CentOS 6.3(内核 v2.6.32)系统上运行。

我将以下函数编译到一个基本的字符驱动模块中作为一个实验,以了解 Linux 内核如何对浮点操作做出反应。

static unsigned floatstuff(void){
    float x = 3.14;
    x *= 2.5;
    return x;
}

...

printk(KERN_INFO "x: %u", x);

编译的代码(这不是预期的)所以我插入了模块并使用dmesg 检查了日志。日志显示:x: 7

这看起来很奇怪;我以为你不能在 Linux 内核中执行浮点运算——保存一些异常,例如 kernel_fpu_begin()。模块是如何进行浮点运算的?

这是因为我使用的是 x86 处理器吗?

【问题讨论】:

  • 为什么内核不能进行浮点运算?
  • 你为什么这么惊讶?毕竟,内核模块只是要由 CPU 执行的另一段代码。只要它可以执行你扔给它的操作码,你就可以了。
  • 另外,很可能在编译期间执行了算术运算,剩下的就是return 7;
  • 这里已经回答了这个问题:stackoverflow.com/questions/13886338/… 这个问题更像是一个不那么正确的陈述,答案是解释性的。你可以在内核中做 FP。
  • kernel_fpu_begin() / end 是不破坏用户空间 FPU 状态所必需的。 没有它,你可以在内核中进行 FP,但你会破坏 FPU 的状态当前进程。 Linux 会进行惰性 FPU 上下文保存,因为某些进程根本不使用 FPU 或 SSE 寄存器。 (不过,越来越多的进程确实使用 SSE。)

标签: c linux gcc linux-kernel x86


【解决方案1】:

我以为你不能在 Linux 内核中执行浮点运算

您不能安全地:无法使用kernel_fpu_begin() / kernel_fpu_end() 并不意味着FPU 指令会出错(至少在x86 上不会)。

相反,它会默默地破坏用户空间的 FPU 状态。这是不好的;不要那样做。

编译器不知道kernel_fpu_begin() 的含义,因此它无法检查/警告在 FPU 开始区域之外编译为 FPU 指令的代码。

可能存在一种调试模式,其中内核会在 kernel_fpu_begin / end 区域之外禁用 SSE、x87 和 MMX 指令,但这会更慢并且默认情况下不会这样做。

虽然有可能:设置 CR0::TS = 1 会使 x87 指令出错,因此可以进行惰性 FPU 上下文切换,并且还有其他位用于 SSE 和 AVX。


许多错误的内核代码会导致严重的问题。这只是众多之一。在 C 语言中,您几乎总是知道何时使用浮点数(除非拼写错误导致 1. 常量或实际编译的上下文中的某些内容)。


为什么 FP 架构状态不同于整数?

Linux 在任何时候进入/退出内核时都必须保存/恢复整数状态。所有代码都需要使用整数寄存器(除了以jmp 而不是ret 结尾的巨大的FPU 计算直线块(ret 修改rsp)。)

但内核代码通常会避免 FPU,因此 Linux 在系统调用进入时不保存 FPU 状态,仅在实际上下文切换到不同的用户空间进程或kernel_fpu_begin之前保存.否则,通常会返回到同一个内核上的同一个用户空间进程,因此不需要恢复 FPU 状态,因为内核没有触及它。 (如果内核任务确实修改了 FPU 状态,就会发生损坏。我认为这是双向的:用户空间也可能损坏 您的 FPU 状态。

整数状态相当小,只有 16 个 64 位寄存器 + RFLAGS 和段寄存器。即使没有 AVX,FPU 状态也是两倍多:8 个 80 位 x87 寄存器和 16 个 XMM 或 YMM,或 32 个 ZMM 寄存器(+ MXCSR 和 x87 状态 + 控制字)。 MPX bnd0-4 寄存器也与“FPU”集中在一起。此时“FPU 状态”仅表示所有非整数寄存器。在我的 Skylake 上,dmesgx86/fpu: Enabled xstate features 0x1f, context size is 960 bytes, using 'compacted' format.

Understanding FPU usage in linux kernel;现代 Linux 默认情况下不会为上下文切换执行惰性 FPU 上下文切换(仅用于内核/用户转换)。 (但那篇文章解释了 Lazy 是什么。)

大多数进程使用 SSE 在编译器生成的代码中复制/归零小块内存,并且大多数库 string/memcpy/memset 实现使用 SSE/SSE2。此外,硬件支持的优化保存/恢复现在是一件事(xsaveopt / xrstor),因此如果某些/所有 FP 寄存器实际上没有被使用,“急切”的 FPU 保存/恢复实际上可能会做更少的工作。例如如果用vzeroupper 清零,则只保存 YMM 寄存器的低 128b,这样 CPU 就知道它们是干净的。 (并在保存格式中仅用一位标记该事实。)

通过“急切”的上下文切换,FPU 指令始终保持启用状态,因此糟糕的内核代码可能随时损坏它们。

【讨论】:

【解决方案2】:

不要那样做!

在内核空间中,FPU 模式被禁用,原因如下:

  • 它允许 Linux 在没有 FPU 的架构中运行
  • 它避免了每次内核/用户空间转换都保存和恢复整个寄存器集(它可能会使上下文切换时间加倍)
  • 基本上所有内核函数都使用整数来表示十进制数 -> 您可能不需要浮点数
  • 在 Linux 中,当内核空间在 FPU 模式下运行时会禁用抢占
  • 浮点数是邪恶的,may generate very bad unexpected behaviour

如果您真的想使用 FP 编号(并且您不应该),您必须使用 kernel_fpu_beginkernel_fpu_end 原语以避免破坏用户空间寄存器,并且您应该考虑所有可能的问题 (包括安全性)在处理 FP 号码时。

【讨论】:

    【解决方案3】:

    操作系统内核可以简单地在内核模式下关闭 FPU。

    在 FPU 操作时,内核会在浮点运算时打开 FPU,然后关闭 FPU。

    但是你不能打印它。

    【讨论】:

    • 好答案:见this SO answer。它解释说,由于性能原因,FPU 可能会被禁用。
    【解决方案4】:

    不确定这种看法来自哪里。但是内核与用户模式代码在同一处理器上执行,因此可以访问相同的指令集。如果处理器可以进行浮点运算(直接或通过协处理器),内核也可以。

    也许您正在考虑在软件中模拟浮点运算的情况。但即便如此,它仍可在内核中使用(好吧,除非以某种方式禁用)。

    我很好奇,这种感知是从哪里来的?也许我错过了什么。

    Found this。似乎是一个很好的解释。

    【讨论】:

    • 也许我的问题有点误导。我知道 FPU 可以执行这些浮点指令(即机器代码本身与系统无关),但我对如何让我的 C 代码编译而没有关于未定义符号(例如 __fixunssfsi)的 GCC 错误感到困惑'正在编译内核模块。我怀疑这只是 GCC,取决于内核排除的库中的帮助程序,所以我该如何解决这个问题以便生成正确的机器代码——毕竟处理器支持浮点。
    • 让我补充一点,我知道浮点寄存器没有被保存;我并不特别在意这会破坏用户态程序,因为我只是在试验代码以更好地理解这种行为。
    • 我想通了;我需要将此编译器标志传递给 GCC:-mhard-float
    • 关键是浮点状态没有从内核内部正确保存和恢复,例如调度任务时(仅从应用程序内部的角度保存)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-10-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多