怎样才能自动导出.so文件中的函数?

问题描述 投票:1回答:1

在Windows中,要调用DLL中的函数,该函数必须具有显式的导出声明。例如,__declspec(dllexport).def文件。

除了Windows之外,即使函数没有导出声明,我们也可以在.so(共享对象文件)中调用函数。对于我来说,制作.so比使用.dll容易得多。

同时,我很好奇非Windows如何在没有显式导出声明的情况下由其他程序调用.so中定义的函数。我粗略猜测.so文件中的所有函数都会自动导出,但我不确定。

shared-libraries
1个回答
1
投票

.so文件通常是类Unix操作系统中的DSO(动态共享对象,a.k.a共享库)。您想知道如何使这样的文件中定义的符号对运行时加载程序可见,以便在执行时将DSO动态链接到某个程序的进程中。这就是你所说的“出口”。 “导出”是一个有点Windows / DLL-ish的术语,也容易与“外部”或“全局”混淆,所以我们会说动态可见。

我将解释如何在使用GNU工具链构建的DSO的上下文中控制符号的动态可见性 - 即使用GCC编译器(gccg++gfortran等)编译并与binutils链接器ld(或兼容的替代方案)链接编译器和链接器)。我将用C代码说明。其他语言的机制相同。

目标文件中定义的符号是C源代码中的文件范围变量。即未在任何块内定义的变量。块范围变量:

{ int i; ... }

仅在执行封闭块时定义并且在目标文件中没有永久位置。

GCC生成的目标文件中定义的符号是本地的或全局的。

可以在定义它的目标文件中引用本地符号,但是目标文件根本不会显示它以进行链接。不适用于静态链接。不适用于动态链接。在C中,文件范围变量定义在默认情况下是全局的,如果使用static存储类限定则是本地的。所以在这个源文件中:

foob​​ar.c(1)

static int foo(void)
{
    return 42;
}

int bar(void)
{
    return foo();
}

foo是当地的象征,bar是全球性的。如果我们用-save-temps编译这个文件:

$ gcc -save-temps -c -fPIC foobar.c

然后GCC将在foobar.s中保存汇编列表,在那里我们可以看到生成的汇编代码如何记录bar是全局的而且foo不是:

foob​​ar.s(1)

    .file   "foobar.c"
    .text
    .type   foo, @function
foo:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $42, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   foo, .-foo
    .globl  bar
    .type   bar, @function
bar:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    call    foo
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   bar, .-bar
    .ident  "GCC: (Ubuntu 8.2.0-7ubuntu1) 8.2.0"
    .section    .note.GNU-stack,"",@progbits

汇编程序指令.globl bar意味着bar是一个全局符号。没有.globl foo;所以foo是当地的。

如果我们检查目标文件中的符号,请使用

$ readelf -s foobar.o

Symbol table '.symtab' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS foobar.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     5: 0000000000000000    11 FUNC    LOCAL  DEFAULT    1 foo
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
     9: 000000000000000b    11 FUNC    GLOBAL DEFAULT    1 bar

消息是一样的:

     5: 0000000000000000    11 FUNC    LOCAL  DEFAULT    1 foo
     ...
     9: 000000000000000b    11 FUNC    GLOBAL DEFAULT    1 bar

对象文件中定义的全局符号以及仅全局符号可供静态链接器用于解析其他对象文件中的引用。实际上,本地符号只出现在文件的符号表中,以供调试器或其他一些目标文件探测工具使用。如果我们用最小的优化重做编译:

$ gcc -save-temps -O1 -c -fPIC foobar.c
$ readelf -s foobar.o

Symbol table '.symtab' contains 9 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS foobar.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
     8: 0000000000000000     6 FUNC    GLOBAL DEFAULT    1 bar

然后foo从符号表中消失。

由于全局符号可用于静态链接器,因此我们可以将程序与从另一个目标文件调用foobar.obar链接:

main.c中

#include <stdio.h>

extern int foo(void);

int main(void)
{
    printf("%d\n",bar());
    return 0;
}

像这样:

$ gcc -c main.c
$ gcc -o prog main.o foobar.o
$ ./prog
42

但正如您所注意到的,我们不需要以任何方式更改foobar.o以使bar对加载器动态可见。我们可以将它链接到共享库中:

$ gcc -shared -o libbar.so foobar.o

然后动态链接相同的程序与该共享库:

$ gcc -o prog main.o libbar.so

这没关系:

$ ./prog
./prog: error while loading shared libraries: libbar.so: cannot open shared object file: No such file or directory

...哎呀。只要我们让加载器知道libbar.so在哪里就可以了,因为我的工作目录不是默认情况下缓存的搜索目录之一:

$ export LD_LIBRARY_PATH=.
$ ./prog
42

目标文件foobar.o.symtab部分中有一个符号表,包括(至少)静态链接器可用的全局符号。 DSO libbar.so在其.symtab部分也有一个符号表。但它也有一个动态符号表,在它的.dynsym部分:

$ readelf -s libbar.so

    Symbol table '.dynsym' contains 6 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
         1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __cxa_finalize
         2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
         3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
         4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
         5: 00000000000010f5     6 FUNC    GLOBAL DEFAULT    9 bar

    Symbol table '.symtab' contains 45 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
         ...
         ...
        21: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
        22: 0000000000001040     0 FUNC    LOCAL  DEFAULT    9 deregister_tm_clones
        23: 0000000000001070     0 FUNC    LOCAL  DEFAULT    9 register_tm_clones
        24: 00000000000010b0     0 FUNC    LOCAL  DEFAULT    9 __do_global_dtors_aux
        25: 0000000000004020     1 OBJECT  LOCAL  DEFAULT   19 completed.7930
        26: 0000000000003e88     0 OBJECT  LOCAL  DEFAULT   14 __do_global_dtors_aux_fin
        27: 00000000000010f0     0 FUNC    LOCAL  DEFAULT    9 frame_dummy
        28: 0000000000003e80     0 OBJECT  LOCAL  DEFAULT   13 __frame_dummy_init_array_
        29: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS foobar.c
        30: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
        31: 0000000000002094     0 OBJECT  LOCAL  DEFAULT   12 __FRAME_END__
        32: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS
        33: 0000000000003e90     0 OBJECT  LOCAL  DEFAULT   15 _DYNAMIC
        34: 0000000000004020     0 OBJECT  LOCAL  DEFAULT   18 __TMC_END__
        35: 0000000000004018     0 OBJECT  LOCAL  DEFAULT   18 __dso_handle
        36: 0000000000001000     0 FUNC    LOCAL  DEFAULT    6 _init
        37: 0000000000002000     0 NOTYPE  LOCAL  DEFAULT   11 __GNU_EH_FRAME_HDR
        38: 00000000000010fc     0 FUNC    LOCAL  DEFAULT   10 _fini
        39: 0000000000004000     0 OBJECT  LOCAL  DEFAULT   17 _GLOBAL_OFFSET_TABLE_
        40: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __cxa_finalize
        41: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
        42: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
        43: 00000000000010f5     6 FUNC    GLOBAL DEFAULT    9 bar

动态符号表中的符号是动态可见的符号 - 可供运行时加载程序使用。你可以看到bar出现在.symtab.dynsymlibbar.so中。在这两种情况下,符号在GLOBAL(=绑定)列中具有bind,在DEFAULT(=可见性)列中具有vis

如果你想让readelf只显示动态符号表,那么:

readelf --dyn-syms libbar.so

会这样做,但不适用于foobar.o,因为目标文件没有动态符号表:

$ readelf --dyn-syms foobar.o; echo Done
Done

所以联系:

$ gcc -shared -o libbar.so foobar.o

创建libbar.so的动态符号表,并用foobar.o的全局符号表(以及GCC通过defauilt添加到链接的各种GCC样板文件)中的符号填充它。

这看起来像你的猜测:

我粗略猜测.so文件中的所有函数都会自动导出

是正确的。事实上它很接近,但不正确。

看看如果我像这样重新编译foobar.c会发生什么:

$ gcc -save-temps -fvisibility=hidden -c -fPIC foobar.c

让我们再看看汇编列表:

foob​​ar.s(2)

...
...
    .globl  bar
    .hidden bar
    .type   bar, @function
bar:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    call    foo
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
...
...

注意汇编程序指令:

    .hidden bar

那之前没有。 .globl bar仍在那里; bar仍然是一个全球性的象征。我仍然可以在这个程序中静态链接foobar.o

$ gcc -o prog main.o foobar.o
$ ./prog
42

我仍然可以链接这个共享库:

$ gcc -shared -o libbar.so foobar.o

但我不能再动态链接这个程序:

$ gcc -o prog main.o libbar.so
/usr/bin/ld: main.o: in function `main':
main.c:(.text+0x5): undefined reference to `bar'
collect2: error: ld returned 1 exit status

foobar.obar仍然在符号表中:

$ readelf -s foobar.o | grep bar
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS foobar.c
     9: 000000000000000b    11 FUNC    GLOBAL HIDDEN     1 bar

但它现在在输出的HIDDEN(= vis)列中标记为visibility

bar仍然在libbar.so的符号表中:

$ readelf -s libbar.so | grep bar
    29: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS foobar.c
    41: 0000000000001100    11 FUNC    LOCAL  DEFAULT    9 bar

但这一次,它是一个LOCAL符号。它不会被libbar.so的静态链接器使用 - 正如我们刚才看到的,当我们的链接失败时。它根本不在动态符号表中:

$ readelf --dyn-syms libbar.so | grep bar; echo done
done

所以-fvisibility=hidden在编译foobar.c时的效果是让编译器在.globl中将.hidden符号注释为foobar.o。然后,当foobar.o链接到libbar.so时,链接器会将每个全局隐藏符号转换为libbar.so中的本地符号,这样只要libbar.so与其他内容链接,它就无法用于解析引用。并且它不会将隐藏符号添加到libbar.so的动态符号表中,因此运行时加载程序无法看到它们动态地解析引用。

到目前为止的故事:当链接器创建共享库时,它会在动态符号表中添加在输入对象文件中定义的所有全局符号,并且不会被编译器隐藏。这些成为共享库的动态可见符号。默认情况下不会隐藏全局符号,但我们可以使用编译器选项-fvisibility=hidden隐藏它们。此选项引用的可见性是动态可见性。

现在,使用-fvisibility=hidden从动态可见性中删除全局符号的能力看起来并不是很有用,因为我们使用该选项编译的任何目标文件似乎都不会为共享库提供动态可见的符号。

但实际上,我们可以单独控制目标文件中定义的哪些全局符号将是动态可见的,哪些不会。让我们改变foobar.c如下:

foob​​ar.c(2)

static int foo(void)
{
    return 42;
}

int __attribute__((visibility("default"))) bar(void)
{
    return foo();
}

您在此处看到的__attribute__语法是GCC语言扩展,用于指定标准语言中无法表达的符号属性 - 例如动态可见性。 Microsoft的declspec(dllexport)是Microsoft语言扩展,与GCC的__attribute__((visibility("default")))具有相同的效果。但是对于GCC,在目标文件中定义的全局符号默认拥有__attribute__((visibility("default"))),并且您必须使用-fvisibility=hidden进行编译以覆盖它。

像上次一样重新编译:

$ gcc -fvisibility=hidden -c -fPIC foobar.c

现在foobar.o的符号表:

$ readelf -s foobar.o | grep bar
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS foobar.c
     9: 000000000000000b    11 FUNC    GLOBAL DEFAULT    1 bar

尽管bar,再次显示DEFAULT-fvisibility=hidden能见度。如果我们重新连接libbar.so

$ gcc -shared -o libbar.so foobar.o

我们看到bar回到动态符号表中:

$ readelf --dyn-syms libbar.so | grep bar
     5: 0000000000001100    11 FUNC    GLOBAL DEFAULT    9 bar

因此,-fvisibility=hidden告诉编译器将全局符号标记为隐藏,除非在源代码中我们明确指定该符号的反补贴动态可见性。

这是从我们希望动态可见的目标文件中精确选择符号的一种方法:将-fvisibility=hidden传递给编译器,并在源代码中单独指定__attribute__((visibility("default"))),仅用于我们想要动态可见的符号。

另一种方法是不将-fvisibility=hidden传递给编译器,并在源代码中单独指定__attribute__((visibility("hidden"))),仅用于我们不希望动态可见的符号。所以,如果我们再次改变foobar.c

foob​​ar.c(3)

static int foo(void)
{
    return 42;
}

int __attribute__((visibility("hidden"))) bar(void)
{
    return foo();
}

然后使用默认可见性重新编译:

$ gcc -c -fPIC foobar.c

bar恢复隐藏在目标文件中:

$ readelf -s foobar.o | grep bar
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS foobar.c
     9: 000000000000000b    11 FUNC    GLOBAL HIDDEN     1 bar

在重新密封libbar.so后,bar再次缺席其动态符号表:

$ gcc -shared -o libbar.so foobar.o
$ readelf --dyn-syms libbar.so | grep bar; echo Done
Done

专业方法是将DSO的动态API最小化到指定的范围。使用我们讨论过的设备,这意味着使用-fvisibility=hidden进行编译并使用__attribute__((visibility("default")))来公开指定的API。动态API也可以使用一种名为version-script的链接器脚本与GNU链接器一起控制和版本化:这是一种更专业的方法。

进一步阅读:

© www.soinside.com 2019 - 2024. All rights reserved.