Cómo saber el tamaño de una función en Código Objeto

Por motivos complicados necesitaba conocer el tamaño de una función en C, una vez compilada a código objeto. A priori no hay forma simple de saberlo.

Acabé volcando el listado de funciones y sus direcciones a partir del código objeto, ordenando las funciones por su dirección, buscando la dirección de la función que me interesa y buscando la dirección de la función que la sigue. Restando ambas obtenía el tamaño de la función.

Años más tarde volví a revisar el tema. Lo que me molestaba era lo que sigue:

Tomemos el siguiente código C:

1
2
3
 int a(int x) {
     return 2*x;
 }

Si compilamos el código anterior y generamos código fuente en ensamblador de x86 con gcc -S -O9 a.c, vemos lo siguiente:

         .file   "a.c"
         .text
         .p2align 4
         .globl  a
         .type   a, @function
 a:
 .LFB0:
         .cfi_startproc
         endbr64
         leal    (%rdi,%rdi), %eax
         ret
         .cfi_endproc
 .LFE0:
         .size   a, .-a
         .ident  "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0"
         .section        .note.GNU-stack,"",@progbits
         .section        .note.gnu.property,"a"
         .align 8
         .long    1f - 0f
         .long    4f - 1f
         .long    5
 0:
         .string  "GNU"
 1:
         .align 8
         .long    0xc0000002
         .long    3f - 2f
 2:
         .long    0x3
 3:
         .align 8
 4:

La clave del asunto es la línea 14: .size a, .-a. Según el manual de gas, eso guarda como tamaño de la función a la resta entre la posición actual del ensamblador y la primera instrucción de a. Es decir, resta a la dirección final de la función la dirección inicial. Es decir, obtenemos el tamaño.

Aparentemente, entonces, se está generando esa información y se guarda en el código objeto. ¿Cómo acceder a ella?

Compilemos a código objeto con gcc -O9 -c a.c. Esto genera un fichero binario de 1344 bytes.

Veamos ahora un par de comandos:

jcea@jcea:/tmp/ram$ nm -S a.o
0000000000000000 0000000000000008 T a

Usando nm con el parámetro -S, el primer campo es la dirección de la función y el segundo es el tamaño de la misma, ambos en hexadecimal.

El manual de nm dice lo siguiente respecto a parámetro -S:

Print both value and size of defined symbols for the "bsd" output style. This option has no effect for object formats that do not record symbol sizes, unless --size-sort is also used in which case a calculated size is displayed.

Podemos utilizar otro comando más:

jcea@jcea:/tmp/ram$ objdump -t a.o

a.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 a.c
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
0000000000000000 l    d  .note.gnu.property     0000000000000000 .note.gnu.property
0000000000000000 l    d  .eh_frame      0000000000000000 .eh_frame
0000000000000000 l    d  .comment       0000000000000000 .comment
0000000000000000 g     F .text  0000000000000008 a

El comando objdump con el parámetro -t nos da la información que queremos. Aquí podemos ver, en la última línea, que el primer campo es la dirección inicial de la función y el quinto campo es su tamaño, en hexadecimal.

El manual dice lo siguiente:

Here the first number is the symbol's value (sometimes refered to as its address). The next field is actually a set of characters and spaces indicating the flag bits that are set on the symbol. These characters are described below. Next is the section with which the symbol is associated or ABS if the section is absolute (ie not connected with any section), or UND if the section is referenced in the file being dumped, but not defined there.

After the section name comes another field, a number, which for common symbols is the alignment and for other symbol is the size. Finally the symbol's name is displayed.