当SHARED lib引用它们时,为什么STATIC lib中的未使用对象包含在最终二进制文件中?

时间:2018-02-02 14:19:21

标签: c unix linker shared-libraries static-libraries

要点:

STATIC和SHARED lib之间的交叉使用函数导致STATIC lib的所有对象(甚至未使用!)都包含在最终的二进制文件中!

你不明白我的意思吗?的: - P

坐下来阅读下面的完整故事! 名字已经改变,以保护无辜。示例的目标是简单性和可重复性。

预告片: SSCCE 可用! (简短,自包含,正确(可编译),例如:http://www.sscce.org/

一开始,我有:

  • 二进制(main)调用存储在STATIC库(fun1a())中的函数(libsub.a)。 main也有内部函数(mainsub())。

  • 包含 SEVERAL 对象的STATIC库(libsub.a),每个对象都包含其他来源使用的多个函数。

编译main会导致二进制文件 ONLY 包含引用函数的对象副本(STATIC lib)。 在下面的示例中,main将只包含对象shared1.o的函数(因为main调用func1a())和 NOT 函数shared2.o(因为没有参考)。

好的!

  main.c                 libsub.a    
+-------------+        +------------+
| main        |        | shared1.o  |
|  func1a()   | <----> |   func1a() |
|  mainsub()  |        |   func1b() |
+-------------+        |    ----    |
                       | shared2.o  |
                       |   func2a() |
                       |   func2b() |
                       +------------+

作为改进,我想允许外部&#39;人们能够通过他们自己的代码覆盖main中调用的函数,而无需重新编译 MY 二进制文件。

无论如何我都没有提供源代码,也没有我的静态库。

为此,我打算提供一个&#34;准备填写&#34;功能骨架源。 (这被称为USER-EXIT?!) 使用SHARED / DYNAMIC lib可以做到恕我直言。 可以覆盖的函数是main(mainsub())的内部函数或共享函数(func1a() ...),并且将存储在共享库(.so)中以在链接期间添加/引用

创建了新的来源,以&#39; c&#39;为前缀,其中包含&#39;客户端&#39;版本标准&#39;功能。 使用(或不使用)覆盖功能的切换超出范围。只要假设UE为真,那就覆盖了。

cmain.c是一个包含Client_mainsub()的新来源,可以被替换为&#39; mainsub()

cshared1.c是一个包含Client_func1a()的新来源,可以被替换为&#39; func1a()确实shared1.c中的所有功能都可以替换为cshared1.c

cshared2.c是一个包含Client_func2a()的新来源,可以被替换为&#39; func2a()

概述变为:

     main.c                          libsub.a                       clibsub.so
   +-----------------------+     +------------------------+     +--------------------+
   | main                  |     | shared1.o              |     | cshared1.o         |
   |  func1a() {}          |     |   func1a()             |     |   Client_func1a()  |
   |  mainsub()            | <-> |   { if UE              | <-> |    {do ur stuff }  |
   |  { if UE              |     |     Client_func1a()    |     |                    |
   |     Client_mainsub()  |     |     return           } |     | cshared2.o         |
   |     return           }|     |   func1b()             |     |   Client_func2a()  |
   +-----------------------+     |        -------         |    >|    {do ur stuff }  |
                ^                | shared2.o              |   / +--------------------+
    cmain.c     v                |   func2a()             |  /
   +--------------------+        |   { if UE              | /
   | cmain              |        |     Client_func2a()    |<
   |   Client_mainsub() |        |     return           } |
   |    {do ur stuff }  |        |   func2b()             |
   +--------------------+        +------------------------+

同样,由于main不调用func2a()也不调用func2b(),因此(STATIC)对象shared2.o不包含在二进制文件中,并且不引用(SHARED){ {1}}也存在。 好的!

最后,简单地覆盖功能是不够的(或太多了!)。 我希望外部人员可以调用我的函数(或者不是)...但允许他们在之前和/或正确之后做一些事情我的功能。

所以我们不会将Client_func2a()愚蠢地替换为func2a(),而是粗略地使用伪代码:

Client_func2a()

请记住, shared2.c | cshared2.c (assume UE=true) | func2a() { |Client_func2a() { if UE {} | Client_func2a() ==> do (or not) some stuf PRE call | | if (DOIT) { // activate or not standard call | UE=false | func2a() // do standard stuff | UE=true | } else | { do ur bespoke stuff } | | do (or not) some stuf POST call | } <== } else { do standard stuff } } 会提供给其他人,他们可以(或不会)在提供的骨架上做自己的事情。

(注意:将cshared2.c设置为false并在UE中恢复为true可避免Client_func2a()调用中的无限循环!;-))

现在出现了我的问题。

在这种情况下,结果二进制文件现在包含func2a()对象,尽管 NO 调用主要是shared2.oshared2.c的任何函数! !!

搜索之后看起来是因为交叉调用/引用:

cshared2.c

那么为什么shared2.o contains func2a() that may call Client_func2a() cshared2.o contains Client_func2a() that may call func2a() 二进制包含shared2.o?

main

请注意,只需添加评论>dump -Tv main main: ***Loader Section*** ***Loader Symbol Table Information*** [Index] Value Scn IMEX Sclass Type IMPid Name [0] 0x00000000 undef IMP RW EXTref libc.a(shr_64.o) errno [1] 0x00000000 undef IMP DS EXTref libc.a(shr_64.o) __mod_init [2] 0x00000000 undef IMP DS EXTref libc.a(shr_64.o) exit [3] 0x00000000 undef IMP DS EXTref libc.a(shr_64.o) printf [4] 0x00000000 undef IMP RW EXTref libc.a(shr_64.o) __n_pthreads [5] 0x00000000 undef IMP RW EXTref libc.a(shr_64.o) __crt0v [6] 0x00000000 undef IMP RW EXTref libc.a(shr_64.o) __malloc_user_defined_name [7] 0x00000000 undef IMP DS EXTref libcmain.so Client_mainsub1 [8] 0x00000000 undef IMP DS EXTref libcshared.so Client_func1b [9] 0x00000000 undef IMP DS EXTref libcshared.so Client_func1a [10] 0x00000000 undef IMP DS EXTref libcshared.so Client_func2b <<< but why ??? ok bcoz func2b() is referenced ... [11] 0x00000000 undef IMP DS EXTref libcshared.so Client_func2a <<< but why ??? ok bcoz func2a() is referenced ... [12] 0x110000b50 .data ENTpt DS SECdef [noIMid] __start [13] 0x110000b78 .data EXP DS SECdef [noIMid] func1a [14] 0x110000b90 .data EXP DS SECdef [noIMid] func1b [15] 0x110000ba8 .data EXP DS SECdef [noIMid] func2b <<< but why this ? Not a single call is made in main ??? [16] 0x110000bc0 .data EXP DS SECdef [noIMid] func2a <<< but why this ? Not a single call is made in main ??? (以及func2a())即可解决链接问题(打破十字架)......但由于我希望保持共享,因此无法解决lib!?

行为发生在使用IBM XL C / C ++ 12.1的AIX 7.1上,但在Linux上看起来是相同的(Red Hat 5 + GCC 5.4,编译参数中有一些小变化)

func2b()

所以我发现这肯定是一种误解。谁能解释一下?

这里承诺的是SSCCE。 您可以通过重新创建/下载以下小文件来重播我的问题并运行go.sh(请参阅脚本中的注释)

Edit1 :在问题中添加了代码,而不是建议的外部网站

的main.c

IBM XL C/C++ for AIX, V12.1 (5765-J02, 5725-C72)
Version: 12.01.0000.0000
Driver Version: 12.01(C/C++) Level: 120315
C Front End Version: 12.01(C/C++) Level: 120322
High-Level Optimizer Version: 12.01(C/C++) and 14.01(Fortran) Level: 120315
Low-Level Optimizer Version: 12.01(C/C++) and 14.01(Fortran) Level: 120321

cmain.c

#include <stdio.h>
#include "inc.h"

extern void func1a (), func1b ();

int UEXIT(char* file, char* func)
{
    printf("      UEXIT file=<%s>   func=<%s>\n",file,func);
    return 1;   /* always true for testing */
}


main (){
    printf(">>> main\n");
    func1a ();
    mainsub ();
    printf("<<< main\n");
}

mainsub () {
    printf(">>> mainsub\n");

    if(UEXIT("main","mainsub")) {
        Client_mainsub1();
        return;
    }
    printf("<<< mainsub\n");
}

inc.h

#include <stdio.h>
#include "inc.h"

void Client_mainsub1 () {
    printf(">>>>>> Client_mainsub1\n");
    printf("<<<<<< Client_mainsub1\n");
return;
}

shared1.c

extern int UEXIT(char * fileName, char * functionName);

shared2.c

#include <stdio.h>
#include "inc.h"

void func1a (){
    printf(">>>>> func1a\n");
    if(UEXIT("main","func1a")) {
        Client_func1a();
        return;
    }
    printf("<<<<< func1a\n");
}

void func1b (){
    printf(">>>>> func1b\n");
    if(UEXIT("main","func1b")){
        Client_func1b();
        return;
    }
    printf("<<<<< func1b\n");
}

cshared1.c

#include <stdio.h>
#include "inc.h"

void func2a (){
    printf(">>>>> func2a\n");
    if(UEXIT("main","func2a")) {
        Client_func2a();
        return;
    }
    printf("<<<<< func2a\n");
}

void func2b (){
    printf(">>>>> func2b\n");
    if(UEXIT("main","func2b")){
        Client_func2b();
        return;
    }
    printf("<<<<< func2b\n");
}

cshared2.c

#include <stdio.h>
#include "inc.h"

void Client_func1a () {
    int standardFunctionCall = 0;
    printf("\t>>>> Client_func1a\n");
    if (standardFunctionCall) {
        func1a();
    }
    printf("\t<<< Client_func1a\n");
    return;
}


void Client_func1b () {
    int standardFunctionCall = 0;
    printf("\t>>>> Client_func1b\n");
    if (standardFunctionCall) {
        func1b();
    }
    printf("\t<<< Client_func1b\n");
    return;
}

go.sh

#include <stdio.h>
#include "inc.h"

void Client_func2a () {
    int standardFunctionCall = 0;
    printf("\t>>>> Client_func2a\n");
    if (standardFunctionCall) {
        func2a();           /* !!!!!! comment this to avoid crossed link with shared2.c !!!!! */
    }
    printf("\t<<< Client_func2a\n");
    return;
}


void Client_func2b () {
    int standardFunctionCall = 0;
    printf("\t>>>> Client_func2b\n");
    if (standardFunctionCall) {
        func2b();           /* !!!!!! ALSO comment this to avoid crossed link with shared2.c !!!!! */
    }
    printf("\t<<< Client_func2b\n");
    return;
}

Edit2 :根据要求添加了RedHat版本的go.sh脚本

gored.sh

#!/bin/bash

## usage :
## . ./go.sh
## so that the redefinition of LIBPATH is propagated to calling ENV ...
##    otherwise :  "Dependent module libcshared.so could not be loaded."


# default OBJECT_MODE to 64 bit (avoid explicitely setting -X64 options...)
export OBJECT_MODE=64
export LIBPATH=.:$LIBPATH

# Compile client functions for target binary
cc -q64 -c -o cmain.o cmain.c

# (1) Shared lib for internal function
cc -G -q64 -o libcmain.so cmain.o


# Compile common functions
cc -c shared2.c shared1.c

# Compile client common functions overwrite
cc -c cshared2.c cshared1.c


# (2) Built libsub.a for common functions (STATIC)
ar -rv libsub.a  shared1.o shared2.o

# (3) Built libcshared.so for client common functions overwrite (SHARED)
cc -G -q64 -o libcshared.so cshared1.o cshared2.o


# Finally built binary using above (1) (2) (3)
# main only call func1a() , so should only include objects shared1
# But pragmatically shared2 is also included if cshared2 reference a possible call to func2() in shared2 !!!!????
#   Check this with "nm main |grep shared2" or "nm main |grep func2" or "dump -Tv main |grep func2"
cc -q64 -o main main.c -bstatic libsub.a -bshared libcmain.so  libcshared.so

# result is the same without specifying -bstatic or -bshared
#cc -q64 -o main2 main.c libsub.a libcmain.so  libcshared.so


#If I split libcshared.so into libcshared1.so and libcshared2.so it is also the same :
#cc -G -q64 -o libcshared1.so cshared1.o
#cc -G -q64 -o libcshared2.so cshared2.o
#cc -q64 -o main4 main.c -bstatic libsub.a -bshared libcmain.so  libcshared1.so libcshared2.so

#If I do not inlcude libcshared2.so, binary is of course well working, without reference to cshared2 nor shared2 . 
# So why linker chooses to add STATIC shared2.o only if libcshared2.so is listed ?
# Is there a way to avoid this add of unused code ?
#cc -q64 -o main4 main.c -bstatic libsub.a -bshared libcmain.so  libcshared1.so

或者单个.tar.bz2中的完整上述文件(没有gored.sh)。 (6KB)。

https://pastebin.com/KsaqacAu

只需复制/粘贴新文件(ex ## usage : ## . ./gored.sh ## so that the redefinition of LD_LIBRARY_PATH is propagated to calling ENV ... ## otherwise : "Dependent module libcshared.so could not be loaded." export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH # Compile client functions for target binary gcc -fPIC -c cmain.c # (1) Shared lib for internal function gcc -shared -o libcmain.so cmain.o # Compile common functions gcc -c shared2.c shared1.c # Compile client common functions overwrite gcc -fPIC -c cshared2.c cshared1.c # (2) Built libsub.a for common functions (STATIC) ar -rv libsub.a shared1.o shared2.o # (3) Built libcshared.so for client common functions overwrite (SHARED) gcc -shared -o libcshared.so cshared1.o cshared2.o # Finally built binary using above (1) (2) (3) # main only call func1a() , so should only include objects shared1 # But pragmatically shared2 is also included if cshared2 reference a possible call to func2() in shared2 !!!!???? # Check this with "nm main |grep shared2" or "nm main |grep func2" or "dump -Tv main |grep func2" gcc -o main main.c libcmain.so libcshared.so libsub.a #If I split libcshared.so into libcshared1.so and libcshared2.so it is also the same : gcc -shared -o libcshared1.so cshared1.o gcc -shared -o libcshared2.so cshared2.o cc -o main2 main.c libcmain.so libcshared1.so libcshared2.so libsub.a #If I do not inlcude libcshared2.so, binary is of course well working, without reference to cshared2 nor shared2 . # So why linker chooses to add STATIC shared2.o only if libcshared2.so is listed ? # Is there a way to avoid this add of unused code ? cc -o main3 main.c libcmain.so libcshared1.so libsub.a )即可。然后输入

poc.uue

你应该 poc.tar.bz2

解压缩,解压进入poc文件夹并运行

uudecode poc.uue

然后

. ./go.sh

或者如果在RedHat下

dump -Tv main 

nm main 之后的结果示例:

gored.sh

编辑3:ASCII ART! : - )
这是“视觉”和“视觉”。使用未使用的对象/引用的最终状态我认为链接器包含错误。或者至少不够智能,无法检测到未使用。 也许这是正常的,或者有一个选项可以避免在最终二进制文件中使用未使用的静态代码。这看起来并不像一个复杂的情况,因为被标记为“未使用!”并且#39;代码没有任何关联?不是吗?

poc>nm main |grep func2
*                 U Client_func2a
                 U Client_func2b
0000000000400924 T func2a
000000000040095d T func2b
poc>nm main2 |grep func2
                 U Client_func2a
                 U Client_func2b
0000000000400934 T func2a
000000000040096d T func2b
poc>nm main3 |grep func2
poc>

欢迎任何建设性的答案让我走上正确的道路。

感谢。

2 个答案:

答案 0 :(得分:3)

这是令人费解的链接器行为的简化说明 你:

<强>的main.c

extern void foo(void);

int main(void)
{
    foo();
    return 0;
}

<强> foo.c的

#include <stdio.h>

void foo(void)
{
    puts(__func__);
}

<强> bar.c

#include <stdio.h>

extern void do_bar(void);

void bar(void)
{
    do_bar();
}

<强> do_bar.c

#include <stdio.h>

void do_bar(void)
{
    puts(__func__);
}

让我们将所有这些源文件编译成目标文件:

$ gcc -Wall -c main.c foo.c bar.c do_bar.c

现在我们尝试链接一个程序,如下所示:

$ gcc -o prog main.o foo.o bar.o
bar.o: In function `bar':
bar.c:(.text+0x5): undefined reference to `do_bar'

未定义的函数do_bar仅在定义中引用 bar的{​​{1}}和bar未被引用 该计划。为什么然后链接失败?

很简单,此链接失败,因为我们告诉链接器链接 bar.o 进入程序;它确实如此; bar.o包含bar的定义, 引用do_bar,它没有在链接中定义。 bar不是 已引用,但do_bar - 由bar引用,该程序已在程序中链接。

默认情况下,链接器要求链接中引用的任何符号 一个程序的定义在链接中。如果我们强迫它链接定义 bar的{​​{1}},它将要求do_bar的定义,因为没有。{ do_bar的定义它实际上 <{1}}的定义。它如果链接 bar的定义,它不会质疑我们是否需要来链接它, 如果答案为否,则允许对bar进行未定义的引用。

链接失败可以通过以下方式解决:

do_bar

现在在这个例子中,在程序中链接$ gcc -o prog main.o foo.o bar.o do_bar.o $ ./prog foo 只是无偿的。我们 也可以通过 not 成功链接,告诉链接器链接bar.o

bar.o

gcc -o prog main.o foo.o $ ./prog foo bar.o都是多余的 执行do_bar.o,但该程序只能与两者相关联,或者两者都不相关

但是假设mainfoo在同一个文件中定义了?

它们可能在同一个目标文件bar中定义:

foobar.o

然后:

ld -r -o foobar.o foo.o bar.o

现在,链接器无法链接$ gcc -o prog main.o foobar.o foobar.o: In function `bar': (.text+0x18): undefined reference to `do_bar' collect2: error: ld returned 1 exit status 的定义而不链接 foo的定义。因此,我们必须再次链接bar

的定义
do_bar

这样关联,$ gcc -o prog main.o foobar.o do_bar.o $ ./prog foo 包含progfoobar的定义:

do_bar

$ nm prog | grep -e foo -e bar 000000000000065d T bar 0000000000000669 T do_bar 000000000000064a T foo =已定义的函数符号)。

同样,Tfoo可能在同一个共享库中定义:

bar

然后这个联系:

$ gcc -Wall -fPIC -c foo.c bar.c
$ gcc -shared -o libfoobar.so foo.o bar.o

和以前一样失败,并且可以用同样的方式修复:

$ gcc -o prog main.o -L. -lfoobar -Wl,-rpath=$(pwd)
./libfoobar.so: undefined reference to `do_bar'
collect2: error: ld returned 1 exit status

当我们链接共享库$ gcc -o prog main.o do_bar.o -L. -lfoobar -Wl,-rpath=$(pwd) $ ./prog foo 而不是对象时 文件libfoobar.so,我们的foobar.o有一个不同的符号表:

prog

这一次,$ nm prog | grep -e foo -e bar 00000000000007aa T do_bar U foo 不包含progfoo的定义。它 包含未定义的引用(bar)到U,因为它调用foo, 当然,在运行时,foo中的定义将满足该引用。 自从该程序以来,甚至没有对libfoobar.so的未定义引用,也不应该引用 从不致电bar

但是,bar仍然包含<{> prog定义,现在未引用 来自符号表中的所有函数。

这与你自己的SSCCE相呼应,但这种方式不那么复杂。在你的情况下:

  • 目标文件do_bar是 链接到该计划,以提供libsub.a(shared2.o)func2a的定义。

  • 必须找到并链接这些定义,因为它们分别在func2b的定义中被引用 和Client_func2a,在Client_func2b

  • 中定义 必须链接
  • libcshared.so以提供libcshared.so的定义。

  • 必须找到并链接Client_func1a的定义,因为它是 引自Client_func1a

  • 的定义
  • func1a调用func1a

这就是我们看到的原因:

main

在你的程序的符号表中。

将定义链接到程序中并不常见 它不调用的函数。它通常以我们所见过的方式发生:联系, 递归地解析以$ nm main | grep func2 U Client_func2a U Client_func2b 00000000004009f7 T func2a 0000000000400a30 T func2b 开头的符号引用,发现它需要一个定义 main的{​​{1}},它只能通过链接某个对象文件ffile.o来获取 它还链接了函数file.o的定义,它永远不会被调用。

是多么奇怪的是最终得到像g这样的程序,就像我上一版main一样, 其中包含与解析相关联的未调用函数(例如prog)的定义 来自程序中 not 的另一个未调用函数(例如do_bar)定义的引用。 即使存在冗余的函数定义,通常我们也可以将它们链接回一个或多个 链接中的目标文件,其中第一个冗余定义与其一起被拉入 一些必要的定义。

这种奇怪的原因是:

bar

因为必须链接的第一个冗余函数定义(gcc -o prog main.o do_bar.o -L. -lfoobar -Wl,-rpath=$(pwd) )是 通过链接共享库bar提供,同时libfoobar.so的定义 do_bar所要求的是该共享库或任何其他共享库中的 not , 但是在目标文件中

bar提供的bar的定义将保留在libfoobar.so 程序与该共享库链接。它不会在物理上与之相关联 程序。这是动态联系的本质。但是所需的任何目标文件 链接 - 它是一个独立的目标文件,如do_bar.o还是一个 链接器从libsub.a(shared2.o)这样的存档中提取 - 只能是 实际链接到程序中。所以多余的do_bar出现在 prog的符号表。但多余的bar,解释了为什么do_bar存在, 不是那里。它位于libfoobar.so的符号表中。

当您在程序中发现死代码时,您可能希望链接器更智能。 通常,可以更聪明,但需要付出一些额外的努力。你需要问它垃圾收集部分, 在此之前,您需要让编译器通过生成 data-sections 来准备方法 目标文件中的功能部分。见How to remove unused C/C++ symbols with GCC and ld?,和 the answer

但这种修剪死代码的方式不适用于不寻常的情况 死代码在程序中链接,以满足共享库的冗余引用 联系所要求的。链接器只能递归地垃圾收集未使用的部分 输出到程序中的那些,它只输出输入的部分 来自目标文件,而不是来自要动态链接的共享库。

避免mainprog中的死代码的正确方法是不要做那种特殊的联系 共享库将包含程序未调用但必须调用的未定义引用 通过将死对象代码链接到程序来解决。

相反,当您构建共享库时,要么不在其中留下任何未定义的引用, 或者只保留未定义的引用,这些引用应该由它自己的动态依赖项来满足。

因此,构建我的libfoobar.so的正确方法是:

$ gcc -shared -o libfoobar.so foo.o bar.o do_bar.o

这给了我一个共享库,其API为:

void foo(void);
void bar(void);

对于谁想要其中一个或两个,并且没有未定义的引用。然后 我构建的程序是foo的客户端:

$ gcc -o prog main.o -L. -lfoobar -Wl,-rpath=$(pwd)
$ ./prog
foo

它不包含死代码:

$ nm prog | grep -e foo -e bar
                 U foo

同样,如果您构建没有未定义引用的libshared.so,例如:

$ gcc -c -fPIC shared2.c shared1.c
$ ar -crs libsub.a  shared1.o shared2.o
$ gcc -shared -o libcshared.so cshared1.o cshared2.o -L. -lsub

然后关联您的程序:

$ gcc -o main main.c libcmain.so  libcshared.so

它也没有死代码:

$ nm main | grep func
                 U func1a

如果您不喜欢libsub.a(shared1.o)libsub.a(shared2.o)这一事实 通过此解决方案物理链接到libcshared.so,然后采取 其他正统的链接共享库的方法:在func*中保留所有libcshared.so函数未定义:make libsub 共享库,然后是libcshared.so的动态依赖项。

答案 1 :(得分:0)

如果您只想摆脱未使用的功能,则可能不需要使用共享库。对于GCC,请尝试this。对于XL,请将-fdata-sections -ffunction-sections替换为-qfuncsect。一个重要的相关主题是使用导出/导入列表和可见性选项。它们控制链接到库中的额外符号是否导出到库外部。有关详细信息,请参阅here