Emulando a Linus Torvalds: Crea tu propio sistema operativo desde 0 (V)

Este artículo lo escribí para el blog en español DesdeLinux el 11 de enero de 2014 y ahora lo dejo aquí, en mi blog personal. El artículo está tal cual, sin ninguna modificación desde aquella fecha.

En esta quinta entrega veremos una tabla bastante parecida a la GDT tanto en teoría como en uso, nos referimos a la IDT. Las siglas de IDT hacen referencia a Interrupts Description Table y es una tabla que se usa para manejar las interrupciones que se produzcan. Por ejemplo, alguien hace una división entre 0, se llama a la función encargada de procesar. Estas funciones son los ISR (Interrupt Service Routines). Así pues vamos a crear la IDT y añadir algunos ISR.

Lo primero vamos a declarar las estructuras correspondientes a la IDT:

struct Entry{
    uint16_t base_low;
    uint16_t sel;
    uint8_t always0;
    uint8_t flags;
    uint16_t base_high;
} __attribute__((packed));

struct Ptr{
    uint16_t limit;
    uint32_t base;
} __attribute__((packed));

Como se observa si comparáis con la GDT la estructura Ptr es idéntica y la Entry es bastante parecida. Por consiguiente las funciones de poner una entrada (SetGate) e instalar (Install) son muy parecidas.

void ND::IDT::SetGate(uint8_t num,uint32_t base,uint16_t sel, uint8_t flags)
{
	idt[num].base_low=(base & 0xFFFF);
	idt[num].base_high=(base >> 16) & 0xFFFF;
	idt[num].sel=sel;
	idt[num].always0=0;
	idt[num].flags=flags;
}

Instalar:

idtptr.limit=(sizeof(struct ND::IDT::Entry)*256)-1;
idtptr.base=(uint32_t)&idt;
ND::Memory::Set(&idt,0,sizeof(struct ND::IDT::Entry)*256);
ND::IDT::Flush();

Si nos fijamos veremos que la función de instalar usa la función ND::Memory::Set que habíamos declarado en el otro post. También podemos apreciar como no hacemos ninguna llamada a SetGate todavía y llamamos a ND::IDT::Flush, para esta función usamos otra vez la sentencia asm volatile:

asm volatile("lidtl (idtptr)");

Si todo va bien y hacemos un arreglo estético debería quedar así:

NextDivel-IDT

Bien, ahora vamos a empezar a rellenar la IDT con interrupciones. Aquí voy a crear solo una pero para el resto se haría igual. Voy a hacer la interrupción de división por cero. Como bien sabrán en matemáticas no se puede dividir un número entre 0. Si esto ocurre en el procesador se genera una excepción ya que no puede continuar. En la IDT la primera interrupción en la lista (0) corresponde a este suceso.

Añadimos esto entre el seteo de memoria y el flush dentro de la función Install de la IDT:

ND::IDT::SetGate(0,(unsigned)ND::ISR::ISR1,0x08,0x8E);

La función de callback va a ser ND::ISR::ISR1 que es bastante simple aunque debemos usar ASM:

void ND::ISR::ISR1()
{
	asm volatile(
    "cli \n"
    "pushl 0 \n"
    "pushl 0 \n"
    "jmp ND_ISR_Common \n");
}

NDISRCommon lo definiremos como una función en lenguaje C. Para ahorrar ficheros y mejorar legibilidad podemos usar extern “C”{}:

extern "C"

void ND_ISR_Common()
{
    asm volatile(
    "pusha \n"
    "push %ds \n"
    "push %es \n"
    "push %fs \n"
    "push %gs \n"
    "movw $0x10, %ax \n"
    "movw %ax, %ds \n"
    "movw %ax, %es \n"
    "movw %ax, %fs \n"
    "movw %ax, %gs \n"
    "movl %esp, %eax \n"
    "push %eax \n"
    "movl $ND_ISR_Handler, %eax \n"
    "call *%eax \n"
    "popl %eax \n"
    "popl %ds \n"
    "popl %es \n"
    "popl %fs \n"
    "popl %gs \n"
    "popa \n"
    "addl 8, %esp \n"
    "iret \n"
    );
}

Este código en ASM puede ser un poco difícil de entender pero esto es así porque vamos a declarar una estructura en C para acceder a los datos que genere la interrupción. Obviamente si no quisieras eso podrías llamar simplemente en ND::ISR::ISR1 al Kernel Panic o algo por el estilo. La estructura tiene una forma tal que así:

struct regs{
	uint32_t ds;
	uint32_t edi, esi, ebp, esp, ebx, edx, ecx, eax;
	uint32_t int_no, err_code;
	uint32_t eip, cs, eflags, useresp, ss;
};

Y por último hacemos la función NDISRHandler (también con link del C) en que mostramos un kernel panic y una pequeña descripción del error según el que tenemos en una lista de errores.

extern "C"

void ND_ISR_Handler(struct regs *r)
{
	if(r->int_no < 32) {
    	ND::Panic::Show(exception_messages[r->int_no]);
		for(;;);
	}

}

Bien y con esto ya somos capaces de manejar esta interrupción. Con el resto de interrupciones pasaría parecido salvo que hay algunas que devuelven parámetros y usaríamos la estructura reg para obtenerlo. Sin embargo te preguntarás que como sabemos si funciona de verdad. Para probar si funciona vamos a introducir una sencilla línea después del ND::IDT::Install():

int sum=10/0;

Si compilamos nos dará un warning y si tratamos de ejecutarlo nos saldrá una bonita pantalla:

NextDivel-ISR

Y con esto termina este post, creo que es uno de los más extensos pero bastante funcional.

loading...

Emulando a Linus Torvalds: Crea tu propio sistema operativo desde 0 (IV)

Este artículo lo escribí para el blog en español DesdeLinux el 5 de enero de 2014 y ahora lo dejo aquí, en mi blog personal. El artículo está tal cual, sin ninguna modificación desde aquella fecha.

Bienvenidos de nuevo a esta serie de posts titulada “Emulando a Linus Torvalds”. Hoy veremos la GDT. Primero tenemos que ver que es la GDT. Según Wikipedia:

The Global Descriptor Table or GDT is a data structure used by Intel x86-family processors starting with the 80286 in order to define the characteristics of the various memory areas used during program execution, including the base address, the size and access privileges like executability and writability

Que traducido sería una Tabla de Descriptores Global, una estructura de datos usada en los procesadores Intel x86 desde el 80286 para definir las características de varias áreas de memoria usadas durante la ejecución del programa.

Resumiendo, si estamos en un procesador Intel x86 deberemos definir una GDT para un correcto uso de la memoria. Nosotros no vamos a hacer mucha complicación y vamos a definir 3 entradas en la tabla:

  • Una entrada NULL, obligatoria para todas las tablas.
  • Una entrada para la sección data, usaremos el máximo, que en 32 bits son 4 GB.
  • Una entrada para la sección code, usaremos el máximo, que en 32 bits son 4 GB.

Como veis data y code usarán el mismo espacio. Bien, ahora vamos a implementarlo. Para ello usaremos dos estructuras, la primera se encargará de contener un puntero hacia los datos reales de nuestra GDT. Y la segunda será un array con las entradas de la GDT. Primero vamos a definirlas.

struct Entry{
	uint16_t limit_low;
    uint16_t base_low;
	uint8_t base_middle;
    uint8_t access;
    uint8_t granularity;
    uint8_t base_high;
} __attribute__((packed));

struct Ptr{
	uint16_t limit;
    uint32_t base;
} __attribute__((packed));

Habrán observado un curioso __attribute__((packed)) al final de las estructuras. Esto le dice al GCC que no optimice las estructuras porque lo que queremos es pasar los datos tal cual al procesador. Ahora vamos a hacer una función para instalar la GDT. Antes deberemos haber declarado las estructuras, ahora vamos a inicializarlas.

struct ND::GDT::Entry gdt[3];
struct ND::GDT::Ptr gp;
void ND::GDT::Install()
{
	gp.limit=(sizeof(struct ND::GDT::Entry)*3)-1;
	gp.base=(uint32_t)&gdt;
}

Así conseguimos el construir el puntero que va hacia nuestra tabla de 3 entradas.

Ahora definimos una función común para poner los datos en las entradas

void ND::GDT::SetGate(int num, uint32_t base, uint32_t limit, uint8_t access,uint8_t gran)
{
	gdt[num].base_low=(base & 0xFFFF);
	gdt[num].base_middle=(base >> 16) & 0xFF;
	gdt[num].base_high=(base >> 24) & 0xFF;
	gdt[num].limit_low=(limit & 0xFFFF);
	gdt[num].granularity=(limit >> 16) & 0x0F;
	gdt[num].granularity |= (gran & 0xF0);
	gdt[num].access=access;
}

Y la llamamos 3 veces desde la función de instalar

ND::GDT::SetGate(0,0,0,0,0); /* NULL segmente entry */
ND::GDT::SetGate(1,0,0xFFFFFFFF,0x9A,0xCF); /* 4 GiB for Code Segment */
ND::GDT::SetGate(2,0,0xFFFFFFFF,0x92,0xCF); /* 4 GiB for Data segment */

Por último debemos decirle al procesador que tenemos una GDT, para que la cargue, y en nuestro caso al cargar el kernel con GRUB, sobreescribir la GDT de GRUB. Para cargar la GDT existe una instrucción en asm llamada lgdt (o lgdtl dependiendo de la sintaxis), vamos a usarla.

asm volatile("lgdtl (gp)");
asm volatile(
	"movw $0x10, %ax \n"
	"movw %ax, %ds \n"
    "movw %ax, %es \n"
    "movw %ax, %fs \n"
    "movw %ax, %gs \n"
    "movw %ax, %ss \n"
    "ljmp $0x08, $next \n"
    "next: \n"
);

Bien una vez hayamos terminado esto nuestro sistema ya contará con GDT. En el siguiente capítulo veremos la IDT, una tabla muy parecida a la GDT pero con interrupciones. Yo he puesto unos mensajes de estado y confirmación con la GDT así que NextDivel ahora luce así:

NextDivel-GDT

Emulando a Linus Torvalds: Crea tu propio sistema operativo desde 0 (III)

Este artículo lo escribí para el blog en español DesdeLinux el 1 de enero de 2014 y ahora lo dejo aquí, en mi blog personal. El artículo está tal cual, sin ninguna modificación desde aquella fecha.

NextDivel-3

Continuamos esta serie de posts sobre cómo crear nuestro sistema operativo. Hoy no nos vamos a centrar en un tema sino que vamos a definir algunas funciones útiles de ahora en adelante. En primer lugar vamos a definir 3 funciones que cumplan la función de memcpy, memset y memcmp:

void* ND::Memory::Set(void* buf, int c, size_t len)
{
	unsigned char* tmp=(unsigned char*)buf;
	while(len--)
	{
		*tmp++=c;
	}
	return buf;
}
void* ND::Memory::Copy(void* dest,const void* src, size_t len)
{
	const unsigned char* sp=(const unsigned char*)src;
	unsigned char* dp=(unsigned char*)dest;
	for(;len!=0;len--) *dp++=*sp++;
	return dest;
}
int ND::Memory::Compare(const void* p1, const void* p2, size_t len)
{
	const char* a=(const char*)p1;
	const char* b=(const char*)p2;
	size_t i=0;
	for(;i<len;i++)
	{
		if(a[i] < b[i])
			return -1;
		else if(a[i] > b[i])
			return 1;
	}
	return 0;
}

Todas ellas se auto-implementan. Estas funciones yo las he sacado de una pequeña librería del C, la implementación suele ser parecida en todos los sistemas operativos. Ahora vamos a hacer 3 funciones simulares pero para manipular strings. Cumplirían la función de strcpy, strcat y strcmp.

size_t ND::String::Length(const char* src)
{
	size_t i=0;
	while(*src--)
		i++;
	return i;
}
int ND::String::Copy(char* dest, const char* src)
{
	int n = 0;
	while (*src)
	{
		*dest++ = *src++;
		n++;
	}
	*dest = '\0';
	return n;
}
int ND::String::Compare(const char *p1, const char *p2)
{
  int i = 0;
  int failed = 0;
  while(p1[i] != '\0' && p2[i] != '\0')
  {
    if(p1[i] != p2[i])
    {
      failed = 1;
      break;
    }
    i++;
  }
  if( (p1[i] == '\0' && p2[i] != '\0') || (p1[i] != '\0' && p2[i] == '\0') )
    failed = 1;

  return failed;
}
char *ND::String::Concatenate(char *dest, const char *src)
{
  int di = ND::String::Length(dest);
  int si = 0;
  while (src[si])
    dest[di++] = src[si++];
  
  dest[di] = '\0';

  return dest;
}

Vamos ahora con unas funciones bastante interesantes. Con estas funciones podremos leer y escribir en los puertos del hardware. Esto normalmente se hace con ASM y corresponde (en x86) a las instrucciones in y out. Para llamar de una manera fácil a ASM desde C se usa la instrucción asm, con el peligro que conlleva de que no es portable. A esta sentencia le añadimos el volatile para que GCC no intente optimizar ese texto. Por otra parte la instrucción asm tiene una forma curiosa de aceptar parámetros, pero eso creo que se entiende mejor viendo los ejemplos.

uint8_t ND::Ports::InputB(uint16_t _port)
{
	unsigned char rv;
	asm volatile("inb %1, %0" : "=a"(rv) : "dN"(_port));
	return rv;
}
uint16_t ND::Ports::InputW(uint16_t port)
{
	uint16_t rv;
	asm volatile("inw %1, %0" : "=a"(rv) : "dN"(port));
}
void ND::Ports::OutputB(uint16_t port, uint8_t value)
{
	asm volatile("outb %1, %0" : : "dN"(port), "a"(value));
}

Y hasta aquí el post 3, hoy no hemos hecho nada vistoso pero sí hemos definido una funciones que nos vendrán bien de cara a un futuro. Aviso a los usuarios de 64 bits que estoy trabajando en solucionar un bug que impide compilar correctamente en 64 bits. En el siguiente post veremos un componente importante de la arquitectura x86, la GDT.

Emulando a Linus Torvalds: Crea tu propio sistema operativo desde 0 (II)

Este artículo lo escribí para el blog en español DesdeLinux el 29 de diciembre de 2013 y ahora lo dejo aquí, en mi blog personal. El artículo está tal cual, sin ninguna modificación desde aquella fecha.

Bienvenidos a otro post sobre como crear nuestro propio sistema operativo, en este caso NextDivel.

Si retomamos el código del primer post al final de todo nos debería haber salido algo como esto:

NextDivel-1

Si esto es correcto podemos continuar. Voy a usar el sistema y la estructura que tengo en GitHub (http://github.com/AdrianArroyoCalle/next-divel) ya que es más cómodo para mí y para vosotros. Como se puede apreciar el texto es un texto básico, no resulta atractiv0. Puede parecer algo más del montón. Pero como dice el dicho, para gustos colores, y en nuestro sistema operativo habrá colores. Los primeros colores que vamos a poder poner van a ser los que definen las tarjetas VGA y son 16:

  1. Negro
  2. Azul
  3. Verde
  4. Cyan
  5. Rojo
  6. Magenta
  7. Marrón
  8. Gris claro
  9. Gris oscuro
  10. Azul claro
  11. Verde claro
  12. Cyan claro
  13. Rojo claro
  14. Magenta claro
  15. Marrón claro
  16. Blanco

Estos colores los vamos a definir en un header para tenerlo más a mano y quizá en un futuro formar parte de la API del sistema. Así creamos el archivo ND_Colors.hpp en el include de NextDivel.

#ifndef ND_COLOR_HPP
#define ND_COLOR_HPP

typedef enum ND_Color{ 
	ND_COLOR_BLACK			= 0,
	ND_COLOR_BLUE			= 1,
	ND_COLOR_GREEN			= 2,
	ND_COLOR_CYAN			= 3,
	ND_COLOR_RED			= 4,
	ND_COLOR_MAGENTA		= 5,
	ND_COLOR_BROWN			= 6,
	ND_COLOR_LIGHT_GREY		= 7,
	ND_COLOR_DARK_GREY		= 8,
	ND_COLOR_LIGHT_BLUE		= 9,
	ND_COLOR_LIGHT_GREEN	= 10,
	ND_COLOR_LIGHT_CYAN		= 11,
	ND_COLOR_LIGHT_RED		= 12,
	ND_COLOR_LIGHT_MAGENTA	= 13,
	ND_COLOR_LIGHT_BROWN	= 14,
	ND_COLOR_WHITE			= 15

} ND_Color;
#endif

A su vez vamos a definir nuevas funciones para escribir en pantalla de una manera más cómoda (no, todavía no vamos a implementar printf, sé que lo estais deseando). Crearemos un archivo y su header para un set de funciones relacionadas con la pantalla (NDScreen.cpp y NDScreen.hpp). En ellas vamos a crear funciones para: cambiar el color de las letras y el fondo, escribir frases y letras, limpiar la pantalla y desplazarnos por la pantalla. Seguimos usando las pantallas VGA pero ahora usaremos unos bytes que darán el color. ND_Screen.cpp quedaría como:

#include <ND_Types.hpp>
#include <ND_Color.hpp>
#include <ND_Screen.hpp>

uint16_t *vidmem= (uint16_t *)0xB8000;
ND_Color backColour = ND_COLOR_BLACK;
ND_Color foreColour = ND_COLOR_WHITE;
uint8_t cursor_x = 0;
uint8_t cursor_y = 0;

/**
 * @brief Gets the current color
 * @param side The side to get the color
 * */
ND_Color ND::Screen::GetColor(ND_SIDE side)
{
	if(side==ND_SIDE_BACKGROUND){
		return backColour;
	}else{
		return foreColour;
	}
}
/**
 * @brief Sets the color to a screen side
 * @param side The side to set colour
 * @param colour The new colour
 * @see GetColor
 * */
void ND::Screen::SetColor(ND_SIDE side, ND_Color colour)
{
	if(side==ND_SIDE_BACKGROUND)
	{
		backColour=colour;
	}else{
		foreColour=colour;
	}
}
/**
 * @brief Puts the char on screen
 * @param c The character to write
 * */
void ND::Screen::PutChar(char c)
{
	uint8_t  attributeByte = (backColour << 4) | (foreColour & 0x0F);
	uint16_t attribute = attributeByte << 8;
	uint16_t *location;
	if (c == 0x08 && cursor_x)
	{
		cursor_x--;
	}else if(c == '\r')
	{
		cursor_x=0;
	}else if(c == '\n')
	{
		cursor_x=0;
		cursor_y=1;
	}
	if(c >= ' ') /* Printable character */
	{
		location = vidmem + (cursor_y*80 + cursor_x);
		*location = c | attribute;
		cursor_x++;
	}
	if(cursor_x >= 80) /* New line, please*/
	{
		cursor_x = 0;
		cursor_y++;
	}
	/* Scroll if needed*/
	uint8_t attributeByte2 = (0 /*black*/ << 4) | (15 /*white*/ & 0x0F);
	uint16_t blank = 0x20 /* space */ | (attributeByte2 << 8);
	if(cursor_y >= 25)
	{
       int i;
       for (i = 0*80; i < 24*80; i++)
       {
           vidmem[i] = vidmem[i+80];
       }

       // The last line should now be blank. Do this by writing
       // 80 spaces to it.
       for (i = 24*80; i < 25*80; i++)
       {
           vidmem[i] = blank;
       }
       // The cursor should now be on the last line.
       cursor_y = 24;
   }
}
/**
 * @brief Puts a complete string to screen
 * @param str The string to write
 * */
void ND::Screen::PutString(const char* str)
{
	int i=0;
	while(str[i]) 
	{
		ND::Screen::PutChar(str[i++]);
	}
}
/**
 * @brief Cleans the screen with a color
 * @param colour The colour to fill the screen
 * */
 void ND::Screen::Clear(ND_Color colour)
{
   // Make an attribute byte for the default colours
   uint8_t attributeByte = (colour /*background*/ << 4) | (15 /*white - foreground*/ & 0x0F);
   uint16_t blank = 0x20 /* space */ | (attributeByte << 8);

   int i;
   for (i = 0; i < 80*25; i++)
   {
       vidmem[i] = blank;
   }

   // Move the hardware cursor back to the start.
   cursor_x = 0;
   cursor_y = 0;
}
/**
 * @brief Sets the cursor via software
 * @param x The position of X
 * @param y The position of y
 * */
void ND::Screen::SetCursor(uint8_t x, uint8_t y)
{
	cursor_x=x;
	cursor_y=y;
}

El header será muy básico así que no lo incluyo aquí, pero destacar la definición del tipo ND_SIDE

typedef enum ND_SIDE{
		ND_SIDE_BACKGROUND,
		ND_SIDE_FOREGROUND
}ND_SIDE;

También mencionar que hacemos uso del header NDTypes.hpp, este header nos define unos tipos básicos para uint8t, uint16t, etc basado en los char y los int. Realmente este header es el en el estándar C99 y de hecho mi NDTypes.hpp es un copia/pega del archivo desde Linux, así que podeis intercambiarlos y no pasaría nada (solo hay definiciones, ninguna función).

Para probar si este código funciona vamos a modificar el punto de entrada en C del kernel:

	ND::Screen::Clear(ND_COLOR_WHITE);
	ND::Screen::SetColor(ND_SIDE_BACKGROUND,ND_COLOR_WHITE);
	ND::Screen::SetColor(ND_SIDE_FOREGROUND,ND_COLOR_GREEN);
	ND::Screen::PutString("NextDivel\n");
	ND::Screen::SetColor(ND_SIDE_FOREGROUND,ND_COLOR_BLACK);
	ND::Screen::PutString("Licensed under GNU GPL v2");

Y si seguimos estos pasos obtendríamos este resultado

NextDivel-3

Gracias a estas funciones que hemos creado podemos empezar a hacer pequeñas GUI, como por ejemplo un kernel panic que mostraremos cada vez que haya un error irrecuperable. Algo tal que así:

NextDivel-4

Y esta pequeña GUI la hicimos solamente con estas funciones:

void ND::Panic::Show(const char* error)
{
	ND::Screen::Clear(ND_COLOR_RED);
	ND::Screen::SetColor(ND_SIDE_BACKGROUND, ND_COLOR_WHITE);
	ND::Screen::SetColor(ND_SIDE_FOREGROUND, ND_COLOR_RED);
	ND::Screen::SetCursor(29,10); //(80-22)/2
	ND::Screen::PutString("NextDivel Kernel Error\n");
	ND::Screen::SetCursor(15,12);
	ND::Screen::PutString(error);
}

Y aprovecho para daros las gracias por la excelente acogida que tuvo el primer post.

Emulando a Linus Torvalds: Crea tu propio sistema operativo desde 0 (I)

Este artículo lo escribí para el blog en español DesdeLinux el 27 de diciembre de 2013 y ahora lo dejo aquí, en mi blog personal. El artículo está tal cual, sin ninguna modificación desde aquella fecha.

En esta serie vamos a emular a Linus Torvalds, vamos a crear nuestro sistema operativo desde 0. En este primer episodio vamos a ver el arranque y pondremos un texto en pantalla desde nuestro kernel.

LinusTorvalds

En mi caso el sistema operativo se llama NextDivel. La primera decisión que debemos hacer nada más plantearnos el sistema operativo es ¿cuál va a ser el bootloader?

Aquí existen múltiples variantes, e incluso podríamos crear uno nosotros; sin embargo, en este tutorial voy a usar GRUB, porque la mayoría conoce más o menos algo de él. Creamos una carpeta que será el root de nuestro sistema operativo y allí creamos la carpeta /boot/grub

mkdir nextroot && cd nextroot
 mkdir -p boot/grub 

Allí creamos el fichero grub.cfg de la siguiente manera:

menuentry "NextDivel" {
	echo "Booting NextDivel"
	multiboot /next/START.ELF
	boot
}

En este fichero hemos visto como GRUB cargará nuestro kernel, en este caso, en /next/START.ELF. Ahora debemos crear nuestro kernel.

Para ello necesitaremos el GCC y GAS (el ensamblador del proyecto GNU, suele venir con el gcc). Así pues vamos a crear el kernel.

Primero hacemos un archivo llamado kernel.asm. Este archivo contendrá el punto de inicio de nuestro kernel y además definirá el multiboot (una característica de algunos bootloaders como GRUB). El contenido de kernel.asm será:

[asm]
.text
.globl start
start:
jmp multiboot_entry
.align 4
multiboot_header:
.long 0x1BADB002
.long 0x00000003
.long -(0x1BADB002+0x00000003)
multiboot_entry:
movl $(stack + 0x4000), %esp
call NextKernel_Main
loop: hlt
jmp loop
.section “.bss”
.comm stack,0x4000
[/asm]

Todo lo relacionando con multiboot es simplemente seguir la especificación nada más. Todo empezará en start, llamará a multiboot_entry, habremos definido el multiboot header en los primeros 4k y lo pondremos (con movl).

Más tarde llamamos a NextKernel_Main que es nuestra función en C del kernel. En el loop hacemos un halt para parar el ordenador. Esto se compila con:

as -o kernel.o -c kernel.asm 

Ahora vamos a entrar a programar en C. Pensarás que ahora todo es pan comido, ponemos un printf en main y ya está, lo hemos hecho.

Pues no, ya que printf y main son funciones que define el sistema operativo, ¡pero nosotros lo estamos creando! Solo podremos usar las funciones que nosotros mismos definamos.

En capítulos posteriores hablaré de como poner nuestra propia libraría del C (glibc, bionic, newlibc) pero tiempo al tiempo. Hemos hablado que queremos poner texto en pantalla, bueno veremos como lo hacemos.

Hay dos opciones, una es llamar a la BIOS y otra es manejar la memoria de la pantalla directamente. Vamos a hacer esto último pues es más claro desde C y además nos permitirá hacerlo cuando entremos en modo protegido.

Creamos un fichero llamado NextKernel_Main.c con el siguiente contenido:

int NextKernel_Main(/*struct multiboot *mboot_ptr*/)
{
	const char* str="NextDivel says Hello World", *ch;
	unsigned short* vidmem=(unsigned short*)0xb8000;
	unsigned i;
	for(ch=str, i=0;*ch;ch++, i++)
		vidmem[i]=(unsigned char) *ch | 0x0700;

	return 0;
}

Con esto manipulamos directamente la memoria VGA y caracter a caracter lo vamos escribiendo. Compilamos desactivando la stdlib:

gcc -o NextKernel_Main.o -c NextKernel_Main.c -nostdlib -fPIC -ffreestanding 

Si has llegado hasta aquí querrás probar ya tu nuevo y flamante sistema operativo, pero todavía no hemos terminado. Necesitamos un pequeño fichero que diga al compilador en que posición del archivo dejar cada sección. Esto se hace con un linker script. Creamos link.ld:

ENTRY(start)
SECTIONS
{
	. = 0x00100000;

	.multiboot_header :
	{
		*(.multiboot_header)
	}
    .text :
    {
        code = .; _code = .; __code = .;
        *(.text)
        . = ALIGN(4096);
    }

    .data :
    {
        data = .; _data = .; __data = .;
        *(.data)
        *(.rodata)
        . = ALIGN(4096);
    }

    .bss :
    {
        bss = .; _bss = .; __bss = .;
        *(.bss)
        . = ALIGN(4096);
    }

    end = .; _end = .; __end = .;
}

Con esto definimos la posición de cada sección y el punto de entrada, start, que hemos definido en kernel.asm. Ahora ya podemos unir todo este mejunje:

gcc -o START.ELF kernel.o NextKernel_Main.o -Tlink.ld -nostdlib -fPIC -ffreestanding -lgcc 

Ahora copiamos START.ELF al /next dentro de nuestra carpeta que simula el root de nuestro sistema operativo. Nos dirigimos a la carpeta root de nuestro sistema operativo nuevo con la consola y verificamos que hay dos archivos: uno /boot/grub/grub.cfg y otro /next/START.ELF.

Vamos al directorio superior y llamamos a una utilidad de creación ISOs con GRUB llamada grub-mkrescue

grub-mkrescue -o nextdivel.iso nextroot 

Una vez hayamos hecho esto tendremos una ISO. Esta ISO puede abrirse en ordenadores x86 (64 bits también) y máquinas virtuales. Para probarlo, voy a usar QEMU. Llamamos a QEMU desde la línea de comandos:

qemu-system-i386 nextdivel.iso 

Arrancará SeaBIOS y más tarde tendremos GRUB. Después si todo va correcto veremos nuestra frase. Pensarás que esto es difícil, te respondo, sí lo es.

Realmente crear un sistema operativo es difícil y eso que este de aquí no hace nada útil. En próximos capítulos veremos como manejar colores en la pantalla, reservar memoria y si puedo, como obtener datos del teclado.

Si alguien no quiere copiar todo lo que hay aquí, tengo un repositorio en GitHub (más elaborado) con el sistema operativo NextDivel. Si quieres compilar NextDivel solo tienes que tener git y cmake:

git clone https://github.com/AdrianArroyoCalle/next-divel 
cd next-divel 
mkdir build && cd build 
cmake .. 
make 
make DESTDIR=next install 
chmod +x iso.sh 
./iso.sh 
qemu-system-i386 nextdivel.iso

Os animo a colaborar en NextDivel si tienes tiempo y ganas de crear un sistema operativo. Quizá incluso superior a Linux… el tiempo lo dirá.