当静态链接 C 库时,是否将整个库添加到可执行文件中?

When a C library is statically linked does the whole library get added to the executable?

背景信息:我正在尝试比较执行某些数值计算的两段代码的内存需求。为此,我将编译后的 C 代码的大小与静态链接的数学库进行比较。

但是,我发现了一些奇怪的结果,这些结果似乎表明正在添加整个库。我在下面描述了一个 MWE

// Program ex1.c
# include<math.h>
void main (void)
{
    float a = exp(2);

}

// Program ex2.c
# include <math.h>
void main(void)
{
    float a = exp(2);
    float b = pow(3,4);
    float c = sin(3.14159);
}

我编译文件如下:

gcc -static -o ex1static.out ex1.c -lm
gcc -static -o ex2static.out ex2.c -lm

如果程序 1 的编译对象只包含 exp() 的代码,而程序 2 的编译对象包含 exp()、pow() 和 sin() 的代码,那么第二个将是比第一个大。但两个对象的大小相同,均为 912.6 kB。

为什么会发生这种情况,是否有任何方法可以确保只将所需的代码部分添加到对象中?

静态库是目标文件的存档,静态库中的链接仅添加解析至少一个未定义引用的存档的那些目标文件成员。

为了确保只添加所需的代码,静态库需要由小目标文件组成,最好每个文件中包含一个导出的全局文件。

除此之外,如果使用 -ffunction-sections/-fdata-sections 编译库,然后将 --gc-sections 传递给链接器,您可以获得类似的效果。

-ffunction-sections -fdata-sections 方法基本上等同于每个源一个全局的方法,但是使用源文件来建立边界更加灵活,因为有时可能需要将事物组合在一起(较大的翻译单元可能会导致更紧凑和更优化的代码)。

无论如何,在你的情况下(lib 不受你的控制),你可以尝试的是 -Wl,--gc-sections(gcc 的 -Wl 选项为 gcc 应该传递给链接器的内容加上前缀) 通过您的示例和 glibc,我能够从原来的 849KiB 中减少大约 41KiB。

不是很令人印象深刻,但是 glibc 无论如何都没有考虑静态链接。 使用 libc 库可以获得更好的结果,例如 musl-libc.

for ex in ex{1,2}.c; do for flg in '' -Wl,--gc-sections; do echo "$ex $flg"; musl-gcc -O0 $ex -static -lm $flg call.c && \ls -l a.out ; done ; done
ex1.c 
-rwxrwx--- 1 pjmp pjmp 8064 Jun 29 19:11 a.out
ex1.c -Wl,--gc-sections
-rwxrwx--- 1 pjmp pjmp 7744 Jun 29 19:11 a.out
ex2.c 
-rwxrwx--- 1 pjmp pjmp 8064 Jun 29 19:11 a.out
ex2.c -Wl,--gc-sections
-rwxrwx--- 1 pjmp pjmp 7744 Jun 29 19:11 a.out

现在好多了,但您可能想知道为什么示例 1 和 2 的大小相同。

如果你添加-Wl,--print-map,你会发现musl-libc的相关目标文件根本没有被包含 在任一情况下。原因是,gcc 知道这些标准函数,它通过插入操作码而不是生成的函数调用来作弊。您可以通过添加一个由另一个翻译单元促进的间接层来在某种程度上击败 gcc 的作弊。

call.c:

double call1(double(*X)(double A), double A) { return X(A); }
double call2(double(*X)(double A,double B), double A, double B){ return X(A,B); }

Ex1.c

# include<math.h>
double call1(double(*X)(double A), double A);
double call2(double(*X)(double A,double B), double A, double B);
int main (void)
{
    float a = call1(exp,2);
}

Ex2.c

# include <math.h>
double call1(double(*X)(double A), double A);
double call2(double(*X)(double A,double B), double A, double B);
int main(void)
{
    float a = call1(exp,(2));
    float b = call2(pow,3,4);
    float c = call1(sin,(3.14159));
}

现在这给了我:

Ex1.c 
-rwxrwx--- 1 pjmp pjmp 8216 Jun 29 19:15 a.out
Ex1.c -Wl,--gc-sections
-rwxrwx--- 1 pjmp pjmp 7984 Jun 29 19:15 a.out
Ex2.c 
-rwxrwx--- 1 pjmp pjmp 17088 Jun 29 19:15 a.out
Ex2.c -Wl,--gc-sections
-rwxrwx--- 1 pjmp pjmp 16856 Jun 29 19:15 a.out

——这两个例子之间有一个明显的区别,这要归功于 musl 的组成方式 many small source/object files 以便在静态链接时不会添加(或不多于)相关的引用代码。