Tag Archives: desenvolvimento

Sistema Operacional miniOS-0.05: Código ASM de inicialização

Continuando a serie de posts sobre como fazer um sistema operacional. O código desta parte também já esta no repositório.

Nesse post vamos finalmente ter alguma ação, vamos codificar algo. E mais interessante ainda, em ASM.

O que ser assembly?

asm

A linguagem Assembly(vamos chamar simplesmente de ASM) é uma linguagem de baixo nível, que quer dizer que tem uma abstração quase inexistente da linguagem de maquina.

Devido a sua pouca abstração, o ASM normalmente é extremamente não portável. Isso quer dizer que, o ASM é extremamente atrelado a ISA e ao SO para a qual esta se programando.

Existem diversas variações de ASM, e cada um tem suas características. Uns podem ser mais portáveis, ter uma linguagem de mais alto nível e/ou outras funcionalidades.

O paradigma de programação em ASM normalmente é bem linear e não estruturado. E a forma mais básica da linguagem é através de mnemônicos de instruções, se repetindo a cada linha do programa, imitando a linguagem de maquina.

Como ASM é altamente ligado a ISA, normalmente vamos precisar de uma implementação diferente para cada ISA. De vez em quando podemos contornar isso, mas no nosso kernel por enquanto estamos implementando para i386 apenas, então não vamos nos preocupar tanto com isso.

Conhecer totalmente ASM não é obrigatório, mas entender um pouco do que vai ser codificado mais a frente é necessário. Não vamos codificar muito em ASM, apenas o que realmente for preciso.

Porque precisamos do ASM?

Infelizmente, não é possível escrever totalmente o kernel em C/C++. Mas porque, você se pergunta?

C/C++ não fornece funcionalidades para garantir que o cabeçalho do mboot fique no começo do kernel. Se você declara dados em C/C++, esses dados ficam na secção “.data”, “.rodata” e/ou outras(como foi abordado em posts passados). Teoricamente, você pode escrever em ASM diretamente em C/C++, mas isso é conversa para o futuro.

Quando você executa uma função em C/C++, você teoricamente já esta usando a pilha, então não da para indicar uma função de C/C++ como ponto de inicio do kernel sem usar uma pilha. Não temos como garantir com C/C++ que essa pilha previamente configurada esteja usando uma área de memoria valida. Isso pode fazer com que o código sobrescreva uma área de memoria importante ou até acesse uma área de memoria mapeada para alguma outra finalidade, podendo causar erros.

Não podemos garantir que funções em C/C++ sejam iniciadas sem alterar registradores específicos. Então se indicarmos uma função de C/C++ como ponto de inicio do kernel, quando ela for iniciada vamos perder informações de alguns registradores. Isso não tem nenhuma forma de contornar(que eu saiba, acho que rótulos do C/C++ não podem garantir isso…), e isso vai se tornar mais evidente quando estiver programando o IDT, GDT e etc

Se existir formas de fazer isso apenas em C/C++ padrão/portável(ou o mais possível…), por favor me avisem :p.

Implementando nossa rotina de inicialização em ASM

Vamos criar o arquivo /minios/kernel/arch/i386/pc/start.s. Nessa pasta porque é dependente da ISA que estamos usando(i386 no caso, só para separar).

Nesse arquivo temos algo parecido com:

.section .mboot

/* Multi Boot header */
.set MBOOT_PAGE_ALIGN, 1<<0
.set MBOOT_MEM_INFO, 1&lt;<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;">&#65279;</span>&lt;1
.set MBOOT_HEADER_MAGIC, 0x1BADB002
.set MBOOT_HEADER_FLAGS, MBOOT_PAGE_ALIGN | MBOOT_MEM_INFO
.set MBOOT_HEADER_CHECKSUM, -(MBOOT_HEADER_MAGIC + MBOOT_HEADER_FLAGS)

.globl mboot
.globl start

.align 4
mboot:
	.long	MBOOT_HEADER_MAGIC
	.long	MBOOT_HEADER_FLAGS
	.long	MBOOT_HEADER_CHECKSUM

	.long	mboot
	.long	code
	.long	data_end
	.long	bss_end
	.long	start

.section .text

initial_stack:
	.globl	initial_stack
	.zero	4

multiboot_magic:
	.globl	multiboot_magic
	.zero	4

multiboot_addr:
	.globl multiboot_addr
	.zero	4

start:
	cli

	movl %eax, multiboot_magic
	movl %ebx, multiboot_addr
	mov $stack_top, %esp
	movl %esp, initial_stack

.hang:
	hlt
	jmp  .hang

/* Create stack on bss section */
.section .bss

.align 16
stack_bottom:
	.skip 16384 # 16 KiB
stack_top:

Vamos agora a uma explicação mais detalhada. Vamos focar em pontos importantes mais a frente.

.section .mboot

Na primeira linha, com o section garantimos que essa secção vai ficar em .mboot, que reservamos para o começo do kernel no linker, como explicado no post anterior.

.set MBOOT_PAGE_ALIGN, 1<<0
.set MBOOT_MEM_INFO, 1<<1
.set MBOOT_HEADER_MAGIC, 0x1BADB002
.set MBOOT_HEADER_FLAGS, MBOOT_PAGE_ALIGN | MBOOT_MEM_INFO
.set MBOOT_HEADER_CHECKSUM, -(MBOOT_HEADER_MAGIC + MBOOT_HEADER_FLAGS)

Algumas definições do Multi Boot. Basicamente o numero magico que identifica o cabeçalho e algumas informações que vamos passar para o gerenciador de boot.

Vamos utilizar apenas opções básica, então isso já resolve. Para entender melhor o valor de cada campo e outras opções, vocês podem consultar a pagina da GNU com todas as informações.

.align 4
mboot:
	.long	MBOOT_HEADER_MAGIC
	.long	MBOOT_HEADER_FLAGS
	.long	MBOOT_HEADER_CHECKSUM

	.long	mboot
	.long	code
	.long	data_end
	.long	bss_end
	.long	start

Esse é o cabeçalho binário do Multi Boot propriamente dito, que devemos manter no começo do kernel para que o gerenciador de boot consiga as informações necessárias e identifique o kernel como inicializável.

Perceba que é necessário que esses bytes sejam alinhados em 4, como especificado pelo padrão.

Os últimos 5 inteiros são exatamente os endereços de memoria que marcamos no linker: O endereço do cabeçalho, o endereço onde se inicia o código que vamos copiar para a memoria, o endereço até onde vamos carregar para a memoria, o endereço final da área BSS(essa área vai ser preenchida com zeros pelo gerenciador de boot, de acordo com o padrão) e o endereço de onde o gerenciador de boot vai começar a executar o kernel!

Esses últimos 5 inteiros são importante, mas é bom ressaltar que não vamos usar e eles estão ali apenas por compatibilidade. O formato do binário do kernel que vamos usar é ELF(que o mboot já sabe interpretar o cabeçalho com essas informações), então todas essas informações já estão no kernel(além de contar com outras vantagens, como um kernel menor). Se você não colocar no cabeçalho especificamente para usar essas informações(colocando 1 no bit 16 em MBOOT_HEADER_FLAGS), ele já vai carregar elas do cabeçalho do padrão ELF. Você deve estar se perguntando porque essas informações vão ficar ai? Bom, caso no futuro eu mude de formato, fica ai a opção(acho que alguns sistemas podem não ter suporte a ELF…). Também não estou certo se todos os compiladores podem gerar executáveis no formato ELF.

.section .text

initial_stack:
	.globl	initial_stack
	.zero	4

multiboot_magic:
	.globl	multiboot_magic
	.zero	4

multiboot_addr:
	.globl multiboot_addr
	.zero	4

Apenas definindo algumas variáveis globais em ASM e preenchendo com zeros. Essas variáveis não precisam ser definidas aqui, poderiam ser definidas em C/C++, acho. Provavelmente vamos remover elas quando os códigos em C/C++ começarem a aparecer.

Vamos falar da função de cada uma dessas variáveis quando atribuirmos os valores a elas mais a frente.

start:
	cli

FINALMENTE!!11!1

Aqui é o rotulo/endereço de onde o seu kernel vai começar a executar depois que o gerenciador de boot fizer todo o trabalho duro. Esse rotulo vai ser tipo um “main()” do ASM. Apesar de ser difícil programar em ASM, você poderia começar a fazer sua baguncinha por aqui…..

Mas em outro dia, porque primeiramente temos que desabilitar as interrupções com a instrução cli. Porque executamos essa instrução logo? Porque, o computador esta com uma tabela de interrupções que foi configurada pelo gerenciador de boot e outras, que podem fazer comportamentos inesperados(na verdade, acho que o gerenciador de boot já desabilita, mas no futuro vamos fazer isso de novo quando for desativar o kernel).

No futuro, podemos mover essa instrução para uma função em C/C++ para ficar mais organizado, mas por enquanto estamos usando apenas ASM.

	movl %eax, multiboot_magic
	movl %ebx, multiboot_addr
	mov $stack_top, %esp
	movl %esp, initial_stack

Aqui, fazemos apenas algumas atribuições, mas elas são importantes. Muito tem a ver com o porque precisamos usar ASM no lugar de C/C++ na inicialização, como discutimos antes.

Primeiro, precisamos do conteúdo do registrador EAX, que copiamos para a área de memoria/variável chamada multiboot_magic. Porque? O gerenciador de boot vai colocar nesse registrador um numero magico(2badb002), que identifica que o kernel deu boot corretamente por um gerenciador compatível com mboot versão 1.

Segundo, precisamos do conteúdo do registrador EBX, que copiamos para a área de memoria/variável chamada multiboot_addr. Porque? Ele contem o endereço do cabeçalho do mboot, para que o kernel possa extrair informações extras que o gerenciador de boot possa passar. Na verdade não vamos usar isso, mas não custa nada armazenar.

E, para finalizar, deixamos de usar a pilha que estamos usando atualmente(que é de uma área de memoria que o gerenciador de boot usou) para utilizar uma área de memoria segura indicada pelo rotulo stack_top. Para quem não sabe, o registrador ESP indica onde esta a pilha, caso você não entenda como isso funciona, apenas assuma que isso é necessário. É necessário usar uma área de memoria valida e que possa crescer, por isso vamos alocar essa memoria na secção BSS.

No caso, essa são as únicas operações que precisamos fazer em ASM por agora. Se colocássemos uma função em C/C++ no lugar do código em ASM, o C/C++ iria usar essa pilha que o gerenciador de boot indicou anteriormente e adicionalmente provavelmente iriamos perder os conteúdos dos registradores EAX e EBX, porque o código de inicialização da função de C/C++ usaria esses registradores.

.hang:
	hlt
	jmp  .hang

Aqui é o final do programa. Futuramente, vamos passar essa parte do código para C/C++ também.

Porque precisamos desse código? Basicamente, hlt para e espera pela próxima interrupção que o computador vai receber. Se por algum motivo alguma interrupção acontecer(não era para acontecer, mas pode), vamos executar hlt de novo, até que o computador pare de vez.

Basicamente, um loop infinito executando a instrução para parar.

.section .bss
.align 16
stack_bottom:
	.skip 16384 # 16 KiB
stack_top:

Dentro da secção BSS, reservamos 16384(vulgo, 16KiB) de memoria alinhada a 16 e marcamos o começo e o final dessa área para ser a nossa pilha.

A secção BSS é a área de memoria que definimos para essas finalidades. Mas, porque precisamos alinhar? Porque o x86 diz que precisamos(tá, tem motivos para isso, mas como você é obrigado não tem opção).

Lembra que alguns passos atrás falamos sobre a pilha? Lá usamos essa área de memoria que reservamos aqui, simples assim.

Não precisamos nos preocupar com a ordem, apenas precisamos colocar os dados nas secções corretas. Isso é valido para a pilha ficar na BSS, quanto qualquer outro código que usamos mais a cima ou depois.

Executando

Agora podemos executar o código depois de compilado usando o qemu. Precisamos de dois requerimentos para isso:

  1. O emulador de i386 ou parecido
  2. Um GCC que produza executáveis para i386

Você pode fazer um toolchain para a arquitetura que você vai usar, que envolve produzir todo um ambiente de compilação para a arquitetura especifica. Mas, i386 é muito comum, então pode ser que você tenha o compilador já no seu sistema. Enfim, não vou entrar em detalhes disso, porque não vai ser meu foco.

Considerando que tudo esta certo, você poderia compilar e executar o SO assim:

git clone -b 'V00.05' https://bitbucket.org/psycho_mantys/minios
mkdir minios/build
cd minios/build
cmake ..
make
../tools/boot_qemu.sh

Pronto! Claro, esses comandos são para usar um ambiente linux/unix e usando linha de comando. No seu ambiente, você pode utilizar qualquer outra ferramenta gráfica ou procedimento que achar mais interessante para baixar, compilar e rodar o sistema operacional.

Analisando a execução

No meu ambiente(e espero que seja parecido em outros também), depois de executar o kernel usando o qemu, vamos ter uma imagem mais ou menos assim:

run_qemu

Certo, o que mudou você se pergunta? Exatamente, nada :/.

Apesar de todo o trabalho que fizemos até agora, praticamente o nosso kernel não faz nada(nenhuma alteração visual na tela, ainda vamos chegar lá). Poderíamos executar algum código no rotulo “start” além do que já colocamos lá, mas teríamos que programar em ASM, que não é nosso escopo por agora.

Como vamos saber que estamos fazendo tudo certo então? Basicamente, de duas formas.

A primeira é olhando a saída do comando “/tools/boot_qemu.sh” que estamos usando para executar o kernel. Nesse script codificamos para que ele informe o conteúdo dos registradores depois de um tempo, que é quando o kernel já deve estar parado no loop infinito. Como nosso kernel esta em conformidade com o padrão mboot versão 1, então, no registrador EAX o gerenciador de boot deve colocar o valor magico(um magic number) “2badb002” para garantir que o kernel foi iniciado por um gerenciador de boot compatível(você poderia atribuir algum valor magico a esse registrador também e verificado depois).

Analisando a saída, podemos ver que isso é verdade:

run_qemu_cli_editado

Um problema que pode acontecer é do seu kernel não ter ou não estar com o cabeçalho do mboot certo. Se você estiver implementando da forma correta no “start.s”(na secção correta e os dados certo), você também pode verificar no arquivo binário do kernel se o cabeçalho esta certo e no lugar correto. Como podemos fazer isso? Você pode abrir o arquivo binário(usando um visualizador de binário com o comando hexdump ou até qualquer editor de sua escolha) e verificar se os dados estão no começo. Também tem como verificar isso usando o grub-file. Para uma forma mais conveniente, eu vou usar o comando “objdump” e verificar se a secção esta em um offset do arquivo perto do começo:

run_objdump

Como podemos ver, a secção esta localizada pelo inicio do arquivo, o que é bom.

Recapitulando o processo todo

Agora que temos algo executando, creio que seja importante recapitular o processo todo de inicialização passo a passo de forma direta:

  1. O gerenciador de boot procura no arquivo binário do kernel o cabeçalho do mboot
  2. Gerenciador de boot carrega para a memoria o conteúdo do kernel indicado no endereço no quinto campo do cabeçalho até o endereço do final(que é indicado no sexto campo) ou usa o valores no cabeçalho ELF
  3. Gerenciador de boot preenche com zeros a memoria da BSS até o endereço de memoria indicado no sétimo campo do cabeçalho ou o indicado no cabeçalho ELF
  4. Gerenciador de boot coloca o numero magico no registrador EAX para indicar que o kernel foi inicializado pelo padrão do mboot versão 1
  5. Com todo o ambiente configurado, o gerenciador de boot passa o fluxo de execução para o endereço que é indicado no oitavo campo cabeçalho ou o indicado no cabeçalho ELF
  6. Executamos o código do kernel

Uma boa leitura sobre o multiboot versão 1 é o manual da GNU. Apesar de não explicar direito o processo de compilação e muita coisa ser implementada em ASM e sem explicação, é uma consulta bem explicativa no mínimo para o processo de boot.

Palavras finais

O foco dessa parte não é ensinar ASM, mas é um conhecimento interessante de se ter, pelo menos o básico.

Em vários pontos em fiquei tentado a implementar o mboot versão 2. Apesar de serem parecidos, não tem muito necessidade dentro do que estou fazendo. Fica mais fácil fazer com o padrão 1 porque já sei o que fazer mesmo. Talvez no futuro eu reveja isso.

Algumas coisas eu estou relembrando, e uma parte do processo esta sendo refazer algumas coisas. Caso algo seja estranho ou pareça errado, seria bom que alguém me avise.

Uma boa coisa é se alguém souber melhores formas de fazer alguma coisa, é bom me avisar pelos comentários, ou dar sugestões para melhorar. Não existe uma melhor forma única de fazer tudo, mas estamos tentando utilizar de todas as melhores pelo menos.

Não sei se a didática esta boa, sinto que estou sendo um pouco prolixo de vez em quando, mas acho que é bom em alguns momentos. E como estou implementando enquanto vou postando, não sei ainda a melhor maneira.

Enfim, qualquer ideia falem ai.

Advertisements

Sistema Operacional miniOS-0.04: Linker

Estou continuando a serie de posts sobre como fazer um sistema operacional, fazendo o meu próprio sistema.

Quando essa parte do tutorial estiver publicada no meu blog, você já poderão conferir no repositório os fontes dos arquivos.

Agora vamos falar um pouco sobre linker e porque precisamos nos preocupar com isso quando estamos codificando um sistema operacional.

Linker

Linker

Credito Wikipédia

Um linker é um programa que vai fazer a ligação entre os diversos módulos do programa. Esses módulos podem ser arquivos objeto gerado por códigos fonte, biblioteca externas ou qualquer coisa parecida. Ele vai transformar todos os símbolos do seu programa(como variáveis e funções) em endereços de memoria física, que pode ser tanto para um arquivo no computador quanto para a memoria.

Com toda a certeza, você deve saber que o computador não usa o nome da sua função que você declara em “C” para chamá-la/executá-la. Nem o nome das variáveis, que são acessadas através de endereços de memoria também. Isso também é uma afirmação valida para quando você programa em ASM, os rótulos que você usa não são usados diretamente. Quem traduz previamente esses nomes para endereços físicos e é responsável por ligar todos esses elementos é o programa conhecido como linker.

Para construir nosso SO, precisamos mudar um pouco o comportamento desse linker, através de um linker script(que posso encurtar para chamar apenas de linker, para facilitar).

Porque precisamos de um linker?

Precisamos que o kernel siga alguns padrões para que ele possa ser reconhecido e iniciado corretamente pelos gerenciadores de boot(tal como o LILO e GRUB). Para que isso aconteça, precisamos seguir o padrão “Multi Boot”, que vamos falar um pouco mais a frente o como vamos fazer isso.

Mais para o futuro, vamos precisar que construtores e destrutores sejam executados antes do inicio da função “main()” e depois que ela terminar. Para isso, através do linker, vamos precisar saber onde eles estão guardados na memoria.

Precisamos disso porque não existe maneira de garantir isso em ASM ou em C apenas, se existisse como, poderíamos usar o linker script padrão do sistema mesmo.

O que é o Multi Boot(mboot)?

Inicializar o sistema do computador é um trabalho complicado. Existem diversos hardwares com varias formas que ficaram ainda mais complexas com o passar dos anos por causa de modos de compatibilidade e outras coisas. Para que você não se preocupe com a inicialização do computador, foi criado os gerenciadores de boot. Para que o gerenciador de inicialização entenda que seu programa é um kernel iniciável de forma mais fácil, foi criado o padrão multi boot. Existem outras formas, mas vamos usar o mboot apenas.

Para que o gerenciador de boot saiba que seu kernel usa o padrão, e para que ele receba algumas informações necessárias, no começo do arquivo do kernel(ele procura nos primeiros 8K bytes) você deve colocar uma estrutura de dados que contem um numero magico(para que ele possa identificar o começo da estrutura entre os dados) e mais alguns dados. Tudo isso mais a frente vamos codificar diretamente em ASM, mas por enquanto, precisamos apenas garantir que o linker coloque a estrutura no começo.

O multi boot tem mais de uma versão, que se diferencia pelas informações que tem na estrutura binaria no começo do kernel. Como não vamos precisar das funcionalidades da segunda versão, vamos implementar apenas a primeira mesmo.

Para entender melhor em detalhes o formato binário do mboot, você pode consultar o manual da GNU.

Mas, por enquanto não vamos implementar nada além do linker script, só depois, então não se preocupe por estar um pouco abstrato demais.

Construindo nosso linker script

memory

Esquema da memoria do nosso kernel

O Nosso script vai ser algo parecido com isso:

ENTRY(start)

SECTIONS
{
	. = 1M ;
	.text : ALIGN(4096)
	{
		code = .; _code = .; __code = .;
		KEEP(*(.mboot*))
		*(.header*) ;
		*(.text*) ;

		*(.gnu.linkonce.t*) ;
	}
	code_end = .; _code_end = .; __code_end = .;

	.rodata : ALIGN(4096)
	{
		rodata = .; _rodata = .; __rodata = .;

		start_ctors = .;
		*(SORT(.ctors*))
		end_ctors = .;

		start_dtors = .;
		*(SORT(.dtors*))
		end_dtors = .;

		*(.rodata*)
		*(.gnu.linkonce.r*)
	}
	rodata_end = .; _rodata_end = .; __rodata_end = .;

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

		*(.data)
		*(.gnu.linkonce.d*)
	}
	data_end = .; _data_end = .; __data_end = .;

	.bss : ALIGN(4096)
	{
		bss = .; _bss = .; __bss = .;
		sbss = .;
		*(COMMON)
		*(.bss)
		*(.gnu.linkonce.b*)
	}
	bss_end = .; _bss_end = .; __bss_end = .;

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

Vamos agora a uma explicação mais detalhada. Vamos focar em pontos importantes mais a frente.

ENTRY(start)

Primeiramente, precisamos dizer que a função que vamos iniciar chamando é a “start“. Porque não “main()“, como o tradicional? Porque vamos precisar executar alguns códigos antes de chamar a função “main()”, que vamos descobrir melhor em passos futuros.

	. = 1M ;

Depois, vamos colocar os dados do kernel apenas depois da posição de memoria 0x00100000, mas porque? O gerenciador de boot usa a mesma memoria que o kernel, então essa memoria pode estar sendo usada para outras finalidades ou para passar dados para o kernel que acabou de iniciar. Além do que essa memoria pode estar sendo mapeada para outras coisas.

Essa posição inicial é relativa quando o programa estiver na memoria, seguindo os padrões de um arquivo executável que o gerenciador de boot vai preparar para executar. Ou seja, o arquivo real do kernel não vai ter 1M sem nada no começo(vai existir uns poucos dados do formato do binário, na verdade), apenas vai existir esse 1M de diferença quando ele estiver sendo executado na memoria.

	.text : ALIGN(4096)
	{
	....
	.rodata : ALIGN(4096)
	{
	....
	.data : ALIGN(4096)
	{
	....
	.bss : ALIGN(4096)
	{
	....

Vamos passar por todas essas secções criadas de uma vez. Elas são padrão em programas, porque basicamente precisamos das seguintes seções em um programa:

  • .text O código executável do programa fica nessa área de memoria
  • .rodata Como é de se esperar, as variáveis constantes e globais ficam nessa área de memoria
  • .data Área das variáveis globais que são inicializadas em tempo de compilação
  • .bss Variáveis globais não inicializadas ficam aqui

Outras seções, como common, sbss, gnu.linkonce.* e outras são apenas variações dessas áreas, mas são usadas pelo compilador e estão ai apenas para facilitar e prevenir erros no futuro.

Também vale a pena ressaltar que essas secções são alinhadas com paginas de memoria do tamanho de 4K, para ser mais fácil fazer paginação no futuro e ser mais otimizado.

		KEEP(*(.mboot*))

Lembra que precisamos garantir que a estrutura que identifica o kernel como mboot no começo do arquivo do kernel que vai ser gerado? É aqui que garantimos isso. Todas as seções que comecem com .mboot vão ser colocadas nesse local, no começo. O KEEP garante que ele vai ser colocado ali, independente se o linker trocar de lugar essa secção(por causa de otimizações ou por não ser referenciado em nenhum outro lugar).

		start_ctors = .;
		*(SORT(.ctors*))
		end_ctors = .;

		start_dtors = .;
		*(SORT(.dtors*))
		end_dtors = .;

Mais um pedaço de código que vamos usar mais a frente. Vamos ter a indicação de onde começa e termina os construtores e destrutores de objetos globais, justamente para executá-los no momento certo.

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

Futuramente, para saber qual memoria esta disponível para ser usada, vamos precisar saber onde a memoria usada pelo kernel termina. Essa marcação vai ajudar nisso. Usamos vários nomes para essa variável por comodidade.

	code_end = .; _code_end = .; __code_end = .;
....
	rodata_end = .; _rodata_end = .; __rodata_end = .;
....
	data_end = .; _data_end = .; __data_end = .;
....
	bss_end = .; _bss_end = .; __bss_end = .;

Adicionei essas marcações apenas para facilitar encontrar o final das secções. Você poderia até fazer sem elas, mas faz mais sentido fazer com as marcações. E fica mais intuitivo quando for implementar o multi boot no ASM.

Palavras finais

O que temos que focar aqui é a estrutura de como o kernel vai ser usado montado na memoria(e também no arquivo) e o que precisamos para ser compatível com o multi boot.

Aprender a linguagem de script do linker é algo que não é necessário, apenas precisamos saber por alto ela para que possamos garantir algumas coisas.

Algumas coisas aqui só vão ser utilizados mais para a frente, e a questão do boot nos próximos posts.

Caso alguém note algo que eu escrevi errado, algum conceito que esteja errado e/ou alguma dica, pode mandar mensagem para minha pessoa de forma amigável, tanto por comentário quanto por qualquer outra forma.

PS:. BUG FIX

Certo, enquanto produzia a parte a frente do post, notei que alguns bugs(o que é estranho, porque eu não erro :p) existiam no código antigo. Como já dei “push” e voltar agora seria muito complicado, caso notem alguma coisa diferente no repositório do que esta no post, pegue a ideia do que esta no post e use a versão correta. A versão correta pode ser ou do código que o post esta mostrando, ou a versão que vai aparecer.

De qualquer forma, eu já acertei esse post. Mas fica uma lista do que era:

  • Erro no CMake.
    • Faltou incluir um código para compilar o kernel em outra pasta sem ser a do projeto.
    • Pequenos ajustes para que o CMake funcionasse sem a opção de uma biblioteca C, que eu vou adicionar no futuro.
  • O Linker faltou algumas marcações

Encare essa nota de rodapé como um log de alterações do futuro. Não precisa se preocupar com essa secção, e se encontrar algum erro no continuo espaço tempo, use o do futuro :p.


LibreSSL e porque o openssl ainda é perigoso

Certo, em épocas de heartbleed e coisas do tipo, começamos a perceber que existem alguns problemas com algumas implementações não tão modernas

Mais especificamente com o OpenSSL, recentemente na tentativa de evitar outros erros futuros, pessoas começaram a olhar e relatar problemas adicionais com o openSSL. Devido a vários problemas, foi resolvido criar um fork do projeto chamado “libreSSL“.

Realmente não gosto muito da ideia de se fazer um fork de forma desnecessária, mas procurando sobre o assunto, encontrei esses slides que justificam o clone:

http://www.openbsd.org/papers/bsdcan14-libressl/mgp00001.html

Recentemente soltei um twett sobre um post falando sobre a baixa qualidade em códigos científicos. Acho que esse é um caso de grande parte das coisas que são motivos do clone até.

Ah, também acho interessante que o projeto é vinculado ao openBSD, mas ainda assim ele é portável. Quer dizer, acho que a maior parte das coisas feitas no openBSD são portáveis mesmo.

UPDATE:

Um link muito útil que o Dielson colocou para entender o Heartbleed:

http://www.dwheeler.com/essays/heartbleed.html


C/C++: Accessors(getters) e mutators(setters) de forma fácil usando macros

Continuando

Anteriormente, acho que até o ultimo post que eu fiz, falei sobre accessors(getters) e mutators(setters).

Isso foi a algum tempo atrás, e apesar de ser uma maneira muito elegante, ainda é uma maneira um pouco repetitiva, no sentido que você deve se preocupar e repetir algumas partes do processo. Isso é uma parte do processo que pode ser feita de forma automática, com muitas vantagens, com uma ajudinha de macros.

Estava vendo que o Murilo fez algo de forma parecida, e achei muito interessante. Resolvi copiar e alterar para o modo como eu mostrei em meu post passado, e mudei algumas coisas, como o nome de “declare” para “declare_am”, só para fica mais explicito….

Vejam agora que toda vez que eu quiser declarar uma variável data do tipo inteiro com getters e setters, é só usar declare_am(int,data)! Muito fácil.

Segue o código abaixo, usem para facilitar a vida, e qualquer discussão estamos ai :].

#include	<iostream>
#include	<cstdlib>

#define declare_am(type, name) \
private: type _##name; \
public: \
void name(const type& var){\
	_##name = var;\
}\
\
const type & name(){\
	return _##name; \
}\

class Teste_accessor_mutator{
	declare_am(int,data);
	declare_am(int,x);
	declare_am(int,y);
};

int main( int argc, char *argv[] ){
	Teste_accessor_mutator teste;

	teste.data(100);
	teste.x(1);
	teste.y(2);
	std::cout << teste.data() << std::endl ;
	std::cout << teste.x() << std::endl ;
	std::cout << teste.y() << std::endl ;
	return EXIT_SUCCESS;
} /* ---------- end of function main ---------- */


Aquisição e desalocação de recursos com objetos (Um pouco de RAII)

O que é a RAII?

Falar de RAII é ser sempre um pouco obvio, mas os conceitos que vem com ela de vez em quando não estão “fixos”. Sabendo o que é a técnica, o que ela faz e como implementar, podemos usa-la de forma melhor.

A RAII é um padrão de projeto que especifica que todos os recursos de um objeto devem ser adquiridos/alocados quando o objeto for inicializado.

E também, todos os recursos que um objeto tem que são exclusivos dele, devem ser liberados/desalocados no destrutor do objeto.

Fazendo isso, você tem um objeto que respeita o RAII.

Por que usar o RAII?

Bom, existem vários motivos para se usar o RAII. O mais convincente deles talvez seja que o C++(assim como a linguagem D), foi projetada para esse designe. Isso porque o C++ assume que o que você cria no objeto só vai existir no escopo daquele objeto.

Isso seria como o escopo de funções, você aloca/adquiri recursos no começo da sua função, como no exemplo abaixo:

/* exemplo de como seria o escopo */
void foo( int &x ){

	vector<double> tempArray;
	int *aux = new int ;

/* Aqui continua a sua função */
. . . .

No começo da função “foo”, você adquiriu o recurso “tempArray” e alocou um inteiro “aux“.

Isso seria justamente o que você iria fazer no construtor do objeto, e que deve ser o inicio do escopo de um objeto, comparado ao escopo de uma função.

A primeira coisa que é executado no objeto, pouco depois de adquirir recursos declarados será o construtor do objeto. Então, seria uma classe assim que seria a equivalente da função “foo”:

class foo {
/**Aqui você "adquiri" um recurso quando o objeto começa a existir.
 * Esse recurso deve ser deslocado automaticamente e executado seu destrutor
 * quando o objeto deixar de existir, porque saiu do "escopo".
 */
	vector<double> tempArray;
	int *aux;
/* Agora o construtor. Aqui você "aloca" os seus recursos dinâmicos.
 * Aqui é preferível que você também defina atributos do objeto.
 */
	foo( int &x ) {
		this->aux = new int ;
/* Aqui continua a sua classe */
. . . . .

Esse é o inicio do seu objeto, mais o RAII ainda não esta completo. O RAII também deve abranger o destrutor, porque é ele que vai “desalocar/liberar” os recursos que o seu objeto.

Quando é requisitado que o objeto deixe de existir, o destrutor vai ser executado e depois será destruído os atributos do objeto, Isso porque o destrutor, comparado com uma função, representa o final do código que está na função, antes da função acabar e sair de escopo. Por exemplo, esse é o final da função “foo” que foi mostrada no começo:

	/*Final da função foo, final do escopo da função.
	* Será desalocado todos os recursos dinâmicos alocados nessa
	* Função.
	*/
	delete aux ;
	/*Ainda temos o objeto tempArray que pertence a esse escopo. Mas,
	* como já é o final do escopo, esse recurso terá chamado seu destrutor
	* e será desalocado automaticamente.
	*/
}

Esse código seria o final da função foo, que esta nos ajudando a entender o como funciona o escopo de um objeto.

Esse código equivale ao que a esse destrutor do objeto:

~foo(){
	/*O destrutor de foo foi chamado. Isso significa que o objeto vai
	* deixar de existir, então será desalocado todos os recursos dinâmicos
	* que pertenção exclusivamente a esse objetos. No caso desse objeto,
	* this->aux contem um recurso que alocamos no construtor, então ele
	* precisa ser deletado:
	*/
	delete this->aux ;
	/*Ainda temos o objeto tempArray que pertence ao escopo do nosso objeto.
	* Mas, como já é o final do escopo, esse recurso terá chamado seu destrutor
	* e será desalocado automaticamente.
	*/
}

Bom, pelo que da para ver com esse construtor e esse destrutor, o objeto terá seus recursos adquiridos no construtor e desalocados no destrutor.

Vejam que isso se adequá de forma perfeita em “C++“. Parece até redundância ficar falando isso, mas não é assim em algumas linguagens e algumas pessoas não usam esse conceito direito.

Código Exception Safe

Também é possível escrever código “exception safe“, ou seja, código que seja seguro para o uso de excessões.

Isso significa que você poderá usar excessões em qualquer parte do código, de forma que o seu programa gerado não tenha nenhum problema de vazamento de memória ou consistência por causa de uma excessão gerada em alguma parte de seu código.

Você garante isso porque você defini muito bem o comportamento de um objeto. Se você consegue definir as ações do destrutor e do construtor bem, você garante que fora do escopo tudo daquele objeto vai ser destruído quando ele for finalizado. Então não importa se você chegou no final da sua rotina, o objeto vai ser eliminado assim que aquele escopo deixar de existir. Isso garante o “exception safe“.

(Caso você não saiba muito sobre exceções e “exception safe“, clique em “exception safe” no link no título desse tópico e veja o artigo Bjarne, o criador.)

Por que Não usar o RAII?

Não existe motivos para não usar RAII, existem motivos para não usar RAII completamente.

Então, porque não usar RAII completamente? Simplesmente porque, em alguns casos, os recursos que o objeto adquiri para si são muito grandes, e talvez seja mais vantajoso iniciar o objeto com uma parte do recurso ou até mesmo adiar a aquisição do recurso.

Isso ocorre por exemplo quando o objeto precisa de um vetor muito grande ou quando você não sabe se o recurso será realmente necessário ou não. Então, como uma estratégia para otimizar os recursos que seu objeto vai consumir, você opta por uma inicialização tardia ou sob-demanda.

Bons exemplos sobre isso seriam a classe da biblioteca padrão vector e list. As duas são exemplos dos dois casos.

Mas, independente da construção do objeto, a destruição ainda tem que ter o mesmo modo de agir. O único problema é que deve se checar no destrutor se o recurso já foi alocado e depois libera-lo ou não. Caso você faça uso de algum tipo de “smart pointer” (um dia falo mais sobre eles . . .) você não ira precisar nem verificar se o recurso foi alocado, ficam isso ao encargo do “smart pointer”.

Considerações finais

Creio que passei bem o pensamento por traz desse assunto. Falar sobre RAII é, de certa forma, falar do obvio. Mas no fundo todo o design pattern é falar um pouco sobre o obvio, só que o obvio que a gente nunca usa….

Também estou criando coragem. Hoje estou escrevendo sobre RAII, mas daqui a pouco vou escrever sobre “smart pointers“. Esse sim será um assunto BEM mais interessante. Mas por enquanto, fica só como projeto do futuro.

E como final, se lembrem sempre:

  • Construtor => Você deve garantir que o objeto tenha seus atributos definidos e que todos os recursos sejam adquiridos/alocados.
  • Destrutor => Você deve garantir que todos os recursos que pertencem ao objeto sejam desalocados/liberados.

Esse sempre será o comportamento desejável, previsível e esperado de uma classe bem feita.


%d bloggers like this: