El utilitario size
permite listar los tamaños de las secciones de un archivo binario ejecutable pasado como parámetro, junto con su tamaño total. De esta forma es posible comprender cuánta memoria será utilizada al ejecutar el binario y a su vez examinar cómo está compuesto un ejecutable: cuánta memoria se dedica a datos inicializados; cuánta se utiliza para instrucciones; y cuánta se utiliza para variables o espacio para datos no inicializados.
En ocasiones resulta necesario comprender cómo utiliza la memoria RAM un ejecutable. Cuánta memoria se dedica a instrucciones, cuánta se "pierde" en datos y variables, etc. Por ejemplo cuando se está optimizando un programa o porción de código, al menos en cuanto a uso de memoria respecta.
Veamos un ejemplo analizando cómo se inicializan las variables en el lenguaje C según el scope (ámbito) donde se declaren.
El siguiente programa escrito en C (test.c
) abre el archivo "/etc/apt/sources.list" en modo lectura y vuelca su contenido por salida estándar (pantalla) recuperando de a 100 bytes por vez:
#include <stdio.h> #include <stdlib.h> #define F "/etc/apt/sources.list" #define BUFFER_SIZE 100 int main() { FILE *f; char buffer[BUFFER_SIZE]; f = fopen(F,"r"); while ( fgets(buffer,BUFFER_SIZE,f) != NULL ) { printf("%s",buffer); } return 0; }
Veamos qué resulta al compilar el programa con gcc
(GNU C Compiler):
emi@hal9000:/tmp$ gcc -Wall -o test test.c
Se crea el binario ejecutable test
:
emi@hal9000:/tmp$ ls -la test* -rwxr-xr-x 1 emi emi 16712 Jan 3 11:12 test -rw-r--r-- 1 emi emi 344 Jan 3 11:12 test.c
Como se trata de un sistema GNU/Linux de 64 bits, el formato del ejecutable es "ELF 63-bit":
emi@hal9000:/tmp$ file test test: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=e6db579250084fcd059c6d0cb8a45b014b83eadd, not stripped
Para conocer el tamaño resultante de cada sección del programa en el espacio de memoria virtual, es necesario contar con el utilitario size
. Este es parte del paquete binutils:
emi@hal9000:/tmp$ size test text data bss dec hex filename 1682 600 8 2290 8f2 test
Por defecto muestra un formato de salida similar a la versión de size de Berkeley. Este formato muestra una única línea por cada binario pasado como parámetro totalizando la memoria utilizada por cada sección. Sin embargo, mediante la opción --format
es posible hacer que se comporte como la versión de size de System V:
emi@hal9000:/tmp$ size --format=SysV test test : section size addr .interp 28 680 .note.ABI-tag 32 708 .note.gnu.build-id 36 740 .gnu.hash 36 776 .dynsym 216 816 .dynstr 144 1032 .gnu.version 18 1176 .gnu.version_r 32 1200 .rela.dyn 192 1232 .rela.plt 72 1424 .init 23 4096 .plt 64 4128 .plt.got 8 4192 .text 417 4208 .fini 9 4628 .rodata 31 8192 .eh_frame_hdr 60 8224 .eh_frame 264 8288 .init_array 8 15848 .fini_array 8 15856 .dynamic 480 15864 .got 40 16344 .got.plt 48 16384 .data 16 16432 .bss 8 16448 .comment 28 0 Total 2318
Este formato detalla todas las secciones encontradas en el programa.
Volviendo al código del programa, dentro de la función "main" se declara un arreglo de 100 bytes que funciona como buffer de disco a memoria:
int main() { FILE *f; char buffer[BUFFER_SIZE];
Tal vez el lector despistado o programador novicio pudiera esperar que la sección .bss
(espacio de memoria para variables o datos no inicializados) ocupe al menos 100 bytes. No obstante, apenas ocupa 8 bytes:
emi@hal9000:/tmp$ size --format=SysV test | grep .bss .bss 8 16448
Esto se debe a que, en el lenguaje C, las variables locales de una función se almacenan en la pila (stack) utilizando un espacio que se conoce como "stack frame", y se inicializan durante la llamada a la función dependiendo de la convención de llamado (caller rules vs. calee rules). Y, tal como se ha visto, el buffer está declarado dentro de la función "main".
Veamos qué pasa ahora si se declara como variable global del programa:
emi@hal9000:/tmp$ cat test.c #include <stdio.h> #include <stdlib.h> #define F "/etc/apt/sources.list" #define BUFFER_SIZE 100 char buffer[BUFFER_SIZE]; int main() { FILE *f; f = fopen(F,"r"); while ( fgets(buffer,BUFFER_SIZE,f) != NULL ) { printf("%s",buffer); } return 0; }
Se observa que la declaración del buffer ha sido movida fuera de la función "main".
emi@hal9000:/tmp$ gcc -Wall -o test test.c
Al compilar nuevamente, es posible comprobar que la sección .bss
ahora ha "engordado" debido a que debe alojar la variable global buffer
:
emi@hal9000:/tmp$ size test text data bss dec hex filename 1698 600 136 2434 982 test
Lo mismo ocurre si se declara a la variable dentro de la función, pero como variable estática:
emi@hal9000:/tmp$ cat test.c #include <stdio.h> #include <stdlib.h> #define F "/etc/apt/sources.list" #define BUFFER_SIZE 1000 int main() { FILE *f; static char buffer[BUFFER_SIZE]; f = fopen(F,"r"); while ( fgets(buffer,BUFFER_SIZE,f) != NULL ) { printf("%s",buffer); } return 0; }
Se agrega el modificador "static" delante de la variable declarada dentro de la función "main". A su vez el tamaño del arreglo se cambia por 1000:
emi@hal9000:/tmp$ gcc -Wall -o test test.c
Al compilar nuevamente, la sección .bss
ocupa al menos 1000 bytes:
emi@hal9000:/tmp$ size test text data bss dec hex filename 1698 600 1032 3330 d02 test
Tal como se comprueba a través de estos experimentos, no sólo puede size
ser útil para optimizar o depurar programas, sino que además puede resultar una buena herramienta para el aprendizaje y comprensión de tecnologías o lenguajes de programación.
Referencias
- x86 Assembly Guide - Calling Convention
- Memory Layout of C Programs
- Computer Science 315 Lecture Notes - 10.4. Memory Segments