lunes, 22 de agosto de 2011

Generar ejecutables pequeños con VC++ 9.0 y Visual Studio 2008

En esta entrada voy a explicar cómo configurar Visual C++ 9.0, en el entorno Visual Studio 2008, para que genere ejecutables lo más pequeños posibles.

Me he basado en este artículo, que explica lo mismo para VC++ 6.0:
http://www.catch22.net/tuts/minexe

Para hacer las pruebas vamos a realizar un programa muy sencillo, que muestra un mensaje y se cierra.

#include <stdio.h>

int main(int argc, char* argv[])
{
	printf("Hola");
	return 0;
}

Este sencillo programa ocupa 30 Kb. Lo primero que hay que hacer cambiar la configuración del proyecto que por defecto está en modo DEBUG. Cambiándola a RELEASE conseguimos que el ejecutable baje a 7 kb. Ahora sobre esta configuración iremos cambiando cosas para disminuir aun mas el peso.

El compilador VC++ como se explica en el artículo que puse al principio, define su propio punto de entrada, donde hace distintas inicializaciones y ya después de todo eso ejecuta el main. Esquema de esta función:

int mainCRTStartup()
{
    int retval;
    init_heap();
    parse_command_line();
    init_global_vars();
    init_exception_handling();
    // finally call the user-defined main
    retval = main();
    // terminate all threads and exit
    ExitProcess(retval);
}

Esto supone código y dependencias extra. Podemos decir a VC++ que nuestro punto de entrada sea nuestra función main, de esta forma ignorará su función por defecto.

Para ello en Visual Studio, en las propiedades de nuestro proyecto en el apartado “Propiedades de configuración > Vinculador > Avanzadas” en “Punto de entrada” pondremos “main”.

image

Con esto reducimos el ejecutable hasta los 3.5 kb.

Hay que tener cuidado porque al saltarnos la función inicial de VC++ (donde se llamaba a “parse_command_line()”) ya no podemos acceder a los argumentos mediante argc y argv. Si necesitamos acceder a los mismos se puede hacer con la API GetCommandLine.

Sigamos adelgazando el ejecutable, veamos las secciones que tiene:

image

Una sección que nos podemos ahorrar es la de .reloc. Las relocalizaciones. Esta sección es mas típica de DLLs, que pueden ser cargadas en diferentes direcciones de memoria. En este ejecutable aparece porque esta activada la opción de DYNAMICBASE para que el ejecutable sea compatible con ASLR. Podemos confiar en que nuestro ejecutable conseguirá cargarse en la dirección base que tiene configurada por defecto y que no necesitará las relocalizaciones, en los ejecutables no suele dar problemas esto.

Para desactivar esta opción vamos a “Propiedades de configuración > Vinculador > Avanzadas”, “Dirección base aleatoria” y seleccionamos Deshabilitar.

image

Ahora el ejecutable pesa 3.37 kb.

Volvemos a ver las secciones y se ve como hay información de depuración en EOF. En la tabla de directorios también aparece:

image

Solo es una ruta hacia el fichero pdb de nuestro equipo el cual contiene toda la información de depuración, no nos hace ahorrar mucho, pero cuanto mas simple mantengamos el ejecutable mejor.

“Propiedades de configuración > Vinculador > Depuración”, “Generar información de depuración”, “No”.

Dejamos el ejecutable en los 3 kb.

Otra sección que se puede eliminar es la sección de recursos, .rsrc. se puede ver que solo contiene el archivo de manifiesto. En él se especifica los permisos con los que se necesita arrancar el ejecutable (UAC). Además también se incluye aquí algunas dependencias, ensamblados, como es el caso de MSVCR90.DLL, que solo se puede cargar desde el manifiesto (no entiendo muy bien el por qué, se admiten explicaciones en los comentarios y lo incorporo aquí).

El hecho de depender de MSVCR90.DLL hace además que sea necesario instalar el “Paquete redistribuible de Microsoft Visual C++ 2008” en aquellos equipos donde se vaya a ejecutar la aplicación.

¿Opciones?

  • Usar otra librería en tiempo de ejecución para C. Aquí podemos ver las alternativas:
    http://msdn.microsoft.com/en-us/library/abx4dbyh(v=VS.90).aspx
    Por defecto se usa “Multithreaded, dynamic link” /MD. La cual requiere la carga de la DLL MSVCR90.DLL.
    Se puede usar la opción “Multithreaded, static link”. La librería se “linka” estáticamente, nos libramos de DLLs externas pero el ejecutable incrementa su tamaño en 37 Kbytes.
  • Usar solamente la API de Windows, así no dependemos de librerías y no aumentamos el tamaño del ejecutable.

Como buscamos disminuir el tamaño del ejecutable elegimos la segunda opción. Así que nos toca buscar una equivalencia al printf con la API de Windows:

#include <windows.h>

int main()
{
	WriteConsoleA(GetStdHandle(STD_OUTPUT_HANDLE), "Hola", 4, NULL, NULL);
	return 0;
}

Como ya no se va a usar la librería en tiempo de ejecución para C, la quitamos. “Propiedades de configuración > Vinculador > Entrada” en “Omitir todas las bibliotecas predeterminadas” ponemos Si.

image

Ahora al compilar veremos un error:

image

Desaparece poniendo en “Propiedades de configuración > C/C++ > Generación de código” en “Comprobación de seguridad de buffer” No (/GS-).

image

Ya podremos quitar el manifiesto ya que no necesitamos permisos especiales para el UAC, ni la carga de ningún ensamblado. Con ello quitaremos también al sección .rsrc.

En “Propiedades de configuración > Vinculador > Archivo de manifiesto” en “Generar manifiesto” ponemos No.

image

Nos quitamos al sección .rsrc. 1 kb menos, el ejecutable pesa 2 kb.

Ahora solo tenemos 2 secciones .text y .rdata.

image

En .text tenemos el código mínimo para ejecutar nuestro programa. Sin inicializaciones ni cosas raras.

image

En .rdata están la cadena “Hola” y la Import Table.

Lo que sigue dando tamaño al ejecutable es la existencia de las 2 secciones alineadas. Para ello es posible combinar una en la otra con la siguiente directiva:

#include <windows.h>

#pragma comment(linker,"/merge:.rdata=.text")

int main()
{
	WriteConsoleA(GetStdHandle(STD_OUTPUT_HANDLE), "Hola", 4, NULL, NULL);
	return 0;
}

Con esto dejamos el ejecutable en 1 kb. Lo mas bajo que es posible dejarlo sin editar el ejecutable a mano. Subo aquí el proyecto de Visual Studio.

Para reducirlo mas, ~130 bytes, se puede leer este texto: Tiny PE, es una pasada, aunque el ejecutable generado no es compatible con Windows 7.

Saludos!

2 comentarios:

  1. Muy bueno!! La verdad que se agradece muchísimo al tener que reversear el binario por muy tonto que sea ;)

    ResponderEliminar
  2. Te tengo una pregunta.

    Quiero hacer un ejecutable o un batch. lo que sea. Que al abrir muestre una pagina web alojada en un servidor externo.

    Doy doble click y me abre un cuadro de 600x500 y me muestra una pagina en el

    ResponderEliminar