El increíble programa menguante. RELOADED
LINUX
El increíble programa menguante. RELOADED
2017-04-25
Por
Don Bit0

En el número 5 de la primera época de Occam's Razor escribimos un artículo sobre como hacer nuestros programas mú pequeñitos. Conseguimos reducir el típico Hola Mundo a unos pocos Kilobytes... Pero todavía podemos hacerlo mejor!. Sigue leyendo para descubrir todos los secretos de los CiberJívaros!
Y por qué? os preguntaréis. Lo primero, porque mola. Lo segundo, porque mola conocer todos los detalles de como funcionan los programas que ejecuta nuestro ordenador... Y lo tercero, porque, a veces, solo algunas veces, necesitamos hacer que nuestro programa sea muy pequeño para que quepa en algún sistema con muy poca memoria o escaso espacio libre de almacenamiento... léase routers, y otras alimañas del Internet de las Cosillas.

Sin más preámbulos vamos al temita.

Nuestra Referencia

Para comenzar este nuevo viaje hacia lo minúsculo, debemos comenzar en algún punto. Y ese punto va a ser, más o menos, donde lo dejamos la última vez. Nuestro Hola Mundo estático con dietlibc (Listado 1).

$ cat <<EOM > hola.c
#include <stdio.h>

int main (void) {printf ("Hola Mundo!\n"); return 0;}
EOM
$ diet gcc -Os -o hola-ref hola.c
$ ls -lh hola-ref | awk '{print $5,$9}'
7.1K hola-ref
  
Listado 1. Programa de referencia

7 K para un binario estático no está nada mal. Si recordáis conseguimos reducirlo aún más, pero para este artículo vamos a comenzar desde este punto.

Evidentemente, imprimir un mensaje en la consola no require 7K de código, y nosotros no estamos añadiendo nada, por lo tanto, podemos concluir que el compilador está añadiendo un mogollón de código al programa. Lo que vamos a ver en este artículo es como ir eliminando todo ese código que añade el compilador hasta reducir nuestro programa de referencia a su mínima expresión.

Destripando printf

Si observamos nuestro programa con detenimiento, lo único que hacemos es llamar a la función printf. En otras palabras, no tenemos mucho donde rascar, así que vamos a ver que hace esta función (Cuadro 1).
$ objdump -d hola-ref | grep -A 5 "<main>:"
0000000000400144 <main>:
  400144:	50                   	push   %rax
  400145:	bf 4a 05 40 00       	mov    $0x40054a,%edi
  40014a:	e8 1b 03 00 00       	callq  40046a <puts>
  40014f:	31 c0                	xor    %eax,%eax
  400151:	5a                   	pop    %rdx
  
Cuadro 1. Desensamblado de la función main

Bueno, vemos que realmente, estamos llamando a la función puts, que también es una función de la librería estándar. En realidad, el compilador ha detectado que nuestra cadena es estática (no contiene cadenas de formato) y en lugar de ejecutar printf que trataría de interpretar la cadena en busca de cadenas como %d o %s para imprimir valores, llama directamente a la función puts que no hace nada de eso y se va a ejecutar mucho más rápido.

Bien, veamos pues que hace puts (Cuadro 2):

$ objdump -d hola-ref | grep -A 24 "<puts>:"
000000000040046a <puts>:
  40046a:	53                   	push   %rbx
  40046b:	48 83 cb ff          	or     $0xffffffffffffffff,%rbx
  40046f:	31 c0                	xor    %eax,%eax
  400471:	48 89 d9             	mov    %rbx,%rcx
  400474:	48 89 fa             	mov    %rdi,%rdx
  400477:	f2 ae                	repnz scas %es:(%rdi),%al
  400479:	48 89 d7             	mov    %rdx,%rdi
  40047c:	48 f7 d1             	not    %rcx
  40047f:	48 8d 34 19          	lea    (%rcx,%rbx,1),%rsi
  400483:	e8 c3 ff ff ff       	callq  40044b <__stdio_outs>
  400488:	85 c0                	test   %eax,%eax
  40048a:	74 1b                	je     4004a7 <puts+0x3d>
  40048c:	be 01 00 00 00       	mov    $0x1,%esi
  400491:	bf 77 05 40 00       	mov    $0x400577,%edi
  400496:	e8 b0 ff ff ff       	callq  40044b <__stdio_outs>
  40049b:	85 c0                	test   %eax,%eax
  40049d:	0f 94 c0             	sete   %al
  4004a0:	0f b6 c0             	movzbl %al,%eax
  4004a3:	f7 d8                	neg    %eax
  4004a5:	eb 02                	jmp    4004a9 <puts+0x3f>
  4004a7:	89 d8                	mov    %ebx,%eax
  4004a9:	5b                   	pop    %rbx
  4004aa:	c3                   	retq
  
Cuadro 2. Desensamblado de la función puts

Así, a bote pronto, y sin entrar en todos los detalles, la función primero calcula la longitud de la cadena (eso es el repnz scas hacia el principio) y luego llama a la función __stdio_outs para imprimir la cadena. La segunda llamada a __stdio_outs imprime un retorno de carro... Sí, puts añade un retorno de carro automáticamente a la cadena a imprimir.

Ahora ya solo nos queda ver que hace la función __stdio_outs (Cuadro 3).

$ objdump -d hola-ref | grep -A 24 "<__stdio_outs>:"
000000000040044b <__stdio_outs>:
  40044b:	53                   	push   %rbx
  40044c:	48 89 f2             	mov    %rsi,%rdx
  40044f:	48 89 f3             	mov    %rsi,%rbx
  400452:	48 89 fe             	mov    %rdi,%rsi
  400455:	bf 01 00 00 00       	mov    $0x1,%edi
  40045a:	e8 61 00 00 00       	callq  4004c0 <__libc_write>
  40045f:	48 39 d8             	cmp    %rbx,%rax
  400462:	0f 94 c0             	sete   %al
  400465:	0f b6 c0             	movzbl %al,%eax
  400468:	5b                   	pop    %rbx
  400469:	c3                   	retq
  
Cuadro 3. Desensamblado de la función __stdio_outs

Después de mover un poco los parámetros que recibe, llama a la función __libc_write. Bien... veamos que hace esta función (Cuadro 4):

$ objdump -d hola-ref | grep -A 24 "<__libc_write>:"
00000000004004c0 <__libc_write>:
  4004c0:	b0 01                	mov    $0x1,%al
  4004c2:	e9 ad fc ff ff       	jmpq   400174 <__unified_syscall>
  
Cuadro 4. Desensamblado de la función __libc_write

Vale. Parece que nos estamos acercamos.... una más! (Cuadro 5)

$ objdump -d hola-ref | grep -A 24 "<__unified_syscall>:"
0000000000400174 <__unified_syscall>:
  400174:	b4 00                	mov    $0x0,%ah

0000000000400176 <__unified_syscall_16bit>:
  400176:	0f b7 c0             	movzwl %ax,%eax
  400179:	49 89 ca             	mov    %rcx,%r10
  40017c:	0f 05                	syscall

000000000040017e <__error_unified_syscall>:
  40017e:	48 3d 7c ff ff ff    	cmp    $0xffffffffffffff7c,%rax
  400184:	76 0f                	jbe    400195 <__you_tried_to_link_a_dietlibc_object_against_glibc>
  400186:	f7 d8                	neg    %eax
  400188:	50                   	push   %rax
  400189:	e8 08 00 00 00       	callq  400196 <__errno_location>
  40018e:	59                   	pop    %rcx
  40018f:	89 08                	mov    %ecx,(%rax)
  400191:	48 83 c8 ff          	or     $0xffffffffffffffff,%rax

0000000000400195 <__you_tried_to_link_a_dietlibc_object_against_glibc>:
  400195:	c3                   	retq

0000000000400196 <__errno_location>:
  400196:	b8 18 10 60 00       	mov    $0x601018,%eax
  40019b:	c3                   	retq
  
Cuadro 5. Desensamblado de la función __unified_syscall

Bien!. Al fin llegamos a algún lado. Como podéis ver en el segundo bloque encontramos una instrucción syscall. Esta es la forma de llamar al kernel, de ejecutar una syscall en un sistema Linux x86 de 64bits. Quizás recordéis la infame int 0x80 utilizada en los sistema de 32bits... bueno, pues esto es lo mismo.

Tras la llamada al sistema, nos encontramos un pequeño fragmento de código que comprueba errores y actualiza la variable errno con el código de error pertinente. Bien, si ahora volvemos hacia atrás y vamos mapeando los distintos valores de los registros, veremos que lo que realmente estamos haciendo es llamando a la syscall WRITE en la salida estándar (stdout, descriptor de fichero 1) con la cadena que pasamos como parámetro a la función puts y la longitud de esa cadena, la cual calculamos al principio de todo. Dejamos como ejercicio para el lector este interesante proceso de reconstrucción inversa :).

Reduciendo puts a la mínima expresión

Ahora estamos en condiciones de escribir una versión mínima de puts. Tened en cuenta que todo el código que hemos visto en la sección anterior, está ahí por una buena razón, y en el caso general es necesario. Sabiendo que es lo que hace ese código, y siendo conscientes de las necesidades de nuestro caso partícular, nos podemos plantear hacer lo que vamos a hacer a continuación, pero no creáis que esto se puede hacer siempre.

Bien, con todo lo que hemos contado hasta el momento, podríamos pasar de puts y utilizar directamente la llamada al sistema write, con la longitud de la cadena pre-calculada. De esta forma nos ahorramos el código para calcular la longitud de la cadena (un strlen de toda la vida). Si queréis añadir ese código vosotros mismos y crear vuestra propia versión de puts... bueno, es un ejercicio interesante y os animamos a hacerlo.

Como hemos comentado, en lugar de puts, vamos a utilizar write para intentar reducir el tamaño del programa. Utilizando C, el programa quedaría más o menos así (Listado 2):

#include <unistd.h>

int main (void)
{
  write (1, "Hola Mundo\n", 13);
  return 0;
}
  
Listado 2. Nuestro HOLA MUNDO sin printf

La función write que estamos utilizando en este programa todavía pertenece a la librería estándar. Si no me creéis... bueno, ahora ya deberíais saber como comprobarlo :) (objdump y grep son tus amigos :).

Pero, para reducir puts a la mínima expresión tenemos que ir un paso más allá. Tenemos que implementar nuestra propia llamada a write... y esto lo haremos en ensamblador!!!!. Vamos a generar un fichero llamado write.s y escribir el siguiente programa en ensamblador (Listado 3):

.text
.global _write

_write: mov $1, %rax
	syscall
	ret
Listado 3. Implementación de write en ensamblador para x86_64

Sí, así de fácil. Solo necesitamos poner en el registro rax el número de la llamada al sistema. La razón es que los parámetros que pasamos a una función C en un programa de 64bits se almacenan en registros, y da la casualidad de que son los mismos registros que tenemos que utilizar para las llamadas al sistema... así que simplemente no los tocamos y ya está.

Para poder utilizar esta función tendremos que cambiar el write de nuestro programa C, por _write, el nombre que hemos utilizado en nuestro código ensamblador.

Ahora ya podemos compilar nuestro nuevo Hola Mundo que, en teoría, no utiliza la librería C estándar.

$ gcc -o hola-write hola-write.c write.s
  

Genial!... bueno, más o menos. Si comprobáis el tamaño del ejecutable que hemos obtenido sigue siendo de unos 7K.... Algo más tendremos que hacer.

Casi Todo es Mentira

Te habrán contando muchas veces que la función main es lo primero que se ejecuta. Para la mayoría de los mortales (léase programadores de aplicaciones), dicha frase puede considerarse correcta... pero la verdad es que NO LO ES!.

No me creéis?. Pos vamos a comprobarlo:

$ readelf -h hola-ref | grep Entry
  Entry point address:               0x400153
$ objdump -d hola-ref  | grep "<main>"
0000000000400144 <main>:
  4003b1:	e8 8e fd ff ff       	callq  400144 <main>
  
Cuadro 6. Dirección de la función main y punto de entrada del program!

WoW... El punto de entrada del programa (0x400153 en mi caso, puede ser diferente en vuestro sistema) no se corresponde con la función main, la cual, en mi caso, está en 0x4003b1.

Entonces que es lo primero que ejecuta el programa?

$ objdump -d hola-ref  | grep "0*400153"
0000000000400153 <_start>:
  400153:	5f                   	pop    %rdi
  
Cuadro 7. El punto de entrada es la función _start!

Bueno, antes de continuar, y por si alguien no está familiarizado con readelf y objdump, ahí va una pequeña explicación. El flag -h de readelf le indica al programa que solo debe mostrar la cabecera ELF del ejecutable, la cual contiene el auto-denominado punto de entrada... vamos, la posición en memoria donde el programa comenzará su ejecución.

Por otra parte, el flag -d de objdump nos permite desensamblar el binario que pasamos como parámetro. objdump, como parte de su salida, mostrará los símbolos que pueda encontrar en el binario. Como no hemos stripeado el binario, los nombre de las funciones están ahí, y por eso podemos encontrar la función main con grep.

Así que sí. Lo primero que se ejecutar al lanzar un programa no es la función main, sino una función llamada _start que el compilador añade de porque sí durante el proceso de compilación. Bueno, realmente es necesaria, pero no vamos a hablar de esos en este artículo.

Pos vale... vamos a pasar de main y escribir nuestra propia función _start (Listado 4).

int _write (int, void *, int);

int
_start (void)
{
	_write (1, "Hola Mundo!\n", 13);
	return 0;
}
  
Listado 4. Moviendo nuestro código a nuestra propia función _start

Ahí está. Como podéis ver también hemos eliminado el include y añadido sólamente el prototipo de _write. Compilemos y a ver que pasa:

$ gcc -static -o hola-start hola-start.c write.s

Sí... un mogollón de errores. A ver como lo solucionamos. Fijémonos en tres de ellos:

$ gcc -static -o hola-start hola-start.c write.s
(...)
/tmp/ccNDWG8J.o: In function `_start':
hola-start.c:(.text+0x0): multiple definition of `_start'
(...)
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/crt1.o:/build/eglibc-MjiXCM/eglibc-2.19/csu/../sysdeps/x86_64/start.S:118: first defined here
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/crt1.o: In function `_start':
(.text+0x20): undefined reference to `main'
(...)
  
Cuadro 8. Errores de compilación con nuestra función _start
Bueno, parece que estamos redefiniendo _start. O dicho de otra forma, el compilador sigue añadiendo el código de _start como siempre y al encontrar nuestra redefinición la cosa peta de mala manera. Además, ese código añadido, todavía espera encontrar una función llamada main.

Deshaciéndonos de la librería estándar

Bien, para poder solucionar este problema, tenemos que decirle al compilador que no queremos que añada su función _start... que ya nos ocupamos nosotros. Esto lo podemos hacer utilizando el flag -nostartfiles... el nombre es bastante obvio :

$ gcc -nostartfiles -static -o hola-start hola-start.c write.s
  

Parece que le ha gustado. Veamos como vamos :).

$ ls -lh hola-start | awk '{print $5,$9}'
5.5K hola-start
  

Una mejora al fin. Veamos si el programa sigue funcionando:

$ ./hola-start
Hola Mundo!
Segmentation fault

Vaya, funciona pero se estrella al terminar.

Terminando correctamente

Bien, lo que pasa es que _start hace algunas cosas y si nos la cargamos tan alegremente pues... el programa tiene problemas para terminar. Podéis echarle un ojo al ensamblador para sacar vuestras propias conclusiones, pero lo que pasa es que necesitamos llamar a la llamada del sistema EXIT para terminar el programa. Cuando usamos la librería estándar, el código que añade el compilador se encarga de todo, y el return de main, de hecho, está retornando a _start que llama a exit eventualmente.

Así que lo que tenemos que hacer es ejecutar la llamada al sistema EXIT cuando nuestro programa termine. Manos a la obra. Vamos a añadir una implementación de exit en nuestro fichero en ensamblador que ahora llamaremos sistema.s... llamarlo write no tiene mucho sentido any more (Listado 5).

.text
.global _write, __exit

_write: mov $1, %rax
	syscall
	ret

__exit:  mov $0x3c, %rax
	syscall
	ret
Listado 5. Añadiendo la función _exit para terminar el proceso

Como imaginaréis, el número que identifica la llamada EXIT es el 0x3c. También tendremos que añadir la llamada en nuestro programa principal que quedará como (Listado 6):

int _write (int, void *, int);
int __exit (int);

int
_start (void)
{
	_write (1, "Hola Mundo!\n", 13);
	__exit (0);
}

Listado 6. Nuestro nuevo hola mundo en C

Con todo esto:

$ gcc -nostartfiles -static -o hola-sys hola-sys.c sistema.s
$ ./hola-sys
Hola Mundo!
$  ls -lh hola-sys | awk '{print $5,$9}'
1.5K hola-sys
Cuadro 9. Al fin nuestro hemos conseguido reducir el programa

Güay!... 1.5 Kbytes. Esto ya es una cosa más razonable. Si, además lo pasamos por strip -s nuestro hola mundo se queda en algo menos de 1Kbyte. Y si usamos sstrip el binario se nos queda en unos 400 bytes!.

Y hasta aquí podemos leer. Si ejecutamos objdump sobre el binario ya solo obtendríamos algo tal que así (Cuadro 10):

$ objdump -d hola-sys

hola-sys:     file format elf64-x86-64


Disassembly of section .text:

000000000040010c <_start>:
  40010c:	55                   	push   %rbp
  40010d:	48 89 e5             	mov    %rsp,%rbp
  400110:	ba 0d 00 00 00       	mov    $0xd,%edx
  400115:	be 44 01 40 00       	mov    $0x400144,%esi
  40011a:	bf 01 00 00 00       	mov    $0x1,%edi
  40011f:	e8 0c 00 00 00       	callq  400130 <_write>
  400124:	bf 00 00 00 00       	mov    $0x0,%edi
  400129:	e8 0c 00 00 00       	callq  40013a <__exit>
  40012e:	5d                   	pop    %rbp
  40012f:	c3                   	retq

0000000000400130 <_write>:
  400130:	48 c7 c0 01 00 00 00 	mov    $0x1,%rax
  400137:	0f 05                	syscall
  400139:	c3                   	retq

000000000040013a <__exit>:
  40013a:	48 c7 c0 3c 00 00 00 	mov    $0x3c,%rax
  400141:	0f 05                	syscall
  400143:	c3                   	retq

Cuadro 10. Y este es todo el código que ahora contiene nuestro programa

Que es, prácticamente, todo el código que hemos escrito...

Todavía Más?

Sí... todavía podemos ir un poco más lejos, pero ahora ya rozando los límites de lo razonable :). No podemos reducir mucho más el código en sí, pero un ejecutable es algo más que código. Contiene información adicional que el sistema utiliza para cargar el programa en memoria y ejecutarlo.

Así que, es posible reducir esa información también al mínimo. No vamos a profundizar mucho en esa cuestión. Primero, porque este artículo ya es un poco largo, y segundo por que no lo vamos a hacer mejor que en este enlace.

A modo de resumen, lo que vamos a hacer es generar el fichero ejecutable byte a byte. O en otras palabras vamos a generar la estructura del fichero ELF y de esta forma reducirla a la mínima expresión. El artículo que mencionamos más arriba se centra en binarios de 32bits. Puesto que todo este artículo se ha centrado en los 64bits, aquí tenéis la versión del hola mundo reducida a su mínima expresión para esta arquitectura. El programa lo adaptó a los 64 bits otra persona que está encantada de que se difunda (Listado 7).

BITS 64
	org 0x400000
ehdr:                                           ; Elf64_Ehdr
        db      0x7F, "ELF", 2, 1, 1, 0         ;   e_ident
        times 8 db      0
        dw      2                               ;   e_type
        dw      0x3e                            ;   e_machine
        dd      1                               ;   e_version
        dq      _start                          ;   e_entry
        dq      phdr - $$                       ;   e_phoff
        dq      0                               ;   e_shoff
        dd      0                               ;   e_flags
        dw      ehdrsize                        ;   e_ehsize
        dw      phdrsize                        ;   e_phentsize
        dw      1                               ;   e_phnum
        dw      0                               ;   e_shentsize
        dw      0                               ;   e_shnum
        dw      0                               ;   e_shstrndx
	
ehdrsize      equ     $ - ehdr 
phdr:                                           ; Elf64_Phdr
        dd      1                               ;   p_type
        dd      5                               ;   p_offset
	dq      0
        dq      $$                              ;   p_vaddr
        dq      $$                              ;   p_paddr
        dq      filesize                        ;   p_filesz
        dq      filesize                        ;   p_memsz
        dq      0x1000                          ;   p_align
  
phdrsize      equ     $ - phdr
	

_start:
	mov     rax, 1
	mov     rdi, 1
	mov     rsi, msg
	mov     rdx, 14
	syscall	

	mov    rax, 60
	mov    rdi, 0
	syscall

	msg 	db      "Hola Mundo!",0x0a
	key 	db      0

	filesize equ $ - $$

Listado 7. Hola mundo construyendo el fichero ELF a mano

Al final del código podéis identificar fácilmente todo el ensamblador que hemos escrito a lo largo de este artículo. La principal diferencia es que la cadena a imprimir se almacena en el segmento de código (.text.rodata). La parte inicial del fichero es la cabecera y el segmento para el código (lo que en lenguaje Élfico se conoce como Program Header). Los ELFs de 64bits tienen una estructura un poco diferente a los de 32bits, en resumen, algunos campos son de 64bits (de ahí los dq Define QuadWord). Siguiendo las definiciones en el fichero elf.h, que deberíais tener en vuestro sistema, el proceso es bastante directo.

El código utiliza las directivas del ensamblador NASM, así que tendréis que utilizarlo para generar el programa. Además tendremos que decirle a NASM que genere un fichero exactamente con el contenido de nuestro fichero fuente en lugar de un fichero ELF (ya hemos puesto nosotros las cosas para que el resultado sea un fichero ELF :). Esto lo conseguimos de la siguiente forma:

$ nasm -f bin -o hola64 hola64.asm
$ chmod +x hola64
172 hola64
Cuadro 11. Generando el binario a partir del ELF creado manualmente

172 bytes!!!! WOW!!!!

Portabilidad

Bueno, ahora que hemos recorrido todo el camino, es el momento de hablar un poco de las desventajas de todo este proceso y de porqué solo deberías hacer este tipo de cosas si no te queda otro remedio... y creedme, a veces eso pasa.

La primera de las razones es que, no importa lo buenos que creáis que sois. La librería estándar lleva ahí muchísimos años, y muchísima gente muy lista se ha encargado de que sea lo mejor posible. Quizás podáis mejorar una pequeña parte para un caso muy concreto, pero, en general, y sin ofender, nuestro código va a ser peor.

La segunda de las razones es que, en cuanto empezamos a escribir ensamblador nuestro programa deja de ser portable. Ya no es posible simplemente recompilar el programa para otra plataforma. Todo el código en ensamblador debe ser re-escrito. Y para muestra un botón. Vamos a portar nuestro minúsculo hola mundo a una plataforma ARM. El programa C se quedaría igual, pero el ensamblador hay que cambiarlo como ya os comentamos. Sería algo así (Listado 8):

.text
.globl _write, _exit

_write:	mov r7, #4
	swi #0

__exit: mov r7, #1
       swi #0
Listado 8. Nuestra mini librería C estándard para ARM

Compilamos con un comando como este:

$ arm-linux-gnueabihf-gcc -static -nostartfiles  -marm -o hola-arm hola-sys.c sistema-arm.s
$  ls -lh hola-arm | awk '{print $5,$9}'
1.3K hola-arm
Cuadro 12. Compilando para ARM

Bien, algunos comentarios:

  • Lo primero es que los números que identifican las llamadas al sistema son diferentes
  • También lo son los registros. Por ejemplo, en lugar de usar rax (que no existe en ARM), debemos usar r7.
  • Si no sabes que es arm-linux-gnueabihf-gcc. Léete esto.
  • Por alguna razón, cuando mezclamos C y ensamblador con ARM tenemos que utilizar el flag -marm para generar código de 32bits. De lo contrario el programa C generará código de 32bits y el ensamblador generará código Thumb (16 bits)... y en ese caso las cosas no funcionan sin más. Para ver lo que esto significa, ejecutad objdump en el binario y mirar el código máquina generado por cada instrucción... todas son de 32 bits!.
  • El binario final ocupa unos 1.3Kbytes y tras ejecutar strip se queda en 712bytes... No está mal

Colofón

Y así concluye este viaje que empezó con un ejecutable de unos 800Kbytes hace algunos años y que ha terminado con una versión de tan solo 172 bytes. Ahora, esperamos, tendréis una visión más clara de la estructura de un programa y de la cantidad de código que se genera automáticamente incluso para la aplicación más tonta del mundo.

Esperamos que también hayáis aprendido un poco mejor como funciona la libería estándar y como permite escribir código portable... no importa que procesador utilicemos, solo tenemos que recompilar con el compilador adecuado y nuestro código funcionará en la nueva plataforma.

SOBRE Don Bit0
No os podemos contar mucho sobre Don Bit0. Es un tipo misterioso que de vez en cuando colabora con nosotros y luego, simplemente se desvanece. Como os podéis imaginar por su nick, Don Bit0, está cómodo en el bajo nivel, entre bits, y cerquita del HW que lo mantiene calentito.