【问题标题】:Query on -ffunction-section & -fdata-sections options of gcc查询 gcc 的 -ffunction-section & -fdata-sections 选项
【发布时间】:2011-05-15 13:25:08
【问题描述】:

以下在 GCC 页面中提到的功能部分和数据部分选项:

-ffunction-sections
-fdata-sections

如果目标支持任意部分,则将每个函数或数据项放入输出文件中自己的部分。函数的名称或数据项的名称决定了输出文件中节的名称。 在链接器可以执行优化以提高指令空间中的引用局部性的系统上使用这些选项。大多数使用 ELF 对象格式和运行 Solaris 2 的 SPARC 处理器的系统都有具有此类优化的链接器。 AIX 将来可能会有这些优化。

只有在这样做有显着好处时才使用这些选项。 当您指定这些选项时,汇编器和链接器将创建更大的对象和可执行文件,并且速度也会变慢。指定此选项,如果同时指定此选项和 ​​-g,您可能会遇到调试问题。

我的印象是这些选项将有助于减小可执行文件的大小。为什么这个页面说它会创建更大的可执行文件?我错过了什么吗?

【问题讨论】:

    标签: c gcc optimization linker size


    【解决方案1】:

    有趣的是,使用-fdata-sections 可以使您的函数的字面量池,从而使您的函数本身更大。我特别在 ARM 上注意到了这一点,但在其他地方可能也是如此。我正在测试的二进制文件只增长了四分之一个百分点,但它确实增长了。查看已更改功能的反汇编,原因就很清楚了。

    如果目标文件中的所有 BSS(或 DATA)条目都分配给单个部分,则编译器可以将该部分的地址存储在函数字面量池中,并使用函数中该地址的已知偏移量生成加载访问您的数据。但是,如果您启用-fdata-sections,它会将每条 BSS(或 DATA)数据放入其自己的部分,并且由于它不知道这些部分中的哪些可能会在以后被垃圾收集,或者链接器将按什么顺序放置所有这些部分进入最终的可执行映像,它不能再使用来自单个地址的偏移量加载数据。因此,它必须为每个使用的数据在文字池中分配一个条目,一旦链接器弄清楚最终图像中的内容和位置,它就可以使用实际地址修复这些文字池条目数据。

    所以是的,即使使用-Wl,--gc-sections,生成的图像也会更大,因为实际的函数文本更大。

    下面我添加了一个小例子

    下面的代码足以看到我正在谈论的行为。请不要被 volatile 声明和全局变量的使用所迷惑,这两者在实际代码中都是有问题的。在这里,它们确保在使用 -fdata-sections 时创建两个数据段。

    static volatile int head;
    static volatile int tail;
    
    int queue_empty(void)
    {
        return head == tail;
    }
    

    本次测试使用的GCC版本为:

    gcc version 6.1.1 20160526 (Arch Repository)
    

    首先,如果没有 -fdata-sections,我们会得到以下内容。

    > arm-none-eabi-gcc -march=armv6-m \
                        -mcpu=cortex-m0 \
                        -mthumb \
                        -Os \
                        -c \
                        -o test.o \
                        test.c
    
    > arm-none-eabi-objdump -dr test.o
    
    00000000 <queue_empty>:
     0: 4b03     ldr   r3, [pc, #12]   ; (10 <queue_empty+0x10>)
     2: 6818     ldr   r0, [r3, #0]
     4: 685b     ldr   r3, [r3, #4]
     6: 1ac0     subs  r0, r0, r3
     8: 4243     negs  r3, r0
     a: 4158     adcs  r0, r3
     c: 4770     bx    lr
     e: 46c0     nop                   ; (mov r8, r8)
    10: 00000000 .word 0x00000000
                 10: R_ARM_ABS32 .bss
    
    > arm-none-eabi-nm -S test.o
    
    00000000 00000004 b head
    00000000 00000014 T queue_empty
    00000004 00000004 b tail
    

    arm-none-eabi-nm 我们看到 queue_empty 有 20 个字节长(14 hex),arm-none-eabi-objdump 的输出显示函数末尾有一个重定位字,它是 BSS 部分的地址(未初始化数据的部分)。函数中的第一条指令将该值(BSS 的地址)加载到 r3 中。接下来的两条指令相对于 r3 加载,分别偏移 0 和 4 个字节。这两个载荷是head和tail值的载荷。我们可以在arm-none-eabi-nm 的输出的第一列中看到这些偏移量。函数末尾的nop是字对齐字面量池的地址。

    接下来我们将看看添加 -fdata-sections 时会发生什么。

    arm-none-eabi-gcc -march=armv6-m \
                      -mcpu=cortex-m0 \
                      -mthumb \
                      -Os \
                      -fdata-sections \
                      -c \
                      -o test.o \
                      test.c
    
    arm-none-eabi-objdump -dr test.o
    
    00000000 <queue_empty>:
     0: 4b03     ldr   r3, [pc, #12]    ; (10 <queue_empty+0x10>)
     2: 6818     ldr   r0, [r3, #0]
     4: 4b03     ldr   r3, [pc, #12]    ; (14 <queue_empty+0x14>)
     6: 681b     ldr   r3, [r3, #0]
     8: 1ac0     subs  r0, r0, r3
     a: 4243     negs  r3, r0
     c: 4158     adcs  r0, r3
     e: 4770     bx    lr
        ...
                 10: R_ARM_ABS32 .bss.head
                 14: R_ARM_ABS32 .bss.tail
    
    arm-none-eabi-nm -S test.o
    
    00000000 00000004 b head
    00000000 00000018 T queue_empty
    00000000 00000004 b tail
    

    我们立即看到 queue_empty 的长度增加了 4 个字节到 24 个字节(18 十六进制),并且在 queue_empty 的字面量池中现在需要进行两次重定位。这些重定位对应于所创建的两个 BSS 部分的地址,每个部分对应一个全局变量。这里需要有两个地址,因为编译器无法知道链接器最终会将这两个段放入的相对位置。查看 queue_empty 开头的指令,我们看到有额外的负载,编译器必须生成单独的加载对来获取节的地址,然后获取该节中变量的值。这个版本的 queue_empty 中的额外指令并没有使函数的主体更长,它只是占据了以前是 nop 的位置,但一般情况下不会这样。

    【讨论】:

    • 为什么链接器修复地址后函数文本变大了?你能添加一个解释差异的汇编输出吗?
    • 编译器必须生成多次加载来获取多个数据段的地址,而不是在非 fdata-sections 情况下为单一数据段的开头进行一次加载。我会按照你的建议添加一个例子。
    • 感谢您的回答。我很好奇,您知道使用-ffunction-sections -fdata-sections 是否实际上等同于每个符号(函数或数据)使用一个源文件,或者是否存在任何技术差异?
    • @PSkocik,我猜想从库中导出符号的方式或处理静态或通用存储的方式会有一些细微差别。但这是一个非常好的一阶近似值。
    • 所有描述是否也适用于与这些选项一起使用的静态链接可执行文件,或者情况会改变吗?
    【解决方案2】:

    使用这些编译器选项时,您可以添加链接器选项-Wl,--gc-sections,它将删除所有未使用的代码。

    【讨论】:

    • 如果我们不通过-gc-sections,那么这将没有任何效果。正确的?另外,有没有其他方案可以达到同样的效果?
    • @Jay:是的,你最终会得到一个包含大量未使用代码的二进制文件。对于分析,它可以被删除。仍然适用于调试。
    【解决方案3】:

    您可以在静态库上使用-ffunction-sections-fdata-sections,这将增加静态库的大小,因为每个函数和全局数据变量都将放在一个单独的部分中。

    然后在与此静态库链接的程序上使用-Wl,--gc-sections,这将删除未使用的部分。

    因此,最终的二进制文件将比没有这些标志时更小。

    不过要小心,因为-Wl,--gc-sections 会破坏事物。

    【讨论】:

    【解决方案4】:

    添加一个额外的步骤并构建一个.a 存档,我得到了更好的结果:

    1. 首先,gcc和g++与-ffunction-sections-fdata-sections标志一起使用
    2. 然后,所有.o 对象都被放入带有ar rcs file.a *.o.a 存档
    3. 最后,使用-Wl,-gc-sections,-u,main 选项调用链接器
    4. 总而言之,优化设置为-Os

    【讨论】:

    • 很好奇为什么 -u,main 指向链接器?其他带有 -Wl,-gc-sections 文件大小优化的 SO 条目也不使用 -u,main
    【解决方案5】:

    我前一阵子尝试过,查看结果似乎尺寸增加来自具有不同对齐方式的对象的顺序。通常,链接器对对象进行排序以使它们之间的填充保持较小,但看起来这只适用于一个部分,而不是各个部分。因此,您通常会在每个函数的数据部分之间获得额外的填充,从而增加整体空间。

    对于带有 -Wl,-gc-sections 的静态库,删除未使用的部分很可能会弥补小幅增加。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-01-11
      • 2019-10-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多