FrancisBFTC / KiddieOS_Development

O KiddieOS é um sistema operacional open-source básico em desenvolvimento pelo curso gratuito D.S.O.S [Desenvolvendo Sistemas Operacionais Simples]. A intenção deste sistema será: Criar, editar ou excluir arquivos, codificar em uma linguagem própria do sistema, criar objetos visuais e automatizados (desenhos) através desta linguagem, utilizar uma interface simples e intuitiva, criar novas interfaces gráficas, como: Janelas, botões, campos, etc... e estimular crianças, jovens e adultos a programar numa linguagem simples dentro do sistema operacional KiddieOS. A intenção do curso D.S.O.S é dá início ao desenvolvimento de sistemas operacionais utilizando a linguagem Assembly e entender a fundo sobre diversos conceitos internos deste tipo de sistema. Aqui neste repositório serão armazenados arquivos de APIs do KiddieOS, a imagem de disco para teste e futuramente - todo o sistema operacional completo. Visite o link abaixo para nos acompanhar no curso do Youtube, se inscreva neste canal para se manter atualizado e siga-me no GitHub. Vejo vocês lá:
MIT License
46 stars 5 forks source link

KiddieOS_v1.3.9: Sistema de Permissões, Criação de Diretórios, Abertura de Arquivos e Serviços DOS + Loader MZ #9

Closed FrancisBFTC closed 7 months ago

FrancisBFTC commented 1 year ago

Features/Funcionalidades

Nesta versão 1.3.9 do KiddieOS foi trabalhado em recursos de gerenciamento de permissões de arquivos durante o sistema de abertura no Driver FAT16 e criação de diretórios, incluindo a implementação de quatro novos comandos: MKDIR, OPEN, CHMOD e DEL. Há atualizações em relação a rotinas do DOS no Kernel, para operações de entrada e saída de usuário, como também para abertura de arquivos. Também abordamos mais a fundo sobre o Carregador MZ e suas características. Temos outras atualizações essenciais e simples que serão discutidas ao longo da especificação.

Abertura de Arquivos: Rotina OpenThisFile

De acordo com as especificações das atualizações anteriores, temos visto sobre rotinas repetitivas do Driver do FAT16 que efetuam busca de arquivos para assim efetuar uma operação, LoadThisFile e WriteThisFile é um exemplo destas rotinas que realizam a busca para leitura e escrita, respectivamente. E se pudéssemos separar a busca de outras operações? Desta forma o usuário poderá "controlar" melhor o que e como fazer em suas operações sobre arquivos, pois então, é o que a rotina OpenThisFile inicialmente faz, ela auxilia exatamente nesta questão de operações futuras.

A rotina inicialmente trabalha na busca do arquivo como as outras rotinas comuns, no entanto, ela faz algum gerenciamento um pouco mais otimizado, pois além dela procurar o arquivo, ela também vai compreender "se" o arquivo pode ser acessado ou não, então primeiramente ela verifica no registrador DL (parte baixa de DX) o modo de acesso e o modo de compartilhamento, onde o primeiro são os 2 bits LSB que é o tipo de abertura (0 - leitura; 1 - escrita; 2 - leitura/escrita) e o segundo são os 2 bits MSB da negação/permissão de abertura do arquivo por outros processos (0 - não negar; 1 - negar tudo; 2 - negar escrita; 3 negar leitura). Caso um dos valores forem maior que o limite, a abertura é encerrada com um erro de "Modo de Acesso não permitido".

A busca segue e quando o arquivo é encontrado, novas verificações e processos são feitos, como isolar os 6 bits do endereço de ES:DI na entrada de HighCluster do arquivo e verificar o tipo de usuário numerado pelo registrador DH (parte alta de DX), O que internamente na rotina foi atribuído a CH. Existem 5 tipos de acesso, ou, usuário no KiddieOS na seguinte hierarquia: Outros/Convidado, Grupos de Usuários/Rede, Usuário/Autor, Administrador/Root & Sistema/Kernel. Mais tarde no tópico de permissões, será abordado com mais detalhes sobre estes tipos mas o que o sistema Open precisa é verificar o tipo e desviar para uma tarefa específica de abertura. Por exemplo: Para outros e grupos de usuários a rotina vai isolar e deslocar os devidos bits do endereço de HighCluster da entrada FAT e saltar para a rotina padrão de usuário, se for usuário ela verifica diretamente a rotina padrão de usuário verificando se o arquivo tem permissão apenas pra administrador ou ambos e se for administrador ela descarta a verificação da rotina padrão de usuário e imediatamente já verifica os modos de acesso.

Porque que HighCluster é utilizado para permissões ao invés de um campo apropriado? Justamente porque o FAT16 ignora o campo de Cluster alto devido a utilização apenas do campo de Cluster baixo (LowCluster) por ser clusters de 16 bits e como o FAT16 não contém um sistema de permissões padrão e nenhum campo atribuído a isto, foi reservado ao KiddieOS o privilégio de utilizar este campo para permissões gerais de 4 tipos de usuário, excluindo o sistema pois ele contém a prioridade máxima. Por isso foi nomeada esta versão alternativa do FAT16 do KiddieOS para KFAT (KiddieOS FAT16) que é uma adaptação de recursos do FAT16, levando em conta que isto não prejudica na compatibilidade de acesso a arquivos em outros sistemas modernos pelo fato deste campo não ser utilizado, como: Windows. Na rotina padrão de usuário a rotina verifica apenas um único bit, que vai determinar se o arquivo pode ser acessado apenas por administradores ou se pode ser acessado tanto pra usuários como administradores.

Isto porque admins também são usuários, porém com privilégios mais altos, então o que diferencia um do outro é que usuários são contas logadas que podem ser autores dos arquivos, enquanto que administradores podem ser outras contas ou a mesma que pode ou não ser autor do arquivo, mesmo assim ele tem o privilégio de usufruir dos acessos, caso seja atribuída. A rotina de usuário deve verificar se o bit 5 das permissões está limpa, se sim, então o usuário não tem acesso, pois todas as permissões do arquivo foi atribuído apenas para administradores, retornando o código de erro "Acesso Negado" e esta verificação é feita tanto para grupos de usuários quanto para convidados, com uma diferença: As próximas verificações será uma comparação dos 5 bits de permissão com os 2 bits do modo de acesso, o que significa que tais 5 bits estão em posições diferentes do campo da entrada FAT para tipos de usuários diferentes, por isso as verificações dos tipos e isolamento dos bits apropriados no início é fundamental para o correto processamento do gerenciamento de permissões.

Caso o arquivo tiver sendo aberto para somente leitura, é isolado o bit de permissão de leitura, caso tiver sendo aberto para somente escrita, é isolado o bit de permissão de escrita mas caso for pra ambos os acessos, é isolado os dois bits e comparado com 3, caso não for igual a 3 (para leitura/escrita), um código de erro é retornado de "Acesso Negado", sendo igual a 3, ele executa os primeiros processos para encontrar a lista de arquivos abertos, que será discutida mais a adiante. Porém, se os bits de acessos individuais (Somente leitura e Somente escrita) tiverem zerados na permissão após o isolamento, então é erro de acesso negado também. Aprovando nestas verificações, Agora entra a busca de uma estrutura inicial que pode estar toda zerada inicialmente, caso não tiver arquivos abertos no sistema, e é exatamente este processamento que o tipo de sistema executa diretamente, isto é, lá no início das verificações, se o tipo de "usuário" que tiver acessando esta rotina for o próprio "sistema" ou código do kernel embutido e não uma conta logada acessando um comando, então todas verificações subsequentes de modos de acesso com permissão são ignoradas/descartadas, pois o sistema tem prioridade máxima e pode fazer qualquer coisa, logo sendo o sistema, a rotina desta estrutura é executada diretamente.

A rotina desta estrutura primeiro procura dentre 512 bytes de um buffer, possíveis 16 entradas, pois cada arquivo aberto vai ocupar 32 bytes por guardar informações da própria entrada do FAT, se o cluster da entrada FAT for igual ao cluster do mesmo deslocamento no campo do buffer, significa que aquele arquivo já foi aberto e é aí que é verificado os modos de compartilhamento do arquivo entre processos, mas se o cluster não for igual, então é verificado se a entrada é zerada, caso sim , pode-se ocupar aquele espaço para o novo arquivo aberto. O registrador SI passa a apontar para este espaço, onde a entrada do arquivo será movido para este espaço, identificando que este arquivo está aberto, esta é a lista de arquivos abertos que mencionei anteriormente e também a estrutura cujo endereço será retornado para a aplicação. No entanto, se todas as 16 entradas foram buscadas para um novo arquivo a ser aberto e todas elas estão ocupadas, a rotina retorna o código de erro "Nenhum manipulador disponível", pra resolver este problema podemos aumentar o buffer/estrutura de memória para comportar mais entradas, o que consequentemente vai ocupar ainda mais a memória de apenas 1 MB que temos que "economizar" para o modo real, então deixaremos estas futuras atualizações para o modo protegido que irá acessar toda a memória alta.

Após o espaço encontrado ser referenciado, então o utilizamos para verificar os modos de compartilhamento. Ora, o sharing mode (ou modo de compartilhamento) consiste em identificar se o acesso de abertura atual está permitido para realização da operação em arquivos abertos por processos anteriores, e tais processos anteriores podem ter negado esta permissão, atribuindo bits na entrada FAT, que por ventura copiou esta entrada para o buffer de referência (a estrutura), logo se este arquivo foi aberto antes, obviamente teremos que analisar no buffer de referência e não na entrada, pois a entrada pode ser constantemente alterada por outras funcionalidades. Se caso o arquivo não tiver sido aberto, sabemos que os bits estarão zerados, como todos os valores, logo o sistema tem permissão. Primeiro é analisado os 2 bits MSB, caso for zero, os bits de compartilhamento do novo processo são inseridos neste campo e a estrutura é criada, ou copiada da entrada FAT para ela. Isto permite que dois sistemas distintos possa abrir o arquivo para leitura e outro para escrita por exemplo mas negando o acesso de um terceiro ou quarto sistema, então o primeiro pode permitir o segundo, o segundo pode permitir apenas uma característica para o terceiro e o terceiro pode negar tudo do quarto e assim por diante. Porém, esta estratégia é sujeita a conflitos e pode sofrer atualizações a medida do tempo.

Caso os 2 bits MSB não for zero, então é filtrado os tipos de modo de compartilhamento. Lembrando que este sistema só é executado quando o arquivo está sendo aberto pela segunda vez. Então se o modo de compartilhamento do processo anterior for 1, é retornado o código de erro "Modo de compartilhamento não habilitado", se for 2, é identificado se há negação de escrita em relação aos modos de acesso atual (Somente escrita ou escrita/leitura), se for 3, então é verificado se há negação de leitura em relação ao modo de acesso (Somente leitura ou leitura/escrita), em ambos os casos, os valores sendo comparados, o erro de Acesso Negado é retornado. Mas se os valores não for comparados/iguais, a estrutura é criada, movendo os 32 bytes da entrada FAT do arquivo encontrado para a posição de memória livre referenciado pelo registrador SI do Buffer de 16 entradas limites, o endereço do registrador SI é copiado para AX, e esta será a referência retornada a aplicação.

Em conclusão, se o arquivo tem permissão daquele tipo de acesso na abertura, a rotina cria a estrutura com sucesso e retorna a referência, que também é chamada de Handler em linguagens alto-nível, mas se o arquivo tem até permissão atribuída para o modo de acesso, mas um outro processo negou este acesso pelo modo de compartilhamento, então é preciso que o segundo processo espere o primeiro fechar o arquivo para o segundo proceder. Isto significa que a próxima rotina para fechar arquivos consistirá em zerar os valores apontados pelo Handler (O endereço do espaço ocupado do arquivo aberto), e zerar outros possíveis buffers com dados temporários de arquivos. Todos os códigos de erros são numéricos e deve ser tratado pela aplicação na apresentação de Strings corretas. Tanto o código dos erros possíveis, quanto os bits de acesso e compartilhamento são padronizados por rotinas do DOS, o que nos permite uma maior compatibilidade entre sistemas. Para compreendermos melhor os bits de permissão, vamos falar sobre os tipos de permissões do KiddieOS no próximo tópico.

Gerenciamento de Bits de Permissões e Usuários

Além das leituras de bits pela rotina OpenThisFile durante a abertura de arquivos, os bits precisam primeiro ser escritos de forma padrão durante a criação de um arquivo, isto é feito na rotina CreateFile que utiliza dois campos normalmente não usados por outros sistemas: O campo reserved e o campo HighCluster de uma entrada FAT. Considerando que o KFAT é uma adaptação do FAT16 para o KiddieOS, estes dois campos é comumente utilizado no KiddieOS para gerenciamento de permissões, onde será armazenado bits de permissão pelo comando CHMOD que será visto mais adiante. Durante a criação de um arquivo, no campo reserved é definido o valor 0x3F e no campo HighCluster o valor 0xFFFF, veremos o que significa estes dois valores.

O valor 0x3F em binário é 00111111b e se trata dos 5 bits de permissão + o bit adicional para administrador. Quando um determinado bit está definido (setado pra 1), significa que o arquivo tem a permissão daquela propriedade, se está limpo (setado pra 0), o arquivo não tem aquela permissão. Logo cada bit se relaciona com um tipo de permissão:

Para enfatizar o conceito, um bom exemplo é se conter o valor 00101b, que significa permissão de execução e escrita somente. O valor 00010b significa um arquivo de permissão somente leitura. 11000b é um arquivo que contém permissão de modificação de entradas e deleção, porém não contém as permissões de execução, leitura e escrita, e assim por diante. O bit 4 relacionado a modificação de arquivos se trata de "modificação de entradas", ou seja, tudo que significa alterar um valor nas propriedades do arquivo, como: Alterar o nome (renomear), alterar o atributo (pra oculto por exemplo), ou até mesmo alterar as permissões (comando chmod). Portanto, para não se perder o acesso permanente do arquivo, administradores contém uma prioridade acima do usuário quanto a forma de alterar as permissões, desta forma, nas próximas atualizações o administrador (quando o bit 5 tiver limpo) poderá ignorar o bit 4 e conter um privilégio de poder utilizar comandos de modificação de entradas, exceto pra outros tipos de usuários. Então se o bit 5 estiver definido, as permissões são atribuídas a usuários e administradores, mas se tiver limpo, as permissão são atribuídas apenas pra administradores e nenhum usuário comum terá acesso ao arquivo. O bit 2 será reservado para execução de programas no Shell, permitindo ou negando a execução deles por um dado tipo de usuário. Bit 3 é a permissão de exclusão de arquivos, então se o usuário movimentar um arquivo de um diretório para outro, é o mesmo que ele estar excluindo de um diretório e recriando em outro.

Já o valor 0xFFFF em binário é 11111 11111 1 11111, onde cada agrupamento de 5 bits é uma permissão completa de um tipo de usuário, por exemplo: Lendo da direita para esquerda, os primeiros 5 bits são as cinco permissões para usuário, o bit único individual é o bit de administrador, os próximos 5 bits do meio são as mesmas permissões porém para grupos de usuários e os últimos 5 bits são as permissões para outros ou convidados do sistema (que não possui nenhuma conta). Para saber mais sobre os tipos de usuários, veremos as numerações de cada tipo:

Cada um destes valores numéricos é inserido no registrador DH antes de qualquer chamada de gerenciamento de arquivos, no entanto, estes valores são colocados pelo próprio sistema e não pelo usuário, pois são valores fixos que não podem ser alterados pelo usuário, em teoria, porque no modo real não há uma proteção tão evidente para evitar disso acontecer. Digamos que o usuário conheça o endereço de memória direto para a rotina OpenThisFile do Driver FAT16, se ele conhecer os parâmetros e decidir colocar um valor em DH de forma manual, mesmo que ele não seja administrador ou tipo sistema, ele poderá ter qualquer prioridade apenas burlando o sistema mesmo que não seja permitido, pois o Kernel em modo real não contém nenhuma proteção de espaço de memória. Portanto, é necessário um gerenciamento de memória, tanto em modo real, quanto em modo protegido, para evitar que "programas" em um espaço predefinido acesse códigos de espaços de memória mais baixos. Isto significa que devemos atribuir tipos de permissões também para a memória, que será gerenciado pelo processador, na recusa de saltos para determinados endereços, o que será discutido nas próximas atualizações sobre gerenciamento de memória. Mas se o usuário decidir apenas inserir o número em DH pelo programa, a estratégia não vai funcionar pois seja lá qual interrupção ou comando tiver executando, o valor em DH será substituído pelo valor correto.

Os números de tipos de usuários serão inseridos pelo próprio sistema através de valores salvos em variáveis não acessíveis pelo usuário, serão variáveis do kernel em espaços de memória protegidos. Nas próximas atualizações, um novo comando de autenticação será criado, para armazenar dados no arquivo autorun.ini, onde o sistema subirá o nível do tipo convidado ou guest de 0 para 2, ou seja, para o número do tipo de usuário comum, no entanto, se esta conta tiver dentro de um grupo ou uma rede, a subida será apenas de um único número. O usuário inicial terá privilégios especiais pra se tornar administrador por ele próprio, porém a partir deste momento, apenas ele poderá adicionar outros como administradores, veremos mais sobre isto futuramente. Agora iremos introduzir aos novos comandos do Shell e suas operações.

Comando OPEN: Abrindo arquivos

O comando open é tão simples quanto a rotina OpenThisFile do FAT16, no entanto, é um comando temporário apenas para testar a abertura de arquivos. Ele executa quase as mesmas operações de outros comandos, como formatar nome de arquivo na CLI, salvar endereço de memória do diretório e seus dados e realizar a busca encadeada de diretórios e subdiretórios para encontrar o arquivo. Porém ele contém novas operações para chamar a rotina OpenThisFile com determinados parâmetros e são eles: AX = endereço de memória do diretório atual; DL = modo de acesso e modo de compartilhamento; DH = tipo de usuário; SI = nome do arquivo pré-formatado. Caso há algum erro na abertura, a rotina OpenThisFile define a Carry Flags que pode ser testada pela instrução JC de salto quando a Carry Flags está definida e a rotina também retorna o código de erro.

Caso não há nenhum erro, a carry flags é limpa e AX conterá o "Handler" ou "Manipulador" que é a referência mencionado anteriormente, isto é, o endereço da estrutura onde contém os dados do arquivo aberto. Mas se houver erros, AX terá como retorno o código de erro que podem ser: 01h = Modo de compartilhamento não habilitado; 02h = Arquivo não encontrado; 03h = Caminho não encontrado; 04h = Nenhum manipulador disponível; 05h = Acesso negado; 0Ch = Modo de acesso não permitido; Caso for alguns destes erros, o comando exibirá a String do erro, mas caso não for nenhum destes erros, a rotina imprimirá na tela um erro desconhecido. Agora se não houver nenhum tipo de erro e a abertura for sucessiva, então a rotina imprime uma mensagem de sucesso e o número do handler do arquivo. Este handler pode ser utilizado em funções de leitura ou escrita de arquivos.

Comando CHMOD: Atribuindo permissões

O Comando chmod contém várias combinações de parâmetros na CLI, então podemos conhecer sua estrutura de parâmetros para facilitar o processo. É possível adicionar permissões de usuário/admin especificando o parâmetro u=, porém, para apenas admin é preciso especificar o parâmetro a=. Para grupos de usuários, o parâmetro é o g= e outros como convidados o parâmetro é o o=. Se o usuário quer atribuir permissões para mais de um tipo, ele precisa separar os parâmetros por vírgula ou espaços, como: u=xxxxx,g=xxxxx ou a=xxxxx o=xxxxx. No primeiro exemplo é atribuído 5 permissões para usuários/admins e grupos de usuários, no segundo exemplo é atribuído 5 permissões para somente admins e outros. Os valores que substituem a letra "x" podem ser m, d, x, r e w; Não necessariamente nesta ordem, pois a ordem não é obrigatória, por exemplo: u=mdxrw está correto e a=wrxdm também está correto. Também não é preciso especificar todos as permissões, no entanto, é necessário cautelar-se pois se o valor da permissão não é especificado, o comando entenderá que você estará atribuindo 0 a permissão, ou seja, negando ela. Portanto, é preciso já conhecer os tipos de permissões que já estão atribuídas ao arquivo, este conhecimento será feito através de novo comando para enxergar detalhes dos arquivos que será realizado em versões futuras. Daremos um exemplo de um usuário que atribui permissão de somente modificação, leitura e escrita a grupos de usuários e usuários comuns para um arquivo "datas.txt":

chmod u=mrw,g=mrw datas.txt

Automaticamente, pelo fato dos valores "d" e "x" não serem especificados, isto irá limpar as permissões de deleção (d) e execução (x) do arquivo, enquanto que o arquivo datas.txt terá a permissão de modificação (m), leitura (r) e escrita (w). Uma outra forma de inserir permissões é por números octais, que são separadas a cada 3 bits, pois valores no sistema numérico octal inicia-se de 000b até 111b em binário, isto é, de 0 a 7 em decimal. Considerando que temos 5 bits de permissão para 3 tipos de usuários + 1 bit adicional de administrador, nós temos 16 bits totais. Os bits M e D (bit 4 e 3, respectivamente) vão de 0 a 3 em octal pois contém 2 bits, já os bits X, R e W (bit 2, 1 e 0, respectivamente) vão de 0 a 7 em octal pois contém 3 bits e os últimos 6 bits MSB é relacionado ao usuário (5 bits de permissão + 1 de admin), os 5 bits do meio é relacionado aos grupos de usuários e os primeiros 5 bits LSB é relacionado a outros, subdivididos desta forma: 111 111 11 111 11 111; Portanto, se pegarmos cada separação de bits, nós teríamos o valor octal completo: 773737 - esta é a permissão máxima no KiddieOS ao invés de 777 como em outros sistemas mais modernos. Somente o 1ª dígito contando da esquerda pra direita que pode ir de 0 a 7, pois ele se trata de 1 bit de admin + 2 bits de modificação e deleção na parte de usuário comum, o que totaliza 3 bits. O 2ª dígito são 3 bits para execução, leitura e escrita, que também vai de 0 a 7. O valor 37 do meio (3ª e 4ª dígitos) são do grupo de usuários, desta forma, o 3ª dígito só vai de 0 a 3, pois se trata de modificações e deleções, enquanto que o 4ª dígito vai de 0 a 7 pois se trata de execução, leitura e escrita. Da mesma forma são os últimos dígitos 37 que terá as mesmas ordens de permissões, porém para o tipo outros/convidado. Para inserir permissões usando números octais, é necessário especificar o parâmetro -O (de Octal) antes dos dígitos, um exemplo vem a seguir:

chmod -O 253723 datas.txt

O valor 2 em binário será 010, o que seria permissão apenas pra administrador de apenas modificação e não deleção de arquivos, enquanto que o valor 5 em binário será 101 que é a permissão de execução e escrita, porém não leitura. Em grupos de usuários 3 e 7 será 11111 que atribui todas as permissões mencionadas e para outros/convidado o valor 2 e 3 será 10011 que terá permissão de modificação, leitura e escrita, exceto deleção e execução. Entenderemos como o código funciona brevemente para atribuir as permissões.

Primeiro é verificado em "NextFlags" os parâmetros u=, a=, g=, o= e -O, caso for u= por exemplo, o bit 5 é definido em BX pela instrução OR em "UsersAndAdmin" e a rotina subsequente "CheckFlags" irá identificar qual valor de permissão se trata, se é m, d, x, r ou w, se for um destes valores, CX terá um número diferente correspondente a posição do bit de permissão para ser deslocado o 1 em AX, pois será este valor em AX que será definido em BX com a instrução OR, o que significa que cada letra de permissão identificada irá definir um determinado bit em BX. A rotina CheckFlags é retornada novamente para verificar a próxima letra incrementando SI e então este loop é feito analisando cada letra, definindo cada bit, até que o caractere seja espaço " " ou vírgula ",", sendo um destes dois, o loop cessa saltando para "CheckNextUser" que irá incrementar SI e voltar para "NextFlags" lá no início para identificar o próximo tipo de usuário, podendo ser a=, g= ou o=. Todos estes parâmetros executa um procedimento um pouco diferente do UsersAndAdmin do parâmetro u=, pois a= para rotina Admin, g= para rotina Groups e o= para rotina Others irá incrementar SI e mover o valor 0, 6 e 11 para CX, respectivamente. CX = 0 em casos de Admin pois após a execução de CheckFlags novamente para verificar as letras de permissão, o valor da posição do bit de permissão será somado mais o que está em CX. Um exemplo é se ele vale 0 (para usuários o padrão também é 0), e a permissão é modificação, 0 será somado + 4, pois o bit estará na posição 4. Porém, se o tipo é Others, por exemplo, o valor será CX = 11, pois na atribuição de permissão de modificação, será somado 11 + 4 = 15, logo o bit de modificação para "Outros" em BX será definido na posição 15 e assim por diante.

Apenas na rotina Admin, quando o parâmetro é "a=", a posição de bit 5 do registrador BX deve ser zerado usando a instrução AND com o valor 0xFFDF. Se em CheckFlags, nenhuma das letras ou caracteres de permissão foi identificado, ou foi identificado letras diferentes das determinadas, então ERR.CHMOD é executado, mostrando um erro de "Formato de permissão inválida", mas se os parâmetros em NextFlags (que analisa os parâmetros de tipos de usuários ou parâmetro -O de Octal) não for nenhum deles, significa que há uma chance de ser o primeiro caractere do nome de arquivo que terá a permissão, no entanto, também há uma chance do byte lido ser 0, e quando é 0, significa que nenhum arquivo foi atribuído na CLI, logo é apresentado um erro de "Nenhum arquivo especificado", mas caso não for 0 mas uma variável especial chamada "HasPermission" ser 0, então o erro de "Nenhuma permissão especificada" é apresentada pois HasPermission sempre é definida quando o código encontra e executa uma das permissões de um dos tipos de usuários. Caso o final da String não ser 0 e HasPermission ser 1, então temos um nome de arquivo com parâmetros de permissões antes do nome, logo devemos executar a Rotina "AttribFile" para formatar o nome de arquivo na CLI, efetuar a busca de diretórios e definir os parâmetros para a chamada da rotina WriteThisEntry, a mesma rotina utilizada em comandos REN e ATTRIB, no entanto o deslocamento do registrador DX será 12 (Offset para campo de permissão na entrada FAT) e o valor de BX terá todos os bits corretamente especificados de acordo com a linha de comando.

Além disso, na rotina inicial NextFlags nós verificamos se o parâmetro é -O, se for, então o usuário irá especificar valores Octais. O procedimento já é diferente dos anteriores, começando por adicionar SI + 2, pois de -O para o primeiro espaço antes do 1ª Octal tem 2 bytes de deslocamento, e em CheckOctal que é a rotina subsequente, ela já incrementa +1 em SI para apontar ao 1ª digito do Octal, como sempre HasPermission é definido para 1. As outras diferenças se trata de copiar os valores da linha de comando diretos para o registrador BX, apenas fazendo uma conversão básica inicial, o que torna o processo mais simples. Na conversão primeiramente é verificado se o dígito em ASCII (Já que valores na CLI são Strings) é menor que 0x30 ou maior que 0x37 em hexadecimal, se for um dos dois, é um erro de Formato inválido, pois os valores ASCII de cada dígito deve ser entre 0x30 a 0x37, isto é, do caractere "0" ao caractere "7". Passando nestas verificações, o valor ASCII é copiado para AL e imediatamente subtraído por 0x30, pois digamos que o valor é 0x31 correspondente ao ASCII "1", pra converter este valor pra octal, basta subtrairmos 0x31 pelo valor inicial de "0" em ASCII, ou seja, 0x31 - 0x30 = 0x01. O hexadecimal 0x01 será o Octal 1. Se o ASCII é "3" por exemplo, 0x33 - 0x30 = 0x03, correspondente ao Octal 3, e assim por diante. Considerando que AL contém o valor numérico do octal, CX é incrementado para verificar a ordem do dígito, um exemplo é se for a ordem 1, significa que é um Octal de usuário para Modificação e deleção, mas se CX vale 2, a ordem 2 se trata do octal de usuário para Execução, Leitura e Escrita.

A depender da Ordem em CX, uma rotina diferente é executada para deslocar o valor numérico de AX (O que foi convertido pra octal) para uma posição específica daquela ordem. Um exemplo é se o dígito octal se refere a grupos de usuários para execução, leitura e escrita, então deslocaríamos 9 bits em AX, mas se for para "outros" nas mesmas permissões, deslocaríamos 11 bits em AX. Após o deslocamento correto da ordem, é efetuado um OR entre BX e AX, logo o que tiver em BX não será zerado, apenas definido. Isto significa que os bits deslocados em AX correspondente ao binário do octal será definido em BX na mesma posição deslocado sem afetar os outros bits. No entanto, antes de qualquer deslocamento, é primeiro verificado se AX é maior que 7 ou maior que 3, vai depender da ordem do dígito. Por exemplo: Se o dígito for o 1ª, ele verifica se é maior que 7, se for então é um erro de formato inválido, mas se o dígito for o 3ª ou o 5ª, ele verifica se é maior que 3, se for também é um erro de formato inválido. Em todos os outros dígitos restantes, o valor deve ser menor ou igual a 7. Cada caractere/dígito é analisado individualmente, até que o caractere seja espaço " ", quando for, então CheckTheFile é executado para verificar se o próximo byte é 0 ou não, sendo 0 é um erro mas não sendo 0, é um arquivo, logo AttribFile é executado. Como mencionamos anteriormente, AttribFile vai chamar a rotina WriteThisEntry do driver FAT16, que vai processar a rotina ChangePermission (Nova rotina implementada em relação a versão anterior) através do offset 12 em DX para definir tanto o valor de BL para ES:DI+DX, onde BX é isolado com 0x003F, quanto ES:DI+DX+8 com o valor BX completo sem isolamentos, após isto, DI é subtraído menos 20 para apontar a entrada inicial e SaveEntry é executado para salvar a entrada no disco.

Comando MKDIR: Criando Diretórios

Antes de qualquer coisa, foi preciso atualizar duas regiões pequenas porém necessárias para a criação de diretórios ser possível: Em CreateFile no início de escrita de valores de entrada do arquivo na memória, na parte de atribuir tipo de arquivo (ARCHIVE, FOLDER, etc..) é definido por padrão o tipo 0x20 (ARCHIVE), porém é comparado se a variável LoadingDir é 1, caso for, então o valor de AL é alterado para 0x30 (FOLDER) e atribuído na memória, se não, o valor permanece em 0x20. A mesma comparação de LoadingDir é feito na 2ª região do código, que é a parte do "tamanho de arquivo", normalmente no FAT16, criação de diretórios define 0 para tamanho inicial do arquivo, pois ele contém 0 bytes, no entanto quando se trata de arquivos do tipo 0x20, deve ser inserido de fato o tamanho do arquivo em bytes na última entrada FAT, por isso esta verificação também é feita. Portanto, uma nova variável como parâmetro é utilizada nas chamadas de WriteThisFile, sendo esta variável o LoadingDir. Porque utilizamos a mesma rotina de criação de arquivos para criação de pastas? A resposta é bem simples: Pastas também são arquivos! Isto porque pastas também contém dados na área de dados, contém entradas FAT e Cadeia de Cluster na tabela FAT, o mesmo que arquivos do tipo 0x20, a diferença é que o tipo de pastas é 0x30 e que seus dados é uma estrutura padrão para todas as pastas.

O Comando MKDIR é tão simples quanto qualquer outro comando, pois ele utiliza as mesmas operações que foram impostas em outros comandos, por exemplo: Ele executa Format_Command_Line para formatar o nome da pasta na CLI, igual outros comandos o utiliza para formatar nomes de arquivos. Após isto é inserido o valor do endereço atual de memória do diretório alterado por CD (ou ainda não alterado) e é comparado este valor com 0x0200, se for igual, então AX é zerado mas se for diferente, AX terá o número de Cluster da 1ª entrada inicial do diretório atual. Porque esta comparação é feita? Justamente pelo motivo dos dados da nova pasta ter que "apontar" pro diretório anterior e o diretório anterior da pasta será o diretório atual fora da pasta, este diretório atual é o Cluster da 1ª entrada - A entrada "." que especifica o Cluster do diretório atual. Sendo 0x0200 em AX como endereço de diretório, estamos dizendo que é o diretório raíz, logo o Cluster do diretório atual por padrão é 0x0000, mas se não for 0x0200, significa que não é um diretório raíz, será um subdiretório (pastas dentro de pastas), então é pego o cluster da entrada do diretório atual (do subdiretório) para armazenar em AX.

Após isto, é copiado uma estrutura pré-definida de 64 bytes para a memória de dados do novo "arquivo", claro, estamos falando da pasta. Como uma pasta também é um arquivo que contém dados, os dados são copiados para a mesma memória quando estamos criando um arquivo normal com dados, no entanto, é preciso definir o valor de AX (O cluster do diretório atual) no deslocamento SI + 0x3A, onde SI aponta para a estrutura, que se trata do deslocamento 0x20 (32 bytes da 1ª entrada da estrutura) + 0x1A (Offset 26 para o campo de LowCluster). Considerando isto, temos que a estrutura de dados desta pasta, é nada mais nada menos que 2 entradas de 32 bytes cada, sendo a primeira nomeada com ". " (ponto seguido de 10 espaços) e a segunda nomeada com ".. " (dois pontos seguido de 9 espaços), totalizando 11 caracteres de nome na entrada. O offset 11 (12ª campo) das duas entradas é o valor 0x10, correspondente ao tipo "DIRETÓRIO" e o Cluster da 2ª entrada no Offset 0x1A é o Cluster do diretório atual (que dentro dela será o diretório anterior), enquanto que a 1ª entrada terá de fato o número de Cluster da pasta, porém este número ainda é desconhecido, pois devemos esperar que o FAT16 realize o gerenciamento de espaço livre pra encontrar o 1ª cluster vazio e assim determinar o número deste dado. Após isto a rotina WriteThisFile é executada da mesma forma como se tivesse criando arquivos, no entanto CX é definido pra 64, pois a estrutura total de dados tem 64 bytes e é necessário que CX tenha este valor pois no driver ele é calculado para efetuar outras operações. A variável LoadingDir também é definida para 1.

Também temos uma nova rotina no driver FAT16 que é o "CheckIfFolder" que vai identificar se o acesso de escrita é para criar pastas ou arquivos, sendo para arquivos, ele retorna sem executar nada, mas sendo para pastas, a rotina escreve o número de Cluster identificado do diretório sendo criado na 1ª entrada da área de dados (Após o processamento de FreeSpaceCluster para gerenciamento de espaço livre), determina data e hora de criação dos diretórios (a estrutura de 64 bytes) escrevendo os valores de data/hora convertidas para entrada FAT nas duas entradas da estrutura dos diretórios e define também a data/hora de modificação/acesso. Após isto, nós temos pastas facilmente sendo criadas pelo comando MKDIR. No entanto, alguns bugs foram identificados como "Criação de vários Subdiretórios" o que não é possível na versão atual, como também criação de pastas dentro de diretórios externos, pois para isto é preciso executar a rotina Load_File_Path que está programada para carregar diretórios sendo o último um arquivo com extensão, mas para criação de pastas, o último nome será também uma pasta, então ele tentará carregar a última pasta com nome de arquivo indefinido, portanto é preciso realizar mais algumas adaptações pra tornar 100% estável a criação de vários subdiretórios e pastas em diretórios externos, isto será corrigido nas versões posteriores.

Comando DEL: Excluindo Arquivos

Para compreendermos a deleção de arquivos, devemos primeiro nos atentar sobre a questão "O que é excluir um arquivo?" ou até mesmo "Como os dados são excluídos?". Pois então, na teoria de deleção da matéria, é praticamente impossível excluir um dado, pois dados na tecnologia são bits eletrônicos, que por sua vez é energia e a energia nunca pode ser destruída, apenas transformada. Então mesmo que os bits são zerados, ou melhor, o estado elétrico sofrer a transição de ligado para desligado, ela será aterrada e evaporada para formato de calor, onde o calor será distribuído pelo ar através de refrigeração. No entanto, não vamos nos aprofundar em conceitos de física mas apenas se conscientizar de que um dado nunca é 100% destruído. Porém, no ramo de sistemas operacionais, quando deletamos um dado qualquer, na verdade eles estão sendo zerados, mas isto se trata exatamente dos dados brutos do arquivo? Pois é, necessariamente não! Imagine que você precise excluir um arquivo de 3 GB, se tivéssemos que zerar 3,2 bilhões de bytes na área de dados (Vamos considerar um sistema FAT32) + 32 bytes da entrada do arquivo + 6.291.456 números de Clusters de 32 bits cada (Considerando Clusters de 512 bytes), levaríamos um tempo absurdamente grande de processamento apenas para deleção de um arquivo, o que seria praticamente inviável. Mesmo que limpássemos todos os dados possíveis de um arquivo de apenas 1 MB, estaríamos zerando 1.052.704 bytes totais, o que poderia levar um tempo considerável. Então este processamento para deleção dos dados totais de todos os arquivos é executada reservadamente pela "Formatação do disco", mas para deleção de arquivos individuais pelo usuário, é apenas necessário a deleção de Entradas FAT e Clusters.

Portanto, no mesmo exemplo de exclusão de um arquivo de 1 MB, excluiríamos apenas 4096 + 32 bytes do arquivo, ao invés de 1.052.704 bytes. Os dados do arquivo alocados na Área de dados ainda estarão lá, serão excluídas/zeradas apenas as entradas FAT que identificam os arquivos, como suas propriedades gerais e os Clusters encadeados que apontam para os setores da área de dados, desta forma, quando novos arquivos são criados, eles substituem os dados do arquivo anteriormente excluído. É justamente por este motivo, que sistemas modernos contém formas de hacking na área de segurança da informação para recuperar dados excluídos do disco, pois mesmo que entradas + clusters são zerados, os dados brutos ainda permanecem e assim, se nenhum novo arquivo é criado, os clusters que estão zerados no meio da tabela tem uma possível chance de ser do tal arquivo excluído, tornando possível a identificação do número do setor de dados que contém os dados brutos do arquivo. É claro que isto é mais possível em sistemas modernos como Windows ou Linux, pois estes sistemas operacionais quando criam/editam arquivos no FAT16, eles procuram definir os clusters do arquivo nos últimos clusters da tabela FAT, deixando espaços zerados fragmentados do disco. Por este motivo, o FAT16 não suporta desfragmentação de disco como o NTFS, no entanto, é possível implementar nossas próprias soluções para evitar fragmentação ou até mesmo, criar sistemas de desfragmentação, tanto durante a exclusão quanto opcionalmente escolhida pelo usuário. É nesta perspectiva que o KFAT (Versão do FAT16 do KiddieOS) irá se basear.

O KFAT prioriza, na criação ou edição de arquivos existentes ou novos arquivos, reaproveitar primeiros clusters vazios/zerados, encadeando números de Clusters mesmo que de posições completamente distintas e longes na tabela. Quando deletamos um arquivo que foi criado depois de alguns e antes de outros, nós estamos zerando clusters que estão na parte central da tabela, e assim, os próximos arquivos criados terá o gerenciamento de espaços livres que irá procurar os primeiros clusters zerados, que será do arquivo deletado, desta forma, evitando que o disco fique fragmentado. Portanto, em plataformas Windows e Linux quando trabalhando com FAT16, a desfragmentação é a única maneira de impedir a recuperação de dados perdidos, enquanto que no KiddieOS não há uma maneira fácil de recuperar arquivos, se novos arquivos foram criados. Sabendo destas atribuições veremos as atualizações que foram feitas no driver FAT16 e o novo comando DEL.

Primeiramente, o arquivo é buscado na memória com a rotina DeleteThisFile, quando encontrado, é executado a rotina DeleteFile que vai processar uma nova rotina separada que é o ClearFATClusters, esta rotina separada era embutida em outros códigos de criação de arquivos, então foi possível facilitar o processo separando-o para acesso por qualquer outras funcionalidades. O ClearFATClusters vai limpar cada número de cluster na tabela FAT começando pelo primeiro offset onde o cluster atual do arquivo aponta, identificado pelo deslocamento +0x1A na entrada do arquivo, até o cluster final do arquivo que é 0xFFFF. Depois será calculado o setor inicial da tabela FAT multiplicando o cluster do arquivo por 2 e dividindo por 512, somando mais o setor inicial do FAT que é 7. Então é calculado a quantidade de setores FAT que serão escritos, a partir do setor inicial, subtraindo a diferença do último setor FAT onde contém o 0xFFFF pelo setor inicial, após isto, é regularizado a memória em BX para o primeiro byte de memória do setor inicial da tabela FAT e após os cálculos, CX terá a quantidade de setores FAT, ES o segmento de memória do FAT, BX o endereço regularizado do Offset de memória do setor inicial do FAT e AX o setor inicial do FAT, a rotina WriteLogicalSectors escreve os clusters alterados do FAT da memória para o disco. Os registradores que apontam para a entrada do arquivo encontrado são desempilhados e a partir deste endereço, os dados da entrada do arquivo são zerados, assim como nos clusters, e a rotina SaveEntry é executada para salvar o endereço de memória com as entradas zeradas no disco. No entanto, ainda tem um problema se caso a entrada do arquivo estiver no meio de outros arquivos, quando ela é zerada, os arquivos subsequentes são omitidos durante a leitura, pois a leitura irá ler até a primeira entrada zerada e ignorar os próximos arquivos, isto será corrigido em versões posteriores.

O comando DEL no Shell16 apenas formata o nome do arquivo, salva o endereço dos diretórios, realiza a busca encadeada de diretórios até o nome do arquivo recarregando novos endereços, executa a rotina DeleteThisFile com os parâmetros pré-definidos, restaura o endereço dos diretórios e retorna a rotina. Uma nova rotina também foi criada para recarregar diretórios após modificações, isto é feito em Load_File_Path que é chamado em vários comandos e no comando CD que tem seu próprio código semelhante a Load_File_Path com outras modificações.

Compatibilidade DOS: Rotinas de Serviços do DOS

O sistema operacional DOS (Disk Operating System), inicialmente como 86-DOS, é um sistema desenvolvido por Tim Paterson e modificado pela Microsoft atendendo o nome por MS-DOS. É um sistema operativo single-user e single-task com funções de kernel não-reentrantes: Só podem ser usadas por um programa de cada vez. Muitos desenvolvedores fizeram inúmeros sistemas relacionados com DOS criando ramificações como PC-DOS, DR-DOS, FreeDOS, entre outros. O sistema operacional continha várias funcionalidades de chamada de sistemas para usuários, desde funções comum de entrada de teclado do usuário e saída de texto para a interface de linha de comandos até funções mais sofisticadas como gerenciamento de arquivos, gerenciamento de memória e extensões do DOS que permitia aplicativos de 32 bits serem executados. No entanto, no Kernel do KiddieOS foi somente implementado os serviços iniciais e principais do DOS.

A forma de executar os serviços ou chamadas de sistemas do DOS é pela interrupção 21h, onde usamos a instrução INT da CPU para executar um endereço definido na IVT para o modo real 16-bit ou definido na IDT para o modo protegido 32-bit, porém os serviços do DOS padrão trabalham apenas com o modo real 16-bit. Desta forma, o kernel do KiddieOS primeiramente configura a IVT no endereço 21h 4 a partir do endereço 0000h:0000h de memória, inserindo o endereço da Label DOS_INT_21H, que está no final do arquivo kernel.asm. O DOS_INT_21H primeiro salva os registradores como DS, AX e BX e define DS (Segmento de Dados) para o mesmo segmento de CS (Segmento de Código) e calcula o valor de AX atribuindo em BX o deslocamento da rotina/serviço que será executada. Portanto, é necessário conter um vetor de endereços de serviços para especificar em BX através do número de função em AX. Um exemplo é se AX conter o valor 01h, então será acessado o endereço 01h 2 no vetor, se for 03h será acessado o endereço 03h * 2 no vetor, e assim por diante. Desta forma, nos permite adicionar novas rotinas com maior facilidade, deixando espaços reservados para outras funções.

Foi implementadas no vetor DOS_SERVICES nove endereços de novas rotinas que serão as funções: 01h, 02h, 05h, 06h, 07h, 09h, 0Ah, 3Dh e 4Ch. Estes números de funções são inseridas em AH durante a programação do programa DOS e requere também outros parâmetros como DX, AL, BX, entre outros. Abaixo será descrito com mais detalhes o que cada função faz:

Um detalhe importante é que todas estas rotinas executam funções e interrupções da BIOS, pois se trata do modo real 16-bit. No entanto, algumas funcionalidades são otimizadas, como na impressão de Strings ou caracteres que executam uma rotina especial do Shell chamada PrintData que pode efetuar a rolagem de editor em caracteres especiais ou imprimir em sistemas numéricos hexadecimais. A função de abertura de arquivos também utiliza endereços do SHELL definidos no Kernel.asm para configurar o segmento atual de diretórios, Realizar a cópia entre buffers (do segmento do programa DOS para o segmento do Kernel), formatar nome de arquivo na CLI, carregar diretórios e executar a rotina do driver FAT16 OpenThisFile. É claro, não devemos nos esquecer de que o tipo de usuário é definido em DH pelo próprio sistema, no entanto ainda é um valor fixo de testes, sendo por padrão usuário comum, porém este valor estático será alterado para uma variável dinâmica que é configurada durante as autenticações futuras. O código de erro é retornado pela função 3Dh, cabendo ao usuário imprimir as Strings correspondentes aos erros, diferentemente do comando OPEN que já se responsabiliza por este ato.

Nesta sessão foram vistos as rotinas de serviços DOS implementados no Kernel do KiddieOS, possibilitando assim ser chamadas estas funções dentro do próprio kernel ou principalmente por usuários que programam aplicações compatíveis com DOS. As funções implementadas são compatíveis com DOSBox e EMU8086 e em versões posteriores, serão trabalhadas nas funções em sequência do DOS, até chegar na criação de diretórios, arquivos, modificações e deleções. Nas próximas versões também levaremos em conta a criação otimizada de leitura e escrita de arquivos a partir de uma abertura inicial considerando ainda as permissões que poderão ser verificadas tanto nestas operações como nas operações provenientes dos comandos SHELL. Veremos agora como funciona o executável MZ e como foi trabalhado dentro do Shell a leitura e interpretação da estrutura do formato de programa DOS.

Loader MZ: Carregador de Executável MZ de Programas DOS

O formato de executável DOS MZ é o formato de arquivo executável usado para arquivos .EXE no DOS. O arquivo pode ser identificado pela String ASCII "MZ" (em Hexadecimal 4D 5A) no início do arquivo, que é o número mágico. "MZ" são as iniciais de Mark Zbikowski, um dos principais desenvolvedores do MS-DOS. A estrutura inicial deste formato de arquivo, é o cabeçalho executável do DOS que contém informações gerais do programa, como: Tamanho do programa, offset do código, número de itens relocáveis, quantidade de parágrafos do cabeçalho (1 parágrafo tem 16 bytes), endereços de pilha e endereços de segmento de código.

Uma outra estrutura alternativa é a Tabela de realocações que está na sequência após o cabeçalho. O offset/deslocamento para a tabela de realocação a partir do endereço inicial também é armazenado em um dos campos do cabeçalho, no entanto, é possível que não há entradas na tabela de realocação ou então poderia haver apenas uma entrada, que pode ser lida e configurada pelo Loader seguindo as especificações. Desta forma, quando há 0 ou 1 entrada na tabela de realocação, normalmente o cabeçalho terá 32 bytes de tamanho e isto é utilizado pelo Loader para saber onde os dados do programa realmente começa. Abaixo veremos detalhes sobre cada campo da estrutura do "Header" do executável MZ:

Vamos lá! Entenderemos cada parte desta estrutura a fim de compreendermos como o carregador MZ do KiddieOS funciona. Esta estrutura fica no início do binário do programa DOS antes dos dados e código do programa principal e cada campo/variável contém o tamanho de uma WORD, isto é, 2 bytes de tamanho (podendo ir de 0 a 65535). Exceto a tabela de realocação que tem um tamanho total variável contendo N entradas de 4 bytes cada (2 bytes para o offset e 2 bytes para o segmento). Vamos levar alguns aspectos em considerações de acordo com alguns estudos e incontáveis testes que fiz, então é provável que este tópico seja o maior de todos.

Quando dizemos que um segmento é "realocável" em alguns campos como: SS Inicial e CS Inicial; Queremos dizer que, o valor que estiver ali será multiplicado por 16, o que significa um deslocamento de 4 bits a esquerda do valor, Ex.: SS << 4 = SS 16; CS << 4 = CS 16. Porque devemos efetuar esta multiplicação? Isto se dá o fato de que o endereço é linear e relativo, isto é, se queremos acessar um valor no endereço 001Ah:0001h, devemos multiplicar o segmento 001Ah por 16 e somar mais o offset que é 0001h, como: 001Ah 16 + 0001h e teremos o endereço relativo 0x1A1. A memória linear em modo protegido é endereçada comumente desta forma, enquanto que a memória no modo real é endereçada como uma memória segmentada, assim como no exemplo do endereço 001Ah:0001h. No entanto, ambos os endereços (Segmentado e Linear) incrivelmente apontam para os mesmos valores, pois se tratam de um mesmo endereço. Então, se o campo CS Inicial é o segmento realocável de código, que aponta para o código principal do programa DOS, e em sequência o campo anterior é offset sendo o IP Inicial (Ponteiro de Instrução), isto significa que o cálculo CS << 4 + IP será o endereço relativo do código principal e o endereço correto. No entanto é preciso se ater que um carregador MZ pode e deve definir um segmento de código/dados próprio para onde o programa DOS será carregado e muitas das vezes, o programa DOS foi compilado em outra plataforma, portanto, não há como esta outra plataforma "adivinhar" o segmento que o seu kernel irá escolher, é justamente por isso que é necessário somar o resultado dos campos (CS_Inicial << 4 + IP_Inicial) mais o segmento que seu kernel escolheu. Um exemplo é se o campo do CS Inicial for igual a 0x0015 e o campo IP_Inicial for igual a 0x0000 (É comum ser zerado pois o endereço inicial de código por padrão é 0) e por ventura o seu kernel decide carregar o programa DOS no segmento 0x4000, então o cálculo será: (0x4000 16) + (0x0015 * 16 + 0x0000) = 0x40150. Claro, estamos falando de um endereço linear completo, mas em modo real, costumamos acessar pelo endereço segmentado, então será o endereço: 0x4000:0x0150; Onde calculamos apenas o CS_Inicial e IP_Inicial como endereço relativo e preservamos o endereço segmentado 0x4000 em modo real.

Um outro detalhe ainda sobre o mesmo assunto de endereços, é o TAMANHO DO CABEÇALHO que geralmente tem um número de parágrafos e cada parágrafo é geralmente 16 bytes neste contexto (Apenas observe os bytes de apresentação da memória em um depurador, é normal você ver 16 bytes em cada linha), e como no início falamos que é normal o cabeçalho inteiro ter no total 32 bytes, caso houver 0 ou 1 entradas na tabela de realocação, logo é comum vermos o valor 2 neste campo de "tamanho do cabeçalho", pois 16 bytes * 2 parágrafos = 32 bytes totais. Você deve se lembrar que mencionei sobre o endereço do código inicial ser 0x0000, correto? Então, isto significa que os endereços de variáveis de Strings e Inteiros acessados pelo programa DOS também podem ser zerados se eles tiverem no início, e de forma proporcional, eles estando em qualquer região do programa, sendo no final ou no início, eles terão um endereço "incorreto" pois na verdade o endereço 0x0000 não são as variáveis e sim o início da estrutura MZ. Então devemos calcular proporcionalmente a soma de um endereço de uma variável + o tamanho do cabeçalho, logo se ela tiver o endereço 0x0000, na verdade o endereço correto da variável será 0x0020 (20h em decimal é 32, pois a estrutura MZ tem 32 bytes), se ela tiver o endereço 0x0010, o endereço correto será 0x0030 e assim por diante. Mas como vamos calcular isso em cada variável do programa no binário antes do carregamento se ao menos nem "sabemos" onde elas são acessadas no código? O MZ contém algum campo que mostra onde elas são acessadas? Pois é, não tem! Então existe uma forma estratégica de solucionar isto utilizada pelo KiddieOS: que é deslocar o número de bytes da estrutura MZ para a direita 4 bits (Atenção: Deslocar para a direita e não para a esquerda), o que equivale a dividir o valor por 16. Se o tamanho da estrutura ser 20h em hexadecimal, então será o valor 02h, se for 30h em hexadecimal, será o valor 03h e assim por diante. Tudo oque resta é somar o segmento de carregamento do binário + o tamanho da estrutura deslocado 4 bits a direita. Exemplo: Se for 0x5000 o segmento e 32 bytes de tamanho da estrutura, o endereço completo será 0x5002.

Então se temos uma String com o endereço 0x0000, sabendo que ela deveria ser 0x0020, porque ela está 32 bytes deslocada pra baixo no binário, e somamos o segmento inicial + (0x20 >> 4), isto significa que qualquer procedimento que acessar esta String, qualquer um vai utilizar o par "DS:Endereço", isto é, todos os endereços no programa são acessados diretamente ou indiretamente usando o segmento de dados DS e DS é onde terá o valor 0x5002 no nosso exemplo, portanto, se o endereço de uma String for 0x0000 e estiver em BX (BX = 0x0000), e sabendo que o acesso é DS:BX, teremos o endereço completo: 0x5002:0x0000, agora tente calcular o endereço linear deste endereço segmentado pra comprovar nossa estratégia: (0x5002 << 4) + 0x0000 = 0x50020. Perceba que o 0x02 somado no segmento antes, automaticamente se converteu para 0x20 no offset do endereço linear, seria como se calculássemos: 0x5000 << 4 + 0x20, sendo a memória segmentada 0x5000:0x0020. Em conclusão, tanto um como o outro é a mesma coisa e conseguimos adicionar o tamanho da estrutura em todas as variáveis acessíveis pelo programa. E pra isto ser realmente possível, introduzimos o conceito da Tabela de realocação. A tabela de realocação é nada mais nada menos que entradas de 4 bytes cada que contém endereços apontando pra região do código onde terá "definição de segmentos". Se lembra quando falamos que os endereços incorretamente vem com a diferença de 32 bytes? Então, isto significa que os endereços que estiverem ali na tabela de realocação devem ser acessados usando o segmento definido pelo kernel + tamanho da estrutura / 16 (deslocado 4 bits a direita), isto é, se o segmento for o mesmo do exemplo anteriormente e o offset de uma das entradas da tabela de realocação for 0x0050, então o endereço completo seria: 0x5002:0x0050. Veremos algo similar que estaria neste endereço:

segment CODE_SEG
_start:
    mov ax, DATA_SEG
    mov ds, ax
    mov es, ax

O endereço 0x5002:0x0050 no nosso exemplo poderia estar apontando para a instrução mov ax, DATA_SEG, pois assim como a definição de segmento de código segment CODE_SEG, existe outra definição anteriormente para dados como segment DATA_SEG relacionada a variáveis e strings. No entanto, no código binário o DATA_SEG será um valor zerado e este valor zerado é colocado em DS e ES, logo, se acessarmos uma variável de endereço 0x0000 colocada em BX com o segmento de dados sendo 0x0000 também, o endereço DS:BX seria 0x0000:0x0000 e isto estaria incorreto, então é preciso que o carregador do MZ no sistema operacional defina qual é o segmento que de fato seria atribuído a DS no programa, para isto precisamos "filtrar" quais seriam as regiões do programa no código principal onde "segmentos" são definidos. Se encontramos a primeira região no endereço de exemplo 0x5002:0x0050 (onde o 0x0050 foi pego pelo campo "Offset" da tabela de realocação - os primeiros 2 bytes) sendo o código citado acima, então devemos mover 0x5002 (O segmento calculado) para o próprio endereço 0x5002:0x0050, desta forma, após atualizar o código na memória durante a interpretação do MZ, temos:

segment CODE_SEG
_start:
    mov ax, 0x5002
    mov ds, ax
    mov es, ax

Agora sim! Todas as variáveis acessadas pelo programa pelo par DS:BX ou DS:DX, etc.. terá o segmento 0x5002:ENDEREÇO. Portanto, as diferenças do tamanho da estrutura em relação a variáveis no programa será descartada e todas as variáveis terão os endereços corretos. Porém, devemos nos atentar que o campo da estrutura MZ ITENS DE REALOCAÇÃO vai conter o número/quantidade de entradas na tabela de realocação, então devemos fazer as mesmas modificações no código binário apontado pelos endereços de todas as entradas, pois este código citado anteriormente pode estar também em várias outras regiões definindo outros segmentos com outros nomes diferentes como: RODATA_SEG, DATA_SEG, BS_SEG, etc.. OBS.: Os nomes dos segmentos não tem um padrão obrigatório, então você pode inserir qualquer nome. Outro fator que é simples porém importante é que o endereço da instrução que movimenta o DATA_SEG para AX para definir DS ou outros tipos de segmentos, onde é apontado pela entrada da tabela de realocação, não pode ser substituído e sim "adicionado". Isso é muito importante, pois se o DATA_SEG for 0x0000 pelo fato das variáveis está no início do programa, apenas movimentar o segmento 0x5002 substituindo o 0x0000 seria suficiente porém se as variáveis estão definidas no final do programa, sabemos que o DATA_SEG não seria 0x0000, ele seria outro valor e substituir este valor faria com que o programa não funcionasse, então deverá ser adicionado o segmento no valor ao invés de substituído.

Diante de todas estas explicações, já desmistificamos o que seria a tabela de realocação, não é mesmo? Se você pensou que é o campo INFORMAÇÕES DE SOBREPOSIÇÃO, então acertou! É aí que está a tabela de realocação e este nome "Sobreposição" se trata da gente "sobrepor" os endereços de tamanho WORD diretamente no código binário, onde é definido segmentos, mas sobrepor neste contexto não seria substituir e sim "Alterar" ou "Adicionar" um segmento válido ao valor que está sendo referenciado ali no código. No entanto, ainda falta alguns campos pra discutirmos sobre, como o CS Inicial e SS Inicial, que claro já falamos neles em outro contexto mas devemos saber que o CS Inicial é o deslocamento de onde o código do programa se inicia, e já sabemos que devemos calculá-los CS << 4, isto é, multiplicando por 16 para ter o endereço relativo, no entanto, se temos o tamanho da estrutura de 2 parágrafos, cada parágrafo valendo 16 bytes, sendo então 32 bytes no tamanho da estrutura, o CS << 4 ainda teria uma diferença de 32 bytes a menos do que o endereço da instrução original, então é preciso somar +32 o cálculo de CS para atribuir a BX e assim empilhar BX após empilhar o segmento escolhido para o carregamento do seu programa para assim saltar pra este novo código. É importante salientar que desta vez, o segmento não será somado + 02h igual nos exemplos anteriores, isto se você decidir somar manualmente o CS << 4 + 32, desta forma, seria o endereço 0x5000:(CS << 4 + 32), no entanto, se você decide não realizar esta soma manualmente, poderia sim realizar a soma anterior de 0x5000 + (32 >> 4) ou 0x5002 pra definir o próprio segmento de código (Já que o segmento de código é o mesmo com o de dados), então seria 0x5002:(CS << 4). Conclusão: Ainda se trata do mesmo endereço.

O SS Inicial é um endereço genérico que também pode efetuar a mesma estratégia: SS << 4; E defini-lo no registrador SS para segmento de pilha, como também o SP Inicial definindo para o ponteiro de pilha SP. O KiddieOS opta por trabalhar com o seu próprio Segmento de pilha ignorando o valor da estrutura, apenas considerando o valor de SP Inicial da estrutura e o fator de considerar o SP existe um motivo: Isto é predefinido explicitamente no código do programa DOS! Então abaixo mostrarei um código inicial em Assembly da sintaxe FASM para gerar programas DOS em formato MZ e entenderemos de fato oque acontece por baixo dos panos:

format  MZ                 ; CRIA FORMATO MZ
entry   code:start         ; DEFINE ENTRADA DO PROGRAMA PRINCIPAL
stack   512                ; DEFINE O TOPO DA PILHA

segment data
    message db "Hello World",'$'      ; STRING HELLO WORLD

segment code
    start:
        mov    ax, data           ; SEGMENTO DE DADOS EM AX
        mov    ds, ax             ; DS = AX (SEGMENTO DE DADOS)
        mov    es, ax             ; ES = AX (O MESMO SEGMENTO) 

        mov    ah, 09h            ; FUNÇÃO PARA IMPRIMIR STRINGS
        mov    dx, message        ; STRING HELLO WORLD
        int    0x21               ; INTERRUPÇÃO DO DOS

        mov     ah, 4Ch           ; FUNÇÃO PARA FINALIZAR PROGRAMA
        mov     al, 00h           ; CODIGO DE RETORNO DA FINALIZAÇÃO
        int     0x21              ; INTERRUPÇÃO DO DOS

Por mais que o código está autoexplicativo, vamos destrinchá-lo em relação a nossa estrutura de executável. A primeira diretiva é o format que pode criar diversos formatos de executáveis: PE, NE, ELF32, MZ, etc... O escolhido é o MZ, então ele vai gerar uma estrutura específica que é exatamente esta que estamos estudando e definir no campo ASSINATURA o número mágico "MZ". A segunda diretiva é o entry que é o ponto de entrada que vai determinar no campo CS Inicial da estrutura o endereço do código de programa (onde começa o "start"), então é preciso especificar o segmento de código predefinido code (endereço de CS Inicial) e a label da rotina inicial start (Endereço de IP Inicial). A terceira diretiva é o stack e nela vamos focar um pouco mais, pois ela decide sobre vários campos ali da estrutura, começando por ALOCAÇÃO MÍNIMA que terá o número de parágrafos "mínimos" exigidos pelo programa, considerando que um parágrafo é 16 bytes e ali na diretiva stack temos o topo da pilha com o valor 512 (200h em hexadecimal), logo 512 / 16 = 32 (20h em hexadecimal), então no campo ALOCAÇÃO MÍNIMA terá o valor 20h, correspondente a 32 parágrafos mínimos exigidos pelo programa. Isto porque este é o valor do topo da pilha definido em SP e quando usamos as instruções PUSH para empilhar, SP é subtraído menos a quantidade de bytes do registrador empilhado, portanto nós temos que alocar/reservar um espaço de memória de 512 bytes, pois este será o espaço reservado para a pilha. Isto já nos entrega que o campo SP Inicial da estrutura também é definida com este valor 200h (512 em decimal).

Acabou por aí? Pior que não! Todo o programa é montado e o número de bytes de opcodes e operandos é contabilizado, incluindo os bytes da estrutura do cabeçalho e uma estrutura PSP que eu não mencionei, que ela está entre o segment code e o final da tabela de alocação (que fica no final do cabeçalho), esta estrutura PSP serve pra armazenar informações extras sobre argumentos do programa, entre outras coisas, ainda não explorei o suficiente mas logo mais venho com atualizações sobre isto. Pois então, o número de todos os bytes contabilizados é inserido no campo BYTES EXTRAS da estrutura MZ. Como também o número de páginas de memória que este programa irá ocupar, logo o número de bytes contabilizados irá servir pra determinar o número de páginas no campo PÁGINAS, conhecendo o quanto que cada página de memória suporta (Estes conceitos são compreendidos como "Paginação"). No entanto, sabemos que algumas regiões do código devem ser analisadas primeiramente pra saber de fato o tamanho de BYTES EXTRAS e o TAMANHO DO CABEÇALHO da estrutura MZ, e estas regiões do código são as especificações do "nome de segmento". Apenas procure no código Assembly quantas vezes atribuímos um nome de segmento a um registrador qualquer definido pela diretiva "segment" e diga a linha da instrução. Hmm... parece que tem uma linha específica com o nome "data" que é definido por "segment" e é colocado em um registrador "AX", mas isto só é feito 1 vez... Então se você disse que é 1 vez e está na linha 10, você acertou! Então, o montador ele calcula esta tal "linha 10" como um endereço de memória pra esta instrução e ele contabiliza também a quantidade de vezes que esta mesma situação acontece, então o contador de vezes é utilizado pra reservar/zerar a cada 4 bytes do campo INFORMAÇÕES DE SOBREPOSIÇÃO e inserir os endereços de todas as linhas que contém uma instrução que move um "nome de segmento", como o segmento "data" em AX, o contador de vezes também é inserido no campo ITENS DE REALOCAÇÃO. Desta forma, cabe ao Loader do Sistema operacional se responsabilizar em efetuar as "sobreposições" que mencionei anteriormente.

Depois de definido a quantidade de ocorrências de nomes de segmentos atribuídos a registradores em ITENS DE REALOCAÇÃO e os seus respectivos endereços em cada uma das entradas alocadas de 4 bytes a partir do campo INFORMAÇÕES DE SOBREPOSIÇÃO, então finalmente agora é possível contabilizar definitivamente todos os bytes do programa e alterar o campo BYTES EXTRAS e PÁGINAS que foi calculado a partir da quantidade de bytes e assim também redefinir o número de bytes totais do cabeçalho (da estrutura) no campo TAMANHO DO CABEÇALHO. O campo TABELA DE REALOCAÇÃO terá o deslocamento partindo do endereço inicial para o campo INFORMAÇÕES DE SOBREPOSIÇÃO. Outros 3 campos que não mencionei é SOBREPOSIÇÃO, ALOCAÇÃO MÁXIMA E SOMA DE VERIFICAÇÃO. O campo SOBREPOSIÇÃO geralmente será 0x0000 e mesmo após muitas combinações de testes, percebi que este campo não muda. ALOCAÇÃO MÁXIMA também contém um valor padrão sendo 0xFFFF, correspondente ao decimal 65535, pois se 65535 parágrafos é o "máximo" que pode ser alocado, é justo porque 65535 x 16 = 1.048.560, o que seria aproximadamente 1 MB, restando apenas 16 bytes para completar 1 MegaByte completo e estamos falando do modo real, onde a memória tem no máximo 1 MB de tamanho, é possível que este valor 0xFFFF nunca será completamente alocado, porque a memória convencional para o kernel em modo real é apenas 640 KB de tamanho. No entanto, se usamos extensores DOS como DPMI para usufruir de uma memória alta acima de 1 MB, então podemos sim alocar até 0xFFFF parágrafos máximos para um programa DOS. Já o campo SOMA DE VERIFICAÇÃO é o que conhecemos como "CheckSum" que se trata da verificação de integridade de um arquivo. O que seria um campo que passaria por um algoritmo de somas de todas as WORDs do arquivo e se o resultado der zero, significa que o arquivo é íntegro e não sofreu modificações. No entanto, não utilizamos este algoritmo e este campo no KiddieOS. Até então, os campos de alocação mínima e máxima também não utilizamos por enquanto, pois não contemos um gerenciamento de memória adequado para este fim, já que só precisamos do SP Inicial para trabalhar com a pilha.

Por fim, o código Assembly do programa DOS terá definido DX com o endereço de message e invocará a interrupção 0x21 do Kernel para imprimir a String, esta String é apontada por DS:DX, então se o Shell carregou o segmento de sobreposição em "data" sendo 0x5000 + (HEADER_SIZE >> 4) = 0x5002, atribuindo a DS, DX contendo o endereço 0x0000 do início da memória do programa após a estrutura, a rotina de serviço dos_write_string será executada pra imprimir os caracteres de DS:DX até encontrar o byte '$', isto é feito utilizando a rotina PrintData do Shell. O programa é finalizado invocado a interrupção 0x21 novamente com a função 4Ch, que na rotina de serviço dos_exit_prog irá somar o ponteiro de pilha +6 para "esquivar" do endereço de retorno de interrupção, desta forma, o próximo endereço de retorno será para o próprio Shell e a partir daí, o Shell vai utilizar a quantidade de bytes do programa pela estrutura MZ para limpar/zerar estes bytes, redefinindo após isto os segmentos de dados originais do kernel, que é 0x3000.

Todas as atualizações foram feitas na rotina Exec.SearchFileToExec para buscar o arquivo do programa, carregá-lo na memória no endereço 0x5000:0x0, detectar o tipo de estrutura verificando o número mágico inicial, e se for MZ, então a pilha do kernel é salva para ser atribuído a nova pilha do programa e a partir daí, tudo se baseia nesta nova pilha. A quantidade de bytes da estrutura é calculada e a quantidade de bytes do programa é armazenada, após verificar se contém entradas de realocação e sendo verdadeiro, sobrepor os valores dos devidos endereços, o endereço relativo de CS da estrutura é calculado com o segmento somando mais o tamanho da estrutura para assim empilhar o segmento e empilhar o offset calculado. Os novos valores da pilha em SP (Endereço de salto) são armazenados em BP (Base Pointer) e executado uma chamada longa da WORD BP (CALL WORD FAR [BP]). Uma chamada/salto "longo" é basicamente saltar/chamar um código que está em outro segmento, alterando assim o registrado CS de segmento de código, enquanto que uma chamada próxima é de um código do próprio segmento, sem alterar CS. Através deste tópico compreendemos as compatibilidades DOS através de suas rotinas e o carregamento de executáveis no formato MZ interpretando toda a estrutura. Adiante finalizaremos com as últimas atualizações mais básicas que tiveram no KiddieOS.

Mais atualizações/updates

Neste tópico será apresentado brevemente atualizações mais básicas no KiddieOS, levando em conta as diversas adaptações para os sistemas mencionados acima funcionar corretamente e as adaptações são:

TODO: Quais as próximas atualizações?

REFERÊNCIAS

Abaixo mostrarei algumas referências em que me baseei para aplicar estas atualizações:

FORMATOS DE EXECUTÁVEIS - MZ: https://wiki.osdev.org/MZ

SISTEMA DE ARQUIVOS FAT: https://wiki.osdev.org/FAT

INTERRUPÇÕES DO DOS E DA BIOS: http://www.ablmcc.edu.hk/~scy/CIT/8086_bios_and_dos_interrupts.htm

FUNÇÕES DO DOS PARA ARQUIVOS: https://www.cin.ufpe.br/~arfs/Assembly/apostilas/Tutorial%20Assembly%20-%20Gavin/ASM6.HTM

DESCRIÇÃO INICIAL DO DOS: https://en.wikipedia.org/wiki/DOS_MZ_executable

DESCRIÇÃO MAIS COMPLETA: https://pt.wikipedia.org/wiki/DOS#:~:text=O%20DOS%20%C3%A9%20um%20sistema,Alguns%20TSR%20podem%20permitir%20multitasking

Muito obrigado se você leu até aqui! Deixe um emoticon logo abaixo pra eu saber se você realmente acessou esta especificação (na carinha sorridente na parte inferior esquerda) e deixe um comentário sobre suas dúvidas ou o que você achou da especificação, pode ser qualquer coisa, até mesmo pra interagir com o tópico. Valeu!

Die-die-apenas commented 1 year ago

dale dog

FrancisBFTC commented 1 year ago

dale dog

Dale cat...