Esta sección explica los pasos dados durante la compilación del núcleo Linux y la salida producida en cada etapa. El proceso de construcción depende de la arquitectura, por lo tanto me gustaría enfatizar que sólo consideraremos la construcción de un núcleo Linux/x86.
Cuando el usuario escribe 'make zImage' o 'make bzImage' la imagen
inicializable del núcleo resultante es almacenado como
arch/i386/boot/zImage
o
arch/i386/boot/bzImage
respectivamente.
Aquí está como es construida la imagen:
vmlinux
, el cual es un fichero ejecutable ELF 32-bits
LSB 80386 estáticamente enlazado al que no se le han eliminado los
símbolos de depuración.
System.map
es producido por nm vmlinux, donde
los símbolos irrelevantes o que no interesan son desechados.
arch/i386/boot
.
bootsect.S
es preprocesado con o sin -D__BIG_KERNEL__,
dependiendo de cuando el objetivo es bzImage o zImage, en
bbootsect.s
o bootsect.s
respectivamente.
bbootsect.s
es ensamblado y entonces convertido en
la forma 'raw binary' llamada bbootsect
(o
bootsect.s
ensamblado y convertido a raw en
bootsect
para zImage).
setup.S
(setup.S
incluye video.S
) es preprocesado en bsetup.s
para bzImage o setup.s
para zImage. De la misma forma
que el código del sector de arranque, la diferencia radica en
que -D__BIG_KERNEL__ está presente para bzImage. El
resultado es entonces convertido en la forma 'raw binary'
llamada bsetup
.
arch/i386/boot/compressed
y se
convierte /usr/src/linux/vmlinux
a $tmppiggy (nombre
temporal) en el formato binario raw, borrando las
secciones ELF .note
y .comment
.
piggy.o
.
head.S
y
misc.c
(todavía en el directorio
arch/i386/boot/compressed
) en los objetos
ELF head.o
y misc.o
.
head.o
, misc.o
y
piggy.o
en bvmlinux
(o vmlinux
para
zImage, ¡no confundas esto con
/usr/src/linux/vmlinux
!). Destacar la diferencia entre
-Ttext 0x1000 usado para vmlinux
y
-Ttext 0x100000 para bvmlinux
, esto es, el
cargador de compresión para bzImage es cargado más arriba.
bvmlinux
a 'raw binary' bvmlinux.out
borrando las secciones ELF .note
y .comment
.
arch/i386/boot
y, usando
el programa tools/build, se concatenan todos ellos:
bbootsect
, bsetup
y compressed/bvmlinux.out
en bzImage
(borra la 'b' extra anterior para zImage
).
Esto escribe variables importantes como setup_sects
y
root_dev
al final del sector de arranque.0x4000 bytes >= 512 + sectores_configuración * 512 + espacio para la pila mientras está funcionando el sector de arranque/configuración
Veremos más tarde de dónde viene esta limitación.
El límite superior en el tamaño de bzImage producido en este paso está sobre los 2.5M para arrancarcon LILO y 0xFFFF párrafos ((0xFFFF0 = 1048560 bytes) para arrancar imágenes directamentee, por ejemplo desde un diskette o CD-ROM (con el modo de emulación El-Torito).
Destacar que mientras que tools/build valida el tamaño del
sector de arranque, la imagen del núcleo y el límite inferior del
tamaño de la configuración, no chequea el límite *superior* de
dicho tamaño de
configuración. Entonces, es fácil construir un núcleo defectuoso
justamente sumándole algún gran ".espacio" al final de setup.S
.
Los detalles del proceso de arranque son específicos de cada arquitectura, por lo tanto centraremos nuestra atención en la arquitectura IBM PC/IA32. Debido al diseño antiguo y a la compatibilidad hacia atrás, el firmware del PC arranca el sistema operativo a la vieja usanza. Este proceso puede ser separado en las siguientes seis etapas lógicas:
El sector de arranque usado para arrancar el núcleo Linux puede ser uno de los siguientes:
arch/i386/boot/bootsect.S
),Consideraremos aquí el sector de arranque de Linux en detalle. Las primeras lineas inicializan las macros convenientes para ser usadas por los valores de segmento:
29 SETUPSECS = 4 /* tamaño por defecto de los sectores de configuración */
30 BOOTSEG = 0x07C0 /* dirección original del sector de arranque */
31 INITSEG = DEF_INITSEG /* movemos el arranque aquí - lejos del camino */
32 SETUPSEG = DEF_SETUPSEG /* la configuración empieza aquí */
33 SYSSEG = DEF_SYSSEG /* el sistema es cargado en 0x10000 (65536) */
34 SYSSIZE = DEF_SYSSIZE /* tamaño del sistema: # de palabras de 16 bits */
(los números a la izquierda son los números de linea del archivo
bootsect.S)
Los valores de DEF_INITSEG
, DEF_SETUPSEG
,
DEF_SYSSEG
y DEF_SYSSIZE
son tomados desde
include/asm/boot.h
:
/* No toques esto, a menos que realmente sepas lo que estás haciendo. */
#define DEF_INITSEG 0x9000
#define DEF_SYSSEG 0x1000
#define DEF_SETUPSEG 0x9020
#define DEF_SYSSIZE 0x7F00
Ahora, consideremos el código actual de bootsect.S
:
54 movw $BOOTSEG, %ax
55 movw %ax, %ds
56 movw $INITSEG, %ax
57 movw %ax, %es
58 movw $256, %cx
59 subw %si, %si
60 subw %di, %di
61 cld
62 rep
63 movsw
64 ljmp $INITSEG, $go
65 # bde - cambiado 0xff00 a 0x4000 para usar el depurador después de 0x6400 (bde).
66 # No tendríamos que preocuparnos por esto si chequeamos el límite superior
67 # de la memoria. También mi BIOS puede ser configurada para poner las tablas
68 # wini de controladoras en la memoria alta en vez de en la tabla de vectores.
69 # La vieja pila quizás tenga que ser insertada en la tabla de controladores.
70 go: movw $0x4000-12, %di # 0x4000 es un valor arbitrario >=
71 # longitud de sector de arranque + longitud de la
72 # configuración + espacio para la pila;
73 # 12 es el tamaño parm del disco.
74 movw %ax, %ds # ax y es ya contienen INITSEG
75 movw %ax, %ss
76 movw %di, %sp # pone la pila en INITSEG:0x4000-12.
Las lineas 54-63, mueven el código del sector de arranque desde la dirección 0x7C00 a 0x90000. Esto es realizado de la siguiente manera:
El motivo por el que este código no usa rep movsd
es
intencionado (hint - .code16).
La línea 64 salta a la etiqueta go:
en la nueva copia hecha
del sector de arranque, esto es, en el segmento 0x9000. Esto y las
tres instruciones siguientes (lineas 64-76) preparan la pila en
$INITSEG:0x4000-0xC, esto es, %ss = $INITSEG (0x9000) y %sp
= 0x3FF4 (0x4000-0xC). Aquí es de dónde viene el límite del tamaño de
la configuración que mencionamos antes (ver Construyendo la Imagen del
Núcleo Linux).
Las lineas 77-103 parchean la tabla de parámetros del disco para el primer disco para permitir lecturas multi-sector:
77 # Las tablas por defecto de parámetros del disco de muchas BIOS
78 # no reconocerán lecturas multi-sector más allá del número máximo especificado
79 # en las tablas de parámetros del diskette por defecto - esto
80 # quizás signifique 7 sectores en algunos casos.
82 # Como que las lecturas simples de sectores son lentas y fuera de la cuestión
83 # tenemos que tener cuidado con esto creando nuevas tablas de parámetros
84 # (para el primer disco) en la RAM. Estableceremos la cuenta máxima de sectores
85 # a 36 - el máximo que encontraremos en un ED 2.88.
86 #
87 # Lo grande no hace daño. Lo pequeño si.
88 #
89 # Los segmentos son como sigue: ds = es = ss = cs - INITSEG, fs = 0,
90 # y gs queda sin usar.
91 movw %cx, %fs # establece fs a 0
92 movw $0x78, %bx # fs:bx es la dirección de la tabla de parámetros
93 pushw %ds
94 ldsw %fs:(%bx), %si # ds:si es el código
95 movb $6, %cl # copia 12 bytes
96 pushw %di # di = 0x4000-12.
97 rep # no necesita cld -> hecho en la linea 66
98 movsw
99 popw %di
100 popw %ds
101 movb $36, 0x4(%di) # parchea el contador de sectores
102 movw %di, %fs:(%bx)
103 movw %es, %fs:2(%bx)
El controlador de diskettes es reinicializado usando el servicio de la BIOS int 0x13 función 0 (reinicializa FDC) y los sectores de configuración son cargados inmediatamente después del sector de arranque, esto es, en la dirección física 0x90200 ($INITSEG:0x200), otra vez usando el servicio de la BIOS int 0x13, función 2 (leer sector(es)). Esto sucede durante las lineas 107-124:
107 load_setup:
108 xorb %ah, %ah # reinicializa FDC
109 xorb %dl, %dl
110 int $0x13
111 xorw %dx, %dx # controladora 0, cabeza 0
112 movb $0x02, %cl # sector 2, pista 0
113 movw $0x0200, %bx # dirección = 512, en INITSEG
114 movb $0x02, %ah # servicio 2, "leer sector(es)"
115 movb setup_sects, %al # (asume todos en la cabeza 0, pista 0)
116 int $0x13 # los lee
117 jnc ok_load_setup # ok - continua
118 pushw %ax # vuelca el código de error
119 call print_nl
120 movw %sp, %bp
121 call print_hex
122 popw %ax
123 jmp load_setup
124 ok_load_setup:
Si la carga falla por alguna razón (floppy defectuoso o que alguien quitó el diskette durante la operación), volcamos el código de error y se intenta en un bucle infinito. La única forma de salir de él es reiniciando la máquina, a menos que los reintentos tengan éxito, pero usualmente no lo tienen (si algo está mal sólo se pondrá peor).
Si la carga de los sectores setup_sects del código de configuración es
realizada con éxito, saltamos a la etiqueta ok_load_setup:
.
Entonces procedemos a cargar la imagen comprimida del núcleo en la
dirección física 0x10000. Esto es realizado para preservar las áreas
de datos del firmware en la memoria baja (0-64K). Después de que es
cargado el núcleo, saltamos a $SETUPSEG:0(arch/i386/boot/setup.S
).
Una vez que los datos no se necesitan mas (ej. no se realizan más
llamadas a la BIOS) es sobreescrito moviendo la imagen entera
(comprimida) del núcleo desde 0x10000 a 0x1000 (direcciones físicas,
por supuesto).
Esto es hecho por setup.S
, el cual prepara las cosas para el
modo protegido y salta a 0x1000, que es el comienzo del núcleo
comprimido, esto es, arch/386/boot/compressed/{head.S,misc.c}
.
Esto inicializa la pila y llama a decompress_kernel()
, que
descomprime el núcleo en la dirección 0x100000 y salta a ella.
Destacar que los viejos cargadores de arranque (viejas versiones de LILO) sólo podían cargar los 4 primeros sectores de la configuración, el cual es el motivo por el que existe código en la configuración para cargar el resto de si mismo si se necesita. También, el código en la configuración tiene que tener cuidado de varias combinaciones de tipo/versión del cargador vs zImage/bzImage y esto es altamente complejo.
Examinemos este truco en el código del sector de arranque que nos permite cargar un núcleo grande, también conocido como "bzImage".
Los sectores de configuración son cargados usualmente en la dirección
0x90200, pero el núcleo es cargado en fragmentos de 64k cada vez usando
una rutina de ayuda especial que llama a la BIOS para mover datos desde
la memoria baja a la memoria alta. Esta rutina de ayuda es referida por
bootsect_kludge
en bootsect.S
y es definida como
bootsect_helper
en setup.S
.
La etiqueta bootsect_kludge
en setup.S
contiene el
valor del segmento de configuración y el desplazamiento del código
bootsect_helper
en él, por lo que el sector de arranque puede
usar la instrucción lcall
para saltar a él (salto entre
segmentos).
El motivo por lo cual esto es realizado en setup.S
es
simplemente porque no existe más espacio libre en bootsect.S (lo cual no es
estrictamente verdad - hay aproximadamente 4 bytes dispersos y al menos
1 byte disperso en bootsect.S
, pero que obviamente no es
suficiente).
Esta rutina usa el servicio de la BIOS int 0x15 (ax=0x8700) para moverlo
a la memoria alta y restablecer %es al punto de siempre 0x10000. Esto
asegura que el código en bootsect.S
no se va fuera de memoria
cuando está copiando datos desde disco.
Existen varias ventajas en usar un cargador de arranque especializado (LILO) sobre un esqueleto desnudo de un sector de arranque:
La última cosa que hace LILO es saltar a setup.S
y entonces
las cosas prosiguen de la forma normal.
Por "Inicialización de Alto Nivel" consideramos cualquier cosa que no
está directamente relacionada con la fase de arranque, incluso
aquellas partes del código que están escritas en ensamblador,
esto es arch/i386/kernel/head.S
, que es el
comienzo del núcleo descomprimido. Los siguientes pasos son realizados:
start_kernel()
,y si ready=1 todas
las otras llaman a arch/i386/kernel/smpboot.c:initialize_secondary()
el cual recarga esp/eip y no retorna.La función init/main.c:start_kernel()
está escrita en C y
realiza lo siguiente:
kmem_cache_init()
, inicializa la mayoría del asignador
slab.mem_init()
, que calcula max_mapnr
,
totalram_pages
y high_memory
y muestra la linea
"Memory: ...".kmem_cache_sizes_init()
, finaliza la inicialización del
asignador slab.fork_init()
, crea uid_cache
, inicializa
max_threx_threads
basándose en la cantidad de memoria
disponible y configura RLIMIT_NPROC
para que
init_task
sea max_threads/2
.init()
que ejecuta execute_command si este ha
sido suministrado a través del parámetro de inicio "init=", o
intenta ejecutar /sbin/init, /etc/init,
/bin/init, /bin/sh en este orden; si todos
estos fallan, ocurre una situación de pánico con la "sugerencia"
de usar el parámetro "init=".Una cosa importante que hay que hacer notar aquí es que el hilo del
núcleo init()
llama a do_basic_setup()
, el cual cuando
vuelve llama a do_initcalls()
, que va a través de la lista
de funciones registradas por medio de las macros __initcall
o
module_init()
y las invoca. Estas funciones no dependen de
otras o sus dependencias han sido manualmente arregladas por el orden de
enlazado en los Makefiles. Esto significa que, dependiendo de las
posición de los directorios en los árboles y de las estructuras en los
Makefiles, el orden en el cual estas funciones de inicialización son
llamadas puede cambiar.
A veces esto es importante, imagínate dos subsistemas A y B,
con B dependiendo de alguna inicialización realizada por A. Si A es
compilada estáticamente y B es un módulo entonces el punto de entrada
de B está garantizado para ser llamado después de que A prepare todo el
entorno necesario. Si A es un módulo, entonces B es también
necesariamente un módulo para que no existan problemas. Pero, ¿qué
pasa si A y B están estáticamente enlazadas en el núcleo? El orden en
el cual son llamadas depende del desplazamiento relativo del punto
de entrada en la sección ELF .initcall.init
de la imagen del
núcleo.
Rogier Wolff propuso introducir una infraestructura jerárquica de
"prioridades" donde los módulos pueden dejar que el enlazador conozca
en que orden (relativo) deberían de ser enlazados, pero todavía
no existen parches disponibles que implementen esto de una forma
suficientemente elegante para ser aceptada en el núcleo.
Por consiguiente, asegúrate de que el orden de enlace es correcto, Si,
en el ejemplo anterior, A y B trabajan bien cuando han sido compilados
estáticamente una vez, trabajarán siempre, tal como han sido
listados secuencialmente en el mismo Makefile. Si no trabajan,
cambia el orden en el cual sus archivos objetos son listados.
Otra cosa de algún valor es la habilidad de Linux de ejecutar un
"programa init alternativo" por medio del pase de la linea de comandos
"init=". Esto es útil para la recuperación desde un /sbin/init
accidentalmente sobreescrito o para depurar a mano los guiones de
inicialización (rc) y /etc/inittab
, ejecutándolos de
uno en uno.
En SMP, el BP (Procesador de arranque) va a través de la
secuencia normal del sector de arranque, configuración, etc... hasta
que llega a start_kernel()
, y entonces sobre
smp_init()
y especialmente
src/i386/kernel/smpboot.c:smp_boot_cpus()
. La función
smp_boot_cpus()
entra en un buche para cada apicid (identificador de cada APIC),
hasta NR_CPUS
, y llama a do_boot_cpu()
en él. Lo que hace
do_boot_cpu()
es crear (esto es: fork_by_hand
) una
tarea vacía para la cpu de destino y escribe en localizaciones bien
conocidas definidas por la especificación Intel MP (0x467/0x469) el EIP del
código del trampolín encontrado en trampoline.S
. Entonces
genera STARTUP IPI a la cpu de destino la cual hace que este AP
(Procesador de Aplicación) ejecute el código en trampoline.S
.
La CPU de arranque crea una copia del código trampolín para cada CPU en la memoria baja. El código del AP escribe un número mágico en su propio código, el cual es verificado por el BP para asegurarse que el AP está ejecutando el código trampolín. El requerimiento de que el código trampolín tenga que estar en la memoria baja es forzado por la especificación Intel MP.
El código trampolín simplemente establece el registro %bx a uno, entra
en modo protegido y salta a startup_32, que es la entrada
principal a arch/i386/kernel/head.S
.
Ahora, el AP empieza ejecutando head.S
y descubriendo que no es
un BP, se salta el código que limpia BSS y entonces entra en
initialize_secondary()
, el cual justamente entra en la tarea
vacía para esta CPU - recalcar que init_tasks[cpu]
ya había
sido inicializada por el BP ejecutando do_boot_cpu(cpu)
.
Destacar que init_task puede ser compartido, pero cada hilo vacío debe de
tener su propio TSS. Este es el motivo por el que
init_tss[NR_CPUS]
es una array.
Cuando el sistema operativo se inicializa a si mismo, la mayoría del código y estructuras de datos no se necesitarán otra vez. La mayoría de los sistemas operativos (BSD, FreeBSD, etc.) no pueden deshacerse de esta información innecesaria, gastando entonces valiosa memoria física del núcleo. El motivo que ellos no lo realizan (ver el libro de McKusick 4.4BSD) es que "el código relevante está propagado a través de varios subsistemas y por lo tanto no es factible liberarlo". Linux, por supuesto, no puede usar tal escusa porque bajo Linux "si en principio algo es posible, entonces ya está implementado o alguien está trabajando en ello".
Por lo tanto, como he dicho anteriormente, el núcleo Linux sólo puede ser compilado como un binario ELF, y ahora adivinamos el motivo (o uno de los motivos) para ello. El motivo referente a deshechar el código/datos de inicialización es que Linux suministra dos macros para ser usadas:
__init
- para el código de inicialización__initdata
- para datosEstas evalúan al atributo especificador gcc (también conocido como
"gcc magic") tal como ha sido definido en include/linux/init.h
:
#ifndef MODULE
#define __init __attribute__ ((__section__ (".text.init")))
#define __initdata __attribute__ ((__section__ (".data.init")))
#else
#define __init
#define __initdata
#endif
Lo que esto significa es que si el código es compilado estáticamente
en el núcleo (MODULO no está definido), entonces es colocado en
la sección especial ELF .text.init
, el cual es declarado en el
mapa del enlazado en arch/i386/vmlinux.lds
. En caso contrario
(si es un módulo) las macros no evalúan nada.
Lo que pasa durante el arranque es que el hilo del núcleo "init"
(función init/main.c:init()
) llama a la función específica
de la arquitectura free_initmem()
la cual libera todas las
páginas entre las direcciones __init_begin
e
__init_end
.
En un sistema típico (mi estación de trabajo), esto resulta en la liberación de unos 260K de memoria.
Las funciones registradas a través de module_init()
son
colocadas en .initcall.init
el cual es también liberado en el
caso estático. La actual tendencia en Linux, cuando se está diseñando
un subsistema (no necesariamente un módulo), es suministrar puntos
de entrada init/exit desde las etapas tempranas del diseño para que en
el futuro, el subsistema en cuestión, pueda ser modularizado si se
necesita. Un ejemplo de esto es pipefs, ver fs/pipe.c
. Incluso
si un subsistema nunca fuese convertido a módulo, ej. bdflush (ver
fs/buffer.c
), aún es bonito y arreglado usar la macro
module_init()
contra su función de inicialización,
suministrada aunque no haga nada cuando la función es precisamente
llamada.
Hay dos macros más, las cuales trabajan de una manera similar, llamadas
__exit
y __exitdata
, pero ellas están más
directamente conectadas al soporte de módulos por lo que serán
explicadas en una sección posterior.
Déjanos recalcar qué es lo que le pasa a la linea de comandos cuando se le pasa al núcleo durante el arranque:
arch/i386/kernel/head.S
copia los primeros 2k de ella
fuera de la página cero. Nótese que la actual versión de
LILO (21) corta la linea de comandos a los 79 bytes. Esto es un
fallo no trivial en LILO (cuando el soporte para EBDA grandes
está activado) y Werner prometió arreglarlo próximamente.
Si realmente necesitas pasarle lineas de comando más grandes de
los 79 bytes, entonces puedes usar BCP o codificar tu linea
de comandos en la función
arch/i386/kernel/setup.c:parse_mem_cmdline()
.
arch/i386/kernel/setup.c:parse_mem_cmdline()
(llamada
por setup_arch()
, y esta llamada por
start_kernel()
) copia 256 bytes de la página cero a
saved_command_line
la cual es mostrada por
/proc/cmdline
. Esta misma rutina procesa la opción
"mem=" si está presente y realiza los ajustes apropiados en los
parámetros de la VM.
parse_options()
(llamada por start_kernel()
) el cual procesa algunos
parámetros "dentro del núcleo" (actualmente "init=" y
entorno/argumentos para init) y pasa cada palabra a
checksetup()
.
checksetup()
va a través del código en la sección ELF
.setup.init
y llama a cada función, pasándole la
palabra si corresponde. Nótese que usando el valor de retorno
de 0 desde la función registrada a través de __setup()
,
es posible pasarle el mismo "variable=value" a más de una
función con el "value" inválido a una y válido a otra.
Jeff Garzik comentó: "los hackers que hacen esto son buenos :)"
¿Por qué? Porque esto es claramente específico del orden de
enlazado, esto es, el enlazado del núcleo en un orden tendrá a la
llamada de la funciónA antes de la funciónB y otro los tendrá
en orden inverso, con el resultado dependiendo del orden.Por lo tanto, ¿cómo debemos de escribir el código que procesa la linea
de comandos del arranque? Nosotros usamos la macro __setup()
definida en include/linux/init.h
:
/*
* Usado por la configuración de parámetros de la línea de comandos
* del núcleo
*/
struct kernel_param {
const char *str;
int (*setup_func)(char *);
};
extern struct kernel_param __setup_start, __setup_end;
#ifndef MODULE
#define __setup(str, fn) \
static char __setup_str_##fn[] __initdata = str; \
static struct kernel_param __setup_##fn __initsetup = \
{ __setup_str_##fn, fn }
#else
#define __setup(str,func) /* nada */
endif
Por lo tanto, típicamente la usarás en tu código de esta forma (tomado
del código del controlador real, BusLogic HBA
drivers/scsi/BusLogic.c
):
static int __init
BusLogic_Setup(char *str)
{
int ints[3];
(void)get_options(str, ARRAY_SIZE(ints), ints);
if (ints[0] != 0) {
BusLogic_Error("BusLogic: Obsolete Command Line Entry "
"Format Ignored\n", NULL);
return 0;
}
if (str == NULL || *str == '\0')
return 0;
return BusLogic_ParseDriverOptions(str);
}
__setup("BusLogic=", BusLogic_Setup);
Destacar que __setup()
no hace nada por los módulos, por lo
tanto el código que quiere procesar la linea de comandos del arranque, que puede
ser un módulo o estar estáticamente enlazado, debe de ser
llamado pasándole la función manualmente en la rutina de inicialización
del módulo. Esto también significa que es posible escribir código
que procese los parámetros cuando es compilado como un módulo pero no
cuando es estático o viceversa.