EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16....

24
EXPERIÊNCIA EX BOOTSTRAP EM “REAL MODE” E EM “PROTECTED MODE” EM UM COMPUTADOR I386 Autores: Helio Omoto, Pedro d’Aquino, Rafael Ruppel, Wilson Leão Ver 1.0/2008 1. Objetivo Os objetivos desta experiência são: 1. Apresentar conceitos básicos sobre a arquitetura i386 2. Apresentar um processo de boot (em real mode) e testá-lo 3. Criar um sistema operacional simples que faz boot em modo protegido Pré-requisitos para a realização desta experiência: 1. Ter familiaridade com sistemas operacionais UNIX-like, basicamente comandos de shell. Nesta experiência utilizaremos o Ubuntu 7.04 com o compilador GCC version 4.1.2 2. Ter familiaridade com Virtual Machines,.Utilizaremos a VMWare Workstation Versão 6.0.3 build-80004 Esta experiência foi baseada nos tutorias de Xiaoming Mo e fazem parte da construção do sistema operacional chamado de Skelix [1] . Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

Transcript of EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16....

Page 1: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

EXPERIÊNCIA EX

BOOTSTRAP EM “REAL MODE” E EM “PROTECTED MODE” EM UM COMPUTADOR I386

Autores: Helio Omoto, Pedro d’Aquino, Rafael Ruppel, Wilson LeãoVer 1.0/2008

1. Objetivo

Os objetivos desta experiência são:

1. Apresentar conceitos básicos sobre a arquitetura i386

2. Apresentar um processo de boot (em real mode) e testá-lo

3. Criar um sistema operacional simples que faz boot em modo protegido

Pré-requisitos para a realização desta experiência:

1. Ter familiaridade com sistemas operacionais UNIX-like, basicamente comandos de shell. Nesta

experiência utilizaremos o Ubuntu 7.04 com o compilador GCC version 4.1.2

2. Ter familiaridade com Virtual Machines,.Utilizaremos a VMWare Workstation Versão 6.0.3 build-

80004

Esta experiência foi baseada nos tutorias de Xiaoming Mo e fazem parte da construção do sistema operacional

chamado de Skelix[1].

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

Page 2: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

2. Arquitetura i386

Em 1978 a Intel lançou o processador Intel 8086, um processador de 16 bits que introduziu o x86 (ou IA-32), um conjunto de instruções considerado o maior sucesso comercial da história da computação, sendo largamente usado pelos processadores até os dias de hoje.

O Intel 80386 foi o primeiro processador x86 a ter uma arquitetura de 32 bits. Conhecido como i386, ou simplesmente 386, este processador, ao lado do 486 (seu sucessor), esteve muito presente nos computadores pessoais da década de 90. Isso torna a programação nesses processadores bastante motivante (principalmente para alguém que já teve um 486, como os autores desta experiência).

Essa arquitetura, que ganhou a alcunha de x86, é até os dias atuais a dominante hegemônica do mercado de PCs. Sua sucessora provável será a arquitetura criada pela AMD, chamada de x64, ou de x86-64, que nada mais é do que uma extensão do x86 para processadores de 64 bits.

2.1 Registradores

O i386 apresenta dezesseis registradores. Quatro deles são de uso geral: EAX, EBX, ECX e EDX. Cada um destes possui 32 bits e todos podem ter os seus 16 bits menos significativos acessados retirando-se o E (por exemplo AX acessa os 16 bits menos significativos de EAX). Dentro do AX, você pode acessar o byte mais significativo por AH e o byte menos significativo por AL; o mesmo vale para os demais registradores de uso geral.

Existem seis registradores de segmentação: CS, DS, ES, FS, GS e SS. Estes são os únicos registradores de 16 bits do i386 e terão seu uso melhor explicado na próxima sessão.

O registrador ESP (stack pointer) aponta para o topo da pilha. O registrador EBP (base pointer) funciona como “offset” para a pilh, recomendado para indicar uma posição de base na pilha. Os registradores ESI e EDI são usados para indexação de arrays e o seu uso poderá ser acompanhado na parte prática da experiência. Por fim, o registrador EIP é o ponteiro para a instrução a ser executada. Estes registradores também podem ter os 16 bits menos significativos acessados ao referenciá-los sem o “E” inicial do nome.

2.2 Segmentação

Para terminar a apresentação da arquitetura i386, será mostrada como funciona a segmentação. A segmentação é uma forma de obter proteção da memória, ao se dividir um endereço em duas partes: segmento e offset. Dessa maneira, pode-se colocar cada parte de um programa em um segmento distinto, evitando acessos indevidos. Um exemplo de programa sem proteção é ilustrado abaixo:

Portanto, para conseguir novos “espaços de endereçamentos independentes”, criaram o conceito de Segmentos. Assim, para endereçar uma posição da memória passa a ser preciso indicar qual o segmento e qual o offset dentro daquele segmento. O segmento também pode ter bits para indicar permissões e tamanho.

O endereçamento através de segmentação utiliza a notação <segmento>:<offset>. Além disso, no i386 a implementação de segmentação depende do modo de operação do processador.

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

2

Page 3: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

Real Mode

O real-mode é um modo de compatibilidade com os códigos escritos para o Intel 8086. Este é modo que o processador 386 inicia sua operação. O real-mode opera em 16 bits.

Utilizando a notação <segmento>:<offset>, podemos calcular o endereço real deslocando-se 4 bits para esquerda do registrador de segmento e adicionando ao valor do registrador de offset. Para o endereço 7C00:0189 teremos, portanto:

Para calcular o máximo que podemos acessar:

Através deste valor podemos acessar diretamente 1MB + 65519 bytes, mas o real-mode visa manter a compatibilidade com o 8086, que possuía apenas 20 bits para endereçamento de memória. Então tudo que excede 1MB é reconhecido dentro deste 1MB, ou seja, o endereço 10000F é reconhecido como 0000F, pois os 4 bits a mais são descartados, simulando assim o comportamento obtido no 8086.

Outra coisa importante a se observar é que é possível acessar o mesmo endereço através de combinações distintas. Por exemplo: 07C0:0000 acessaria o mesmo endereço que 00000:7C00.

Protected Mode

No modo protegido, as coisas ficam um pouco mais complicadas. O registrador de segmento funcionará, na verdade, como “seletor” do segmento. Ele será usado como um índice para encontrar qual é o descritor do segmento, dentro de uma tabela de descritores. Por exemplo, considerando uma tabela de descritores bem simplista, como abaixo:

Índice Endereço Inicial0 A00001 500002 7C0003 320004 10000

Neste caso, para acessar o mesmo endereço 7C189 do exemplo do real-mode, deveríamos fazer 0002:0189. Isso porque o descritor de índice 2 que terá o endereço base de 7C000.

O registrador usado como seletor no i386 usa de fato, apenas 13 bits para indexação na tabela, dois bits para indicar qual o nível de permissão da “requisição” e um bit para dizer se deve ser usada a tabela de descritores globais ou a local.

RPL requester privilege level – level de permissão do requisitor

TI Define se é índice na GDT (Global Descriptor Table, quando em 0) ou na LDT (Local Descriptor Table, quando em 1)

Index Índice na tabela selecionada

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

3

Page 4: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

O descritor de segmento tem um formato mais complicado, incluindo endereços de base e limite, nível de permissão necessário e outras informações, como pode ser visto abaixo:

Limit(Bits 15-0) 16-bit menos significativos do limite

Base Address(Bits 15-0) 16-bit menos significativos do endereço de base

Base Address(Bits 23-16) 8-bit do meio do endereço de base

A Informação de acesso, se segmento foi lido(=0) ou gravado(=1) pelo último acesso

Type Bit 41 para segmentos de dado/pilha se podem ser escritos (=1)para segmentos de código se podem ser lidos(=1)

Bit 42 Para segmentos de dado/pilha, indica direção de expansão, para baixo(=1)Para segmento de código, confirmação

Bit 43 Se é segmento de código(=1) ou de dados/pilha(=0)Bit 44 Deve ser 1 para segmentos de código/dado

DPL descriptor privilege level – permissão necessária para acessar o segmento

P Se o segmento está presente (1 por default)

Limit(19-16) 8-bit do meio do limite

U Definido pelo usuário

X Não usado

D se deve executar como 32-bit(=1) ou 16-bit(=0)

G Se o limite usa unidades de 4KB ou 1 byte

Base Address(Bits 31-24) 8-bit mais significativos do endereço de base

Algo que talvez precise de um esclarecimento é que o limite possui apenas 20 bits, então novamente o tamanho

máximo do segmento seria de 1MB. Porém, existe o bit G, que indica se o limite deve ser pensado em unidades de 1 byte ou de 4KB, no caso, ao colocar o bit G em 1, o segmento passa a poder ocupar 4GB (2^20*4KB).

Uma parte importante do seletor e do descritor são as permissões, que dão nome a este modo de operação do processador. Ao tentar acessar um endereço, o processador irá checar a CPL (current privilege level), contida na palavra de status, e a RPL (requester privilege level) contida no seletor. Ele passará a usar como EPL (effective privilege level) o de menor permissão (maior valor, pois quanto maior o valor, menor a permissão). Para poder acessar o segmento o EPL gerado deve ter permissão maior ou igual (valor menor ou igual) que o DPL (descriptor privilege level) do segmento.

Para encerrar o tema de Segmentação, deve ser dito que, quando nenhum segmento for referenciado, ele usa, por padrão, o segmento CS para código, DS para dados e SS para pilha. Ou seja, dadas as instruções:

mov %eax, (%ebx) call 0x1200

A instrução “mov” está acessando posições da memória sem especificar o segmento. Por estar acessando uma posição de memória, ela será considerada como dado e, portanto, o segmento DS será utilizado. A instrução “call” irá executar algum código na posição 1200, por ser uma execução, será considerado como código e, portanto, o segmento CS será utilizado.

Vale observar que a Segmentação, por sua complexidade, não é muito utilizada. Windows e Unix tentam evitar

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

4

Page 5: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

o seu uso ao máximo, e só definem segmentos para código, dados e pilha de seus processos. Este tutorial faz uso dessa técnica por ter sido baseado nos tutoriais de Xiaoming.

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

5

Page 6: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

3. Bootstrap

3.1 Conceitos básicos, processo de boot

Façamos uma pergunta simples, mas de conseqüências extensas: como todo SO inicia magicamente quando ligamos o computador?

Seguindo esta linha de pensamento, entra em cena a BIOS (Basic Input/Output System), cujo principal papel é, atualmente, o carregamento do sistema operacional. A BIOS é armazenado em um chip ROM (Read-Only Memory) e se encontra, na maioria dos casos, soldado na placa-mãe. Nessa memória ROM se encontram mais dois programas:

SETUP, utilizado para configurar alguns parâmetros da BIOS, muitos dos leitores desta apostila já devem ter tido contato com este programa sem sequer saber no que estavam mexendo (curiosidade: inicie o computador e antes da contagem terminar pressione DEL);

POST (Power On Self Test), seqüência de testes realizados para verificar a integridade do hardware do computador. por exemplo, se o módulo de memória está funcinando corretamente. Geralmente se ocorrer algum erro o usuário é avisado com alertas (verifique o manual da sua placa-mãe).

A BIOS também provê diversos serviços, acessíveis através de interrupções. Esses serviços normalmente envolvem comunicação com dispositivos periféricos, tais como disco rígido ou teclado. Quando o computador liga, o vetor de interrupções é preenchido com os endereços de certas funções, de modo que, por exemplo, a instrução “int 16h” pode ser usada para comunicação com o teclado.

Os sistemas operacionais da família DOS, que dominaram o mercado até cerca de 1995, se valiam extensamente dessas funções da BIOS. Os SOs mais modernos, contudo, têm drivers específicos que cuidam da interface com os dispositivos do computador, de modo que a BIOS só é de fato utilizado nos estágios iniciais do carregamento do sistema.

Mas afinal, como um sistema operacional é carregado na memória? Certamente esse trabalho não pode ser deixado todo a cargo da BIOS; caso contrário, cada atualização ou mudança do sistema acarretaria um update no firmware da BIOS. Isso é, naturalmente, inviável.

Então a BIOS deixa a responsabilidade de início para o SO, sendo uma ponte para este. Daí a razão do termo bootstrap (ou suas variantes, “dar boot”, “rebootar” etc.), que podemos aportuguesar por “se levantar pelos próprios cadarços”. Quando iniciamos o computador, o BIOS carrega um pequeno trecho de código, conhecido como bootloader e de responsabilidade do SO, para a memória principal, este trecho de código é então responsável por iniciar o sistema.

Cronologicamente, a partir do botão de Power, temos:

1. Os componentes eletrônicos se encontram em um modo instável de tensão (transitório);2. Quando estabilizado, um sinal de ready (#RDY) é colocado na bus do CPU (no Front-Side Bus, que

carrega informações entre a CPU e a ponte norte da placa-mãe, se tiver curiosidade, pergunte para o professor). Este sinal simboliza que o processo de boot pode começar;

3. O PC “roda” o POST, que foi explicado acima;4. Todas as ROMs encontradas são carregadas na memória principal e rodadas, incluindo a ROM de

vídeo que é a primeira a ser executada;5. O BIOS é carregado na memória principal e assume o controle.

Uma vez em controle, o BIOS segue os seguintes passos:

1. Checa a integridade da ROM, caso encontre algum erro, geralmente, avisa o usuário;2. Lê a CMOS (Complementary Metal Oxide Semiconductor) que contém as configurações de sistema,

data e tempo, por exemplo.3. Inicia os Interrupt Handlers, a partir deste ponto serviços do BIOS podem ser chamados por

interrupções. Entrar no SETUP, por exemplo, pode ser feito aqui;4. Procura o setor de boot do SO, carrega ele na memória e passa controle para este. Nos PCs da IBM, se

este setor não é encontrado o controle é transferido para o IBM Cassette BASIC; em outros PCs o usuário é avisado do erro e o processo de boot é suspendido.

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

6

Page 7: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

Nos itens seguintes veremos este processo com mais detalhes e começaremos a mexer com alguns números em hexadecimal.

3.2 Caso mais simples: boot a partir de um drive de disquete

O primeiro setor da primeira cabeça e do primeiro cilindro da mídia é chamado de bootsector e contém o bootloader (que pode ter no máximo 512 bytes). Para que a BIOS saiba que aquele código corresponde a bootsector válido, há uma espécie de assinatura: os últimos dois bytes do setor precisam ser 0xAA55.

Durante o processo de boot, o BIOS carrega o que há neste setor na memória no endereço 0x7C00 e faz um jump para ele. Os registradores CS, DS, ES, FS e GS são setados em 0. A CPU, neste momento, entra em real mode, onde caching e paginação estão desabilitados.

É nesse momento que começa o processo de “bootstrapping”. Em sistemas operacionais completos, esse código de 512 bytes é responsável por começar o carregamento do sistema. Você pode se perguntar, com razão, como um código tão pequeno consegue uma proeza deste tamanho. Há duas explicações: primeiro, nos sistemas operacionais modernos, esse código carrega não o sistema inteiro, mas um outro carregador – este sem a restrição de tamanho – que termina o trabalho. Além disso, lembre-se dos serviços oferecidos pela BIOS: há, por exemplo, chamadas que lêem teclas digitadas, ou, mais importante, dados do disco rígido. Isso contribui para simplificar o código do bootloader.

3.4 Mudando para Protected Mode

Na experiência serão utilizados os dois modos de operação do 386 descritos anteriormente. Como o processador inicia no real-mode, após o boot será necessário executar um código para entrar no modo protegido.

## enter pmode movl %cr0, %eax orl $0x1, %eax movl %eax, %cr0

Esse código ativa o bit menos significativo do registrador cr0, um dos cinco registradores de controle

presentes no x86. Ele controla diversos parâmetros do processador, como paginação, cache e o modo de operação (real ou protegido).

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

7

Page 8: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

5. Bibliografia

[1] Tutoriais de Xiaoming Mo (1 e 2) do Skelix – http://en.skelix.org/skelixos/

[2] Tanenbaum, Andrew S.; Woodhull, Albert S. – Sistemas Operacionais: Projeto e Implementação

[3] Programando um teclado através da BIOS: http://www.osdever.net/documents/wout_kbd.php?the_id=15

[4] Documentação do “as”, o assembler GNU: http://www.mit.edu:8001/afs/athena.mit.edu/project/gnu/doc/html/as_toc.html#SEC3

[5] Instruções x86: http://www.emu8086.com/assembly_language_tutorial_assembler_reference/8086_instruction_set.html

[6] Visão geral da arquitetura x86: http://en.wikipedia.org/wiki/X86

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

8

Page 9: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

6. Parte Experimental

6.1 Programa de boot simples: tela preta

Nesta seção, vamos criar um bootloader extremamente simples. Leia o apêndice A para instruções sobre como criar uma máquina virtual, e rode-a. Você deve ver uma tela como abaixo:

Essa mensagem, impressa pela BIOS, sinaliza que não foi encontrado nenhum setor bootável. Vamos, então, fazer um setor de boot mais simples possível.

.text

.globl start

.code16start:

jmp start.org 0x1fe, 0x90.word 0xaa55

Esse código, que fica em loop eternamente, será nosso primeiro bootloader. Note a diretriz “.code16”. Isso sinaliza para o ligador que nosso código será rodado num ambiente de 16 bits, como o é o modo real do x86. Para compilá-lo, criaremos um arquivo Makefile com o seguinte conteúdo:

AS=asLD=ld

.s.o:${AS} -a $< -o $*.o >$*.map

all: final.img

final.img: bootsectmv bootsect final.img

bootsect: bootsect.o${LD} --oformat binary -N -e start -Ttext 0x7c00 -o bootsect $<

O trecho sublinhado é o mais importante deste Makefile. A flag –e indica o ponto de entrada do código, e a flag –Ttext diz para o ligador (linker) realocar o código como se a seção de texto (i.e. de código) fosse carregada na posição 0x7c00. Isso é, de fato, o que acontecerá, pois a BIOS carregará o nosso código nesta posição.

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

9

Page 10: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

Compile com o comando make e transfira a imagem gerada, “final.img”, para o computador Windows. Esse arquivo, que nada mais é do que o código binário, sem cabeçalho algum, do programa acima, será usada como imagem do drive de disquete.

Na VMWare, clique em “Edit virtual machine settings” e selecione o “Floppy”. No quadro da direita, escolha a opção “Use floppy image”, e selecione o local onde você gravou o arquivo final.img. Após certificar-se que a caixa “Connected at power on” está marcada, inicie a máquina virtual.

Você deve ver uma tela preta, com um cursor na primeira posição: é o sinal de que o nosso bootloader foi encontrado e está rodando:

6.2 Programa de boot: “Olá, Mundo”

Conseguimos rodar nosso código, mas ele não é exatamente divertido. Vamos melhorar um pouco as coisas imprimindo mensagens na tela.

Enviar caracteres para a placa de vídeo é surpreendentemente fácil. A escrita é feita através de memória mapeada, a partir da posição 0xb8000. Se for gravado 0x41, ‘A’ em ASCII, nesta posição da memória, aparecerá uma letra ‘A’ na tela. A placa de vídeo é organizada tal que um par de bytes forma uma letra: o primeiro byte é o código ASCII, e o segundo especifica a cor.

Vamos usar uma função para imprimir mensagens na tela, que recebe como parâmetro na pilha o endereço de uma string terminada em zero:

# void pustr(char* string)_putstr:

movw $0xb800,%ax # inicia registrador axmovw %ax, %es # inicia registrador de segmento ESxorw %ax, %ax movw %ax, %ds # inicia registrador de segmento DSmovw %sp, %bxmovw 2(%bx), %si # copia end. da str. da pilhaxorw %di, %di # inicia registrador DI

cld # limpa bit de direçãomovb $0x07, %al # cores: letra branca, fundo preto

1:cmp $0, (%si) # chegou ao fim?je 1fmovsbstosb

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

10

Page 11: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

jmp 1b1: ret

Responda às seguintes perguntas:

1) O que fazem as instruções movsb e stosb? Para que elas são usadas no código acima? Use, se necessário, as referências listadas ao fim desta apostila.

2) Qual é o uso da instrução cld?3) Explique o modo como os labels “1” são usados no código.4) Descreva como a função _putstr funciona.

Vamos, então, usar esta função para imprimir uma mensagem na tela:

.text

.globl start

.code16start:

jmpl $0x0, $code

# void pustr(char* string)_putstr:

movw $0xb800,%axmovw %ax, %esxorw %ax, %axmovw %ax, %dsmovw %sp, %bxmovw 2(%bx), %sixorw %di, %di

cldmovb $0x07, %al

1:cmp $0, (%si)je 1fmovsbstosbjmp 1b

1: ret

msg:.string "Ola, mundo!\x0" # declara uma string terminada em 0

code:movw $0x1000, %sp # inicia a pilha com algum valorpush $msg # põe na pilha o argumento de _putstrcall _putstradd $2,%sp #limpa a pilha

1: jmp 1b # loop eterno.org 0x1fe, 0x90.word 0xaa55

Se tudo deu certo, você deve ver “Ola, mundo!” impresso na tela:

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

11

Page 12: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

6.3 Exercícios

1) Faça uma função que recebe um caractere na pilha e o escreve na tela. Use uma instrução MOV comum. Você teve sucesso? Qual foi o problema? Reescreva o código de modo a contorná-lo, mas ainda usando MOV.Resposta: Não é possível usar MOV normal, pois a memória de vídeo começa em 0xb8000, que é um endereço de 20 bits. O modo real só pode acessar diretamente endereços de 16 bits. O compilador irá emitir um erro. A solução é usar segmentação:# void putc(unsigned byte caractere, unsigned short offset)_putc:

push %bpmovw %sp, %bpmovw $0xb800,%axmovw %ax, %esmovb 4(%bp), %almovw 6(%bp), %bxmovb %al, %es:0(%bx)cmp $0, (%bx)pop %bpret

Esse código transfere um caractere de [bp + 4] (i.e. o parâmetro caractere) para es:[bx], onde bx é o parâmetro offset e es é o registrador de segmento que começa com b800.

2) Usando as funções da BIOS, escreva um programa que lê um caractere do teclado e o imprime novamente na tela. Para isso, saiba que as interrupções da família “int 16h” são usadas para a comunicação com o teclado. Em particular, se o registrador “ah” valer 0 e o programa usar “int 16h”, a BIOS vai ler um caractere do teclado, e retorná-lo no registrador “al":

movb $0, %alint 16h# nesse instante, %ax contem um caractere

Resposta:.text.globl start.code16

start:jmpl $0x0, $code

# void putc(unsigned byte caractere, unsigned short offset)_putc:

push %bpmovw %sp, %bp

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

12

Page 13: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

movw $0xb800,%axmovw %ax, %esmovb 4(%bp), %almovw 6(%bp), %bxmovb %al, %es:0(%bx)cmp $0, (%bx)pop %bpret

posicao:.word 0x0

code:movb $0, %ahint $0x16

pushw posicaopush %axcall _putcadd $4,%spaddw $2,posicaojmp code

1: jmp 1b.org 0x1fe, 0x90.word 0xaa55

A única sutileza é se lembrar de que a cada 2 bytes da memória de vídeo, um é para as cores da fonte, de modo que a cada caractere impresso precisamos avançar duas posições. Além disso, dificilmente algum aluno fará um código que tenha suporte a offset na questão anterior.

3.7 Entrando em modo protegido

Agora que já conseguimos fazer o computador ligar, ler nosso setor de boot e imprimir uma mensagem, vamos passar ao modo protegido. Como explicado na primeira parte desta apostila, o modo protegido é o mais usado nos processadores x86. O modo real, que é o padrão quando o computador é ligado, só continua existindo por motivos de compatibilidade: todos os sistemas operacionais rodam inteiramente no modo protegido.

Vamos ver o nosso novo bootsector (bootsector.s):

.text

.globl start

.code16start:

jmpl $0x0, $code

# Estes serão nossos 3 descritores de segmento# que serão carregados na GDT.# O primeiro deve ser todo zero, de acordo com# os manuais dos processadores.# O segundo será o CS (Code Segment), ao passo# que o terceiro será o segmento de dados (DS).

gdt: .quad 0x0000000000000000 # null descriptor.quad 0x00cf9a000000ffff # cs.quad 0x00cf92000000ffff # ds

# Para a instrução lgdt é necessário um número de 48 bits# estruturado da seguinte forma:# - uma word (16 bits) que contenha o tamanho da GDT# - uma dword (32 bits) que contenha o endereço da GDT

gdt_48:.word .-gdt-1 # "." é o endereço atual; equivalente a

# endereçoAtual - endereçoGDT - 1 = tamanhoGDT

.long gdt # o endereço da GDTcode: xorw %ax, %ax

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

13

Page 14: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

movw %ax, %ds # ds = 0x0000 movw %ax, %ss # segmento da pilha = 0x0000

movw $0x1000,%sp

lgdt gdt_48 # carrega a GDT

cli # desabilita interrupções

# entra no modo protegido habilitando# o bit PE do registrador cr0movl %cr0, %eaxorl $0x1, %eaxmovl %eax, %cr0

# faz um long jump para o começo do códigoljmp $0x8, $pmode_start

O código acima nada mais faz além de definir uma GDT com três descritores, carregá-la e entrar em modo protegido. Por fim, ele faz um long jump para a label pmode_start. Vamos analisar os dois descritores que inserimos na GDT, começando pela estrutura básica de um descritor:

O primeiro valor não-nulo que inserimos foi 0x00cf9a000000ffff. Vamos ver o que esse valor representa:

Bits Significado Valor15-0 16 bits inferiores do limite 0xFFFF39-16 24 bits inferiores do endereço de base 0x00000040 Informação de acesso 0 (indiferente)44-41 Tipo Segmento de código; pode ser lido46-45 Nível de privilégio 0 (kernel)47 Presença na memória 1 (não há paginação)51-48 4 bits superiores do limite 0xF52 Definido pelo usuário 0 (indiferente)53 Não utilizado 0 (indiferente)54 32 ou 16 bits? 1 (dados e instruções de 32 bits)55 Limite de 4K ou de 1 byte? 1 (limite em unidades de 4K)63- 56 8 bits superiores do endereço de base 0x00

Ou seja, esse descritor descreve um segmento de código presente na memória que começa no endereço 0x00000000, cujo tamanho é 0xFFFF * 4K = 4GBytes, que tem permissões de nível 0 (kernel) e cujas instruções e dados devem ser tratados como 32 bits.

A única diferença deste descritor pro seguinte é o bit 43: o terceiro descritor representa um segmento de dados (o DS) e tem um bit invertido em relação ao CS para demonstrar isso.

Para carregar a GDT, uma instrução especial, lgdt, está disponível. Ela usa como operando um estrutura de 48 bits formada por {Tamanho da GDT (16 bits), Endereço da GDT(32 bits)}. Definimos essa estrutura na label gdt_48.

A próxima instrução é cli, que desabilita as interrupções. Isso é essencial, porque no modo real quem está lidando com as interrupções é a BIOS, que definiu rotinas de tratamento para cada uma delas. Porém, quando se passa para o modo protegido, o processador começa a se comportar de maneira diferente nas interrupções, e que é incompatível com o código da BIOS. O resultado é que, logo na primeira interrupção (e isso não vai demorar nada), o processador encontrará um erro e será reiniciado (ou, no caso da VMWare, uma caixa de erro aparecerá para o usuário). Desse modo, precisamos desabilitar as interrupções até que já tenhamos registrado rotinas de interrupção adequadas.

Finalmente, nós ativamos o bit 0 do registrador cr0 e o processador começa a operar em modo protegido, como descrito anteriormente. Depois disso, executamos um ljmp para pmode_start, uma label que está definida num outro arquivo (e que será explicado logo abaixo).

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

14

Page 15: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

O ljmp é necessário para que o registrador CS seja atualizado. Esse registrador contém, no modo protegido, o índice para o descritor do segmento de código. Simplesmente ativar o bit de PE no registrador cr0 não faz com que o processador magicamente descubra qual dos descritores registrados na GDT deve ser o descritor do segmento de código; nós precisamos informá-lo explicitamente. É para isso que essa instrução é usada.

Note o valor passado junto com a label: 0x8. Esse valor é o índice do descritor que queremos que se torne o novo descritor do segmento de código. Ou seja, quando for executar a instrução ljmp, o processador vai antes usar 0x8 para descobrir qual é o segmento de código que nós queremos. Vamos relembrar a estrutura de um índice de descritor:

Observando que 0x8 é 1000b em binário, fica fácil percebermos que esse índice representa: Um segmento cujo nível de permissão é 0 (kernel) Um segmento na tabela global (GDT) O segmento de índice 1.

E o segundo segmento que colocamos na GDT foi, justamente, o segmento de código.Vamos olhar, agora, um segundo arquivo, que chamamos de pmode.s. Foi necessário separar o código em dois

arquivos distintos, por causa da diretriz “.code16” do bootsect.s. Como você deve se lembrar, esse comando diz ao assembler que nosso código deve ser montado como se fosse de 16 bits, o que é o correto quando se roda no modo real. Porém, no modo protegido temos acesso à parte de 32 bits do processador, que gostaríamos de usar. Faz-se necessário, portanto, separar o código em duas partes.

.text

.globl pmode_start

pmode_start: jmp code

# Mesma função do item anterior, versão de 32 bits_putstr:

movb $0x07, %almovl 4(%esp), %esimovl $0xb8000, %edi

1: movb (%esi), %bl

cmp $0, %bl je 1f movsb stosb jmp 1b1:

# Codigo protegidocode:1: movl $0x10, %eax # 0x10 é o índice do descritor do segmento de dados movw %ax, %ds # nessas linhas nós mudamos todos

movw %ax, %es # os registradores de segmento movw %ax, %fs # para que eles contenham o índice movw %ax, %gs # do descritor do segmento de dados movw %ax, %ss

movl $0x1000,%esp # note que podemos usar os registradores # estendidos

push $msgcall _putstradd $4,%esp # ponteiros agora ocupam 4 bytes

1: jmp 1b

msg:.string "Ola, mundo de 32-bits!\x0"

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

15

Page 16: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

.org 0x1fe, 0x90

.word 0xaa55

O mais interessante no código é que, logo no começo do código, redefinimos todos os registradores de segmento (ds, es, fs, gs e ss) para que eles agora contenham o índice do descritor do segmento de dados. O valor desse índice é 0x10, o que significa que ele é o 3º descritor da GDT.

Fora isso, o código simplesmente imprime uma string na tela. Podemos, contudo, usar registradores e endereços de 32 bits, o que facilita a programação.

Para compilar o código, use o seguinte Makefile:AS=as -IincludeLD=ld

.s.o:${AS} -a $< -o $*.o >$*.map

all: final.img

final.img: bootsectcat bootsect > final.img@wc -c final.img

bootsect: bootsect.o pmode.o${LD} --oformat binary -N -e start -Ttext 0x7c00 -o bootsect bootsect.o

pmode.o

clean:rm -f *.img kernel bootsect *.o

Note que bootsect.s e pmode.s são ligados no mesmo arquivo objeto. Isso significa que a ordem especificada na linha de comando (“bootsect.o pmode.o”) é importantíssima: de modo contrário, o ligador colocaria o código de pmode.s na posição 0x7c00.

Antes de continuar, responda às perguntas abaixo:1) Como você faria, no modo protegido, uma função que imprime um caractere na tela? Quais facilidades você

encontrou por estar no modo protegido?2) Quais as diferenças entre as funções _putstr deste item e do item anterior? Qual é mais simples?

3.8 Exercícios

1) Viu-se que nos processadores 8086, ao ultrapassar o limite de endereçamento de 1MB, o processador retornava para os endereços iniciais. O que acontece no 386 ao ultrapassar o limite de endereçamento de 4GB? Escreva um programa que teste sua hipótese.Resposta: Funcionamento análogo. Ele retorna para as posições inicias. Um programa que testa isso irá escrever uma letra “B” na posição 0 da memória. Depois irá ler a letra numa posição de 4GB + 1 e escrever na tela. Deverá ser a mesma letra que foi escrita anteriormente.

Usar o mesmo arquivo bootsect.s com os seguintes descritores:

gdt: .quad 0x0000000000000000 # null descriptor.quad 0x00cf9a000000ffff # cs.quad 0x00cf92000000ffff # ds

.quad 0xffcf92ffffffffff # es

Alterar o pmode_start para:

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

16

Page 17: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

.text

.globl pmode_startpmode_start:

jmp code# Codigo protegidocode:1: movl $0x10, %eax movl $0x18, %ebx # ebx terá o terceiro descritor movw %ax, %ds movw %bx, %es movw %ax, %fs movw %ax, %gs movw %ax, %ss movl $0x1000,%esp movb $0x42, %ds:0 # movendo B para ds:0 (posição 000000) movb %es:1, %cl # coloca ES:1 em cl (posição FFFFFFF + 1) movb %cl, 0xb8000 # verifica se exibe na tela o valor B1: jmp 1b

.org 0x1fe, 0x90

.word 0xaa55

2) Tente acessar uma posição acima do limite de um descritor. O que acontece?Resposta: *** Virtual machine kernel stack fault (hardware reset) ***

Programa para simular:

Bootsect.s com redefinição da descriptor table

gdt: .quad 0x0000000000000000 # null descriptor.quad 0x00cf9a000000ffff # cs.quad 0x0040920000000200 # ds definindo posição 200 como limite

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

17

Page 18: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

Pmode.s:

.text

.globl pmode_start

pmode_start: jmp code

# Codigo protegidocode:1: movl $0x10, %eax

movl $0x18, %ebx movw %ax, %ds movw %bx, %es movw %ax, %fs movw %ax, %gs movw %ax, %ss

movl $0x1000,%esp # podemos usar os registradores # estendidos

movb $0x41, 0x210 # acessando posição 210, acima do limitemovb 0x210, %clmovb %cl, 0xb8000

1: jmp 1b

.org 0x1fe, 0x90

.word 0xaa55

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

18

Page 19: EXPERIÊNCIA 4:jkinoshi/2009/ExpEX0Final.doc  · Web view.word 0x0. code: movb $0, %ah. int $0x16. pushw posicao. push %ax. call _putc. add $4,%sp. addw $2,posicao. jmp code. 1:

Laboratório de Microprocessadores - Experiência 10

Apendice A - VMWare

Criar máquina virtual

Na VmWare vá na opção: File->New-> Virtual Machine e clique em Avançar.

Em seguida selecione Custom e clique em Avançar.

Em seguida selecione Other. E na version Other.

Escolha um nome e um local para a máquina virtual e clique em Avançar.

Escolha “Make this virtual machine private” e clique em Avançar.

Na opção “Run this Virtual Machine as...” escolha “User that powers on the virtual machine” e clique em avançar.

Seleciona One para o número de processadores e clique em avançar.

Coloque 256MB para a memória da Virtual Machine e clique em avançar.

Selecione “Use bridged networking” e clique em avançar.

Seleciona “Create a new virtual disk” e clique em avançar.

Selecione “IDE” e clique em avançar.

Escolha 0.5 GB para o disco, marque “Allocate all disk space now” e clique em avançar.

Escolha um nome para o arquivo que funcionará como disco da VMWare e clique em avançar.

O disco será criado e você pode iniciar sua máquina virtual clicando em “Start this virtual machine”.

Você provavelmente verá um erro de “Operating System not found”.

Configurar uma imagem para boot do disquete

Selecione VM->Settings.

Selecione o floppy e clique em “Use floopy image”. Selecione o arquivo a partir do qual você deseja bootar a sua nova máquina virtual.

Reinicie a Máquina clicando no botão “Restart Guest” ou em Power->Reset.

Escola Politécnica da USP/Departamento de Engenharia de Computação e Sistemas Digitais - PCS

19