Bienvenido a la wiki dedicada a los usuarios nuevos en GNU/Linux, este sitio esta orientado a ayudar a los usuarios nuevos... Si deseas contribuir, por favor crate una cuenta.
Conceptos de LiveUSB
De PUFs Wiki
Este artículo no pretende ser una guía rápida de como crear un LiveUSB con KDE/GNOME como la mayoría, sino que pretende fijar conceptos de Linux. Para realizar esa tarea tomaremos el camino de crear una pequeña distribución de Linux que permita arrancar mediante un pendrive. Cuando terminen de leer este artículo no tendrán un pendrive para llevar a la casa de sus amigos y mostrarles lo lindo que anda compiz-fusion en KDE mientras escriben documentos en OpenOffice y navegan con Firefox; pero sí tendrán los conocimientos necesarios para hacer todo eso.
Tabla de contenidos |
[editar] Introducción
Antes de empezar con la construcción del LiveUSB vamos a pensar cual será nuestro camino a seguir. Como sabemos vamos a necesitar de Linux en sí, es decir, el kernel.
Como las unidades flash USB pueden ubicarse en distintos dispositivos (según cuantos discos SATA/SCSI y otros pendrives tengamos) lo que haremos será arrancar utilizando initrd (un disco en RAM basicamente) de modo que nos independicemos del dispositivo USB, y correremos un script que buscará en que unidad está nuestro pendrive, luego lo montará y seguirá iniciando desde allí.
Para realizar todo esto crearemos distintos directorios de trabajo
# mkdir ~/trabajo # mkdir ~/trabajo/kernel # mkdir ~/trabajo/liveusb # mkdir ~/trabajo/initrd # mkdir ~/trabajo/mnt
el directorio kernel sirve para compilar el kernel, liveusb para armar el USB en si, el initrd para el initrd y mnt para montar unidades. Tengamos en cuenta que a diferencia de un LiveCD tenemos la ventaja que al pendrive lo podemos escribir, y no es necesario montar sistemas de archivos temporarios en RAM para directorios modificables como /etc o /var y /tmp (aunque sí sería recomendable, de modo que nuestro pendrive no sufra muchas escrituras).
[editar] El Kernel
Vamos a construir el kernel de Linux teniendo en cuenta lo siguiente:
- Necesitamos soporte initrd.
- Todo lo que necesitemos al momento del arranque lo incluiremos en el kernel.
Para el soporte initrd tendremos que activar las siguientes opciones:
General Setup-->
[*] Initial RAM filesystem and RAM disk (initramfs/initrd) support
Device Drivers-->
Block Devices-->
<*> RAM disk support
para la segunda parte simplemente nos alcanzará con incluir en el kernel soporte para el sistema de archivos ext2 y para dispositivos USB (incluyendo Almacenamiento Masivo).
Para realizar todos los pasos (configuración, compilación e instalación del kernel) ejecutaremos los siguientes comandos:
make O=~/trabajo/kernel menuconfig make O=~/trabajo/kernel all make O=~/trabajo/kernel INSTALL_MOD_PATH=~/trabajo/liveusb modules_install cp ~/trabajo/kernel/arch/i386/boot/bzImage ~/trabajo/liveusb/kernel
Al finalizar estos pasos tendremos la imagen del kernel en el directorio de trabajo que se convertira en el contenido del pendrive, y también tendremos los módulos (que estarán en ~/trabajo/liveusb/lib/modules/version-kernel).
[editar] Initrd
Cuando queremos iniciar el kernel utilzando initrd, el cargador de arranque (LILO, GRUB, Syslinux, etc) se encarga de poner en memoria el kernel en si mismo y luego pone en memoria el contenido de un archivo; este último archivo es nuestra imágen initrd. Cuando el kernel obtiene el control prepara esta imágen (quizás esté comprimida) y la kernel en un disco en RAM, para ser preciso la carga en /dev/ram0.
Una vez finalizado este proceso el kernel se encarga de ejecutar el archivo /linuxrc (que estaba en la imágen initrd) o si especificamos otro archivo mediante la opción init=... ejecutará ese otro archivo también en la imágen initrd. A diferencia de lo que se podría pensar la diferencia entre utilizar /linuxrc para bootear o especificar otro ejecutable no es una mera cuestión de nombres, el proceso es distinto. Si utilizamos /linuxrc el kernel lo trata como un proceso cualquiera, en cambio si especificamos el archivo con init=... lo trata como si fuera el proceso especial init (el proceso número 1 del sistema).
Para crear una imágen initrd podemos utilizar la utilidad cpio, pero nosotros seguiremos otro método que creo resulta más didáctico; crearemos un archivo de un determinado tamaño al cual formatearemos en ext2 y luego lo montaremos mediante los dispositivos /dev/loop/*
[editar] Preparando la Imágen
Lo primero que haremos será preparar la imágen dentro de ~/trabajo/initrd. Como utilizaremos para nuestro proceso init un shell script (se podría haber hecho un programa en C, un script en Perl, en Python, en lo que más les guste) crearemos el soporte necesario para ejecutar estos lindos bichitos.
En primer lugar necesitamos obviamente la shell en si misma, luego necesitaremos los comandos que utilizaremos dentro de nuestro script, y además necesitaremos todas las bibliotecas que estos usen. Empezemos creando la estructura de directorios:
cd ~/trabajo/initrd mkdir bin lib dev cp /bin/cat /bin/chroot /bin/cut /bin/grep /bin/ls /bin/mount /sbin/pivot_root /bin/sh /bin/sleep /bin/umount bin/
al finalizar esta serie de comandos tendremos todos los programas que vamos a usar bajo el directorio bin de nuestra imágen initrd, pero todavía faltan todas las librerías que estos utilizan. Por suerte tenemos el comando ldd el cual nos informa las bibliotecas que utilizan los programas:
ldd /bin/cat
linux-gate.so.1 => (0xb7f4f000)
libc.so.6 => /lib/libc.so.6 (0xb7de7000)
/lib/ld-linux.so.2 (0xb7f50000)
cp /lib/ld-linux.so.2 lib/
cp /lib/libc.so.6 lib/
si realizamos este paso con todos los programas y bibliotecas al final tendremos nuestro directorio lib completo; hay que darse cuenta que la biblioteca linux-gate.so.1 en realidad no es un archivo de biblioteca.
Una vez terminado todavía tendremos que llenar el directorio dev de dispositivos para poderlos usar en nuestro shell script. Hay dos dispositivos importantes (pero que no son estrictamente necesarios) /dev/null y /dev/console. Interesantemente no utilizaremos a console, tan solo null para redirigiar la salida de nuestros comandos. También necesitaremos los dispositivos del estilo /dev/sd?1 para poder montar el pendrive; si bien podríamos pedirle a nuestro script que lea el archivo /proc/partitions para crear de manera dinámica estos dispositivos, resulta mucho más sencillo e igualmente didáctico crear de manera manual suficientes de estos dispositivos como para poder montar nuestro pendrive en cualquiera de nuestros sistemas con discos SCSI o SATA. Para ello podemos hacer algo como esto:
cd dev mknod sda1 b 8 1 mknod sdb1 b 8 17 mknod sdc1 b 8 33 mknod sde1 b 8 65 mknod sdd1 b 8 129 mknod null c 1 3
Listo, ya tenemos nuestra imágen lista, tan solo nos falta crear el shell script que se encargará del proceso de arranque, pero antes crearemos un directorio donde montar el pendrive:
cd .. mkdir pendrive
[editar] El Script
Al script le podemos dar el nombre que queramos, en particular lo llamaré inicio:
cd ~/trabajo/initrd touch inicio chmod +x inicio
El contenido del script será el siguiente:
#!/bin/sh # Le indicamos la ruta donde buscar los programas (/bin), también # le indicamos el número de versión de nuestro sistema y declaramos una variable # auxiliar listo inicialmente igual a 0 PATH="/bin" version=1 listo=0 # Al utilizar el comando dmesg con el flag -n seguido de un número le estamos indicando # qué mensajes del kernel queremos que sean logueados por la consola # con "dmesg -n 1" le pedimos que no muestre ningún mensaje por consola dmesg -n 1 # Nuestro cartel de bienvenida :P echo -e "Linux LiveUSB \033[1;31mPsicoUSB\033[m v0.1" # Montamos /proc momentaneamente para leer los parámetros que le pasamos al kernel. # Los parámetros del kernel se guardan en el archivo /proc/cmdline. Lo que hacemos # es volcar los contenidos de ese archivo en la variable "comandos" y luego # desmontamos /proc. mount -nt proc none /pendrive >/dev/null 2>&1 comandos=`cat /pendrive/cmdline` umount -n /pendrive >/dev/null 2>&1 # Por cada palabra en la variable "comandos" (/proc/cmdline) # Si la palabra que estamos analizando es del estilo # variable=valor # guardamos el nombre de la variable en la variable "cadena" # y el valor en la variable "valor". # Si el nombre de la variable era "espera" entonces imprimimos # un mensaje indicando lo que estamos haciendo y luego dormimos # por la cantidad de segundos que le pasamos como parámetro al kernel. # Por último un mensaje de OK. for i in ${comandos}; do cadena=`echo ${i} | cut -d= -f1` valor=`echo ${i} | cut -d= -f2` if [ ${cadena} = "espera" ]; then echo -n "Esperando al dispositivo USB..." sleep ${valor} echo -e "\033[75G[\033[1;32m OK \033[m]" fi done # Una vez que el dispositivo USB está listo para ser utilizado procedemos # a buscar la unidad del pendrive. Para eso recorremos todos los dispositivos # del estilo /dev/sdX1, donde X=a,b,c,...,z (en realidad creamos menos # dispositivos en /dev) # Por cada una de estas unidades, la montamos en /pendrive como lectura y escritura # Si dentro de esa unidad existe el archivo psicousb (/pendrive/psicousb) y además # sus contenidos son igual al número de versión de nuestro sistema, establecemos # la variable listo en 1 y salimos del ciclo for. # De lo contrario desmontamos la unidad y probamos con otra. echo -n "Buscando unidad del pendrive..." for i in `ls /dev/sd?1`; do mount -n -o rw ${i} /pendrive >/dev/null 2>&1 if [ -r /pendrive/psicousb ]; then if [ "`cat /pendrive/psicousb`" = ${version} ]; then listo=1 break; fi fi umount -n /pendrive >/dev/null 2>&1 done # Si logramos encontrar la unidad adecuada imprimimos un mensaje de OK. # De lo contrario un mensaje de ERROR y ejecutamos una shell, quizás el usuario # sepa montar la partición y seguir manualmente. if [ $listo = "1" ]; then echo -e "\033[75G[\033[1;32m OK \033[m]" else echo -e "\033[72G[ \033[1;31mERROR\033[m ]" exec /bin/sh fi # Ya montado el pendrive en /pendrive, ahora procedo a montarlo como raíz cd /pendrive pivot_root . initrd exec chroot . /sbin/init <dev/console >dev/console 2>&1 while [ true ]; do /bin/sh done
Esta última parte merece una hermosa explicación. En primer lugar cambiamos el directorio de trabajo a "/pendrive", luego utilizamos el comando pivot_root (que se traduce en la llamada al sistema del mismo nombre) indicándole que queremos que el sistema cambie la unidad raíz al directorio actual, y la unidad raíz vieja pase a estar en el directorio "initrd".
Luego ejecutamos el comando chroot cambiando el directorio raíz del proceso actual al directorio de tabajo actual ( "/pendrive" ) y además procedemos a ejecutar /sbin/init (el script de inicio del pendrive, ya fuera de la imágen initrd; la segunda etapa del inicio). Además redirigimos la entrada estándar de dev/console y redirigimos la salida estándar y la salida de error estándar a dev/console. Y además como el comando está precedido de un exec le estamos pidiendo que reemplaze la imágen en memoria de nuestro proceso con la del nuevo proceso (de esta manera este shell script queda completamente muerto, es reemplazado por la segunda etapa).
En caso de que llegara a fallar presentamos una shell al usuario.
Deben notar que siempre que realizamos el montaje de algún dispositivo utilizamos el flag -n, este flag le dice a mount que no se preocupe por el fichero /etc/mtab (el cual no existe ni remotamente, ni siquiera tenemos un directorio /etc en el initrd)
Notar también que hemos recorrido la lista de parámetros del kernel uno a uno, y por cada uno lo hemos dividido en cadena y valor, es decir, si uno de los parámetros es fichero=imagen.bin la variable "cadena" terminará siendo igual a "fichero" y la variable "valor" será igual a "imagen.bin". Luego, si la cadena es "espera" entonces dormimos la cantidad de segundos indicada en la variable "valor" De este modo podremos pasarle al kernel la cadena espera=5 indicándole que el script tiene que dormir durante 5 segundos antes de intentar montar el pendrive. Esto es así porque ni bien arranca el sistema todavía no está el dispositivo USB listo para usarse, si el valor de 5 segundos resultara poco en nuestro sistema podemos aumentarlo a nuestro gusto.
[editar] pivot_root
Quizás parezca raro realizar el chroot habiendo realizado antes el pivot_root. La razón es que si bien el sistema cambió su directorio raíz, el proceso actual sigue teniendo referencias al antiguo directorio raíz, veamos la ejecución comando a comando:
cd /pendrive (PWD=/pendrive) pivot_root . initrd (PWD=/pendrive referido al antiguo directorio raíz) exec chroot . /sbin/init <dev/console >dev/console 2>&1 (PWD=/sbin referido al nuevo directorio raíz)
vean también como la redirección es <dev/console, es decir, con un PATH relativo en lugar de absoluto, pues si utilzáramos /dev/console estaríamos haciendo referencia a un archivo del viejo directorio raíz.
Nota: PWD es el directorio de trabajo actual (¿¿Process Working Directory??)
[editar] ld-linux.so
Si recuerdan cuando preparábamos la imágen del initrd nos encontramos con este archivo de biblioteca que mágicamente aparece en todos los ejecutables/librerías. Para entender qué es este archivo primero debemos entender ligeramente como carga Linux los archivos ejecutables en la memoria.
Cada archivo ejecutable (archivo en formato elf) tiene un campo llamado intérprete, el cual cumple una función similar al #!/bin/sh del shell script. En un shell script logra que sea la shell quien interprete el archivo de texto (el script); en un archivo elf hace exactamente lo mismo. Veamos qué intérprete usan los programas en Linux:
# readelf -l /bin/cat
Elf file type is EXEC (Executable file)
Entry point 0x8048cc0
There are 8 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4
INTERP 0x000134 0x08048134 0x08048134 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x03e54 0x03e54 R E 0x1000
LOAD 0x004000 0x0804c000 0x0804c000 0x001cc 0x00344 RW 0x1000
DYNAMIC 0x004014 0x0804c014 0x0804c014 0x000c8 0x000c8 RW 0x4
NOTE 0x000148 0x08048148 0x08048148 0x00020 0x00020 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
PAX_FLAGS 0x000000 0x00000000 0x00000000 0x00000 0x00000 0x4
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag
06
07
Como pueden ver dice [Requesting program interpreter: /lib/ld-linux.so.2]. Ha aparecido nuestro amiguito, de modo que cada vez que se ejecuta un programa es ld-linux.so el que se encarga de "interpretarlo"; su interpretación consiste simplemente en ver que librerías dinámica usa el programa, mapearlas (cargarlas) en memoria, resolver todos los símbolos no resueltos, y todas las demás tareas que tenga que realizar antes de permitir que se ejecute el programa. De modo que en Linux el enlazador dinámico en tiempo de ejecución corre exclusivamente por parte del espacio de usuario; en otro sistemas no Unix (jejeje) esto corre por cuenta del kernel, lo cual puede mejorar levemente el desempeño, ¡¡¡pero un error en ld-linux.so es mucho menos dañino que un error en el kernel del sistema operativo!!! Veamos que pasa si le pedimos a ld-linux.so que nos dé un poco de información:
# readelf -l /lib/ld-linux.so.2
Elf file type is DYN (Shared object file)
Entry point 0x8d0
There are 7 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00000000 0x00000000 0x1cf30 0x1cf30 R E 0x1000
LOAD 0x01dc80 0x0001dc80 0x0001dc80 0x00910 0x009d0 RW 0x1000
DYNAMIC 0x01defc 0x0001defc 0x0001defc 0x000c0 0x000c0 RW 0x4
GNU_EH_FRAME 0x01cac0 0x0001cac0 0x0001cac0 0x000e4 0x000e4 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
GNU_RELRO 0x01dc80 0x0001dc80 0x0001dc80 0x00380 0x00380 R 0x1
PAX_FLAGS 0x000000 0x00000000 0x00000000 0x00000 0x00000 0x4
Section to Segment mapping:
Segment Sections...
00 .hash .dynsym .dynstr .gnu.version .gnu.version_d .rel.dyn .rel.plt .plt .text __libc_freeres_fn .rodata .eh_frame_hdr .eh_frame
01 .data.rel.ro .dynamic .got .data __libc_subfreeres .bss
02 .dynamic
03 .eh_frame_hdr
04
05 .data.rel.ro .dynamic .got
06
como ven no tiene ningún intérprete, lo cual es lógico, pues el es el intérprete de todos los ejecutables del sistema :) Además, si pedimos más información:
# ldd /lib/ld-linux.so.2 statically linked
lo cual sigue siendo lógico, pues nadie esperaría que el enlazador dinámico fuera a su vez enlazado dinámicamente :)
[editar] linux-gate.so
Otra librería curiosa es linux-gate.so:
# ldd /bin/cat
linux-gate.so.1 => (0xb7fe1000)
libc.so.6 => /lib/libc.so.6 (0xb7e79000)
/lib/ld-linux.so.2 (0xb7fe2000)
como podemos ver esta librería figura en el mapa de memoria del proceso, sin embargo no se correspondo con ningún archivo; esto requiere un poco de historia: En un principio cada vez que un programa quería realizar una llamada a un servicio del sistema utilizaba el mecanismo de interrupción por software (el famoso int 0x80), sin embargo con el tiempo se introdujeron otros mecanismos como sysenter/sysexit, syscall. Lo que logró la introducción de estos mecanismos (además de una mejora en el desempeño) fue una serie de incompatibilidades; para solucionarlas se dispuso que el kernel mapearía una página en el mapa de memoria de cada procesos, cada vez que un proceso quisiera realizar una llamada al sistema simplemente tendría que invocar un procedimiento en esta sección de la memoria, el cual se encargaría de utilizar el mecanismo adecuado (int 0x80, sysenter, etc).
Entonces, linux-gate.so no es un archivo, pero sí es parte del mapa de memoria del proceso, pues el kernel se enarga de mapear esa página en la memoria de cada proceso.
[editar] Creando la Imágen
Ahora que tenemos el directorio listo, nos falta crear el archivo initrd en si, para eso primero tenemos que saber que tan grande necesita ser el archivo, veamos como:
# cd ~/trabajo # du -sh initrd 3,1M initrd
si el directorio ocupa 3,1 MB entonces vamos a necesitar un archivo de imágen que sea un poco más grande (la sobrecarga del sistema de archivos), vayamos por lo seguro y hagamos uno de 4 MB:
# dd if=/dev/zero of=initrd.bin bs=1k count=4k 4096+0 records in 4096+0 records out 4194304 bytes (4,2 MB) copied, 0,0193849 s, 216 MB/s
Esa instrucción simplemente le dice al programa dd que copie del dispositivo de bloques /dev/zero (un dispositivo que no importa cuanto leas siempre te devuelve bloques lleno de ceros) en el archivo initrd.bin, con un tamaño de bloque de 1 KB (bs=1k, bs=Block Size) y que copie 4.000 unidades (count=4k). Ahora tan solo hay que darle formato:
# mkfs.ext2 initrd.bin mke2fs 1.40.2 (12-Jul-2007) initrd.bin no es un dispositivo especial de bloques. ¿Se continúa de todas formas? (s,n) s Etiqueta del sistema de ficheros= Tipo de SO: Linux Tamaño del bloque=1024 (bitácora=0) Tamaño del fragmento=1024 (bitácora=0) 1024 nodos i, 4096 bloques 204 bloques (4.98%) reservados para el súper usuario Primer bloque de datos=1 Maximum filesystem blocks=4194304 1 bloque de grupo 8192 bloques por grupo, 8192 fragmentos por grupo 1024 nodos i por grupo Mientras se escribían las tablas de nodos i: terminado Escribiendo superbloques y la información contable del sistema de ficheros: hecho Este sistema de ficheros se revisará automáticamente cada 36 meses o 180 dias, lo que suceda primero. Utilice tune2fs -c o -i para cambiarlo.
Cuando nos pregunte si realmente queremos formatear un archivo que no corresponde con un dispositivo por bloques simplemente le decimos que si. Si quisiéramos utilizar un sistema de archivos para el cual su utilidad de formateo no quiere formatear un archivo que no sea un dispositivo por bloque simplemente podemos utilizar el dispositivo /dev/loopX (dónde X es un número) de la siguiente manera:
# modprobe loop # losetup /dev/loop/0 initrd.bin # mkfs.ext2 /dev/loop/0 ... # losetup -d /dev/loop/0
Listo, ahora procedemos a montar la imágen de la siguiente manera:
# mount -o loop -t ext2 initrd.bin ~/trabajo/mnt
Y ahora solamente nos resta copiar todo el contenido:
# cp -R ~/trabajo/initrd/* ~/trabajo/mnt/
el -R le dice que copie recursivamente preservando todos los atributos (por ej. que no trate de copiar los contenidos de /dev/sda1, sino que copie el archivo especial que representa el dispositivo). Luego nos encargamos de desmontar la imágen:
# umount ~/trabajo/mnt
Listo, ahora podemos comprimir la imagen si nos apetece:
# gzip -9 initrd.bin
[editar] El Pendrive
Una vez finalizado nuestro initrd nos tenemos que concentrar en preparar el pendrive, el camino no será muy diferente al del initrd, de modo que tenemos que crear una estructura de directorios que contenga librerías y binarios, en particular queremos todos los binarios que nos ayuden en el script más alguna otra cosa interesante para probar el sistema, en mi caso concreto tengo los siguientes binarios bajo /bin:
bash chown df gunzip ln nano rm tar who bunzip2 chroot dmesg gzip ls netstat rmdir tty bzip2 clear du hostname mkdir ping setsid umount cal cp env id mknod pivot_root sleep uname cat cut find ldd mount ps stty uptime chmod date grep less mv pwd sync touch
bajo /sbin:
apagar ifconfig modprobe rmmod udevcontrol udevtrigger blockdev init parar route udevd halt hwclock lsmod reiniciar udevadm udevsettle reboot
y bajo /usr/bin:
ssh wget
además las siguientes librerías en /lib (espero recuerden nuestra charla sobre ldd):
ld-linux.so.2 libncurses.so.5.6 libnss_nis.so.2 libacl.so libncursesw.so.5 libproc-3.2.7.so libacl.so.1 libnsl-2.6.1.so libpthread.so.0 libacl.so.1.1.0 libnsl.so.1 libresolv-2.6.1.so libattr.so.1 libnss_compat-2.6.1.so libresolv.so.2 libblkid.so.1 libnss_compat.so.2 librt.so.1 libbz2.so.1 libnss_dns-2.6.1.so libutil-2.6.1.so libcrypt-2.6.1.so libnss_dns.so.2 libutil.so.1 libcrypt.so.1 libnss_files-2.6.1.so libuuid.so.1 libc.so.6 libnss_files.so.2 libz.so libdl-2.6.1.so libnss_hesiod-2.6.1.so libz.so.1 libdl.so.2 libnss_hesiod.so.2 libz.so.1.2.3 libm.so.6 libnss_nis-2.6.1.so modules libncurses.so libnss_nisplus-2.6.1.so udev libncurses.so.5 libnss_nisplus.so.2
y estas otras en /usr/lib: libcrypt.a libcrypto.so.0.9.8 libcryptsetup.so.0 libssl.so libcrypt_g.a libcryptsetup.a libcryptsetup.so.0.0.0 libssl.so.0.9.8 libcrypto.a libcryptsetup.la libcrypt.so libcrypto.so libcryptsetup.so libssl.a
Recordemos que toda esta estructura de directorios la crearemos en ~/trabajo/liveusb. A diferencia del initrd nosotros no crearemos un directorio /dev estático, sino que nos valdremos de udev para ello, pero antes de poder utilizar necesitamos todos sus binarios en /bin o /sbin o algún otro directorio dentro del PATH; en nuestro caso los hemos copiado dentro de /bin y son los siguientes archivos:
/sbin/udevadm /sbin/udevd /sbin/udevtrigger /sbin/udevcontrol /sbin/udevsettle
para copiarlos nos basta con:
# cp -a /sbin/udev* ~/trabajo/liveusb/bin
Utilizá el parámetro -a puesto que le dice a cp que preserve los archivos, es decir, que no desreferencia enlaces simbólicos y ese tipo de cosas, puesto que:
# ls -l /sbin/udev* -rwxr-xr-x 1 root root 84544 nov 14 10:30 /sbin/udevadm lrwxrwxrwx 1 root root 7 nov 14 10:30 /sbin/udevcontrol -> udevadm -rwxr-xr-x 1 root root 80248 nov 14 10:30 /sbin/udevd lrwxrwxrwx 1 root root 7 nov 14 10:30 /sbin/udevsettle -> udevadm lrwxrwxrwx 1 root root 7 nov 14 10:30 /sbin/udevtrigger -> udevadm
también necesitaremos la carpeta /lib/udev y /etc/udev, pero supongo que ya saben como copiarla :) Dentro de /dev necesitaremos únicamente el archivo de dispositivo console (pues es la entrada y salida estándar que utilizamos en el script anterior), para ello:
# cd ~/trabajo/liveusb/dev # mknod console c 5 1
udev se encargará del resto :)
También necesitaremos el archivo "psicousb" que si recuerdan del shell script del initrd este archivo debe contener el número de versión que en este caso es la uno:
# echo 1 > ~/trabajo/liveusb/psicousb
También queremos la base de datos de capacidades de terminal, ya que sin ella "clear", "less", "nano" y otros programas que hagan uso de ncurses o de la base de datos de capacidades de terminal de manera directa no funcionarán bien. Para incluir la base de datos:
# cp -R /usr/share/tabset ~/trabajo/liveusb/usr/share/ # cp -R /usr/share/terminfo ~/trabajo/liveusb/usr/share/ # cp -R /etc/terminfo ~/trabajo/liveusb/etc/
También copiaremos algunos archivos importantes en /etc:
# cd /etc # cp group passwd host.conf resolv.conf hosts nsswitch.conf timezone localtime ~/trabajo/liveusb/etc/
Los archivos /etc/mtab y /etc/fstab los crearemos durante el arranque. Ahora procedemos a crear algunos archivos exclusivos de nuestra "distribución" (los explicaremos más adelante):
# cd ~/trabajo/liveusb/etc/ # echo -e "Bienvenido a \033[1;31mPsicoUSB\033[m v.01" > msjdia # mkdir red # cd red # echo "10.0.0.254" > gateway # echo "psicousb" > hostname # echo "10.0.0.1 netmask 255.255.255.0" > net.eth0
Notar que 10.0.0.254 es mi gateway por defecto, y 10.0.0.1 es la dirección IP de mi placa de red, de esta forma podemos configurar de manera muy sencilla la red; aunque lo lógico sería o bien preguntarle al usuario o bien intentar utilizar DHCP para iniciar la red, pero me parece que esto es didáctico para ver como puede llegar a funcionar una distribución de Linux real.
El flag "-e" le dice a "echo" que interprete los caracteres de escape del estilo "\033", y la secuencia de caracteres "\033[1;31m" logra que se imprima un mensaje en color rojo, mientras que "\033[m" desactiva los colores.
También vamos a crear un archivo que nos indique qué consolas virtuales queremos en el sistema:
# cd ~/trabajo/liveusb/etc # cat << FIN > consolas > tty2 > tty3 > tty4 > FIN
No hemos introducido una entrada para tty1 puesto que esta siempre se creará. Ahora procedemos a crear los puntos de montaje para las distintas partes del sistema:
# cd ~/trabajo/liveusb # mkdir proc sys initrd
En el directorio initrd estará montada la imágen correspondiente inmeditamente antes de ejecutar nuestro script de inicio, esto es así porque la llamada a pivot_root en el script del initrd cambia el directorio raíz al directorio especificado en el primer argumento, y el viejo directorio raíz lo monta en donde indica el segundo argumento.
También vamos a necesitar el kernel en el disco USB, de modo que podemos copiarlo si no lo habíamos hecho antes.
[editar] El Inicio
Ahora que terminamos de forjar la infraestructura del pendrive procedemos a crear nuestro shell script de inicio, para ello:
# cd ~/trabajo/liveusb/sbin # touch init # chmod +x init
Y ahora procedemos a codificar el script:
#!/bin/bash # Fijamos los lugares donde buscar ejecutables, también establecemos # el color de los mensajes de OK y los mensajes de ERROR, y además # el color normal, además establecemos la variable auxiliar error en cero. PATH=/bin:/sbin:/usr/bin COLOROK=\\033[1\;32m COLORERR=\\033[1\;31m NOCOLOR=\\033[m error=0 # Aquí definimos una pequeña función, que simplemente imprimirá el mensaje # que le pasemos como parámetro seguida de tres puntos suspensivos, además # no imprime el carácter de "nueva línea" al final, y soporte códigos ANSI. # También establece la variable error en cero. # Ej: ecartel hola # Resultado: hola... function ecartel { error=0 echo -ne $*... } # Esta función simplemente imprime el mensaje de OK o ERROR en base al valor # de la variable error. function fcartel { if [ $error -eq 0 ]; then echo -e "\033[75G[${COLOROK} OK ${NOCOLOR}]" else echo -e "\033[72G[${COLORERR} ERROR ${NOCOLOR}]" fi } # La función ejecutar se encarga de evaluar (ejecutar) los comandos pasados # como argumentos. En caso de existir el archivo /dev/null redirige la entrada # y salida estándar de esos comandos a /dev/null, de lo contrario no lo hace. # Esto es así porque temprano durante el proceso de arranque aún no disponemos # del dispositivo null, el cual crearemos más adelante con una llamada a '''mknod'''. # Por último actualiza el valor de la variable error con el resultado de la ejecución. function ejecutar { if [ -e /dev/null ]; then eval $* &>/dev/null else eval $* fi error=$(($error | $?)) } # Montamos los sistemas de archivos /proc y /sys. También montamos /dev utilizando # el sistema de archivos tmpfs, especificándole un tamaño máximo de 5 Megas y el # modo de acceso para el directorio raíz como 775 (rwxrwxr-x). # Luego creamos el dispositivo /dev/null :) # También nos encargamos de crear los directorios /dev/pts y /dev/shm, los cuales # los montamos de manera adecuada. ecartel "Montando sistemas de archivos" ejecutar mount -nt proc none /proc ejecutar mount -nt sysfs none /sys ejecutar mount -nt tmpfs -o size=5m,mode=775 none /dev ejecutar cd /dev ejecutar mknod -m 666 null c 1 3 ejecutar mkdir pts ejecutar mkdir shm ejecutar mount -nt tmpfs -o rw,noexec,nosuid,nodev none /dev/shm ejecutar mount -nt devpts -o rw,nosuid,noexec none /dev/pts ejecutar cd / fcartel # Primero iniciamos udev como un servicio en segundo plano (daemon), luego utilizamos # el comando '''udevtrigger''' para que todos los dispositivos que estaban presentes # al momento de arrancar generen los eventos adecuados para que udev pueda crear # sus archivos de dispositivo bajo /dev. # Por último esperamos que la cola de eventos de udev se vacíe esperando un máximo # de 60 segundos. ecartel "Configurando udev" ejecutar /sbin/udevd --daemon ejecutar /sbin/udevtrigger ejecutar /sbin/udevsettle --timeout=60 fcartel # Aquí pedimos los contenidos de /proc/mounts, le quitamos la entrada rootfs # y la guardamos en /etc/mtab (construcción de mtab). # Luego a los nuevos contenidos de /etc/mtab le quitamos la entrada de initrd # y eso lo guardamos en /etc/fstab. # Notar que interesantemente la redirección a /etc/mtab y /etc/fstab no es # nulificada por la redirección a /dev/null dentro de la función ejecutar. # En caso de que en nuestro sistema no sirva se puede tranquilamente saltear # la función ejecutar. ecartel "Actualizando mtab y fstab" ejecutar "cat /proc/mounts | grep -v rootfs > /etc/mtab" ejecutar "cat /etc/mtab | grep -v initrd > /etc/fstab" fcartel # Ahora procedemos a desmontar initrd puesto que ya no lo necesitamos, y luego # vaciamos los conteidos en ram de /dev/ram0 (el disco en RAM que contenía a initrd) # para que ya no ocupe más espacio. ecartel "Desmontando initrd" ejecutar umount /initrd ejecutar blockdev --flushbufs /dev/ram0 fcartel # Utilizamos el comando '''hwclock''' para obtener la hora del sistema. # El flag '''--hctosys''' le indica "Hardware Clock TO SYStem", lo cual le dice que tome # el horario de la BIOS y lo actualice en el kernel de Linux. # El flag '''--localtime''' le indica que el tiempo está en formato local (en contraposición # con UTC, que sería '''--utc''') ecartel "Actualizando la hora del sistema" ejecutar hwclock --hctosys --localtime fcartel # Primero establecemos el nombre de la máquina volcando los contenidos del archivo # /etc/red/hostname como argumento del comando hostname. # Luego, por cada archivo del estilo /etc/red/net.* (net.eth0, net.eth1, net.nas0, etc) # obtengo la segunda parte (eth0, eth1, nas0, etc) separando a partir del punto y tomando # el segundo fragmento. # Luego ejecuto el comando ifconfig en la interfaz que obtuve del nombre del archivo # y le paso los contenidos del archivo, además de especificarle el comando up. # De modo que si tengo el archivo /etc/red/net.eth0, con los siguientes contenidos: # 10.0.0.1 netmask 255.255.255.0 # Lo que se termina ejecutando es: # ifconfig eth0 10.0.0.1 netmask 255.255.255.0 up ecartel "Configurando interfaces de red" ejecutar hostname `cat /etc/red/hostname` for i in `ls /etc/red/net.*`; do interfaz=`echo ${i} | cut -d'.' -f2` ejecutar ifconfig ${interfaz} `cat ${i}` up done # Aquí establezco el gateway por defecto, el cual está listado en /etc/red/gateway. ejecutar route add default gw `cat /etc/red/gateway` fcartel # Por cada entrada en /etc/consolas vuelco el contenido de /etc/msjdia en la terminal # adecuada. Luego escribo el número de terminal y por último ejecuto Bash en esa terminal. ecartel "Iniciando terminales virtuales" for i in `cat /etc/consolas`; do cat /etc/msjdia > /dev/${i} 2>/dev/null echo "Terminal: ${i}" > /dev/${i} setsid bash -i </dev/${i} >/dev/${i} 2>&1 & done fcartel # Por último ejecuto bash y muestro el mensaje del día en la primer terminal, la cual siempre # estará presente (por eso no la listamos en /etc/consolas). cat /etc/msjdia 2>/dev/null while true; do setsid bash -i </dev/tty1 >/dev/tty1 2>&1 echo -e "${COLORERR}No trates de matar a init!!!${NOCOLOR}" sleep 1 done
Notemos un par de cosas:
- Cuando ejecutamos el servicio udev, si en este punto piden un listado de directorio de /dev verán que udev todavía no se tomó el trabajo de crear ningún dispositivo, sin embargo si introducen otro pendrive verán como se crean los dispositivos /dev/sdb y /dev/sdb1 (o los dispositivos correspondientes); esto es así porque todos los dispositivos que estaban ya conectados en el arranque no generan nuevos eventos de modo que udev los reconozca, para que vuelvan a generar todos los eventos ejecutamos udevtrigger. También podríamos haber hecho lo siguiente, de modo que escribamos en cada archivo uevent dentro de /sys para que el dispositivo correspondiente genere el evento adecuado. Pero además de ser una manera primitiva de hacerlo pronto dejará de ser soportada:
for i in `find /sys -name 'uevent'`; do echo > ${i} done
- Cuando ejecutamos el comando hwclock simplemente suponemos que la hora está en formato local, se podría pedirle al usuario que lo confirme, o bien utilizar un archivo (ej: /etc/reloj) indicando si es local o no. Ej:
function hora_local { hwclock --hctosys --localtime } function hora_utc { hwclock -hctosys --utc } hora=`cat /etc/reloj` if [ $hora = "UTC" -o $hora = "utc" ]; then hora_utc elif [ $hora = "LOCAL" -o $hora = "local" ]; then hora_local else echo "Que hora utiliza Ud.?" select eleccion in local utc; do if [ $eleccion -a $eleccion = "utc" ]; then hora_utc break; elif [ $eleccion -a $eleccion = "local" ]; then hora_local break; fi if [ $REPLY = "UTC" -o $REPLY = "utc" ]; then hora_utc break elif [ $REPLY = "local" -o $REPLY = "LOCAL" ]; then hora_local break fi done if [ ! $REPLY ]; then hora_local fi fi
- Cuando ejecutamos Bash para las consolas virtuales, le anteponemos el comando setsid, esto es así porque es útil que bash pueda realizar manejo de trabajos (por ej. correr trabajos en el fondo, traerlos al frente, suspenderlos con CTRL+Z, interrumpirlos con CTRL+C, etc) pero para ello debe ser el líder de la sesión actual, entonces con setsid simpemente creamos una sesión nueva y establecemos el ID de grupo del proceso de bash como lider de la nueva sesión. Además le decimos bash que se ejecute de manera interactiva con el argumento "-i" y también redirigimos la entrada, salida y salida de error estándar a /dev/ttyX de modo que funcione en la terminal correspondiente. Además le agregamos al argumento un "&" de modo que se ejecute en el fondo y no tengamos que esperar a que termine.
- Una vez que terminamos simplemente mostramos el mensaje del día en la terminal actual, y en un ciclo infinito arrancamos bash en una nueva sesión en la terminal 1. Si bash termina en algún momento indicamos con un cartel rojo que no se puede matar a init, dormimos un segundo y volvemos a iniciar bash. Si en lugar de esto hubiésemos ejecutado en una simple línea a bash, al salir de bash se hubiese provocado un KERNEL PANYC pues el script hubiese terminado, y como el script es el proceso 1 (init) este no puede terminar.
[editar] GRUB
Bien, ya tenemos todo listo, tan solo nos falta el gestor de arranque. Pero antes de empezar aclaremos algo: Cuando arrancamos desde un pendrive USB la BIOS por lo general nos ofrece opciones como USB-FDD, USB-ZIP, USB-CDROM o USB-HDD, aquí confiamos en que la BIOS nos deje arrancar como si fuera un disco rígido, si nuestra BIOS no lo permite quizás podamos utilizar USB-CDROM o USB-ZIP realizando los cambios pertinentes (recuerden que ahora la BIOS emula al pendrive como un CDROM o un ZIP) sin embargo si queremos utilzar USB-FDD estamos perdidas, ya nuestra imagen initrd de 4 MB es mucho más grande que un disquette de 1,44 MB. Como suponemos que la BIOS emulará un disco rígido entonces necesitamos que nuestro dispositivo USB tenga tal estructura (por lo general es así pues nos aparece tanto /dev/sda como /dev/sda1, si no tuviese particiones sería tan solo /dev/sda). Podemos asegurarnos de esto de la siguiente manera:
# fdisk -l /dev/sda Disco /dev/sda: 1027 MB, 1027604480 bytes 32 heads, 62 sectors/track, 1011 cylinders Units = cilindros of 1984 * 512 = 1015808 bytes Disk identifier: 0x68bfc462 Disposit. Inicio Comienzo Fin Bloques Id Sistema /dev/sda1 * 1 1011 1002881 83 Linux
Como ven hay una partición y además hay suficiente espacio como para que GRUB embeba su stage 1.5, bueno, enchufemos nuestro pendrive (supondremos que es /dev/sda1) y ejecutemos los siguientes comandos:
# mkfs.ext2 /dev/sda1 # mount -t ext2 /dev/sda1 ~/trabajo/mnt # cd ~/trabajo # cp -R liveusb/* mnt/ # cp initrd.bin.gz mnt/ # mkdir -p mnt/grub # cp -R /boot/grub/* mnt/grub/* # cat << FIN > mnt/grub/menu.lst > timeout 30 > default 0 > splashimage=(hd0,0)/grub/splash.xpm.gz > > # For booting GNU/Linux > title Live USB > root (hd0,0) > kernel /kernel root=/dev/ram0 ro init=/inicio espera=5 > initrd /initrd.bin.gz > FIN # cd ~/trabajo # umount mnt/
Vean que estamos utilizando una splashimage que Uds. no tiene porque tener, de modo que esa línea la pueden obviar. Vean como le estamos diciendo que la partición raíz es /dev/ram0 (root=/dev/ram0) esto es así porque initrd aparece reflejado en /dev/ram0 y queremos que este sea nuestro dispositivo raíz (hasta que lo cambiemos con pivot_root). También vean que le decimos donde encontrar el proceso init (dentro del initrd) y además le pasamos el parámetro espera=5 que utilizará nuestro script para saber cuanto tiempo esperar antes de escanear los dispositivos USB en busca del pendrive. Y por último noten que le pasamos la imagen del initrd :)
Ahora nos resta instalar GRUB en el pendrive, para ello ejecutamos lo siguiente:
# grub
GNU GRUB version 0.97 (640K lower / 3072K upper memory)
[ Minimal BASH-like line editing is supported. For the first word, TAB
lists possible command completions. Anywhere else TAB lists the possible
completions of a device/filename. ]
grub> root (hd2,0)
Filesystem type is ext2fs, partition type 0x83
grub> setup (hd2)
Checking if "/boot/grub/stage1" exists... no
Checking if "/grub/stage1" exists... yes
Checking if "/grub/stage2" exists... yes
Checking if "/grub/e2fs_stage1_5" exists... yes
Running "embed /grub/e2fs_stage1_5 (hd2)"... 15 sectors are embedded. succeeded
Running "install /grub/stage1 (hd2) (hd2)1+15 p (hd2,0)/grub/stage2 /grub/menu.lst"... succeeded
Done.
grub> quit
Asegúrense que hd2 sea en su caso el disco correcto (el pendrive), de lo contrario pueden utilizar el siguiente comando en grub:
grub> device (hd7) /dev/sda
ahora pueden estar seguros que hd7 es el pendrive (desde luego, en lugar de utilizar /dev/sda utilicen el dispositivo adecuado).
[editar] Tmpfs
Anteriormente dijimos que tmpfs era un disco en RAM, pero la verdad es que era una pequeña mentira para seguir adelante. Un disco en RAM es simplemente un dispositivo por bloques, y como tal no conoce absolutamente nada de archivos, directorios y demás; de modo que necesita una capa de abstracción más, el "sistema de archivo" como por ejemplo ext2, reiserfs, xfs, etc. Sin embargo, estos sistemas de archivos están diseñados para trabajar en discos reales donde siempre se tiene el mismo tamaño, es decir, un disco rígido no puede crecer y achicarse al vuelo (no, un disco rígido no, si me hablan de LVM y demás yerbas quizás si) de modo que un disco en RAM sigue el mismo camino; pero esto es una lástima ya que el disco en RAM está limitado, si nos falta memoria no nos podemos deshacer de un pedazo del disco en RAM, y si el disco en RAM nos queda chico no lo podemos agrandar de manera dinámica (¿¿LVM sobre discos en RAM?? No, no lo creo :P).
Pero aquí llego tmpfs. Este sistema de archivos no requiere un disco en RAM detrás de él, pues utiliza la caché de disco de Linux como dispositivo por bloque; lo interesante de esto es que la caché de Linux está constantemente fluctuando de tamaño y tmpfs es un sistema de archivo diseñado especialmente para manejarse en un medio que cambia de tamaño automáticamente. Supongamos que nos quedamos sin memoria, entonces simplemente podemos borrar un archivo de nuestro directorio tmpfs y automáticamente liberará la memoria; y también podemos decir que inicialmente no consume memoria y a medida que vamos creando archivos el consumo empieza aumentar. Por suerte le podemos establecer un tamaño máximo de modo que el sistema de archivos no crecerá más que eso.
Otra ventaja de tmpfs además de su dinamismo es que reside en la caché de disco de Linux, ¿y qué quiere decir esto? Cuando accedemos a un disco en RAM común y corriente Linux guarda en la caché este acceso como si fuera un disco común y corriente, ¡pero no lo es! sería mucho más eficiente utilizar al mismo disco en RAM como caché. Con tmpfs no hay problemas porque el sistema de archivos utiliza la misma caché, de modo que no hay duplicados y todo se vuelve más eficiente.
[editar] Apagar el Sistema
Si prestaron atención, arriba había unos archivos apagar, reiniciar y parar; esto es así porque si utilizamos los comandos halt, reboot o shutdown que trae nuestro querida distribución no lograremos mcuho. Estos comandos se comunican con el proceso init (a través de la tubería /dev/initctl) para decirle que queremos apagar el sistema; init se encarga luego de cambiar de runlevel, ejecutar los scripts de finalización, quienes a su vez ejecutan halt o reboot con unos flags especiales que le indican que NO se comuniquen con init, sino que simplemente apaguen o reinicien la computadora, puesto que ya está preparada para ser apagada. Como nosotros no tenemos al proceso init tradicional de Linux lo que vamos a hacer es un pequeño script que se encargue de apagar la computadora, el cual será simplemente un envoltorio para reboot y halt.
- /sbin/apagar:
#!/bin/bash
halt -fdihp
- /sbin/reiniciar:
#!/bin/bash
reboot -fdhip
- /sbin/parar:
#!/bin/bash
halt -fdih
Los flags tienen los siguientes significados:
- -f: Forzar el apagado (sin llamar a shutdown/avisar a init)
- -d: Sin escribir wtmp (el cual no tenemos en nuestro pequeño sistema)
- -h: Llevar discos IDE a standby (lo cual hace además que escriban sus caches)
- -i: Apagar las interfaces de red
- -p: Power-off (apagar la computadora)
Listo, ya podemos apagar el equipo.
[editar] initramfs
Hasta ahora hemos utilizado initrd, pero esa es la antigua manera de hacer las cosas. Hoy por hoy se utliza initramfs, ¿por qué? pues porque tiene dos grandes ventajas:
- No hay que crear una imágen de tamaño fijo para luego formatearla y cargarle archivos, simplemente se crea un archivo comprimido con los contenidos, lo cual se hace en una línea de código.
- Initramfs utiliza el sistema de archivos tmpfs, lo cual permite que el espacio ocupado en RAM aumente o disminuya de manera dinámica.
Veamos las diferencias:
- No especificaremos quién será init mediante el parámetro init=..., siempre se ejecutará el archivo /init
- No podemos utilizar pivot_root.
La diferencia más importante es que no podemos utilizar pivot_root. Cuando utilizábamos initrd, este aparecía bajo el dispositivo /dev/ram0, el cual era montado como dispositivo raíz, al utilizar pivot_root estábamos moviendo /dev/ram0 a otro lugar y montábamos un dispositivo nuevo como raíz.
Hay veces que para facilitar el manejo de estructuras de datos o algoritmos se toman decisiones como por ejemplo que una lista nunca pueda quedar vacía; esto es lo que han hecho los desarrolladores del kernel de Linux al decir que la lista de sistemas de archivos montados nunca puede quedar vacía, siempre existe al menos la entrada rootfs. Cuando utilizábamos initrd en realidad teníamos por lo menos dos sistemas de archivos montados:
- rootfs en /
- /dev/ram0 en /
el segundo "tapaba" al primero, al utilizar pivot_root, lo que lográbamos era tener algo así:
- rootfs en /
- /dev/sda1 en /
pero rootfs siempre está. Con initramfs la situación cambia un poco, los contenidos del archivo comprimido se vuelcan directamente sobre rootfs, si quisiéramos utilizar pivot_root le estaríamos diciendo que mueva rootfs a otro lugar, y encima luego lo desmontaríamos; todo eso sería catastrófico puesto que la lista de sistemas de archivos no puede quedar vacía. ¿Qué hace Linux? nos prohíbe utiliar pivot_root.
Para realizar una tarea similar a la que hacía pivot_root haremos lo siguiente:
- Borrar el contenido de / (rootfs) sin descender a los otros sistemas de archivos, de esta manera liberamos memoria. Sería el equivalente a liberar la memoria utilizada por /dev/ram0.
- Montar el pendrive encima de rootfs, de modo que nos queda la siguiente estructura:
- rootfs en /
- /dev/sda1 en /
Todo esta funcionalidad y más la podremos encontrar en BusyBox, pero como no todos tenemos ganas de instalarlo lo que haremos será nuestro propio programa que se encargue de esto, es básicamente un estracto modificado de BusyBox (por si quedan dudas, la licencia del código es la GPL, y el programa original se llama switch_root):
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <dirent.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/fcntl.h> #include <sys/mount.h> #include <sys/stat.h> #include <sys/vfs.h> #ifndef RAMFS_MAGIC #define RAMFS_MAGIC 0x858458f6 #endif #ifndef TMPFS_MAGIC #define TMPFS_MAGIC 0x01021994 #endif #ifndef MS_MOVE #define MS_MOVE 8192 #endif static dev_t raiz; // Esta función simplemente comprueba que la raíz sea válida int camino_valido ( const char *raiz_nueva ) { struct stat s1, s2; struct statfs fs1; // Nos aseguramos que / y la nueva raíz estén en dispositivos diferentes, y además que la nueva raíz // sea un directorio. errno = 0; if ( lstat ( "/", &s1 ) || lstat ( raiz_nueva, &s2 ) || s1.st_dev == s2.st_dev || !S_ISDIR ( s2.st_mode ) ) { if ( errno ) perror ( "lstat" ); else fprintf ( stderr, "Debe elegir un directorio raíz válido\n" ); return -1; } // Nos aseguramos que la vieja raíz esté sombre TMPFS o RAMFS, y además que exista el archivo /init // lo cual quiere decir que probablemente sea initramfs. errno = 0; if ( statfs ( "/", &fs1 ) || lstat ( "/init", &s2 ) || !S_ISREG ( s2.st_mode ) || ( fs1.f_type != TMPFS_MAGIC && fs1.f_type != RAMFS_MAGIC ) ) { if ( errno ) perror ( "statfs/lstat" ); else fprintf ( stderr, "Tipo de sistema de archivos incorrecto.\nDebería ser TMPFS\n" ); return -1; } // Guardamos el dispositivo raíz para después. raiz = s1.st_dev; return 0; } // Borramos todos los archivos recursivamente sin descender a otros dispositivos. int borrar ( const char *camino ) { DIR *dir; struct dirent *entrada; struct stat s; char *nombre; // Si el directorio a borrar está en otro dispositivo, entonces no entramos. if ( lstat ( camino, &s ) || s.st_dev != raiz ) return 0; // Abrimos el directorio if ( ( dir = opendir ( camino ) ) == NULL ) { perror ( "opendir" ); return -1; } // Por cada entrada en el directorio... errno = 0; while ( ( entrada = readdir ( dir ) ) != NULL ) { // Slatamos . y .. if ( entrada->d_name [ 0 ] == '.' && ( entrada->d_name [ 1 ] == '\0' || ( entrada->d_name [ 1 ] == '.' && entrada->d_name [ 2 ] == '\0' ) ) ) continue; // ¿Es un directorio? if ( entrada->d_type == DT_DIR ) { // Entonces generamos una cadena del estilo camino_viejo/directorio // Espacio para camino, /, d_name y '\0' nombre = malloc ( strlen ( entrada->d_name ) + strlen ( camino ) + 2 ); if ( ! nombre ) { fprintf ( stderr, "Error al pedir memoria\n" ); errno = 0; continue; } sprintf ( nombre, "%s/%s", camino, entrada->d_name ); borrar ( nombre ); // Borramos el directorio free ( nombre ); } else unlink ( entrada->d_name ); // Era un archivo, lo borramos errno = 0; } if ( errno ) { perror ( "readdir" ); closedir ( dir ); return -1; } closedir ( dir ); rmdir ( camino ); return 0; } int main ( int argc, char **argv ) { char *raiz_nueva; if ( argc != 3 ) { fprintf ( stderr, "Uso: switch_root raiz_nueva programa\n" ); return -1; } // Nos aseguramos que sea init el que ejecute este programa if ( getpid () != 1 ) { fprintf ( stderr, "No sos INIT, lo siento :(\n" ); return -1; } // Comprobamos la validez de la nueva raíz argv++; raiz_nueva = *argv++; if ( camino_valido ( raiz_nueva ) ) return -1; // Cambiamos de directorio, ahora estamos en la nueva raíz chdir ( raiz_nueva ); // Borramos la raíz vieja if ( borrar ( "/" ) ) fprintf ( stderr, "No se pudo liberar toda la memoria\n" ); // Movemos de manera atómica la raíz nueva a / // Luego cambiamos nuestro directorio raíz al directorio de trabajo actual // lo cual es necesario para que se actualicen algunos campos del proceso // en el kernel de Linux. // Luego cambiamos de directorio de trabajo a /, para que ya no sea algo como /pendrive // es decir, seguimos actualizando nuestro proceso if ( mount ( ".", "/", NULL, MS_MOVE, NULL ) || chroot ( "." ) ) { perror ( "mount/chroot" ); return -1; } chdir ( "/" ); // Cerramos la vieja consola y abrimos la nueva. close ( 0 ); close ( 1 ); close ( 2 ); open ( "/dev/console", O_RDWR ); dup2 ( 0, 1 ); dup2 ( 0, 2 ); // Ejecutamos el nuevo init (que pasamos como argumento) execve ( *argv, argv, NULL ); perror ( "execve" ); return -1; }
Ahora simplemente compilamos:
# gcc cambiar_raiz.c -o cambiar_raiz
ya podemos copiar este ejecutable a /bin dentro de nuestro directorio de trabajo de initrd. Seguidamente procedemos a actualizar el script de inicialización /init:
#cd /pendrive #pivot_root . initrd #exec chroot . /sbin/init <dev/console >dev/console 2>&1 #/bin/sh exec cambiar_raiz /pendrive /sbin/init exec /bin/sh /bin/sh
Nuestro programa se encargó de todo. Noten que antes utilizábamos direcciones relativas (dev/console) y ahora utilizamos direcciones absolutas (/dev/console), esto se puede hacer porque nuestro programa ya actualizó todos los datos y referencias del proceso. Por último, generamos initramfs de la siguiente manera:
# cd ~/trabajo/initrd # find . | cpio -H newc -o | gzip -9 > ../initrd.bin.gz
ahora ya tenemos el archivo ~/trabajo/cpio.gz que podemos guardar en el pendrive. Por último modificamos el archivo menu.lst:
# cd ~/trabajo/liveusb/grub # cat menu.lst | sed 's/root=\/dev\/ram0 ro init=\/inicio //' > menu2.lst # mv menu2.lst menu.lst
el que quiera realizar el trabajo a mano, simplemente deberá cambiar la línea
kernel /kernel root=/dev/ram0 ro init=/inicio espera=5
por la línea
kernel /kernel espera=5
Ya deberíamos tener nuestro initramfs funcionando.
[editar] TIPS
- En lugar de haber copiado un montón de ejecutables (chown, ls, chmod, ps, etc.) podríamos tan solo haber copiado busybox y luego realizar enlaces simbólicos al mismo para poblar el sistema:
# ln -s busybox ls # ln -s busybox chown # ln -s busybox chmod # ln -s busybox ps # ln -s busybox lsattr # ln -s busybox telnet ...
- Podemos volver nuestro entorno un poco más agradable si le agregamos colores al prompt y si le pedimos a comandos como ls o grep que marquen con colores los distintos archivos o coincidencias, para eso podemos crear el archivo /etc/bash/bashrc con los siguientes contenidos:
export PS1="[\033[1;31m\u\033[1;33m@\033[1;32m\h \033[1;34m\W\033[m]# " alias ls="ls --color=auto" alias grep="grep --color=auto"
- Como habrán notado hemos incluído a ext2 como sistema de archivos en lugar del mejorado ext3, esto es así porque ext3 es un sistema con "journaling" lo que se traduce en escrituras en un lugar dado del pendrive una y otra vez, lo cual puede "gastar" la zona en particular; por eso elegimos un sistema sin "journaling". Además, en general, ni ext2 ni ext3 están diseñados para trabajar con pendrives, una mejor opción es JFFS2 que está diseñado con ese propósito, aquí no lo utilizamos para poder trabajar con herramientas más conocidas.
[editar] Recursos
- /etc
- Tutorial de Bash
- Particiones
- Udev
- http://es.wikipedia.org/wiki/Código_escape_ANSI
- http://es.wikipedia.org/wiki/Initrd
[editar] Recursos en Inglés
- http://imil.net/nlk/
- http://typo.submonkey.net/articles/2006/04/13/installing-freebsd-on-usb-stick-episode-2
- http://www.lesbell.com.au/Home.nsf/b8ec57204f60dfcb4a2568c60014ed0f/ec1040fb1ecd6097ca256f0d002361f4?OpenDocument
- http://linuxdevices.com/articles/AT4017834659.html
- http://www.ibm.com/developerworks/linux/library/l-initrd.html?ca=dgr-lnxw01LinuxInitialRam
- http://wiki.debian.org/InitrdReplacementOptions
