2

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

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

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

1.) 制作我们自己的断言宏——基本上,不要使用#include <cassert>,给我们自己的定义assert。这会起作用,但是要修补我们正在使用该调用的所有库assert以包含不同的标头会非常困难。

2.) 修补 libc - 修改 .libc 的实现__assert_fail。这会起作用,但在实践中会非常尴尬,因为这意味着我们无法在不构建日志基础设施的情况下构建 libc。我们可以这样做,以便在运行时,我们可以将函数指针传递给libc“断言处理程序”——这是我们可以考虑的。问题是是否有比这更简单/侵入性更小的解决方案。

3.) 修补 libc 标头,以便__assert_fail标有__attribute__((weak)). 这意味着我们可以在链接时使用自定义实现覆盖它,但是如果我们的自定义实现没有链接,那么我们将链接到常规的 libc 实现。实际上我希望这个功能已经被标记了__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. 在我以这种方式修补标头后,当前正在链接并成功针对 libc 运行的任何现有程序如何中断?这不可能发生,对吧?
  3. 出于某种原因在此处使用弱链接符号是否会产生显着的运行时成本?libc对我们来说已经是一个共享库,我认为动态链接的成本应该超过系统在加载时必须执行的任何关于弱分辨率与强分辨率的案例分析?
  4. 这里有我没有想到的更简单/更优雅的方法吗?

glibc 中的一些函数,特别是strtodmalloc,用特殊的 gcc 属性标记__attribute__((weak))。这是一个链接器指令——它告诉 gcc 这些符号应该被标记为“弱符号”,这意味着如果在链接时找到符号的两个版本,则选择“强”版本而不是弱版本。

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

用例

弱符号可以用作一种机制来提供函数的默认实现,这些函数可以在链接时被更专业的(例如优化的)函数替换。然后将默认实现声明为弱,并且在某些目标上,将具有强声明符号的目标文件添加到链接器命令行。

如果库将符号定义为弱符号,则链接该库的程序可以自由地提供强符号,例如用于定制目的。

弱符号的另一个用例是维护二进制向后兼容性。

但是,在 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

4

2 回答 2

3

您不需要来自 glibcattribute((weak))的符号。__assert_fail只需在程序中编写您自己的 __assert_fail 实现,链接器应该使用您的实现,例如

#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。

于 2018-06-18T18:23:01.163 回答
1

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

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

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

于 2018-06-18T18:26:33.663 回答