背景

在开发一个项目时,使用了非常多的第三方.a静态库文件,导致编译出的可执行文件非常大。这样一是占用ROM空间,二是会导致程序启动加载速度变慢(项目对启动时间非常敏感)。其实,这些静态库中的函数,并非所有都有调用,项目只使用了其中小部分。这种情况下,gcc的“-Wl,–gc-sections”参数,就非常有用。
补充说明:想要达到生成最终可执行文件,只链接.a库中用到的函数,需要在编译生成.a库时,就带有-ffunction-sections参数。

参数说明

英文原版的-Wl,–gc-sections解释说明如下:

6.3.3.2 Compilation options
The operation of eliminating the unused code and data from the final executable is directly performed by the linker.

In order to do this, it has to work with objects compiled with the following options: -ffunction-sections -fdata-sections.

These options are usable with C and Ada files. They will place respectively each function or data in a separate section in the resulting object file.

Once the objects and static libraries are created with these options, the linker can perform the dead code elimination. You can do this by setting the -Wl,–gc-sections option to gcc command or in the -largs section of gnatmake. This will perform a garbage collection of code and data never referenced.

If the linker performs a partial link (-r linker option), then you will need to provide the entry point using the -e / --entry linker option.

Note that objects compiled without the -ffunction-sections and -fdata-sections options can still be linked with the executable. However, no dead code elimination will be performed on those objects (they will be linked as is).

The GNAT static library is now compiled with -ffunction-sections and -fdata-sections on some platforms. This allows you to eliminate the unused code and data of the GNAT library from your executable.

大致说明如下:

  1. 在编译C、Ada源文件(C++也可以),在gcc/g++编译选项中增加-ffunction-sections-fdata-sections,在编译生成的.o目标文件中,会将每个函数或数据段,放在各种单独独立的section中;
  2. 在链接生成最终可执行文件时,如果带有-Wl,--gc-sections参数,并且之前编译目标文件时带有-ffunction-sections-fdata-sections参数,则链接器ld不会链接未使用的函数,从而减小可执行文件大小;
  3. 如果使用了-r的链接参数,来产生重定位的输出,需要显示的调用-e参数来指定程序入口。否则-Wl,--gc-sections不会生效。

关于GNU ld更多参数的说明,可以参考官网

使用示例

测试程序section_test.c如下:

#include <stdio.h>

void func_0(void) {
    printf("%s\n\r", __func__);
}

void func_1(void) {
    printf("%s\n\r", __func__);
}

void func_2(void) {
    printf("%s\n\r", __func__);
}

void func_3(void) {
    printf("%s\n\r", __func__);
}

void func_4(void) {
    printf("%s\n\r", __func__);
}

void func_5(void) {
    printf("%s\n\r", __func__);
}

void func_6(void) {
    printf("%s\n\r", __func__);
}

void func_7(void) {
    printf("%s\n\r", __func__);
}

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

Makefile如下:

.PHONY: test_sections test_normal

all: test_sections test_normal

test_sections:
	gcc -ffunction-sections -fdata-sections -c section_test.c
	gcc -Wl,--gc-sections -o $@ section_test.o
	mv section_test.o with_section.o

test_normal:
	gcc -c section_test.c
	gcc -o $@ section_test.o
	mv section_test.o no_section.o

clean:
	rm -rf *.o main_sections main_normal

执行make后,编译生成2个.o文件和2个可执行程序:no_section.owith_section.otest_normaltest_sections

首先,通过ls -l查看可执行文件大小。可以看到,没有链接未使用函数的test_sections稍微小一点。如果工程中使用的静态库.a多,且未使用的函数也多的话,效果会更显著:

-rwxr-xr-x 1 user user 8864 910 14:40 test_normal
-rwxr-xr-x 1 user user 8272 910 14:40 test_sections

然后,查看.o目标文件,在gcc编译选项-ffunction-sections -fdata-sections是否带来区别:

  • 没有-ffunction-sections -fdata-sections选项的目标文件,只有1个textrodata
  • 带有-ffunction-sections -fdata-sections选项的目标文件,分别有text.func_0text.func_7,以及8个rodata.__func__。说明gcc在编译生成目标文件时,将每个函数或数据段,放在各种单独独立的section
$ readelf -t no_section.o 
There are 13 section headers, starting at offset 0xa08:

Section Headers:
  [Nr] Name
       Type              Address          Offset            Link
       Size              EntSize          Info              Align
       Flags
  [ 0] 
       NULL                   NULL             0000000000000000  0000000000000000  0
  [ 1] .text
       PROGBITS               PROGBITS         0000000000000000  0000000000000040  0
  [ 2] .rela.text
       RELA                   RELA             0000000000000000  0000000000000670  10
  [ 3] .data
       PROGBITS               PROGBITS         0000000000000000  0000000000000148  0
  [ 4] .bss
       NOBITS                 NOBITS           0000000000000000  0000000000000148  0
  [ 5] .rodata
       PROGBITS               PROGBITS         0000000000000000  0000000000000148  0
$ readelf -t with_section.o 
There are 38 section headers, starting at offset 0xce8:

Section Headers:
  [Nr] Name
       Type              Address          Offset            Link
       Size              EntSize          Info              Align
       Flags
  [ 0] 
       NULL                   NULL             0000000000000000  0000000000000000  0
  [ 1] .text
       PROGBITS               PROGBITS         0000000000000000  0000000000000040  0
  [ 2] .data
       PROGBITS               PROGBITS         0000000000000000  0000000000000040  0
  [ 3] .bss
       NOBITS                 NOBITS           0000000000000000  0000000000000040  0
  [ 4] .rodata
       PROGBITS               PROGBITS         0000000000000000  0000000000000040  0
  [ 5] .text.func_0
       PROGBITS               PROGBITS         0000000000000000  0000000000000045  0
  [ 6] .rela.text.func_0
       RELA                   RELA             0000000000000000  0000000000000808  35
  [ 7] .text.func_1
       PROGBITS               PROGBITS         0000000000000000  0000000000000064  0
  [ 8] .rela.text.func_1
       RELA                   RELA             0000000000000000  0000000000000850  35
  [ 9] .text.func_2
       PROGBITS               PROGBITS         0000000000000000  0000000000000083  0
  [10] .rela.text.func_2
       RELA                   RELA             0000000000000000  0000000000000898  35
  [11] .text.func_3
       PROGBITS               PROGBITS         0000000000000000  00000000000000a2  0
  [12] .rela.text.func_3
       RELA                   RELA             0000000000000000  00000000000008e0  35
  [13] .text.func_4
       PROGBITS               PROGBITS         0000000000000000  00000000000000c1  0
  [14] .rela.text.func_4
       RELA                   RELA             0000000000000000  0000000000000928  35
  [15] .text.func_5
       PROGBITS               PROGBITS         0000000000000000  00000000000000e0  0
  [16] .rela.text.func_5
       RELA                   RELA             0000000000000000  0000000000000970  35
  [17] .text.func_6
       PROGBITS               PROGBITS         0000000000000000  00000000000000ff  0
  [18] .rela.text.func_6
       RELA                   RELA             0000000000000000  00000000000009b8  35
  [19] .text.func_7
       PROGBITS               PROGBITS         0000000000000000  000000000000011e  0
  [20] .rela.text.func_7
       RELA                   RELA             0000000000000000  0000000000000a00  35
  [21] .text.main
       PROGBITS               PROGBITS         0000000000000000  000000000000013d  0
  [22] .rela.text.main
       RELA                   RELA             0000000000000000  0000000000000a48  35
  [23] .rodata.__func__.2250
       PROGBITS               PROGBITS         0000000000000000  000000000000014d  0
  [24] .rodata.__func__.2254
       PROGBITS               PROGBITS         0000000000000000  0000000000000154  0
  [25] .rodata.__func__.2258
       PROGBITS               PROGBITS         0000000000000000  000000000000015b  0
  [26] .rodata.__func__.2262
       PROGBITS               PROGBITS         0000000000000000  0000000000000162  0
  [27] .rodata.__func__.2266
       PROGBITS               PROGBITS         0000000000000000  0000000000000169  0
  [28] .rodata.__func__.2270
       PROGBITS               PROGBITS         0000000000000000  0000000000000170  0
  [29] .rodata.__func__.2274
       PROGBITS               PROGBITS         0000000000000000  0000000000000177  0
  [30] .rodata.__func__.2278
       PROGBITS               PROGBITS         0000000000000000  000000000000017e  0

最后,再看一下,不带-Wl,–gc-sections参数生成的可执行文件,与带-Wl,–gc-sections参数生成的可执行文件的差别。很明显,在链接过程中,带-Wl,–gc-sections参数后,可执行文件仅联编了调用的函数。

$ readelf -a test_normal | grep func_
    55: 00000000000006c6    31 FUNC    GLOBAL DEFAULT   14 func_4
    57: 000000000000064a    31 FUNC    GLOBAL DEFAULT   14 func_0
    58: 0000000000000688    31 FUNC    GLOBAL DEFAULT   14 func_2
    60: 0000000000000704    31 FUNC    GLOBAL DEFAULT   14 func_6
    71: 00000000000006a7    31 FUNC    GLOBAL DEFAULT   14 func_3
    72: 0000000000000669    31 FUNC    GLOBAL DEFAULT   14 func_1
    73: 0000000000000723    31 FUNC    GLOBAL DEFAULT   14 func_7
    74: 00000000000006e5    31 FUNC    GLOBAL DEFAULT   14 func_5
readelf -a test_sections | grep func_
    48: 000000000000064a    31 FUNC    GLOBAL DEFAULT   14 func_0
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐