Você está na página 1de 16

Construindo um bootloader

Pode parecer fora de moda esse assunto (coisa de velho que, segundo um jovem
arrogante, só servem para atrasar o emprego de tecnologia – ouvi isso
recentemente e confesso que não me desceu a garganta ainda, dada a arrogância
com que esse “postulado” foi anunciado em uma reunião de revisão), mas existem
aplicações, principalmente relacionadas aos cenários de IoT onde é desejável
incorporar um sistema operacional altamente especializado onde ter capacidades
de habilitar uma CPU no acesso e controle de seus periféricos é necessário em
diversas aplicações. A pretensão desse ensaio é justamente abordar com algum
detalhe como possível construir um bootloader. Por defi nição do Wikipedia o
bootloader, ou gerenciador de boot, é um programa simples com a função de
acessar o disco do computador e carregar o sistema operacional na memória para
assumir o controle do equipamento. Ele pode ser apenas um único programa ou
múlti plos encadeados, desde que um deles execute o carregamento do sistema
operacional.

Considerações iniciais
O boot loader ou bootloader, como é referenciado por alguns autores, é um programa situado no
primeiro setor do disco rígido; e é o setor de onde a inicialização começa. A BIOS lê automaticamente
todo o conteúdo do primeiro setor na memória logo após a alimentação ser ligada. O primeiro setor
também é chamado de Master Boot Record. Na verdade, não é obrigatório que o primeiro setor do
disco rígido inicialize alguma coisa. Esse nome foi formado historicamente porque os desenvolvedores
costumavam inicializar seus sistemas operacionais com esse mecanismo.

No primeiro estágio do trabalho do computador, o controle do hardware é realizado principalmente por


meio das funções do BIOS conhecidas como interrupções. A implementação de interrupções é dada
apenas no Assembler - por isso é ótimo se você o conhece pelo menos um pouco. Mas não é a condição
necessária. Por quê? Usaremos a tecnologia do “código misto”, onde é possível combinar construções
de alto nível com comandos de baixo nível. Isso torna nossa tarefa um pouco mais simples.

Neste ensaio, a principal linguagens de desenvolvimento são C ++. Mas conhecimento da linguagem C,
será útil para interpretar os elementos C ++ necessários. Em geral, mesmo o conhecimento C será
suficiente para modificar o código fonte dos exemplos descritos nesse ensaio. Conhecimentos sobre
Java ou C #, infelizmente, não ajudarão na tarefa. O problema é que o código das linguagens Java e C #
que é produzido após a compilação é intermediário (byte code e MSIL respectivamente). Uma máquina
virtual especial é usada para processá-la (Java Virtual Machine para Java e CLR - common language
runtime para .NET para C # ) que transforma o código intermediário em instruções do processador. Após
essa transformação, ele pode ser executado. Essa arquitetura torna impossível o uso de tecnologia de
código misto. Portanto, para desenvolver o bootloader simples, será preciso conhecer C ou C ++ e algo
sobre o Assembler - linguagem na qual todo o código de alto nível é transformado após a compilação.

Para usar a tecnologia de código misto, você precisa de pelo menos dois compiladores: para Assembler e
C / C ++, e também o linker para associar arquivos de objeto (.obj) ao executável. Agora vamos falar
sobre alguns momentos especiais. Existem dois modos de funcionamento do processador: modo real e
modo protegido. O modo real é de 16 bits e possui algumas limitações. O modo protegido é de 32 bits e
é totalmente usado no trabalho do SO. Quando inicia, o processador funciona no modo de 16 bits.
Portanto, para criar o programa e obter o arquivo executável, você precisará do compilador e vinculador
do Assembler para o modo de 16 bits. Para C / C ++, você precisará apenas do compilador que pode criar
arquivos de objeto para o modo de 16 bits. Os compiladores modernos são feitos apenas para
aplicativos de 32 bits, para que não possamos usá-los. Tentei vários compiladores gratuitos e comerciais
no modo de 16 bits e escolhi o produto da Microsoft. O compilador, juntamente com o linker para
Assembler, C, C ++, está incluído no pacote Microsoft Visual Studio 1.52 e também pode ser baixado do
site oficial da empresa. Alguns detalhes sobre os compiladores que precisamos são dados a seguir.

 ML 6.15 – Assembler compiler da Microsoft para 16-bit mode;


 LINK 5.16 – o linker que pode criar .com files para 16-bit mode;
 CL – С, С++ compiler para 16-bit mode.
Pode-se usar também algumas outras alternativas:
 DMC – free compile for Assembler, C, C++ for 16 and 32-bit mode by Digital Mars;
 LINK – free linker for DMC compiler;
Pode-se também tentar usar produtos da Borland:
 BCC 3.5  – С, С++ compiler that can create files for 16-bit mode;
 TASM - Assembler compiler for 16-bit mode;
 TLINK – linker that can create .com files for 16-bit mode.
Para esse ensaio utilizamos os produtos da Microsoft.

Como um computador se inicializa


Para resolver o desenvolvimento de um bootloader, devemos lembrar como um sistema é inicializado.
Vamos considerar brevemente como os componentes do sistema estão interagindo quando o sistema
está inicializando. Após o controle ter sido passado para o endereço 0000: 7C00, o MBR (Master Boot
Record) inicia seu trabalho e aciona a inicialização do sistema operacional. O carregador de inicialização
que estamos desenvolvendo é apenas para treinamento. Suas tarefas são apenas as seguintes:

 Carregamento correto na memória pelo endereço 0000: 7C00.


 Chamando a função BootMain que é desenvolvida no idioma de alto nível.
 Mostre a mensagem “” Olá, mundo… ”, de baixo nível” no visor.
A primeira entidade é o StartPoint, desenvolvido exclusivamente no Assembler, uma vez que os idiomas
de alto nível não possuem as instruções necessárias. Ele informa ao compilador qual modelo de
memória deve ser usado e qual endereço o carregamento para a RAM deve ser realizado após a leitura
do disco. Ele também corrige os registros do processador e passa o controle para o BootMain, escrito
em linguagem de alto nível.

A próxima entidade - BootMain - é um análogo de main que, por sua vez, é a principal função em que
todo o funcionamento do programa está concentrado. As classes CDisplay e CString cuidam da parte
funcional do programa e mostram a mensagem na tela. Como você pode ver na figura 2, A classe
CDisplay usa CStringclass em seu trabalho.

Indo direto ao ponto ...


Podemos o ambiente de desenvolvimento padrão Microsoft Visual Studio 2005, 2008, 2010, 2015, 2017
ou 2019. Podemos usar outras ferramentas, mas asseguro que essas duas com algumas configurações
tornassem a compilação e o trabalho fáceis e práticos.

Primeiro, devemos criar o projeto do tipo Makefile Project, onde o trabalho principal será executado.

Arquivo-> Novo \ Projeto-> Geral \ Projeto Makefile


Para mostrar nossa mensagem na tela, devemos limpá-la primeiro. Usaremos interrupção especial do
BIOS para esse fim. A BIOS propõe várias interrupções para o trabalho com hardware de computador,
como adaptador de vídeo, teclado e sistema de disco. Cada interrupção possui a seguinte estrutura:

int [number_of_interrupt];

onde number_of_interrupt é o número da interrupção. Cada interrupção possui o número certo de


parâmetros que devem ser definidos antes de chamá-lo. O registro do processador AH é sempre
responsável pelo número de funções da interrupção atual, e os outros registradores geralmente são
usados para os outros parâmetros da operação atual. Vamos ver como o trabalho da interrupção int 10h
é realizado no Assembler. Usaremos a função 00 que altera o modo de vídeo e limpa a tela:

mov al, 02h ; ajusta o modo gráfico para 80x25(texto)


mov ah, 00h ; código da função de mudança do modo de vídeo
int 10h ; chama a interrupção

Consideraremos apenas as interrupções e funções que serão usadas em nosso aplicativo exemplo.
Precisaremos:
int 10h, function 00h – realiza mudança do modo de video e limpa a tela;
int 10h, function 01h – ajusta o tipo do cursor;
int 10h, function 13h – exibe a string na tela;

Codificando
O compilador para C ++ suporta o Assembler embutido, ou seja, ao escrever código em linguagem de
alto nível, você também pode usar linguagem de baixo nível. As instruções do Assembler usadas no
código de alto nível também são chamadas de inserções asm. Eles consistem na palavra-chave __asm e
no bloco das instruções do Assembler entre chaves:

__asm ; palavra chave que mostra o início da inserção asm


{ ; Início do bloco de código asm
… ; porção do código asm a ser implementado
} ; final do bloco de código asm

Para demonstrar o código misto, vamos usar o código Assembler mencionado anteriormente que
executou a limpeza da tela e combiná-lo com o código C ++.

void ClearScreen()
{
__asm
{
mov al, 02h ; ajusta o modo gráfico para 80x25(texto)
mov ah, 00h ; Código da função de mudança do modo de vídeo
int 10h ; chama a interrupção
}
}

Trabalhando com Strings


A classe CString foi projetada para trabalhar com sequências de caracteres. Inclui o método Strlen () que
obtém o ponteiro para a sequência como parâmetro e retorna o número de símbolos nessa sequência.
// CString.h

#ifndef __CSTRING__
#define __CSTRING__

#include "Types.h"

class CString
{
public:
static byte Strlen(
const char far* inStrSource
);
};

#endif // __CSTRING__

// CString.cpp

#include "CString.h"

byte CString::Strlen(
const char far* inStrSource
)
{
byte lenghtOfString = 0;

while(*inStrSource++ != '\0')
{
++lenghtOfString;
}
return lenghtOfString;
}
Implementando o Display
A classe CDisplay foi projetada para o trabalho com a tela. Inclui os seguintes métodos:

1. TextOut () - imprime a string na tela.


2. ShowCursor () - gerencia a representação do cursor na tela: show, hide.
3. ClearScreen () - altera o modo de vídeo e limpa a tela.
// CDisplay.h

#ifndef __CDISPLAY__
#define __CDISPLAY__
//
// cores para a func TextOut
//
#define BLACK 0x0
#define BLUE 0x1
#define GREEN 0x2
#define CYAN 0x3
#define RED 0x4
#define MAGENTA 0x5
#define BROWN 0x6
#define GREY 0x7
#define DARK_GREY 0x8
#define LIGHT_BLUE 0x9
#define LIGHT_GREEN 0xA
#define LIGHT_CYAN 0xB
#define LIGHT_RED 0xC
#define LIGHT_MAGENTA 0xD
#define LIGHT_BROWN 0xE
#define WHITE 0xF

#include "Types.h"
#include "CString.h"

class CDisplay
{
public:
static void ClearScreen();

static void TextOut(


const char far* inStrSource,
byte inX = 0,
byte inY = 0,
byte inBackgroundColor = BLACK,
byte inTextColor = WHITE,
bool inUpdateCursor = false
);

static void ShowCursor(


bool inMode
);
};

#endif // __CDISPLAY__
// CDisplay.cpp

#include "CDisplay.h"

void CDisplay::TextOut(
const char far* inStrSource,
byte inX,
byte inY,
byte inBackgroundColor,
byte inTextColor,
bool inUpdateCursor
)
{
byte textAttribute = ((inTextColor) | (inBackgroundColor << 4));
byte lengthOfString = CString::Strlen(inStrSource);

__asm
{
push bp
mov al, inUpdateCursor
xor bh, bh
mov bl, textAttribute
xor cx, cx
mov cl, lengthOfString
mov dh, inY
mov dl, inX
mov es, word ptr[inStrSource + 2]
mov bp, word ptr[inStrSource]
mov ah, 13h
int 10h
pop bp
}
}

void CDisplay::ClearScreen()
{
__asm
{
mov al, 02h
mov ah, 00h
int 10h
}
}
void CDisplay::ShowCursor(
bool inMode
)

{
byte flag = inMode ? 0 : 0x32;

__asm
{
mov ch, flag
mov cl, 0Ah
mov ah, 01h
int 10h
}
}

Types.h é o arquivo de cabeçalho que inclui definições dos tipos de dados e macros.

// Types.h

#ifndef __TYPES__
#define __TYPES__

typedef unsigned char byte;


typedef unsigned short word;
typedef unsigned long dword;
typedef char bool;

#define true 0x1


#define false 0x0

#endif // __TYPES__

BootMain () é a principal função do programa que é o primeiro ponto de entrada (análogo de main ()).
O processamento principal é realizado aqui.
// BootMain.cpp

#include "CDisplay.h"

#define HELLO_STR "\"Hello, world…\", vindo do baixo nível..."

extern "C" void BootMain()


{
CDisplay::ClearScreen();
CDisplay::ShowCursor(false);

CDisplay::TextOut(
HELLO_STR,
0,
0,
BLACK,
WHITE,
false
);

return;
}

Implementação do segmento assembler de inicialização StartPoint.asm:


;------------------------------------------------------------
.286 ; Tipo de CPU
;------------------------------------------------------------
.model TINY ; modelo de memória
;---------------------- EXTERNS -----------------------------
extrn _BootMain:near ; prototype da func em C
;------------------------------------------------------------
;------------------------------------------------------------
.code
org 07c00h ; para o BootSector
main:
jmp short start ; vai para o main
nop

;----------------------- CODE SEGMENT -----------------------


start:
cli
mov ax,cs ; Setup dos registradores de segmento
mov ds,ax ; Faz DS correto
mov es,ax ; Faz ES correct
mov ss,ax ; Faz SS correct
mov bp,7c00h
mov sp,7c00h ; Setup um stack
sti
; inicia o programa
Call _BootMain
ret

END main ; Fim do programa

Consolidando toda a solução


Agora, quando o código é desenvolvido, precisamos transformá-lo no arquivo do sistema operacional de
16 bits. Esses arquivos são arquivos .com. Podemos iniciar cada um dos compiladores (para Assembler e
C, C ++) a partir da linha de comando, transmitir os parâmetros necessários a eles e obter vários
arquivos de objetos como resultado. Em seguida, iniciamos o linker para transformar todos os
arquivos .obj em um arquivo executável com extensão .com. Vamos automatizar o processo. Para isso,
criamos o arquivo .bat e colocamos comandos com os parâmetros necessários. A figura representa o
processo completo de montagem do aplicativo.
Configuração do arquivo .bat de compilação

Você também pode gostar