A Aldeia Numaboa ancestral ainda está disponível para visitação. É a versão mais antiga da Aldeia que eu não quis simplesmente descartar depois de mais de 10 milhões de pageviews. Como diz a Sirley, nossa cozinheira e filósofa de plantão: "Misericórdia, ai que dó!"

Se você tiver curiosidade, o endereço é numaboa.net.br.

Leia mais...

Oficina

12. O loader Numaboa II

Dom

27

Mai

2007


07:24

(7 votos, média 3.86 de 5) 


Maluco beleza MÓDULO 12 do SO Numaboa

Depois de dois módulos de pura teoria, está na hora de programar um pouco. Com os conhecimentos recém adquiridos (ou revisados), vamos dar uma melhorada no Sistema Operacional NumaBoa. Desta vez vamos criar um loader que carrega um programa a partir do disco e salta para ele. Aliás, é bom saber que existe um outro nome para o loader: é também chamado de bootstrap.

Desabilitando as interrupções

Sempre é aconselhável acertar a pilha antes de começar a fazer qualquer coisa. Como você já sabe, a pilha também é usada para armazenar valores temporários - PUSH põe um valor no topo da pilha e POP tira o valor do topo da pilha. Acontece que a pilha também é usada por instruções de chamada CALL, as quais colocam o endereço de retorno no topo da pilha. Além disto, quando ocorre uma interrupção, também é executada uma instrução de chamada. Este é o motivo pelo qual precisamos desabilitar as interrupções antes de configurar a pilha. Veja como fazê-lo no exemplo abaixo:

[ORG 7C00h] ; Onde a BIOS nos coloca ; --------------------------------------------------------- ; Programa principal ; --------------------------------------------------------- inicio: ; Configurar a pilha. ; Não podemos permitir interrupções durante a configuração cli ; Disabilitar interrupções mov ax, 0x9000 ; Por a pilha em 9000:0000 mov ss, ax ; Transferir o endereço para o registrador ; do segmento da pilha (SS) mov sp, 0 ; Zerar ponteiro do topo da pilha sti ; Habilitar interrupções (SeT Interrupts bit)

O mnemônico da instrução cli é CLear Interrupts, ou seja, CLarear (ou limpar) o bit de Interrupção. Zerando o bit de interrupção desabilita as chamadas. Em contrapartida, sti deriva de SeT Interrupts e habilita as chamadas.

O registrador do segmento da pilha não pode ser acessado diretamente para evitar erros devidos à falta de atenção. Forçando o programador a fazer transferências indiretas, a possibilidade de erros diminui muito. É por isto que transferimos o endereço desejado para a pilha (9000) inicialmente para AX e, num segundo passo, transferimos este valor para SS. Se você ainda se lembra do duplo endereço, sabe que 9000:0000 indica o segmento 9000 e deslocamento 0000. Colocamos então o deslocamento em SP com a instrução mov sp,0. Terminada a configuração da pilha, podemos habilitar novamente as interrupções.

Identificando o drive de boot

A BIOS costuma estar configurada para procurar setores de boot numa determinada sequência, digamos no drive de disquete e depois no HD. Se nosso sistema operacional estiver em disquete e o disquete estiver inserido no drive, o primeiro setor de boot a ser lido será o do disquete. E se não for este o caso? Qual foi o disco utilizado? Sempre é bom saber, para ficar com a faca e o queijo na mão. Para isto, criamos uma variável bootdrv e lhe atribuímos o valor da identificação do drive logo no início do programa. Sabendo que a identificação do drive de boot é colocada no registrador DX, na sua porção baixa DL, basta acrescentar o seguinte código:

[ORG 7C00h] ; Onde a BIOS nos coloca ; --------------------------------------------------------- ; Programa principal ; --------------------------------------------------------- inicio: ; Configurar a pilha. ; Não podemos permitir interrupções durante a configuração cli ; Disabilitar interrupções mov ax, 0x9000 ; Por a pilha em 9000:0000 mov ss, ax ; Transferir o endereço para o registrador ; do segmento da pilha (SS) mov sp, 0 ; Zerar ponteiro do topo da pilha sti ; Habilitar interrupções (SeT Interrupts bit) mov [bootdrv], dl <--- DL indica o drive de boot ... ; --------------------------------------------------------- ; Funções e variáveis do nosso bootstrap ; ---------------------------------------------------------- bootdrv db 0 <--- A identificação do drive de boot ...

Fazendo a leitura de setores

Está na hora de fazer a leitura de alguns setores do disco. Como não temos um sistema operacional ativado (o nosso é que está sendo), precisamos usar interrupções da BIOS. Dê uma olhada no helpPC: escolha "BIOS disk services" e garanto que você há de concordar: a INT 13 é perfeita.

Ressetando o sistema de disco

É interessante referir que, antes de começar a fazer uma leitura de dados, convém ressetar a controladora de disco. Ainda no helpPC, escolha o serviço 0 da INT 13, "Reset disk system". Esta interrupção precisa dos parâmetros AH=0 e DL com o número identificador do disco: 0=drive A; 1=segundo drive de disquete; 80h=drive 0 e 81h=drive 1. Como já armazenamos o número do drive na variável bootdrv, basta referenciá-la. Além disto, esta interrupção tem valores de retorno: AH=status da operação do disco e a flag de carry CF=0 (zero indica sucesso) ou 1 (1 indica erro).

A rotina que teremos pela frente será um tanto longa. Para manter o código legível, é aconselhável criar uma função, que chamaremos de carregar. A primeira rotina desta função será ressetar a controladora de disco:

... ; --------------------------------------------------------- ; Funções e variáveis do nosso bootstrap ; ---------------------------------------------------------- bootdrv db 0 ; A identificação do drive de boot <--- RESSETANDO O SISTEMA DE DISCO carregar: push ds ; Preserva o valor do segmento de dados na pilha .reset: mov ax, 0 ; Serviço que reseta o sistema de disco mov dl, [bootdrv] ; Drive que deve ser resetado int 13h ; INT 13 faz o reset jc .reset ; Falhou -> Tentar novamente (jc=jump on carry) pop ds ; Retoma o valor do segmento de dados da pilha

Observe que, se o sistema não puder ser ressetado devido a uma falha física do disco ou outra razão qualquer, nosso loader vai pendurar no loop .reset <-> jc .reset. Futuramente poderemos corrigir esta falha potencial. Outra novidade é o marcador precedido por um ponto (.reset:). Este ponto indica que o marcador é local e pertence à área do marcador carregar, ou seja, fora desta área ele não pode ser referenciado (mesmo porque não funcionará).

Fazendo a leitura de setores

Se tudo correr bem, está na hora de fazer a leitura dos setores. Faremos a leitura de 5 setores usando outro serviço da INT 13. O serviço 2 desta interrupção é o "Read Disk Sectors" e precisa dos seguintes parâmetros: AH=2 para indicar o serviço, AL=número de setores a serem lidos, CH=trilha/cilindro do disco, CL=número do setor, DH=número da cabeça, DL=número do drive e ES:BX como ponteiro para um buffer. Os valores de retorno da INT 13,2 são: AH=status da operação do disco, AL=número de setores lidos e a flag de carry CF=0 (zero indica sucesso) ou 1 (1 indica erro).

... ; --------------------------------------------------------- ; Funções e variáveis do nosso bootstrap ; ---------------------------------------------------------- bootdrv db 0 ; A identificação do drive de boot carregar: push ds ; Preserva o valor do segmento de dados na pilha .reset: mov ax, 0 ; Serviço que reseta o sistema de disco mov dl, [bootdrv] ; Drive que deve ser resetado int 13h ; INT 13 faz o reset jc .reset ; Falhou -> Tentar novamente (jc=jump on carry) pop ds ; Retoma o valor do segmento de dados da pilha <--- ADICIONANDO A LEITURA .leitura: mov ax,0x1000 ; O buffer deve ficar em 1000:0000 mov es,ax ; Transfere 1000 para ES (Extra Segment) mov bx, 0 ; e transfere 0 de deslocamento para BX ; O resultado é ES:BX com 1000:0000 mov ah, 2 ; Serviço 2 da INT 13 => Ler setores de disco mov al, 5 ; Ler 5 setores (é mais do que suficiente) mov cx, 2 ; Cilindro=0, Setor=2 mov dh, 0 ; Cabeça=0 mov dl, [bootdrv] ; Drive=drive de boot int 13h ; Leia! ES:BX = dados do disco jc .leitura ; falhou -> Tente novamente retn ; Terminada a leitura, retornar

Até já sei... você se embananou com cilindro, setor e cabeça. Por enquanto, deixa quieto. O importante é que tenha compreendido o restante do código.


Dando acabamento ao setor de boot

Agora só faltam dois detalhes para acabar o setor de boot. O primeiro é completar os 512 bytes de que ele precisa e colocar a assinatura 55AA nos dois últimos. O segundo é fazer um salto para o buffer de memória onde se encontra o código que deve ser executado. Mas que código? Calma, é o que ainda vamos programar smile Por enquanto, o acabamento:

[ORG 7C00h] ; Onde a BIOS nos coloca ; --------------------------------------------------------- ; Programa principal ; --------------------------------------------------------- inicio: ; Configurar a pilha. ; Não podemos permitir interrupções durante a configuração cli ; Disabilitar interrupções mov ax, 0x9000 ; Por a pilha em 9000:0000 mov ss, ax ; Transferir o endereço para o registrador ; do segmento da pilha (SS) mov sp, 0 ; Zerar ponteiro do topo da pilha sti ; Habilitar interrupções (SeT Interrupts bit) mov [bootdrv], dl ; DL indica o drive de boot call carregar ; Chamar a função que reseta a controladora ; e faz a leitura de setores do disco jmp far 1000h:0 <--- Salta para o buffer com o código ; --------------------------------------------------------- ; Funções e variáveis do nosso bootstrap ; ---------------------------------------------------------- bootdrv db 0 ; A identificação do drive de boot carregar: push ds ; Preserva o valor do segmento de dados na pilha .reset: mov ax, 0 ; Serviço que reseta o sistema de disco mov dl, [bootdrv] ; Drive que deve ser resetado int 13h ; INT 13 faz o reset jc .reset ; Falhou -> Tentar novamente (jc=jump on carry) pop ds ; Retoma o valor do segmento de dados da pilha .leitura: mov ax,0x1000 ; O buffer deve ficar em 1000:0000 mov es,ax ; Transfere 1000 para ES (Extra Segment) mov bx, 0 ; e transfere 0 de deslocamento para BX ; O resultado é ES:BX com 1000:0000 mov ah, 2 ; Serviço 2 da INT 13 => Ler setores de disco mov al, 5 ; Ler 5 setores (é mais do que suficiente) mov cx, 2 ; Cilindro=0, Setor=2 mov dh, 0 ; Cabeça=0 mov dl, [bootdrv] ; Drive=drive de boot int 13h ; Leia! ES:BX = dados do disco jc .leitura ; falhou -> Tente novamente retn ; Terminada a leitura, retornar <--- ACABAMENTO times 512-($-$$)-2 db 0 ; Preenche com 0 até setor ter 512 bytes dw 0AA55h ; Põe a assinatura do setor de boot

Observe a nova maneira de colocar a assinatura nos dois últimos bytes: dw 0AA55h. Não é erro de digitação, os bytes estão invertidos mesmo. Digamos, por enquanto, que o computador os escreve ao contrário ou... que os escreve da direita para a esquerda. É o little endian em ação smile Observe também que o salto incondicional jmp far 1000:0 possui o adicional far. Far significa longe, portanto trata-se de um salto longo. É necessário porque o salto deve ser feito para um segmento diferente do segmento atual.

O setor de boot está prontinho. Agora é só salvar com o nome de loader3.asm e compilar este código com nasm loader3.asm -f bin -o loader3.bin.


O código que deve ser executado

Poderíamos escolher uma linguagem diferente do Assembly para criar o código que deve ser executado, mas, para não introduzir mais coisas novas do que o estritamente necessário, vamos mantê-la. Mesmo porque o código que deve ser executado já é nosso velho conhecido, pois é muito parecido com o do loader anterior. Veja abaixo:

mov ax, 1000h ; Põe endereço em AX mov ds, ax ; Atualiza o segmento de dados mov es, ax ; Atualiza o segmento extra mov si, msg ; Aponta para a string da mensagem call poeString ; Chama a função para imprimir a mensagem pendura: ; Apenas pendura jmp pendura ; Imprime uma string terminada em 0 (SI=ponteiro para a string) poeString: lodsb ; Copia o byte em DS:SI para AL e incrementa SI or al,al ; Verifica se o byte lido é zero jz short volta ; Se sim, salta para volta mov ah,0x0E ; Serviço 0E da INT 10 da BIOS (Imprimir caracter) mov bx,0x0007 ; Imprimir branco em fundo preto int 0x10 ; Imprimir caracter jmp poeString ; Pegar próximo caracter volta: retn ; Terminada a tarefa, voltar ao ponto de chamada msg db 'Sistema Operacional NumaBoa II',13,10,0

Este é o programa que será transferido para o buffer de memória pelo loader e depois executado. Nosso loader agora tem um companheiro: um KERNEL! Basta salvá-lo como kernel3.asm - escolhi kernel3, apesar de ser o primeiro, apenas para fazer dupla com o loader3. Compile o código com nasm kernel3.asm -f bin -o kernel3.bin para obter o binário puro. Agora, estamos a um passo do teste final.

O disquete de boot da dupla dinâmica

Desta vez, ao invés do usar o debug.exe para transferir os arquivos para o disquete, vamos usar o PartCopy. Está na hora de conhecer este utilitário fantástico criado pelo John S. Fine. Se você ainda não fez o download, aproveite a oportunidade e baixe a versão 2.0 na seção de downloads (Informática / Utilitários). Descompacte o arquivo zip num diretório próprio e, antes de utilizá-lo, dê uma lida nas instruções.

O utilitário PartCopy

O PartCopy, nome derivado de Partial Copy (cópia parcial), faz o que promete: copia porções contíguas de dados puros entre arquivos e/ou discos. É um programa poderoso e deve ser usado com cautela. O formato do comando de linha do PartCopy é o seguinte:

PARTCOPY fonte offset_fonte comprimento destino {offset_destino}
  • O único parâmetro opcional é o offset_destino.
  • Os deslocamentos (offset) e o comprimento devem sempre estar em notação HEXAdecimal.
  • Se o destino for um arquivo, o offset_destino for omitido e o arquivo existir, ele é deletado. Um novo arquivo será criado.
  • Se o destino não for um arquivo, o padrão do offset_destino será 0 (zero) se este for omitido.
  • Qualquer nome que não começar com "-" será um nome de arquivo e será acessado usando os serviços da INT 21 do MS-DOS.
  • -aL acessa o drive de letra L usando os serviços das INT 25 e INT 26 (Disco Absoluto). Apenas o formato "novo" (cx=0xFFFF) é usado.
  • -hN acessa o HD N usando os serviços da INT 13.
  • -fN acessa o drive de disquete N usando os serviços da INT 13.
  • -iN ou -iAAA:N acessam diretamente a IDE do HD N na interface do endereço AAA (padrão é 1F0). Este acesso usa polling com as interrupções desabilitadas, de modo que controladoras secundárias (e seguintes) possam ser gerenciadas sem a preocupação de qual IRQ estejam conectadas. Nesta versão, o acesso direto à IDE é apenas para leitura.

Transferindo arquivos para disquete

Agora que sabemos como usar o PartCopy e já temos os dois arquivos binários, basta fazer a transferência dos mesmo para um disquete. Para o loader3.bin use

partcopy loader3.bin 0 200 -f0

O arquivo fonte é loader3.bin e seu deslocamento é 0 (zero). Portanto, a transferência começa a partir do seu primeiro byte. O comprimento (a quantidade de bytes que deve ser transferida) é 200 em notação hexadecimal, o que corresponde a 512 decimal. -f0 indica que o destino é o drive de disquete A (o primeiro drive). Como o offset_destino foi omitido, o deslocamento no destino será 0 (zero), fazendo com que o primeiro setor do disquete, o setor de boot, receba os dados transferidos. Para o kernel3.bin use

partcopy kernel3.bin 0 200 -f0 200

A única diferença nesta linha de comando é o offset_destino, cujo valor hexadecimal de 200 corresponde a um deslocamento de 512 bytes. Isto coloca nosso kernel no segundo setor do disquete, logo após o setor do boot.

Testando o Sistema Operacional NumaBoa II

Agora fica a seu critério: você pode simplesmente reiniciar sua máquina com o disquete de boot no drive ou fazer uma imagem do disquete usando o RawWrite e acioná-la usando o Bochs.

Considerações finais

Agora não tem o que reclamar - teve programação e novidade à bessa! Espero que tenha ficado claro o grande passo que demos ao criar a dupla dinâmica loader + kernel. Preciso agradecer novamente o texto de Daniel Marjamäki - Daniels NASM Bootstrap Tutorial - do qual extraí o exemplo para poder criar este módulo. O original você encontra na seção de downloads da Aldeia em Tutoriais / Sistemas Operacionais.

покер турниры онлайнсковорода гриль с антипригарным покрытием купитьотзыв написатьноутбук acerалександр лобановскийtranslate thisбиол

Informações adicionais