Consulta sobre las opciones -ffunction-section y -fdata-sections de gcc

8 minutos de lectura

avatar de usuario
Arrendajo

Lo siguiente mencionado en la página GCC para las secciones de función y las opciones de secciones de datos:

-ffunction-sections
-fdata-sections

Coloque cada función o elemento de datos en su propia sección en el archivo de salida si el destino admite secciones arbitrarias. El nombre de la función o el nombre del elemento de datos determina el nombre de la sección en el archivo de salida. Utilice estas opciones en sistemas donde el enlazador puede realizar optimizaciones para mejorar la localidad de referencia en el espacio de instrucciones. La mayoría de los sistemas que utilizan el formato de objeto ELF y los procesadores SPARC que ejecutan Solaris 2 tienen enlazadores con dichas optimizaciones. AIX puede tener estas optimizaciones en el futuro.

Solo use estas opciones cuando haya beneficios significativos al hacerlo. Cuando especifica estas opciones, el ensamblador y el enlazador crearán objetos más grandes y archivos ejecutables y también serán más lentos. No podrá usar gprof en todos los sistemas si especifica esta opción y puede tener problemas con la depuración si especifica tanto esta opción como -g.

Tenía la impresión de que estas opciones ayudarían a reducir el tamaño del archivo ejecutable. ¿Por qué esta página dice que creará archivos ejecutables más grandes? ¿Me estoy perdiendo de algo?

avatar de usuario
Antón Staaf

Curiosamente, usando -fdata-sections puede hacer que los grupos literales de sus funciones y, por lo tanto, sus propias funciones sean más grandes. He notado esto en ARM en particular, pero es probable que sea cierto en otros lugares. El binario que estaba probando solo creció un cuarto de uno por ciento, pero creció. Mirando el desmontaje de las funciones cambiadas, estaba claro por qué.

Si todas las entradas BSS (o DATA) en su archivo de objeto se asignan a una sola sección, entonces el compilador puede almacenar la dirección de esa sección en el grupo de funciones literales y generar cargas con compensaciones conocidas desde esa dirección en la función para acceder a su datos. Pero si habilitas -fdata-sections coloca cada pieza de datos BSS (o DATOS) en su propia sección, y dado que no sabe cuáles de estas secciones podrían recolectarse como basura más adelante, o en qué orden el enlazador colocará todas estas secciones en la imagen ejecutable final, ya no puede cargar datos usando compensaciones desde una sola dirección. Entonces, en su lugar, tiene que asignar una entrada en el grupo literal por datos usados, y una vez que el enlazador haya averiguado qué va a la imagen final y dónde, entonces puede ir y arreglar estas entradas del grupo literal con la dirección real de los datos.

Así que sí, incluso con -Wl,--gc-sections la imagen resultante puede ser más grande porque el texto de la función real es más grande.

A continuación he añadido un ejemplo mínimo

El siguiente código es suficiente para ver el comportamiento del que estoy hablando. Por favor, no se desanime por la declaración volátil y el uso de variables globales, los cuales son cuestionables en código real. Aquí aseguran la creación de dos secciones de datos cuando se usa -fdata-sections.

static volatile int head;
static volatile int tail;

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

La versión de GCC utilizada para esta prueba es:

gcc version 6.1.1 20160526 (Arch Repository)

Primero, sin -fdata-sections obtenemos lo siguiente.

> 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

Desde arm-none-eabi-nm vemos que queue_empty tiene una longitud de 20 bytes (14 hexadecimales), y el arm-none-eabi-objdump la salida muestra que hay una sola palabra de reubicación al final de la función, es la dirección de la sección BSS (la sección para datos no inicializados). La primera instrucción en la función carga ese valor (la dirección del BSS) en r3. Las siguientes dos instrucciones se cargan en relación con r3, compensadas por 0 y 4 bytes respectivamente. Estas dos cargas son las cargas de los valores de cabeza y cola. Podemos ver esas compensaciones en la primera columna de la salida de arm-none-eabi-nm. los nop al final de la función es alinear las palabras de la dirección del grupo literal.

A continuación, veremos qué sucede cuando se agrega -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

Inmediatamente vemos que la longitud de queue_empty ha aumentado en cuatro bytes a 24 bytes (18 hexadecimales), y que ahora hay dos reubicaciones por realizar en el grupo literal de queue_empty. Estas reubicaciones corresponden a las direcciones de las dos secciones BSS que se crearon, una para cada variable global. Debe haber dos direcciones aquí porque el compilador no puede saber la posición relativa en la que el enlazador terminará poniendo las dos secciones. Mirando las instrucciones al principio de queue_empty, vemos que hay una carga extra, el compilador tiene que generar pares de carga separados para obtener la dirección de la sección y luego el valor de la variable en esa sección. La instrucción adicional en esta versión de queue_empty no hace que el cuerpo de la función sea más largo, simplemente ocupa el lugar que antes era un nop, pero ese no será el caso en general.

  • ¿Por qué el texto de la función es más grande después de que el enlazador arregló las direcciones? ¿Puede agregar una salida de ensamblaje que explique la diferencia?

    – Yakov Galka

    1 jun 2016 a las 14:35

  • El compilador tiene que generar múltiples cargas para obtener las direcciones de las múltiples secciones de datos, en lugar de una sola carga para el comienzo de la sección de datos monolíticos en el caso de las secciones que no son fdata. Agregaré un ejemplo como usted sugiere.

    -Anton Staaf

    24 de agosto de 2016 a las 21:56

  • Gracias por la respuesta. Tengo curiosidad, ¿sabes si usar -ffunction-sections -fdata-sections es efectivamente equivalente a usar un archivo fuente por símbolo (función o datos) o si hay alguna diferencia técnica?

    – PSkocik

    29 de septiembre de 2017 a las 12:39

  • @PSkocik, supongo que habrá algunas pequeñas diferencias en cómo se exportan los símbolos de las bibliotecas o cómo se maneja el almacenamiento estático o común. Pero es una aproximación de primer orden bastante buena.

    -Anton Staaf

    2 oct 2017 a las 21:49

  • ¿Todo lo descrito también se aplica a los ejecutables vinculados estáticamente que se usan con estas opciones, o las cosas cambian entonces?

    – nh2

    4 de abril de 2019 a las 3:23

avatar de usuario
leppie

Al usar esas opciones del compilador, puede agregar la opción del enlazador -Wl,--gc-sections eso eliminará todo el código no utilizado.

  • Si no pasamos las secciones -gc, esto no tendrá ningún efecto. ¿Correcto? Además, ¿hay alguna otra opción que pueda lograr el mismo efecto?

    – Jay

    26 de noviembre de 2010 a las 3:58

  • @Jay: Correcto, terminarás con un binario con mucho código sin usar. Para perfilar, se puede quitar. Funciona bien para la depuración todavía.

    – leppie

    26 de noviembre de 2010 a las 4:50

avatar de usuario
joder

Puedes usar -ffunction-sections y -fdata-sections en bibliotecas estáticas, lo que aumentará el tamaño de la biblioteca estática, ya que cada función y variable de datos globales se colocará en una sección separada.

y luego usar -Wl,--gc-sections en el programa que se vincula con esta biblioteca estática, que eliminará las secciones no utilizadas.

Por lo tanto, el binario final será más pequeño que sin esas banderas.

Sin embargo, tenga cuidado, ya que -Wl,--gc-sections puede romper cosas.

  • ¿Cómo -Wl,–gc-secciones pueden romper cosas? algún ejemplo?

    – compañero

    19 de enero de 2017 a las 4:48

  • @buddy “Las cosas se estropearán si el programa usa secciones especiales que deben incorporarse al programa cuando se extraen otras partes de los archivos .o, incluso si no se hace referencia a estas secciones “mágicas” en sí mismas”: elinux.org/images/2/2d/ELC2010-gc-sections_Denys_Vlasenko.pdf

    – Andreas

    29 de septiembre de 2020 a las 12:19

avatar de usuario
Rey Vilo

Obtengo mejores resultados agregando un paso adicional y construyendo un .a archivo:

  1. primero, gcc y g++ se usan con -ffunction-sections -fdata-sections banderas
  2. entonces todo .o los objetos se colocan en un .a archivar con ar rcs file.a *.o
  3. finalmente, el enlazador es llamado con -Wl,-gc-sections,-u,main opciones
  4. para todos, la optimización se establece en -Os.

Lo probé hace un tiempo y, al ver los resultados, parece que el aumento de tamaño proviene del orden de los objetos con una alineación diferente. Normalmente, el enlazador ordena los objetos para mantener pequeño el relleno entre ellos, pero parece que eso solo funciona dentro de una sección, no en las secciones individuales. Por lo tanto, a menudo obtiene un relleno adicional entre las secciones de datos para cada función, lo que aumenta el espacio general.

Sin embargo, para una biblioteca estática con -Wl,-gc-sections, la eliminación de la sección no utilizada probablemente compensará con creces el pequeño aumento.

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad