重定向断言失败消息

Redirecting assert fail messages

我们有一个软件项目具有 real-time 约束,主要用 C++ 编写,但使用了一些 C 库,运行 在 POSIX 操作系统中。为满足 real-time 约束,我们已将几乎所有从 stderr 管道注销的文本移至共享内存环形缓冲区。

我们现在遇到的问题是,当旧代码或 C 库调用 assert 时,消息最终出现在 stderr 中,而不是与其余日志一起出现在我们的环形缓冲区中.我们想找到一种方法来重定向 assert.

的输出

这里我考虑了三种基本方法:

1.) 制作我们自己的断言宏——基本上,不要使用 #include <cassert>,为 assert 给出我们自己的定义。这可行,但要修补我们正在使用该调用 assert 的所有库以包含不同的 header.

将非常困难

2.) Patch libc -- 修改__assert_fail的libc实现。这会起作用,但在实践中会很尴尬,因为这意味着我们不能在不构建日志基础设施的情况下构建 libc。我们可以这样做,以便在 run-time 处,我们可以将函数指针传递给 libc,即 "assert handler"——这是我们可以考虑的事情。问题是是否有比这更简单/侵入性更小的解决方案。

3.) 修补 libc header,使 __assert_fail 标记为 __attribute__((weak))。这意味着我们可以使用自定义实现在 link-time 覆盖它,但是如果我们的自定义实现没有被 link 编辑,那么我们 link 到常规的 libc 实现。其实我希望这个函数 already 会被标记为 __attribute__((weak)) 但我很惊讶地发现它显然不是。

我的主要问题是:选项 (3) 的可能缺点是什么——修补 libc 以便这一行:https://github.com/lattera/glibc/blob/master/assert/assert.h#L67

extern void __assert_fail (const char *__assertion, const char *__file,
               unsigned int __line, const char *__function)
__THROW __attribute__ ((__noreturn__));

也标有__attribute__((weak))?

  1. 我没有想到维护人员还没有这样做有什么充分的理由吗?
  2. 在我以这种方式修补 header 之后,当前正在 linking 和 运行 成功对抗 libc 的任何现有程序怎么会崩溃?这不可能发生,对吧?
  3. 出于某种原因,在此处使用 weak-linking 符号是否会产生显着的 run-time 成本? libc 已经是我们的共享库,我认为动态 linking 的成本应该淹没任何关于系统在加载时必须执行的弱分辨率与强分辨率的案例分析?
  4. 这里有没有我没想到的更简单/更优雅的方法?

glibc 中的一些函数,特别是 strtodmalloc,用特殊的 gcc 属性 __attribute__((weak)) 标记。这是一个 linker 指令——它告诉 gcc 这些符号应该被标记为 "weak symbols",这意味着如果在 link 时发现该符号的两个版本,则 "strong" 弱者被选中

这样做的动机在维基百科上有描述:

Use cases

Weak symbols can be used as a mechanism to provide default implementations of functions that can be replaced by more specialized (e.g. optimized) ones at link-time. The default implementation is then declared as weak, and, on certain targets, object files with strongly declared symbols are added to the linker command line.

If a library defines a symbol as weak, a program that links that library is free to provide a strong one for, say, customization purposes.

Another use case for weak symbols is the maintenance of binary backward compatibility.

然而,在 glibc 和 musl libc 中,在我看来 __assert_fail 函数(assert.h 宏转发到的函数)没有被标记为弱符号。

https://github.com/lattera/glibc/blob/master/assert/assert.h

https://github.com/lattera/glibc/blob/master/assert/assert.c

https://github.com/cloudius-systems/musl/blob/master/include/assert.h

glibc 中的符号 __assert_fail 不需要 attribute((weak))。只需在您的程序中编写您自己的 __assert_fail 实现,链接器 应该 使用您的实现,因为 example:

#include <stdio.h>
#include <assert.h>

void __assert_fail(const char * assertion, const char * file, unsigned int line, const char * function)
{
    fprintf(stderr, "My custom message\n");
    abort();
}


int main()
{
    assert(0);
    printf("Hello World");

    return 0;
}

这是因为当链接器解析符号时,__assert_fail 符号已经由您的程序定义,因此链接器不应选择 libc 定义的符号。

如果你真的需要 __assert_fail 被定义为 libc 中的弱符号,为什么不只是 objcopy --weaken-symbol=__assert_fail /lib/libc.so /lib/libc_with_weak_assert_fail.so。我认为您不需要为此从源代码重建 libc。

如果我是你,我可能会选择打开管道 (2) 和 fdopen(2)'ing stderr 来获取该管道的写入端。我会将管道的读取端作为主 poll(2) 循环(或您系统中的任何等效项)的一部分进行服务,并将内容写入环形缓冲区。

处理实际输出显然较慢,但从你的文章来看,这样的输出很少见,所以影响应该可以忽略不计(特别是如果你已经有一个投票或select这个fd可以搭载)。

在我看来,调整 libc 或依赖工具的副作用可能会在未来崩溃,并且调试起来会很痛苦。如果可能的话,我会选择保证安全的机制并支付性能价格。