C 共享库中的某些 C 符号即使具有显式可见性也不会导出

Some C symbols in a C shared library are not exported even with explicit visibility

前言

我正在用 C 开发一个面向 object 的库,它同时针对 Linux 和 Windows。

目前我正在 Linux 虚拟机(来宾)上进行开发,因为我正在使用 clang 消毒剂。

我正在使用 -fvisibility=hidden 构建,所以我使用 __attribute__((visibility("default"))) 仅导出所需的函数。

据我所知,该属性需要与函数定义一起使用,所以我将它放在源文件中,让 headers 保持干净。

我的库依赖于一种非常简单的面向 object 的方法,在 SO 上找到,它使用 struct base 定义在内部调用 vtable 函数指针的函数。

问题

构建共享库成功,但构建测试失败。

链接器抱怨对某些函数的未定义引用:

[ 17%] Built target clib
Scanning dependencies of target test_undefined_san
[ 20%] Building C object CMakeFiles/test_undefined_san.dir/test/allocator/test_default_allocator.c.o
[ 23%] Linking C executable test_undefined_san
/usr/bin/ld: /tmp/lto-llvm-b74224.o: in function `test_default_allocator':
<path>/test_default_allocator.c:17: undefined reference to `clib_init'
/usr/bin/ld: <path>/test_default_allocator.c:21: undefined reference to `clib_allocate'
/usr/bin/ld: <path>/test_default_allocator.c:23: undefined reference to `clib_deallocate'
/usr/bin/ld: <path>/test_default_allocator.c:26: undefined reference to `clib_finalize'
clang-8: error: linker command failed with exit code 1 (use -v to see invocation)
make[2]: *** [CMakeFiles/test_undefined_san.dir/build.make:160: test_undefined_san] Error 1
make[1]: *** [CMakeFiles/Makefile2:328: CMakeFiles/test_undefined_san.dir/all] Error 2
make: *** [Makefile:95: all] Error 2
The terminal process terminated with exit code: 2

事实是这些函数的定义用 __attribute__((visibility("default"))) 标记。

执行 nm libname.so 会产生有趣的结果。符号不存在!没有隐藏,甚至没有出现在 object!

0000000000001140 T clib_timer_end   <--| other tests, exported correctly
0000000000001110 T clib_timer_start <--| 
                 U clock@@GLIBC_2.2.5
0000000000004028 b completed.7383
                 w __cxa_finalize@@GLIBC_2.2.5
0000000000001040 t deregister_tm_clones
00000000000010b0 t __do_global_dtors_aux
0000000000003df8 t __do_global_dtors_aux_fini_array_entry
0000000000004020 d __dso_handle
0000000000003e00 d _DYNAMIC
0000000000001170 t _fini
0000000000001100 t frame_dummy
0000000000003df0 t __frame_dummy_init_array_entry
00000000000020a8 r __FRAME_END__
0000000000004000 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
0000000000002004 r __GNU_EH_FRAME_HDR
0000000000001000 t _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000001070 t register_tm_clones
0000000000004028 d __TMC_END__

实际代码

Header guards, asserts and various non-related stuff omitted.

allocator.h |这是基础 class

#include <stdbool.h>
#include <stddef.h>

struct clib_allocator;

typedef bool (*clib_alloc_init_t)(struct clib_allocator *);
typedef bool (*clib_alloc_finalize_t)(struct clib_allocator *);
typedef void *(*clib_alloc_allocate_t)(struct clib_allocator *, size_t, size_t);
typedef void (*clib_alloc_deallocate_t)(struct clib_allocator *, void *, size_t, size_t);

struct clib_allocator_vtable
{
    clib_alloc_init_t init;
    clib_alloc_finalize_t finalize;
    clib_alloc_allocate_t allocate;
    clib_alloc_deallocate_t deallocate;
};

struct clib_allocator
{
    struct clib_allocator_vtable vtable;
};

bool clib_init(struct clib_allocator *alloc);
bool clib_finalize(struct clib_allocator *alloc);
void*clib_allocate(struct clib_allocator *alloc, size_t sz, size_t align);
void clib_deallocate(struct clib_allocator *alloc, void *p, size_t sz, size_t align);

allocator.c

// Just calls alloc->vtable.method

#include <clib/allocator/allocator.h>

#define EX __attribute__((visibility("default"))) // just to reduce code

bool EX clib_init(struct clib_allocator *alloc)
{
    return alloc->vtable.init(alloc);
}
bool EX clib_finalize(struct clib_allocator *alloc)
{
    return alloc->vtable.finalize(alloc);
}
void* EX clib_allocate(struct clib_allocator *alloc, size_t sz, size_t align)
{
    return alloc->vtable.allocate(alloc, sz, align);
}
void EX clib_deallocate(struct clib_allocator *alloc, void *p, size_t sz, size_t align)
{
    alloc->vtable.deallocate(alloc, p, sz, align);
}

default_allocator.h |实现分配器

#include <clib/allocator/allocator.h>

struct clib_allocator* clib_get_default_allocator();

default_allocator.c | aligned_alloc/free 包装

// Only clib_get_default_allocator should be exported

#include <clib/allocator/default_allocator.h>

#include <stdlib.h>
#include <assert.h>
#include <threads.h>

static bool nop(struct clib_allocator *CLIB_UNUSED alloc)
{
    return true;
}

static void *aligned_alloc_w(struct clib_allocator *CLIB_UNUSED alloc, size_t sz, size_t align)
{
    return aligned_alloc(align, sz);
}

static void free_w(struct clib_allocator *CLIB_UNUSED alloc, void *p, size_t CLIB_UNUSED sz, size_t CLIB_UNUSED align)
{
    free(p);
}

static once_flag clib_init_default_alloc_flag = ONCE_FLAG_INIT;
static struct clib_allocator clib_default_alloc;

static void clib_init_default_allocator()
{
    clib_default_alloc.vtable.init = nop;
    clib_default_alloc.vtable.finalize = nop;
    clib_default_alloc.vtable.allocate = aligned_alloc_w;
    clib_default_alloc.vtable.deallocate = free_w;
}

#define EX __attribute__((visibility("default"))) // just to reduce code

struct clib_allocator* EX clib_get_default_allocator()
{
    call_once(&clib_init_default_alloc_flag, clib_init_default_allocator);
    return &clib_default_alloc;
}

test_default_allocator.c

#include <cute.h> // test assert library

#include <clib/allocator/default_allocator.h>

void test_default_allocator()
{
    SET_SIMPLE_SCENARIO("Testing defaut allocator");

    TEST_CASE()
    {
        // this is exported if I comment out the other functions
        struct clib_allocator *alloc = clib_get_default_allocator();

        ASSERT(alloc);

        void *p = NULL;

        ASSERT(clib_init(alloc)); // undefined ref

        for (size_t align = 2; align > 4096; align <<= 1)
        {
            p = clib_allocate(alloc, 4096, align); // undefined ref
            ASSERT(p);
            clib_deallocate(alloc, p, 4096, align); // undefined ref
        }

        ASSERT(clib_finalize(alloc)); // undefined ref
    }
}

建筑标志和信息

共享库标志

-Wall
-Wextra
-Werror
-pedantic-errors
-fno-omit-frame-pointer
-fPIC
-m64
-fcolor-diagnostics
-fvisibility=hidden

测试可执行标志

-Wall
-Wextra
-pedantic-errors
-Wno-unused-function
-fsanitize={address, thread, memory, undefined} // 4 different executables

系统信息

Linux Manjaro with kernel 5.13
glibc 2.29
Clang 8.0.1
GCC 9.1.0

忘记将 allocator.c 添加到构建系统...