¿Sabes que es un Binario Multillamada?. ¿Te preguntas como
funciona Busybox
?. ¿Cómo es posible hacer que un programa
se comporte como si fuera otro?. ¿Cómo escribo uno de esos programas?.
Si alguna vez te has preguntado esto, o si tienes curiosidad por la
respuesta tienes que leer este artículo. Sino, bueno, deberías leerlo en
cualquier caso.
Como veréis muy pronto esto de los binarios o programas multillamada es algo bastante guay y que en ciertas ocasiones puede resultar muy útil. Además nos va a ayudar a entender mejor como funcionan los programas y a aprender como crear vosotros mismos estos curiosos entes cibernéticos. Así que, sin más dilación, vamos a comenzar respondiendo esas intrigantes pregunta que hemos planteado al inicio del artículo.
¿Qué es un Multicall Binary?
La verdad que la traducción es horrorosa así que voy a utilizar el término parcialmente traducido Binario Multicall que creo que tiene más sentido. El nombre parece bastante sofisticado, pero simplemente estamos hablando de programas que se comportan de forma diferente dependiendo de como se ejecuten.
Existen más caso, pero quizás uno de los ejemplos más conocidos, al
menos en el mundo de los sistemas embebidos, es Busybox
.
Busybox
es un proyecto que nos ofrece las principales
herramientas que usamos en el espacio de usuario de un sistema GNU/Linux
(ls
, grep
, cat
,…) pero ocupando
muy poco espacio. Normalmente menos de 1Mb.
Para conseguir esto, hace uso de diversas técnicas, como usar versiones de libC más ligeras como por ejemplo uClib, que es mucho más pequeña que la gLibC que usamos normalmente en nuestros ordenadores, o incluir todas esas utilidades en un único binario… Un binario multicall.
Busybox
es hoy por hoy el estándar de facto para el
espacio de usuario de sistemas embebidos basados en Linux, y lo
encontraréis en multitud de routers (proyectos como OpenWRT o Lede lo
utilizan) y otros dispositivos IoT. En otro artículo podemos profundizar
más en Busybox
, pero en esta ocasión de lo que queremos
hablar es los binarios multicall.
Ventajas de los Binarios Multicall
Construir programas de esta manera tiene ciertas ventajas,
especialmente cuando hablamos de sistemas con reducida capacidad de
memoria y almacenamiento. Tomemos una vez más Busybox
como
ejemplo. Busybox
incluye unas 300 utilidades, si miramos
solamente a la información del fichero ELF, asumiendo unos valores
típicos de 10 cabeceras de programa (Program headers) a 56
bytes cada uno y 30 secciones a 64 bytes cada uno:
Cabecera : 64
Program headers : 10 x 56 = 560
Secciones : 30 x 64 = 1920
2544
Utilidades : 300 = 763200 -> 745 KiB
Es decir, simplemente empaquetando las 300 utilidades en una sola nos estaríamos ahorrando unos 700KiB. Bueno, este es un cálculo bastante aproximado, pero pilláis la idea.
Por otra parte, hay gran cantidad de código que es compartido por muchos programas. Pensad por ejemplo, en el manejo de los parámetros de la línea de comandos, prácticamente todas las herramientas del espacio de usuario deben leer e interpretar esos parámetros y eso se hace con el mismo código en todos ellos.
Por supuesto, podríamos usar una librería, si bien en este tipo de sistema con tan pocos recursos, utilizar binarios dinámicos implica más espacio ya que hace falta un linker dinámico y una cierta infraestructura, además de más tiempo de carga puesto que es necesario cargar todos los ficheros de las librerías y resolver los símbolos, lo que en un procesador modesto puede tener un cierto impacto.
Como podéis ver empaquetar nuestras herramientas de esta forma tiene ciertas ventajas, especialmente cuando el tamaño es un tema a tener en cuenta. Quizás la mayor desventaja de esta solución es que no es posible actualizar programas de forma separada. Si queremos incluir una nueva versión de alguna de las utilidades deberemos recompilarlo todo de nuevo.
Ahora que ya sabemos de que estamos hablando veamos como podemos programar estas criaturas.
¿Cómo funciona un Binario Multicall?
Independientemente de lo guay que pueda parecer todo esto, la forma de implementarlo es muy sencilla y se basa en el hecho de que el primer parámetro que recibimos en nuestro programa es la cadena de caracteres que utilizamos para ejecutarlo. Esto es cierto, siempre que ejecutemos nuestro programa a través de la línea de comandos o utilizando herramientas o servicios estándar, puesto que ese es el convenio estipulado para sistema UNIX.
Sabiendo esto, veamos paso a paso como conseguir un binario multicall usando un simple script shell. Empecemos con un sencillo programa que simplemente obtiene la cadena con la que lo hemos invocado.
#!/bin/sh
$PRG=$(basename $0)
echo $PRG
Si damos permisos de ejecución a nuestro programa y lo ejecutamos obtendremos esto:
$ chmod +x test.sh $ ./test.sh test
Observad que pasa ahora si creamos un enlace simbólico a nuestro script:
$ ln -s test.sh test1 $ ./test1 test1
Ahora el script imprime test1
en lugar de
test,sh
. Si comparamos esa cadena en nuestro script y
hacemos que el código a ejecutar sea diferente, habremos conseguido un
script multicall!.
#!/bin/sh
PRG=$(basename $0)
if [ "$PRG" = "test1" ]
then
echo "TEST1"
elif [ "$PRG" = "test2" ]
then
echo "TEST2"
else
echo "test puede ejecutar test1 o test2"
fi
Ahora, creemos un nuevo enlace simbólico llamado test2
y
veamos que pasa:
$ ln -s test.sh test2 $ ./test.sh test puede ejecutar test1 o test2 $ ./test1 TEST1 $ ./test2 TEST2
Era más fácil de lo que parecía eh?. Bien!. Ahora que conocemos la teoría del funcionamiento de todo esto, vamos a ver como poder hacer lo mismo usando el lenguaje C.
Tu primer Multicall Binary
Para nuestro programa en C vamos a intentar duplicar el
comportamiento de Busybox
, el cual es más o menos el
siguiente:
- Cuando se ejecuta sin parámetros muestra un mensaje de ayuda.
- Cuando se ejecuta con el flag
--install dir
se instala en el directorio indicado. - Si se ejecuta pasando como primer parámetro el nombre de una de las
utilidades que implementa, la ejecutará. Es decir,
busybox cat /etc/passwd
es lo mismo quecat /etc/passwd
. - Si se ejecuta uno de los enlaces simbólicos, ejecutará la utilidad indicada por el enlace directamente.
Así que vamos a ello, el programa no tiene mucho que rascar una vez
que sabes lo que tienes que hacer. Comencemos con la función
main
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
int main (int argc, char *argv[]) {
int p_indx = 0;
[0] = strip_path(argv[0]);
argvif (!strcmp (argv[0], "tool")) {
// Ejecutado como tool.... Veamos si hay argumentos
if (argc == 1) { // Sin argumentos muestra ayuda
(stderr, "tool 1.0\n(c) ROOR 2025\n");
fprintf (stderr, " tool --install dir : Para instalar\n");
fprintf (stderr, " Plug-ins:\n tool1 tool2\n");
fprintf return 1;
} else if ((argc == 3) && (!strcmp(argv[1], "--install")) {
// Si el primer parametro es install y tenemos 3 argumentos
// instalamos el Binario Multicall
(argv[2]);
install return 1;
}
// En este punto tenemos que ejecutar uno de los programas
// así que nos saltamos el primer argumento que es tool
--;
argc++;
argv}
[0] = strip_path(argv[0]);
argvif (!strcmp (argv[0], "tool1")) tool1 (argc, argv);
else if (!strcmp (argv[0], "tool2")) tool2 (argc, argv);
}
Hemos incluido ya todos los ficheros de cabecera que vamos a
necesitar para el programa completo. En seguida veremos como implementar
la función strip_path
, pero por ahora solo necesitamos
saber que es el equivalente a basename
que usamos en
nuestro script shell. Básicamente nos devuelve el nombre del programa
que estamos ejecutando sin ningún path al principio. Una vez que tenemos
el nombre del programa solo necesitamos hacer comparaciones de cadenas.
Súper fácil.
Si el nombre del programa es tool
es que lo estamos
ejecutando directamente. En ese caso tenemos tres opciones:
- Si no hemos pasado ningún parámetro (
argc ==1
) mostramos un mensaje de ayuda. - Si hemos pasado 3 parámetros y el segundo es
--install
procedemos a la instalación del multicall binary - En otro caso, eliminamos el primer parámetro y dejamos que el programa continúe.
El resto del programa procesa tanto los enlaces simbólicos como los
argumentos recibidos por el programa tool
. Una vez más
tenemos que limpiar el path del binario a ejecutar para que nuestro
código funciones independientemente de como se ejecuta el programa.
Veamos ahora como implementar el resto de funciones que necesitamos en este programa.
Filtrando Paths
Lo primero que vamos a hacer es escribir la función para eliminar los “paths” al principio del nombre del programa. Sería algo como esto:
char *strip_path (char *str) {
char * aux = str + strlen (str);
while (*--aux != '/' && aux != str);
return *aux == '/' ? aux + 1 : str;
}
La función, simplemente se coloca al final de la cadena y busca hacia
atrás el caracter /
. Si lo encuentra retorna el puntero a
la posición en la que lo encontró + 1, y sino devuelve la cadena
original.
La implementación de esta función no es segura por el uso de
strlen
. La función fallará si se pasa una cadena de
caracteres que no esté terminada con 0. En este caso, puesto que los
únicos parámetros que recibe esta función son los valores de
argv
que genera el interprete de comandos siempre estarán
terminados correctamente.
Instalación
La función de instalación es también bastante sencilla. Ya hemos realizado el proceso manualmente para nuestro script shell, ahora solo tenemos que convertirlo en un programa C. Este es el código:
int install (char *path) {
char cmd[4096], dst[1024], src[1024];
*dir;
DIR
("+ Instalando en %s\n", path);
printf if (access (path, W_OK | R_OK | X_OK) == -1) {
("No puedo acceder al directorio de instalación\n");
printf (1);
exit }
// XXX: Nunca uses system con entradas de usuario!
(cmd, 1024, "cp tool %s/.", path);
snprintf (cmd);
system (src, 1024, "%s/tool", path);
snprintf (dst, 1024, "%s/tool1", path);
snprintf (src, dst);
symlink (dst, 1024, "%s/tool2", path);
snprintf (src, dst);
symlink }
La función recibe como parámetro el directorio en el que queremos instalar nuestro Multicall binary. Lo primero que hace es comprobar si tenemos los permisos de acceso adecuados para poder instalar el programa. Tras eso copia el binario principal a ese directorio y luego crea enlaces simbólicos para cada una de las utilidades incluidas en el Multicall Binary.
Observad que esta función tampoco es segura puesto que estamos
invocando system
con un valor controlado por el usuario. En
este caso, puesto que el programa se va a ejecutar con los permisos del
usuario que lo invoque no hay problema, puesto que cualquier comando que
pueda inyectar y ejecutar con éxito es un comando que puede ejecutar
desde la línea de comandos directamente.
Implementando los comandos
En nuestro binario Mulicall de ejemplo hemos implementado dos programas muy sencillos que simplemente muestran los parámetros que reciben para poder comprobar que estamos pasando los argumentos correctamente. De hecho hemos usado el mismo código para ambos, solo cambiando una cadena de caracteres para poder determinar que se ha ejecutado el código que queremos. Este es el código:
int tool1 (int argc, char *argv[]) {
("Tool1 : %d parametros\n", argc);
printf for (int i = 0; i < argc; printf ("Parametro %d: %s\n", i, argv[i++]));
}
int tool2 (int argc, char *argv[]) {
("Tool2 : %d parametros\n", argc);
printf for (int i = 0; i < argc; printf ("Parametro %d: %s\n", i, argv[i++]));
}
Como podéis ver, si bien estos dos comandos son iguales e inútiles, nos muestran como poder acceder a los parámetros pasados por el usuario a través de la línea de comandos, que es todo lo que necesitamos para poder integrar cualquier programa que tengamos en nuestro binario Multicall.
Probando nuestro Multicall Binary
Ahora que el código está completo os mostraremos como utilizarlo, en caso de que no tengáis experiencia con este tipo de programas.
Si ejecutamos el programa sin más, nos mostrará la ayuda.
$ ./tool tool 1.0 (c) ROOR 2025 tool --install dir : Para instalar Plug-ins: tool1 tool2
Podemos ejecutar los dos comandos tool1
y
tool2
usando tool
de la siguiente forma:
$ ./tool tool1 1 2 3 Tool1 : 4 parametros Parametro 1: tool1 Parametro 2: 1 Parametro 3: 2 Parametro 4: 3 $ ./tool tool2 1 2 3 Tool2 : 4 parametros Parametro 1: tool2 Parametro 2: 1 Parametro 3: 2 Parametro 4: 3
Ahora podemos probar a instalar nuestro binario multicall:
$ mkdir -p /tmp/test/install/tool $ ./tool --install /tmp/test/install/tool/ + Instalando en /tmp/test/install/tool/ $ ls -lh /tmp/test/install/tool/ total 20 -rwxr-xr-x 1 roor roor 17K Sep 3 08:30 tool lrwxrwxrwx 1 roor roor 28 Sep 3 08:30 tool1 -> /tmp/test/install/tool//tool lrwxrwxrwx 1 roor edma 28 Sep 3 08:30 tool2 -> /tmp/test/install/tool//tool $ export PATH=/tmp/test/install/tool/:$PATH
La instalación es bastante sencilla. Podemos ver como el programa
principal se copia en el directorio destino y se generan los enlaces
simbólicos para cada uno de los comandos implementados. Finalmente,
añadimos el nuevo path a la variable PATH
para poder
ejecutar los comandos directamente sin tener que escribir el path
completo, si bien eso funcionaria correctamente gracias a nuestra
función strip_path
.
$ tool tool 1.0 (c) ROOR 2025 tool --install dir : Para instalar Plug-ins: tool1 tool2 $ tool1 1 2 3 Tool1 : 4 parameteros Parameters 1: tool1 Parameters 2: 1 Parameters 3: 2 Parameters 4: 3 $ tool2 1 2 3 Tool2 : 4 parameteros Parameters 1: tool2 Parameters 2: 1 Parameters 3: 2 Parameters 4: 3
Y esto sería todo. Ahora ya sabéis como Busybox
hace su
magia y también como hacerla vosotros mismos!!!
Conclusiones
Esperamos que os haya resultado interesante. Programa un Multicall Binary es bastante sencillo una vez que sabes lo que estás haciendo. Esperamos que esto os resulte útil y nos encantaría saber de los usos que se os ocurren para este tipo de código. Hasta la próxima.
■