如何实现.so文件中的函数自动导出?

Hyu*_*Bae 1 shared-libraries

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

除了 Windows 之外,我们可以调用(共享目标文件)中的函数,.so即使该函数没有导出声明。就这一点而言,我制作 .so 比制作 .dll 容易得多。

同时,我很好奇非 Windows 如何使 .so 中定义的函数能够被其他程序调用而无需显式导出声明。我粗略地猜测 .so 文件中的所有函数都会自动导出,但我不确定。

Mik*_*han 6

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

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

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

{ int i; ... }
Run Code Online (Sandbox Code Playgroud)

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

GCC 生成的目标文件中定义的符号可以是本地的,也可以是全局的

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

foob​​ar.c (1)

static int foo(void)
{
    return 42;
}

int bar(void)
{
    return foo();
}
Run Code Online (Sandbox Code Playgroud)

foo是一种地方象征,bar也是一个全球象征。如果我们使用以下命令编译此文件-save-temps

$ gcc -save-temps -c -fPIC foobar.c
Run Code Online (Sandbox Code Playgroud)

然后 GCC 会将汇编列表保存在 中,我们可以在其中看到生成的汇编代码如何注册全局而非全局foobar.s的事实:barfoo

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
Run Code Online (Sandbox Code Playgroud)

汇编指令.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
Run Code Online (Sandbox Code Playgroud)

消息是一样的:

     5: 0000000000000000    11 FUNC    LOCAL  DEFAULT    1 foo
     ...
     9: 000000000000000b    11 FUNC    GLOBAL DEFAULT    1 bar
Run Code Online (Sandbox Code Playgroud)

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

$ 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
Run Code Online (Sandbox Code Playgroud)

然后foo从符号表中消失。

由于静态链接器可以使用全局符号,因此我们可以将程序与来自另一个目标文件的foobar.o调用链接起来:bar

主程序

#include <stdio.h>

extern int foo(void);

int main(void)
{
    printf("%d\n",bar());
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

就像这样:

$ gcc -c main.c
$ gcc -o prog main.o foobar.o
$ ./prog
42
Run Code Online (Sandbox Code Playgroud)

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

$ gcc -shared -o libbar.so foobar.o
Run Code Online (Sandbox Code Playgroud)

然后将同一程序与该共享库动态链接:

$ gcc -o prog main.o libbar.so
Run Code Online (Sandbox Code Playgroud)

没关系:

$ ./prog
./prog: error while loading shared libraries: libbar.so: cannot open shared object file: No such file or directory
Run Code Online (Sandbox Code Playgroud)

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

$ export LD_LIBRARY_PATH=.
$ ./prog
42
Run Code Online (Sandbox Code Playgroud)

正如我们在本节中所看到的,目标文件foobar.o有一个符号表.symtab,其中(至少)包括静态链接器可用的全局符号。DSOlibbar.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
Run Code Online (Sandbox Code Playgroud)

动态符号表中的符号是动态可见的——可供运行时加载程序使用。您可以看到bar出现.symtab中。在这两种情况下,符号都位于( =绑定) 列和( =可见性) 列中。.dynsymlibbar.soGLOBALbindDEFAULTvis

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

readelf --dyn-syms libbar.so
Run Code Online (Sandbox Code Playgroud)

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

$ readelf --dyn-syms foobar.o; echo Done
Done
Run Code Online (Sandbox Code Playgroud)

所以链接:

$ gcc -shared -o libbar.so foobar.o
Run Code Online (Sandbox Code Playgroud)

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

这使它看起来像你的猜测:

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

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

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

$ gcc -save-temps -fvisibility=hidden -c -fPIC foobar.c
Run Code Online (Sandbox Code Playgroud)

让我们再看一下装配清单:

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
...
...
Run Code Online (Sandbox Code Playgroud)

注意汇编指令:

    .hidden bar
Run Code Online (Sandbox Code Playgroud)

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

$ gcc -o prog main.o foobar.o
$ ./prog
42
Run Code Online (Sandbox Code Playgroud)

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

$ gcc -shared -o libbar.so foobar.o
Run Code Online (Sandbox Code Playgroud)

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

$ 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
Run Code Online (Sandbox Code Playgroud)

在 中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
Run Code Online (Sandbox Code Playgroud)

但它现在被标记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
Run Code Online (Sandbox Code Playgroud)

但这一次,它是一个LOCAL象征。正如我们刚才在链接失败时看到的那样,静态链接器将无法使用它。libbar.so而且它根本不再存在于动态符号表中:

$ readelf --dyn-syms libbar.so | grep bar; echo done
done
Run Code Online (Sandbox Code Playgroud)

-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();
}
Run Code Online (Sandbox Code Playgroud)

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

像上次一样重新编译:

$ gcc -fvisibility=hidden -c -fPIC foobar.c
Run Code Online (Sandbox Code Playgroud)

现在的符号表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
Run Code Online (Sandbox Code Playgroud)

尽管 . 再次显示bar可见度。如果我们重新链接:DEFAULT-fvisibility=hiddenlibbar.so

$ gcc -shared -o libbar.so foobar.o
Run Code Online (Sandbox Code Playgroud)

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

$ readelf --dyn-syms libbar.so | grep bar
     5: 0000000000001100    11 FUNC    GLOBAL DEFAULT    9 bar
Run Code Online (Sandbox Code Playgroud)

因此,-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();
}
Run Code Online (Sandbox Code Playgroud)

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

$ gcc -c -fPIC foobar.c
Run Code Online (Sandbox Code Playgroud)

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

$ readelf -s foobar.o | grep bar
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS foobar.c
     9: 000000000000000b    11 FUNC    GLOBAL HIDDEN     1 bar
Run Code Online (Sandbox Code Playgroud)

重新链接后libbar.sobar再次从其动态符号表中消失:

$ gcc -shared -o libbar.so foobar.o
$ readelf --dyn-syms libbar.so | grep bar; echo Done
Done
Run Code Online (Sandbox Code Playgroud)

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

进一步阅读: