为了在MacOS上捕获浮点异常,我使用了一个提供feenableexcept
功能的扩展。
http://www-personal.umich.edu/~williams/archive/computation/fe-handling-example.c
注意:如果您看到这篇文章是为了了解如何在MacOS(Intel或Apple芯片)上捕获浮点异常,您可能需要跳过汇编讨论,直接阅读下面的详细信息。
现在我想更新这个苹果芯片的扩展,并可能删除一些过时的代码。通过fenv.h
的挖掘,很清楚如何更新苹果芯片的例程feenableexcept
,fegetexcept
和fedisableexcept
。但是,不太清楚如何处理2009年扩展中提供的汇编代码,或者为什么包括这些代码。
上面链接中提供的扩展相当长,所以我只提取涉及程序集的片段:
#if DEFINED_INTEL
// x87 fpu
#define getx87cr(x) __asm ("fnstcw %0" : "=m" (x));
#define setx87cr(x) __asm ("fldcw %0" : "=m" (x));
#define getx87sr(x) __asm ("fnstsw %0" : "=m" (x));
// SIMD, gcc with Intel Core 2 Duo uses SSE2(4)
#define getmxcsr(x) __asm ("stmxcsr %0" : "=m" (x));
#define setmxcsr(x) __asm ("ldmxcsr %0" : "=m" (x));
#endif // DEFINED_INTEL
此代码用于sigaction
机制的处理程序中,该机制用于报告捕获的浮点异常的类型。
fhdl ( int sig, siginfo_t *sip, ucontext_t *scp )
{
int fe_code = sip->si_code;
unsigned int excepts = fetestexcept (FE_ALL_EXCEPT);
/* ... see complete code in link above ... */
if ( sig == SIGFPE )
{
#if DEFINED_INTEL
unsigned short x87cr,x87sr;
unsigned int mxcsr;
getx87cr (x87cr);
getx87sr (x87sr);
getmxcsr (mxcsr);
printf ("X87CR: 0x%04X\n", x87cr);
printf ("X87SR: 0x%04X\n", x87sr);
printf ("MXCSR: 0x%08X\n", mxcsr);
#endif
// ....
}
printf ("signal: SIGFPE with code %s\n", fe_code_name[fe_code]);
printf ("invalid flag: 0x%04X\n", excepts & FE_INVALID);
printf ("divByZero flag: 0x%04X\n", excepts & FE_DIVBYZERO);
}
else printf ("Signal is not SIGFPE, it's %i.\n", sig);
abort();
}
提供了一个捕获异常并通过sigaction
处理它们的示例。对feenableexcept
的调用将是定义了feenableexcept
的系统(例如,非Apple硬件)的本机实现,或者是上面链接的扩展中提供的实现。
int main (int argc, char **argv)
{
double s;
struct sigaction act;
act.sa_sigaction = (void(*))fhdl;
sigemptyset (&act.sa_mask);
act.sa_flags = SA_SIGINFO;
// printf ("Old divByZero exception: 0x%08X\n", feenableexcept (FE_DIVBYZERO));
printf ("Old invalid exception: 0x%08X\n", feenableexcept (FE_INVALID));
printf ("New fp exception: 0x%08X\n", fegetexcept ());
// set handler
if (sigaction(SIGFPE, &act, (struct sigaction *)0) != 0)
{
perror("Yikes");
exit(-1);
}
// s = 1.0 / 0.0; // FE_DIVBYZERO
s = 0.0 / 0.0; // FE_INVALID
return 0;
}
当我在基于Intel的Mac上运行这个程序时,我得到:
Old invalid exception: 0x0000003F
New fp exception: 0x0000003E
X87CR: 0x037F
X87SR: 0x0000
MXCSR: 0x00001F80
signal: SIGFPE with code FPE_FLTINV
invalid flag: 0x0000
divByZero flag: 0x0000
Abort trap: 6
我的问题是:
- 为什么汇编代码和对
fetestexcept
的调用都包含在处理程序中?报告被捕获的异常类型是否都有必要? - 处理程序捕获了一个
FE_INVALID
异常。为什么,excepts & FE_INVALID
是零? sigaction
处理程序在苹果芯片上完全被忽略了。它应该工作吗?或者我不明白使用sigaction
的信号处理工作原理的一些更基本的东西,而不是引发FP异常时会发生什么?
我用gcc和clang编译。
- 详情**:下面是一个从原始代码中提取的最小示例,它提炼了我上面的问题。在这个示例中,我提供了Intel或Apple芯片上MacOS缺少的
feeableexcept
功能。然后我测试了使用和不使用sigaction
的情况。
- 详情**:下面是一个从原始代码中提取的最小示例,它提炼了我上面的问题。在这个示例中,我提供了Intel或Apple芯片上MacOS缺少的
#include <fenv.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#if defined(__APPLE__)
#if defined(__arm) || defined(__arm64) || defined(__aarch64__)
#define DEFINED_ARM 1
#define FE_EXCEPT_SHIFT 8
#endif
void feenableexcept(unsigned int excepts)
{
fenv_t env;
fegetenv(&env);
#if (DEFINED_ARM==1)
env.__fpcr = env.__fpcr | (excepts << FE_EXCEPT_SHIFT);
#else
/* assume Intel */
env.__control = env.__control & ~excepts;
env.__mxcsr = env.__mxcsr & ~(excepts << 7);
#endif
fesetenv(&env);
}
#else
/* Linux may or may not have feenableexcept. */
#endif
static void
fhdl ( int sig, siginfo_t *sip, ucontext_t *scp )
{
int fe_code = sip->si_code;
unsigned int excepts = fetestexcept (FE_ALL_EXCEPT);
if (fe_code == FPE_FLTDIV)
printf("In signal handler : Division by zero. Flag is : 0x%04X\n", excepts & FE_DIVBYZERO);
abort();
}
void main()
{
#ifdef HANDLE_SIGNAL
struct sigaction act;
act.sa_sigaction = (void(*))fhdl;
sigemptyset (&act.sa_mask);
act.sa_flags = SA_SIGINFO;
sigaction(SIGFPE, &act, NULL);
#endif
feenableexcept(FE_DIVBYZERO);
double x = 0;
double y = 1/x;
}
- 无签名的结果**
在英特尔上:
% gcc -o stack_except stack_except.c
% stack_except
Floating point exception: 8
在苹果芯片上:
% gcc -o stack_except stack_except.c
% stack_except
Illegal instruction: 4
以上操作按预期工作,当遇到被零除时代码终止。
- 带sigaction的结果**
英特尔结果:
% gcc -o stack_signal stack_signal.c -DHANDLE_SIGNAL
% stack_signal
In signal handler : Division by zero. Flag is : 0x0000
Abort trap: 6
代码在英特尔上按预期工作。但是,
- 从
fetestexcept
(从信号处理程序调用)返回的值为零。为什么会这样?异常在被处理程序处理之前被清除了吗?
Apple芯片的结果:
% gcc -o stack_signal stack_signal.c -DHANDLE_SIGNAL
% stack_signal
Illegal instruction: 4
信号处理程序被完全忽略了。这是为什么?我是不是错过了一些关于信号如何处理的基本知识?
- 在原始代码中使用程序集(请参见帖子顶部的链接)**
我的最后一个问题是关于文章顶部的原始示例中汇编的使用。为什么汇编用于查询信号处理程序中的标志?使用fetestexcept
还不够吗?或者检查siginfo.si_code
?* 可能的答案:fetestexcept
,当在处理程序内部使用时,不会检测异常(?)。(这就是为什么只从处理程序内部打印0x0000
的原因吗?)*
这里有一个类似问题的相关帖子。如何在M1 Mac上捕获浮点异常?
2条答案
按热度按时间7dl7o3gd1#
事实证明,AArch 64上的MacOS将为未屏蔽的FP异常提供SIGILL,而不是SIGFPE。How to trap floating-point exceptions on M1 Macs?显示了一个示例,包括如何取消屏蔽特定的FP异常,并且是AArch 64上的实际目标的复制品。我不知道为什么MacOS会忽略POSIX标准,并为算术异常提供不同的信号)。
答案的其余部分只涉及x86 asm部分。
我想您还需要了解POSIX信号(如
SIGSEGV
或SIGFPE
)、硬件异常(如页面错误或x86#DE
整数除法异常)与“fp异常”(在FPU状态寄存器中设置标志的事件,或者如果未屏蔽则被视为CPU异常,则捕获以运行内核代码)之间的区别。如果FP异常没有被屏蔽,则意味着FP数学指令可以trap(将执行发送到内核,而不是继续执行下一条用户空间指令),操作系统的trap处理程序决定发送一个POSIX信号(或者修复页面错误本身的问题,然后返回用户空间重新运行出错的指令,也就是被捕获的指令)。
如果FP异常被屏蔽了,它们就不会导致CPU异常(陷阱),所以你只能用
fetestexcept
从同一个线程检查它们。除非AArch 64也像x86那样有两个独立的FP异常掩码/状态(x87和SSE),否则我看不出有什么理由需要内联asm。
fenv.h
函数应该可以工作。遗憾的是,ISO C没有提供一种真正 * 取消屏蔽 * 异常的方法,只有
fetestexcept(FE_DIVBYZERO)
等来检查FP-exception状态下的状态标志(如果任何操作引发了它们,则自上次清除以来,这些标志保持设置)。https://en.cppreference.com/w/c/numeric/fenv/fetestexcept但是MacOS
fenv.h
确实有一些常量可以在FP环境中使用fegetenv
/fesetenv
设置FP异常掩码位,这是GNU Cfeenableexcept
的替代方法。x86上的Asm /intrinsic非常有用,因为它有两个独立的FP系统,即传统的x87和现代的SSE/AVX。
fetestexcept
将测试x87或SSE异常,具体取决于编译器默认用于FP数学的异常。(SSE 2用于x86-64,除了使用x87的long double...)因此有理由同时检查这两种异常,以确保它与fetestexcept
匹配。此外,x87状态字具有精度控制位(使其始终舍入到与双精度或浮点数相同的尾数精度,而不是舍入到完整的80位),并且MXCSR具有DAZ / FTZ(非规格化为零/刷新为零),以禁用逐渐下溢,因为如果发生这种情况,速度会很慢。
fenv
不提供这种功能。x86内联asm非常幼稚,而且很糟糕
如果您确实需要这些x87操作的 Package 器,请到其他地方仔细寻找。
#define setx87cr(x) __asm ("fldcw %0" : "=m" (x));
是超级坏的。它告诉编译器x是一个纯输出(由asm模板编写),但实际上运行了一个从它读取的asm指令。我预计它会在除调试构建之外的任何情况下坏(因为死存储消除)。ldmxcsr
Package 器也是如此,它甚至更没用,因为#include <immintrin.h>
有_mm_setcsr
它们都必须是
asm volatile
,否则它们就被认为是输入的纯函数,所以如果没有输入只有一个输出,编译器就可以假设它总是写相同的输出并进行相应的优化,所以如果你想多次读取status来检查一系列计算之后的新异常,编译器可能只会重用第一个结果。(With只有输入操作数而不是输出操作数,
fldcw
的正确 Package 器将隐含地是volatile的)。另一个复杂的问题是编译器可能会选择早于或晚于预期执行FP操作,一种解决方法是使用FP值作为输入,如
asm volatile("fnstsw %0" : "=am"(sw) : "g"(fpval) )
。(我还使用"a"
作为可能的输出之一,因为有一种形式的指令会写入AX而不是内存(当然,您需要它是uint16_t
或short
)。或者使用
"+g"(fpval)
读+写“输出”操作数来告诉编译器它读/写fpval,所以这必须在使用它的某些计算之前发生。在这个答案中,我不打算尝试完全正确的版本,但这正是我们要寻找的。
我最初猜测
s = 0.0 / 0.0;
可能不会编译为带有clang的AArch 64除法指令。如果不使用类似下面的语句,您可能只会得到一个编译时常量NaN,并优化掉一个未使用的结果您可以检查编译器的asm输出,以确保存在实际的FP除法指令。
顺便说一句,ARM和AArch 64不会在整数除以0时陷入陷阱(不像x86),但如果FP异常没有屏蔽,希望FP操作可以。但如果这仍然不起作用,那么是时候阅读asm手册并查看编译器asm输出了。
xqkwcwgp2#
GCC在gfortran/config中有fpu-aarch64.h头文件,它实现了在Apple M上处理FP异常所需的一切。