在共享库中处理信号的惯用方法

发布于 2025-02-10 08:30:01 字数 1646 浏览 1 评论 0 原文

我有一个共享库,偶尔通过设计扔 sigsegv 。我可以找出 sigsegv 是否是由我引起的,然后将其处理。但是,在实现其他分支时(即,当它不是我的 sigsegv )时,我遇到了一些问题。我的主要问题是,如果将处理程序设置为 sig_dfl ,该怎么办。这是我要制作通用的当前代码(因为目前仅支持一些信号,并且依赖于 linux 的默认行为,而不仅仅是任何POSIX):

void call_next_sighandler(struct sigaction* act, int signo, siginfo_t* info, void* context)
{
  if (act->sa_flags & SA_SIGINFO)
  {
    if (act->sa_sigaction)
    {
      act->sa_sigaction(signo, info, context);
    }
  }
  else
  {
    if (act->sa_handler == SIG_IGN)
    {
      return;
    }
    else if (act->sa_handler == SIG_DFL)
    {
      // we only support a few signals, all of which just dump core:
      //       SIGFPE       P1990      Core    Floating-point exception
      //       SIGSEGV      P1990      Core    Invalid memory reference
      //       SIGTRAP      P2001      Core    Trace/breakpoint trap
      //
      // Therefore we just unregister ourselves and let the process crash

      sigaction(signo, act, nullptr);
      return;
    }
    else
    {
      act->sa_handler(signo);
    }
  }
}

struct sigaction old_sigsegv;
void handle_sigsegv(int signo, siginfo_t* info, void* context)
{
  if (is_my_sigsegv(context))
    handle_my_sigsegv(context);
  else
    call_next_sighandler(&old_sigsegv, signo, info, context);
}

我遇到的另一个问题是如何我将旧信号处理程序存储在自己的模块中。如果在我之后加载了另一个模块,并且他们也决定处理信号会发生什么?他们只需将我的信号处理程序存储在其模块和链条中。但是,这意味着当我卸载时,他们的信号处理程序将调用无效的内存。或作为替代方案,如果我将收到的旧处理程序注册,则删除了新模块的处理程序。我唯一能提出的解决方案是分配我卸载时不会消失的可执行内存,但是真的没有更好的方法吗?

I have a shared library that occasionally throws SIGSEGV by design. I can find out if a SIGSEGV is caused by me, and if it is then handle it. However I ran into some problems when implementing the other branch (ie. when it isn't my SIGSEGV). My primary problem is what if a handler was set to SIG_DFL. This is my current code which I want to make generic (as it currently only supports a few signals, and relies on the default behaviors of Linux, not just any POSIX):

void call_next_sighandler(struct sigaction* act, int signo, siginfo_t* info, void* context)
{
  if (act->sa_flags & SA_SIGINFO)
  {
    if (act->sa_sigaction)
    {
      act->sa_sigaction(signo, info, context);
    }
  }
  else
  {
    if (act->sa_handler == SIG_IGN)
    {
      return;
    }
    else if (act->sa_handler == SIG_DFL)
    {
      // we only support a few signals, all of which just dump core:
      //       SIGFPE       P1990      Core    Floating-point exception
      //       SIGSEGV      P1990      Core    Invalid memory reference
      //       SIGTRAP      P2001      Core    Trace/breakpoint trap
      //
      // Therefore we just unregister ourselves and let the process crash

      sigaction(signo, act, nullptr);
      return;
    }
    else
    {
      act->sa_handler(signo);
    }
  }
}

struct sigaction old_sigsegv;
void handle_sigsegv(int signo, siginfo_t* info, void* context)
{
  if (is_my_sigsegv(context))
    handle_my_sigsegv(context);
  else
    call_next_sighandler(&old_sigsegv, signo, info, context);
}

Another problem I ran into is how I store the old signal handler in my own module. What happens if another module is loaded after me, and they also decide to handle signals? They will simply store my signal handler in their module and chain to that. However that means that when I'm unloaded, their signal handler will call invalid memory. Or as an alternative if I register back the handler that I received as old, then I remove the new module's handler. The only solution I could come up with is allocating out-of-module executable memory that doesn't go away when I'm unloaded, but is there really no better way?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

葬花如无物 2025-02-17 08:30:01

在审查了标准和其他人的实施之后,我决定进行自我援助。

最合适的图书馆解决方案

根本不会处理信号注册混乱。只需揭露库用户必须调用的“信号处理程序”,然后返回是否处理信号。信号处理程序是过程 - 全球的,因此可以将其视为主要可执行文件的资源。图书馆不应该自己处理他人的资源。虽然这可能会给使用您的库的人带来一些头痛,但最终是最灵活的解决方案。

我最终获得了一个相当简单的功能原型:

LIB_EXPORT int lib_handle_signal(int signo, siginfo_t* info, void* context);

并记录了用户必须在几个信号上调用它。

由于我的库的主要用户是C#可执行文件(由于信号安全功能的限制,您无法编写信号处理程序),因此我仍然必须处理这个问题,除了在单独的库中 相当被认为是主要可执行文件的“一部分”。

默认操作

POSIX信号的默认操作为。对于异常或正常的终止处理程序而言,简单地避开自己并让过程崩溃是一个适当的解决方案,而默认被忽略的解决方案可以简单地忽略。

链接和卸载

解决此问题的最简单方法绝不会卸载。虽然我还没有找到真正的POSIX解决方案,但是有一个简单的方法可以在大多数Unices上起作用:

static void make_permanently_loaded()
{
  static char a_variable_in_the_module;
  // this is not POSIX but most BSDs, Linux and Mac have it
  Dl_info dl_info;
  memset(&dl_info, 0, sizeof(dl_info));
  int res = dladdr(&a_variable_in_the_module, &dl_info);
  assert(res && dl_info.dli_fname);
  // Leak a reference to ourselves
  void* me = dlopen(dl_info.dli_fname, RTLD_NOW | RTLD_NODELETE);
  assert(me);
}

其他实施方式,

虽然我在这里并没有真正发现类似的问题,因此,有一些实现遇到的问题与我做到了,并尽了最大的努力来处理它们。

libsigsegv

libsigsegv简单>和甚至不会尝试将其链接到以前注册的任何内容:

sigaction (sig, &action, (struct sigaction *) NULL);

它也无法处理卸载,如果您将其卸载,则您的过程将中止,然后引起Sigsegv,即使加载之前,您已注册了Sigsegv处理程序。

它可以通过不注册自身并让信号再次发生,从而处理与我相似的未手电信号,这将导致正常或异常的终止。

openjdk/hotspot

java带来 libjsig 挂钩信号 sigaction 。当JRE安装信号处理程序时,Libjsig会退回旧的。当其他人正在安装信号处理程序以信号表明JRE先前安装的信号时,它只会保存新的(并返回以前的旧旧产品)。预计JVM将实施实际的链接,旧处理程序只需要从Libjsig查询。这种方法具有可堆叠的优点 - 可以加载多个不同版本的libjsig,它们将起作用。但是,不幸的是,库的单个副本只能由JRE(或类似)的单个副本使用,因此,如果您不确定没有人会尝试将JRE加载到相同的过程。但是,您可以为您的目的“分叉”它,并简单地将其重命名为IT副本,从而使在同一过程中的JRE旁边加载安全。

热点JVM实现包含信号处理并实际打电话(链接)libjsig保存的处理程序。不幸的是,默认的操作处理分支未能实现,因为它决定将所有意外信号作为意外的信号投掷。但是,蒙版处理代码对于其他任何实施链接的人都非常有用。

libjsig无法解决卸载问题 - 预计库将永远不会卸载。您可以从答案的早期部分添加反卸载代码,以确保是这种情况。

CLR

我没有深入审查它,因为它具有最复杂的操作。

clr Windows异常处理模型)在POSIX信号的顶部以及类似于JRE的单个链接。可以为您的代码范围注册您自己的SEH放松和异常处理信息,因此,如果您不介意提取CLR依赖性,那么这可能值得研究。

结构化异常处理是Windows上的标准异常处理方法,该方法指定了一种放松的信息格式。当收到硬件异常时,堆栈将根据所提供的信息解开,并调用与每个返回地址的代码范围相关的语言处理程序,他们可能会决定处理是否处理异常。这意味着异常(信号)是属于任何代码的“资源”(只要较低的框架不会因编写不正确的过滤功能而偶然地捕获它),与 *NIX的process-global方式不同。 我个人认为这是一种更明智的方法。

After reviewing standards and others' implementations, I decided to do a self-answer.

Most suitable solution for libraries

Simply don't deal with the signal registration mess. Just expose a "signal handler" that must be called by the user of the library, and returns whether a signal was handled or not. Signal handlers are process-global, so they can be considered as a resource of the main executable. Libraries shouldn't deal with others' resources on their own. While this might cause some headaches to whoever is using your library, it is ultimately the most flexible solution.

I ended up with a rather simple function prototype:

LIB_EXPORT int lib_handle_signal(int signo, siginfo_t* info, void* context);

And documented that the user must call it on several signals.

Actual answers to the two concerns

Since my library's primary user is a C# executable (in which you can't write signal handlers due to the restriction to signal-safe functions) I still had to deal with the issue, except in a separate library that is rather considered to be "part of" the main executable.

Default action

The default actions for POSIX signals are actually specified in POSIX. For abnormal or normal termination handlers simply unregistering ourselves and letting the process crash is an appropriate solution, while the default ignored ones can be simply ignored.

Chaining and unloading

The simplest way to solve this issue is simply never unloading. While I haven't found a truly POSIX solution to this, there is a simple one that works on most Unices:

static void make_permanently_loaded()
{
  static char a_variable_in_the_module;
  // this is not POSIX but most BSDs, Linux and Mac have it
  Dl_info dl_info;
  memset(&dl_info, 0, sizeof(dl_info));
  int res = dladdr(&a_variable_in_the_module, &dl_info);
  assert(res && dl_info.dli_fname);
  // Leak a reference to ourselves
  void* me = dlopen(dl_info.dli_fname, RTLD_NOW | RTLD_NODELETE);
  assert(me);
}

Other implementations

While I haven't really found similar problems here on SO, there are a few implementations that encountered the same problems as I did, and tried their best at handling them.

libsigsegv

libsigsegv simply discards the previous handlers and doesn't even attempt chaining to whatever was registered before:

sigaction (sig, &action, (struct sigaction *) NULL);

It also does not handle unloading, your process will abort if you unload it then cause a SIGSEGV, even if prior to loading you had a SIGSEGV handler registered.

It handles unhandled signals similar to me in the question, by unregistering itself and letting the signal happen again which will result in normal or abnormal termination.

OpenJDK / Hotspot

Java brings libjsig which hooks signal and sigaction. When the JRE is installing signal handlers, libjsig backs up the old ones. When someone else is installing signal handlers to signals that the JRE installed prior, it simply saves them new ones (and returns the previous old one). The JVM is expected to implement the actual chaining, the old handlers are only to be queried from libjsig. This approach has the advantage of being stackable - multiple different versions of libjsig may be loaded and they will work. However unfortunately a single copy of the library can only be used by a single copy of a JRE (or similar), so as a library implementer you can't use it if you aren't sure that no one will attempt loading a JRE into the same process. However you can "fork" it and simply make a renamed copy of it for your purposes, making it safe to load next to a JRE in the same process.

The Hotspot JVM implementation contains signal handling and actually calling (chaining) the handlers saved by libjsig. Unfortunately the default action handling branch is not implemented as it instead decides to throw all unexpected signals as an UnexpectedException. However, the mask handling code is very useful for anyone else implementing chaining.

The unloading problem is not solved by libjsig - it is expected that the library will never be unloaded. You can add the anti-unloading code from the earlier part of the answer to make sure this is the case.

CLR

I did not review this in depth because it has the most complicated handling.

The CLR implements SEH exceptions (the Windows exception handling model) on top of POSIX signals, and a single level of chaining similar to the JRE. It might be possible to register your own SEH unwinding and exception handling information for your ranges of code, so if you don't mind pulling in a CLR dependency, this might be worth looking into.

Structured Exception Handling is the standard exception handling method on Windows, which specifies an unwinding information format. When a hardware exception is received, the stack is unwound based on the provided information, language specific handlers associated to the code ranges of every return address are invoked, and they may decide to handle an exception or not. This means exceptions (signals) are "resources" belonging to whatever code causes them (as long as a lower frame doesn't accidentally catch it due to a badly written filter function), unlike the *nix way where they're process-global. In my personal opinion this is a much more sensible approach.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文