Tutoriais e Programação
AoA - Capítulo 1 - REPRESENTAÇÃO DE DADOS
Qua 10 Jan 2007 22:09 |
- Detalhes
- Categoria: Art of Assembly
- Atualização: Domingo, 01 Julho 2012 19:43
- Autor: vovó Vicki
- Acessos: 36247
REPRESENTAÇÃO DE DADOS
A notação binária e a notação hexadecimal provavelmente são os maiores obstáculos para os iniciantes. Apesar de parecerem estranhas no primeiro contato, logo percebemos que suas vantagens superam as desvantagens quando trabalhamos com a linguagem Assembly. Seu uso simplifica a álgebra booleana, o desenho lógico, a representação dos números com sinal, códigos de caracteres e dados compactados. Além disso, é preciso dizer que não há como se tornar um expert no assunto se estas notações numéricas não forem dominadas.
Taí um bom começo. Neste primeiro capítulo vamos revisar alguns conceitos importantes: os sistemas de notação binária e hexadecimal, a organização de dados binários (bits, nibbles, bytes, words e double words), os sistemas de numeração com e sem sinal, as operações aritméticas, lógicas, shift e rotate com valores binários, campos de bits e dados compactados, além do conjunto de caracteres ASCII.
Se você já estiver familiarizado com estes conceitos, não deixe de dar uma passada de olhos no texto. Se tudo for novidade, estude com capricho. Tudo neste capítulo é importante, pois uma boa base é essencial para poder progredir.
Revisão do Sistema Decimal
Em se tratando do sistema decimal é interessante notar que a maioria das pessoas não se dá conta do que realmente ele representa. Quando falamos do número 123, imaginamos uma certa quantidade de objetos que este número representa e esquecemos da sua origem.
Na verdade, 123 representa
(1 x 102) + (2 x 101) + (3 x 100)
ou seja,
100 + 20 + 3 = 123
As posições dos dígitos são numeradas da direita para a esquerda de 0 (zero) até a posição do último dígito da esquerda e são a potência de 10 que multiplica o dígito. Se o valor possuir casas decimais, como por exemplo 123.45, cada casa após a vírgula é numerada de -1 até a posição do último dígito à direita e são a potência de 10 que multiplica o dígito. Portanto
(1 x 102) + (2 x 101) + (3 x 100) + (4 x 10-1) + (5 x 10-2)
ou seja,
100 + 20 + 3 + 0.4 + 0.05 = 123.45
O sistema binário
Por enquanto os sistemas dos computadores operam usando a lógica binária. O computador representa valores usando dois níveis de voltagem, geralmente 0 Volts e +5 Volts. Com estes dois níveis podemos representar dois valores diferentes, que poderiam ser quaisquer valores, mas convencionou-se que representassem 0 (zero) e 1 (um). Estes dois valores, por coincidência, correspondem aos dois dígitos usados pelo sistema de numeração binário. Unindo o útil ao agradável, basta transportar os conhecimentos que temos sobre o sistema decimal para o sistema binário.
Apesar de termos só o Tico e o Teco que, se são apenas dois neurônios, então são binários, nossa cabeça insiste em dar nó. O sistema binário funciona exatamente como o sistema decimal. Se o sistema decimal possui 10 dígitos (de 0 a 9), o sistema binário possui 2 (de 0 a 1). Neste caso, se fazemos potência de 10 para calcular quantidades no sistema decimal, faremos potência de 2 para calcular quantidades no sistema binário. Por exemplo, o valor binário 11001010 representa o valor decimal 202. Veja a seguir:
(1x27) + (1x26) + (0x25) + (0x24) + (1x23) + (0x22) + (1x21) + (0x20)
ou seja,
128 + 64 + 0 + 0 + 8 + 0 + 2 + 0 = 202
Transformar decimal em binário é um pouquinho mais trabalhoso. Já que a nova base deve ser 2, será preciso ir dividindo o decimal por 2 e ir colecionando os restos. Tomemos como exemplo o decimal 458:
458 / 2 = 229 resta 0 229 / 2 = 114 resta 1 114 / 2 = 57 resta 0 57 / 2 = 28 resta 1 28 / 2 = 14 resta 0 14 / 2 = 7 resta 0 7 / 2 = 3 resta 1 3 / 2 = 1 resta 1 1 / 2 = 0 resta 1
Agora será preciso ler os restos de trás para frente para obter a notação binária do decimal 458, ou seja, 111001010. Apesar da notação binária ter pouca importância em linguagens de alto nível, ela aparece com frequência em programas escritos em Assembly.
Formatos binários
Assim como nos números decimais, os zeros colocados à esquerda de dígitos binários não são significativos. Podemos colocar infinitos zeros à esquerda de um número binário que, nem assim, seu valor se modifica. Veja o número binário 101, que corresponde ao 5 decimal:
101 = 0000 0101 = ...00000000000000000000101
Como se convencionou que 1 byte possui oito bits, e um bit (derivado de binary digit) representa um dígito binário, vamos adotar a convenção de grafá-los sempre em múltiplos de quatro casas. Por exemplo, o decimal 5 poderá ser grafado como 0101, 00000101, 000000000101 ou mesmo 0000000000000101. No sistema decimal costumamos separar os dígitos em grupos de três: 1.748.345 é mais legível que 1748345. No sistema binário agruparemos os dígitos em grupos de quatro, também para melhorar a legibilidade. Por exemplo, 0000000000000101 será grafado 0000 0000 0000 0101.
Os dígitos dos números binários são numerados da direita para a esquerda iniciando-se com 0 (zero). Na verdade, esta numeração indica a potência de 2 do dígito em questão. Veja abaixo:
dígitos binários 0 1 0 1 (corresponde a 5 decimal) numeração 3 2 1 0 potência de 2 23 22 21 20
O bit na posição 0 (zero) é denominado de bit de ordem inferior ou baixa (low order) ou menos significativo. O bit na extremidade esquerda é denominado de bit de ordem superior ou alta (high order) ou mais significativo.
Organização de dados
Na matemática pura, qualquer valor pode ter um número infinito de dígitos (lembrando que os zeros colocados à esquerda não alteram o valor do número). Com os computadores a coisa é um pouco diferente, pois trabalham com um número limitado de bits. Os grupos de dígitos binários mais comumente utilizados pelos computadores são: bits únicos, grupos de 4 bits (chamados de nibble), grupos de 8 (chamados de byte), grupos de 16 (chamados de word onde word = palavra), etc. A razão da existência destes grupos é funcional, característica dos chips 80x86.
O bit
A menor "unidade" de dados num computador binário é um bit. Como bit único consegue representar apenas dois valores diferentes (tipicamente zero ou um), fica a impressão de que um bit só consegue representar um número muito limitado de itens. Não é bem assim.
O que precisa ficar claro é a dualidade do bit. Esta dualidade pode se referir a itens de um mesmo tipo ou a itens de natureza completamente diferente. Um bit pode representar 0 ou 1, verdadeiro ou falso, ligado ou desligado, masculino ou feminino, certo ou errado. Um bit também pode representar quaisquer dois valores (como 589 ou 1325) ou duas cores (como azul ou vermelho). Nada impede de que um bit represente dois itens de natureza distinta, como 589 ou vermelho. Podemos representar qualquer par de itens com um bit, mas apenas um único par de itens.
A coisa pode ficar ainda mais complexa quando bits diferentes representarem itens diferentes. Por exemplo, um bit pode representar os valores 0 ou 1, enquanto outro bit adjacente pode representar os valores falso ou verdadeiro. Olhando apenas para os bits, não é possível reconhecer a natureza do que estejam representando. Isto mostra a idéia que está por trás das estruturas de dados num computador: dados são o que você definiu! Se você usar um bit para representar um valor booleano (falso/verdadeiro), então este bit, de acordo com a definição que você deu, representa falso ou verdadeiro. Para que o bit tenha significado, é preciso manter a consistência: se você estiver usando um bit para representar falso ou verdadeiro, este bit, em todos os pontos do seu programa, deve apenas conter a informação falso ou verdadeiro; não pode ser usado para representar cores, valores, ou qualquer outro tipo de item.
Como a maioria dos itens que precisam ser representados possuem mais do que dois valores, bits únicos não são o tipo de dado mais usado. Conjuntos de bits são os tipos mais utilizados em programas e valem uma análise mais detalhada.
O nibble
Um nibble é um conjunto de quatro bits. Não seria um tipo de dado muito interessante não fosse a existência de dois itens especiais: números BCD (binary coded decimal) e números hexadecimais. Um dígito BCD ou um dígito hexadecimal precisam exatamente de quatro bits para serem representado. Com um nibble podemos representar até 16 valores distintos. No caso dos números hexadecimais, cada um dos valores 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E e F é representado por quatro bits. Quaisquer 16 valores distintos podem ser representados por um nibble, mas os mais importantes e conhecidos são os dígitos BCD e hexadecimais.
O byte
Sem dúvida alguma, a estutura de dados mais importante usada pelo microprocessador 80x86 é o byte. Um byte é um conjunto de oito bits, o menor item de dado endereçável no 80x86. Isto significa que o menor item que pode ser acessado individualmente por um programa 80x86 é um valor de oito bits. Para acessar qualquer coisa menor, é preciso ler o byte que contenha os dados e usar uma máscara para filtrar os bits desejados. Os bits de um byte também são numerados da direita para a esquerda, de 0 a 7.
O bit 0 é o bit de ordem baixa (O.B.) ou menos significativo e o bit 7 é o bit de ordem alta (O.A.) ou mais significativo. Os outros bits são referenciados pelos seus números. Observe que um byte possui dois nibbles.
O nibble com os bits de 0 a 3 é o nibble de ordem baixa (O.B.) ou menos significativo e o nibble com os bits de 4 a 7 é o nibble de ordem alta (O.A.) ou mais significativo. Como o byte possui dois nibbles e cada nibble corresponde a um dígito hexadecimal, valores byte são expressos através de dois dígitos hexadecimais.
Como um byte possui 8 bits, ele pode representar 28 = 256 valores diferentes. Geralmente um byte é utilizado para representar valores numéricos positivos de 0 a 255, valores numéricos com sinal de -128 a 127, os códigos dos caracteres ASCII e outros tipos especiais de dados que não necessitam de mais do que 256 valores diferentes. Muitos tipos de dados possuem menos do que 256 itens, de modo que oito bits são suficientes para representá-los.
Como o 80x86 é uma máquina de bytes endereçáveis, é mais eficiente manipular um byte completo do que um bit individual ou um nibble. Por esta razão, a maioria dos programadores usam o byte completo para representar tipos de dados, mesmo quando possuem menos do que 256 itens. Por exemplo, é comum representar os valores booleanos falso e verdadeiro com 0000 0000 e 0000 0001.
O uso mais importante do byte é, provavelmente, o de representar um código de caractere. Todos os caracteres digitados no teclado, mostrados na tela ou impressos numa impressora, possuem um valor numérico. Para padronizar estes valores, criou-se o conjunto de caracteres ASCII. O conjunto ASCII básico possui 128 códigos. Os 128 restantes são utilizados como valores para caracteres adicionais como caracteres europeus, símbolos gráficos, letras gregas e símbolos matemáticos.
O word
O word (palavra) é um grupo de 16 bits, numerados da direita para a esquerda de 0 a 15.
O bit 0 é o menos significativo e o bit 15 o mais significativo. Os restantes são referenciados pelos seus números. Observe que o word é composto por dois bytes. O byte com os bits de 0 a 7 é o byte menos significativo ou de ordem baixa (O.B.) e o byte com os bits de 8 a 15 é o byte mais significativo ou de ordem alta (O.A.).
É claro que um word também pode ser dividido em quatro nibbles. O nibble menos significativo no word, de O.B., é o nibble 0 e o nibble mais significativo no word, de O.A., é o nibble 3.
Com 16 bits é possível obter 216 = 65.536 valores diferentes. Estes podem ser valores numéricos positivos de 0 a 65.535, numéricos com sinal de -32.768 a 32.767 ou qualquer outro tipo de dado que possua até 65.536 valores. Words são usados principalmente para três tipos de dados: valores inteiros, deslocamentos (offsets) e valores de segmento.
Words podem representar valores inteiros de 0 a 65.535 ou de -32.768 a 32.767. Valores numéricos sem sinal são representados pelo valor binário que corresponde aos bits no word. Valores numéricos com sinal usam a forma de complemento de dois (adiante entraremos em detalhe). Valores de segmento, que sempre têm comprimento de 16 bits, constituem o endereço de memória de parágrafos do código, de dados, do segmento extra ou do segmento da pilha.
O double word
O double word (palavra dupla) é o que o nome indica: um par de words. Portanto, um double word é um conjunto de 32 bits.
Naturalmente, um double word pode ser quebrado em 2 words, 4 bytes ou 8 nibbles.
Double words podem representar todo tipo de coisa. Em primeiro lugar estão os endereços segmentados. Outro item comumente representado por um double word são os valores inteiros de 32 bits, que podem ir de 0 a 4.294.967.295, ou números com sinal, que podem ir de -2.147.483.648 a 2.147.483.647. Valores de ponto flutuante de 32 bits também cabem num double word. Na maioria das vezes, os double words são usados para armazenarem endereços segmentados.
O sistema hexadecimal
O grande problema do sistema binário é sua verbosidade. Para representar o valor decimal 202, de apenas três casas decimais, precisamos de oito casas binárias. É óbvio que com um conjunto de dez dígitos possíveis, o sistema decimal pode representar números de uma forma muito mais compacta do que o sistema binário, que possui um conjunto de apenas dois dígitos. Valores grandes precisam de uma infinidade de casas binárias, tornando o número praticamente inutilizável para os mortais comuns. Além disso, as conversões entre decimal e binário são um tanto trabalhosas. Para mal dos pecados, o computador só "pensa" em binário. Resolveu-se então partir para uma solução radical, ou seja, criar um sistema cuja base fosse a mesma do tipo mais usado nos computadores. Como já vimos, os tipos mais utilizados são o byte e o word. Um byte possui oito bits - então foi criado um sistema octal. Um word possui 16 bits - então foi criado o sistema hexadecimal.
Os computadores evoluíram rapidamente de sistemas de 8 bits para sistemas baseados em 16 bits e o sistema hexadecimal ganhou força. Ele oferece exatamente o que precisamos: gera representações numéricas compactas e as conversões entre hexadecimal e binário são simples. Como a base de um número hexadecimal é 16, cada casa representa uma potência de 16. Vamos tomar como exemplo o número hexadecimal 1234:
dígitos hexadecimais 1 2 3 4 numeração 3 2 1 0 potência de 16 163 162 161 160 ou seja (1 x 163) + (2 x 162) + (3 x 161) + (4 x 160) = 4096 + 512 + 48 + 4 = 4660 decimal
Cada dígito hexadecimal pode representar um dos dezesseis valores entre 0 e 15. Como só existem dez dígitos decimais, foi preciso inventar seis dígitos adicionais. Optou-se pelas letras de A a F. Alguns exemplos de números hexadecimais seriam 1234, CADA, BEEF, 0FAB, FADA, FEFE, FAFA, etc. Como vamos nos referir com frequência a números em várias notações, é bom por ordem na casa desde já. Nos textos serão usadas as seguintes convenções:
- Todos os valores numéricos, independente da sua base, começam com um dígito decimal.
- Todos os valores hexadecimais terminam com a letra "h".
- Todos os valores binários terminam com a letra "b".
- Todos os valores decimais terminam com o sufixo "d".
São exemplos válidos: 1234h, 0CADAh, 0FADAh, 4660d, 101b. Dá para notar que os números hexadecimais são compactos e de fácil leitura. As conversões também são fáceis. Veja a seguinte tabela que fornece toda informação necessária para fazer a conversão de hexa para binário e vice versa:
Hexadecimal Binário 0 0000 1 0001 2 0010 3 0011 4 0100 5 0101 6 0110 7 0111 8 1000 9 1001 A 1010 B 1011 C 1100 D 1101 E 1110 F 1111
Para converter um número hexa num número binário, substitui-se simplesmente cada um dos dígitos hexa pelos quatro bits do binário correspondente. Por exemplo, para converter 0ABCDh num valor binário:
hexadecimal A B C D binário 1010 1011 1100 1101
Para converter um número binário em hexa, o processo é tão fácil quanto o anterior. A primeira providência é transformar o número de dígitos do valor binário num múltiplo de quatro. Depois é só substituir. Veja o exemplo abaixo com o binário 1011001010:
binário 1011001010 grupos de 4 dígitos 0010 1100 1010 hexadecimal 2 C A
Operações aritméticas com números Binários e Hexadecimais
À primeira vista, as operações de soma, subtração, multiplicação e divisão (além de outras) parecem muito fáceis de serem realizadas com números hexadecimais e binários. Mas cuidado! Nosso cérebro insiste em trabalhar com a base 10, "vício" adquirido na infância nos primeiros anos de escola. Veja alguns exemplos:
9h + 1h = ?
Se você respondeu 10h, seu cérebro passou-lhe uma rasteira. A resposta correta é 0Ah, que corresponde ao 10 decimal. Mais um exemplo:
10h - 1h = ?
Novamente, se você respondeu 9h, deu outra derrapada. O correto é 0Fh, uma vez que, no sistema deciaml, 16 - 1 = 15.
Com o sistema binário a coisa fica um pouco pior, pois a possibilidade de erro já começa ao se escrever sequências muito longas de 0s e 1s. Moral da história: ou se transforma os valores em decimal, efetua-se a operação e volta-se a transformar o resultado para o sistema original ou... usa-se uma calculadora que faça operações com números binários e hexa. A própria calculadora do Windows, quando no modo científico, é uma boa ferramenta.
Operações lógicas com bits
As principais operações lógicas são AND, OR, XOR e NOT. É imprescindível dominá-las perfeitamente. Costumo usar uns métodos menumônicos para trazê-las de volta à memória. Comecemos com a operação AND.
AND
A tradução de AND é E. Meu mnemônico é "se eu estiver cansada E tiver um lugar para deitar, então eu durmo". Somente se as duas condições forem verdadeiras, o resultado é verdadeiro. Se eu estiver cansada (primeira condição é verdadeira) mas não tiver uma rede ou uma cama para deitar (segunda condição é falsa), então não vou conseguir dormir (resultado falso). Se eu não estiver cansada (primeira condição falsa), mas tenho uma rede para deitar (segunda condição verdadeira), nem por isso vou dormir (resultado falso).
A operação lógica AND é uma operação diádica (aceita exatamente dois operandos) e seus operandos são dois bits. AND pode ser resumida na seguinte tabela, onde 0 representa falso e 1 representa verdadeiro:
AND 0 1 0 0 0 1 0 1
Outra maneira de guardar a operação lógica AND é compará-la com a multiplicação - multiplique os operandos que o resultado também estará correto. Em palavras, "na operação lógica AND, somente se os dois operandos forem 1 o resultado é 1; do contrário, o resultado é 0".
Um fato importante na operação lógica AND é que ela pode ser usada para forçar um resultado zero. Se um dos operandos for 0, o resultado será sempre zero, não importando o valor do outro operando. Na tabela acima, por exemplo, a linha do operando 0 só possui 0s e a coluna do operando 0, também só possui 0s. Por outro lado, se um dos operandos for 1, o resultado é o outro operando. Veremos mais a respeito logo adiante.
OR
A operação lógica OR (cuja tradução é OU) também é uma operação diádica. Meu mnemônico é "se alguém me xingar OU se fizer uma rosquinha, então fico brava". Daí fica fácil fazer a tabela da lógica OR:
OR 0 1 0 0 1 1 1 1
Em outras palavras, "se um dos operandos for verdadeiro, o resultado é verdadeiro; caso contrário, o resultado é falso". Se um dos operandos for 1, o resultado sempre será 1, não importando o valor do outro operando. Por outro lado, se um dos operandos for 0, o resultado será igual ao outro operando. Estes "efeitos colaterais" da operação lógica OR também são muito úteis e também serão melhor analisados logo adiante.
XOR
A tradução de XOR (exclusive OR) é OU exclusivo (ou excludente). Esta operação lógica, como as outras, também é diádica. Minha forma de lembrar é "ir ao supermercado XOR ir ao cinema, preciso me decidir". Como não posso estar nos dois lugares ao mesmo tempo (um exclui o outro), então a tabela da lógica XOR passa a ser a seguinte:
XOR 0 1 0 0 1 1 1 0
Se não for ao supermercado (0) e não for ao cinema (0), então não decidi o que fazer (0). Se for ao supermercado (1) e não for ao cinema (0), então me decidi (1). Se não for ao supermercado (0) e for ao cinema (1), então também me decidi (1). Se quiser ir ao supermercado (1) e ao cinema (1), não decidi nada (0) porque não posso ir aos dois lugares ao mesmo tempo. Em outras palavras, "se apenas um dos operandos for 1, então o resultado é 1; caso contrário, o resultado é 0".
Se os operandos forem iguais, o resultado é zero. Se os operando forem diferentes, o resultado é 1. Esta característica permite inverter os valores numa sequência de bits e é uma mão na roda.
NOT
Esta é a operação lógica mais fácil, a da negação. NOT significa NÃO e, ao contrário das outras operações, aceita apenas um operando (é monádica). Veja a tabela abaixo:
NOT 0 1 NOT 1 0
Operações lógicas com Números Binários e Strings de Bits
Como foi visto, as funções lógicas funcionam apenas com operandos de bit único. Uma vez que o 80x86 usa grupos de 8, 16, 32 ou 64 bits, é preciso ampliar a definição destas funções para poder lidar com mais de dois bits. As funções lógicas do 80x86 operam na base do bit a bit, ou seja, tratam os bits da posição 0, depois os bits da posição 1 e assim sucessivamente. É como se fosse uma cadeia de operações. Por exemplo, se quisermos realizar uma operação AND com os números binários 1011 0101 e 1110 1110, faríamos a operação coluna a coluna:
1011 0101 AND 1110 1110 ----------- 1010 0100
O resultado desta operação foi "ligar" os bits onde os dois são 1. Os bits restantes foram zerados. Se quisermos garantir que os bits de 4 a 7 do primeiro operando sejam zerados e que os bits 0 a 3 fiquem inalterados, basta fazer um AND com 0000 1111. Observe:
1011 0101 AND 0000 1111 ----------- 0000 0101
Se quisermos inverter o quinto bit, basta fazer um XOR com 0010 0000. O bit (ou os bits) que quisermos inverter, mandamos ligado. Os bits zerados não alteram os bits do primeiro operando. Assim, se quisermos inverter os bits 0 a 3, basta fazer um XOR com 0000 1111.
1011 0101 XOR 0000 1111 ----------- 1011 1010
E o que acontece quando usamos um OR com 0000 1111? Os bits 0 não alteram os bits do primeiro operando e os bits 1 forçam os bits para 1. É um método excelente para ligar bits na (ou nas) posições desejadas.
1011 0101 OR 0000 1111 ----------- 1011 1111
Este método é conhecido como máscara. Através de uma máscara de AND é possível zerar bits. Com uma máscara XOR é possível inverter bits e, através de uma máscara OR é possível ligar bits. Basta conhecer as funções e saber lidar com os bits. Quando temos números hexadecimais, o melhor é transformá-los em binário e depois aplicar as funções lógicas... é lógico
Números com sinal
Até agora tratamos os números binários como valores sem sinal. Mas como se faz para representar números negativos no sistema binário? É aí que entra o sistema de numeração do complemento de dois. Vamos lá.
Números com e sem sinal
Os números, no computador, não podem ser infinitos pelo simples fato de que a quantidade de bits disponível para expressá-los é restrita (8, 16, 32, ou qualquer quantidade que nunca será grande demais ). Com um número fixo de bits, o valor máximo também é fixo. Por exemplo, com 8 bits podemos obter no máximo o valor 256. Se quisermos expressar números negativos, teremos que dividir estas 256 possibilidades, metade para os positivos e metade para os negativos. Isto diminui o valor máximo, porém aumenta o valor mínimo. Se a divisão for bem feita, podemos obter -128 a 0 e 0 a 127. O mesmo raciocínio pode ser usado para 16 bits, 32bits, etc. Como regra geral, com n bits podemos representar valores com sinal entre
Muito bem, já sabemos que podemos dividir o espaço dos valores numéricos oferecido pelos bits, mas ainda não sabemos como representar os valores negativos usando os bits. O microprocessador 80x86 usa a notação de complemento de dois. Neste sistema, o bit mais significativo é que sinaliza se o número é positivo ou negativo: se for 0, o número é positivo; se for 1, o número é negativo. Veja os exemplos:
8000h é negativo => 1000 0000 0000 0000 100h é positivo => 0000 0001 0000 0000 7FFFh é positivo => 0111 1111 1111 1111 FFFFh é negativo => 1111 1111 1111 1111
Se o bit O.A. for zero, então o número é positivo e é armazenado como um valor binário padrão. Se o bit O.A. for um, então o número é negativo e é armazenado na forma de complemento de dois. Para converter um número positivo para negativo use o seguinte algoritmo:
- 1. Inverta todos os bits do número com uma operação lógica NOT.
- 2. Adicione 1 ao resultado.
No exemplo a seguir faremos a conversão de +5 para -5 usando o complemento de dois com apenas 8 bits:
decimal 5 0000 0101 (05h) inverter bits 1111 1010 (0FAh) somar 1 1111 1011 (0FBh)
Se repetirmos a operação com o valor encontrado para -5, voltamos a obter o valor original:
decimal -5 1111 1011 (0FBh) inverter bits 0000 0100 (04h) somar 1 0000 0101 (05h)
Agora oberve o que acontece com o hexadecimal 8000h, o menor número negativo com sinal (-32.768):
hexa 8000h binário 1000 0000 0000 0000 (8000h) inverter bits 0111 1111 1111 1111 (7FFFh) somar 1 1000 0000 0000 0000 (8000h) ???
Invertendo 8000h obtemos 7FFFh e, somando 1, voltamos para 8000h. Tem alguma coisa errada pois -(-32768) não pode ser igual a -32768! O que ocorre é que, com 16 bits, não é possível obter o inteiro positivo +32768. Se tentarmos realizar o complemento de dois com o menor número negativo, o processador 80x86 vai dar erro de overflow na aritmética com sinal.
Talvez você esteja pensando que usar o bit mais significativo como flag de sinal e manter o número original fosse uma solução mais lógica. Por exemplo, 0101 seria +5 e 1101 seria -5. Acontece que esta operação depende do hardware. Para o processador, a negação (ou complemento, ou inversão) dos bits é fácil e rápida de ser realizada. Para o programdor, não é preciso realizá-la bit a bit pois o 80x86 possui a instrução NEG que trata todos os bits.
As operações com números negativos não é problema. Imagine a operação de soma com os números +5 e -5, sendo que o -5 foi obtido com o sistema de complemento de dois:
1 1111 1111 5d 0000 0101 -5d + 1111 1011 ------------ 1 0000 0000
Os dígitos em vermelho são os famosos "vai um", que acontecem quando somamos dois bits de valor 1. O bit em azul é o bit que excedeu o comprimento de oito bits, chamado de carry (excedente). Se ignorarmos o carry, o resultado está absolutamente correto, pois 5 + (-5) = 0. É exatamente assim que o processador opera.
Não custa repetir que os dados representados por um conjunto de bits dependem inteiramente do contexto. Os oito bits do valor binário 11000000b podem representar um caracter ASCII, o valor decimal sem sinal 192, o valor decimal com sinal -64, etc. Como programador, é sua a responsabilidade de usar os dados de forma consistente.
Extensão com Sinal e Extensão com Zeros
Como os inteiros no formato de complemento de dois têm um comprimento fixo, surge um pequeno problema. O que acontece quando for preciso transformar um valor de complemento de dois de 8 bits num valor de 16 bits? Este problema, e seu oposto (a transformação de um valor de 16 bits num de 8 bits), pode ser resolvido através das operações de extensão e contração com sinal. O 80x86 trabalha com valores de comprimento fixo, mesmo quando estiver processando números binários sem sinal. A extensão com zeros permite converter pequenos valores sem sinal em valores maiores sem sinal.
Vamos a um exemplo considerando o valor -64. O valor de complemento de dois para este número é 0C0h. O equivalente de 16 bits deste número é 0FFC0h. Agora considere o valor +64. As versões de 8 e de 16 bits deste valor são 40h e 0040h. A diferença entre os números de 8 e de 16 bits com sinal pode ser definida com a seguinte regra: "Se o número for negativo, o byte mais significativo do número de 16 bits contém 0FFh; se o número for positivo, o byte mais significativo do número de 16 bits é zero".
Para fazer a extensão com sinal de um valor com qualquer número de bits para um número maior de bits, basta copiar o bit de sinal para todos os bits adicionais. Por exemplo, para ampliar um número de 8 bits com sinal para um número de 16 bits com sinal, só é preciso copiar o bit 7 do número de oito bits para os bits de 8 a 15 do número de 16 bits. Para ampliar um número de 16 bits para um número de double word (32 bits), simplesmente copie o bit 15 para os bits de 16 a 31 do double word.
A extensão com sinal é necessária quando manipulamos valores com sinal de comprimentos diferentes. É comum precisarmos somar uma quantidade em byte com uma quantidade em word. Neste caso, antes de efetuar a operação, será preciso transformar a quantidade byte numa quantidade word. Outras operações, em particular a multiplicação e a divisão, podem necessitar uma extensão com sinal para 32 bits. É óbvio que não é necessário fazer a extensão com sinal para valores sem sinal. São exemplos de extensão com sinal:
8 bits 80h 28h --- --- 16 bits FF80h 0028h 1020h 8088h 32 bits FFFFFF80h 00000028h 00001020h FFFF8088h
Para ampliar números sem sinal faz-se a extensão com zeros, um processo muito simples de zerar os bytes adicionais. Veja abaixo:
8 bits 80h 28h --- --- 16 bits 0080h 0028h 1020h 8088h 32 bits 00000080h 00000028h 00001020h 00008088h
A contração com sinal, ou seja, converter um valor com determinado número de bits para um valor idêntico com um número menor de bits é um processo um pouco mais complicado. A extensão com sinal sempre é possível, já a contração com sinal nem sempre o é. Por exemplo, o valor decimal -448, representado como hexadecimal de 16 bits, é 0FE40h. Neste caso, é impossível obter este valor com apenas 8 bits, ou seja, a contração com sinal não é possível pois o valor original seria perdido. Aliás, este é um exemplo de overflow que pode ocorrer numa conversão impossível.
Para avaliar se é possível realizar uma contração com sinal é preciso analisar o(s) byte(s) mais significativos que deverão ser descartados: todos precisam conter zero ou 0FFh. Se forem encontrados quaisquer outros valores, não será possível fazer uma contração sem overflow. Além disso, o bit mais significativo do valor resultante precisa coincidir com cada bit removido do número. Veja os exemplos:
16 bits 8 bits Observação FF80h 80h OK 0040h 40h OK FE40h --- Overflow 0100h --- Overflow
Shift e Rotate
Outro conjunto de operações lógicas que podem ser aplicadas em strings de bits são o deslocamento (shift) e a rotação (rotate). As duas categorias ainda podem ser subdivididas em deslocamento para a esquerda (left shift), deslocamento para a direita (right shift), rotação para a esquerda (left rotate) e rotação para a direita (right rotate). Estas operações se revelaram extremamente úteis para os programadores da linguagem Assembly.
A operação de deslocamento para a esquerda move uma posição para a esquerda cada um dos bits de uma string de bits (veja ao lado). O bit zero é deslocado para a posição 1, o da posição 1 para a posição 2, etc. Surgem naturalmente duas perguntas: "O que vai para o bit zero?" e "Para onde vai o bit 7?" Bem, isto depende do contexto. Nós colocaremos um bit zero na posição zero e o bit sete "cai fora" nesta operação.
Observe que deslocar o valor para a esquerda é o mesmo que multiplicá-lo pela sua base (ou radix). Por exemplo, deslocar um número decimal para a esquerda em uma posição (adicionando um zero à direita do número) o multiplica por 10 (a sua base):
1234 SHL 1 = 12340 (SHL 1 = shift esquerda 1 posição)
Como a base de um número binário é dois, o deslocamento em uma posição para a esquerda multiplica-o por 2. Se deslocarmos um valor binário duas vezes para a esquerda, ele é multiplicado duas vezes por 2, ou seja, é multiplicado por 4. Se o deslocarmos três vezes, será multiplicado por 8 (2*2*2). Como regra, se deslocarmos um valor binário para a esquerda n vezes, isto o multiplicará por 2n (ou 2 elevado a n).
Uma operação de shift para a direita funciona do mesmo modo que a anterior, exceto que os dados são deslocados no sentido oposto. O bit sete é movido para a posição seis, o bit seis para a cinco e assim sucessivamente. Numa operação de deslocamento para a direita, introduzimos um zero no bit sete e o bit zero será descartado.
Como o deslocamento para a esquerda equivale a uma multiplicação pela base, não é de se admirar que um deslocamento para a direita equivale a uma divisão pela base. No sistema binário, se fizermos n deslocamentos para a direita, o valor será dividido por 2n.
Existe um problema relacionado à divisão efetuada por um shift para a direita: um shift para a direita equivale a uma divisão de um número sem sinal por 2. Por exemplo, se deslocarmos uma posição para a direita a representação sem sinal de 254 (0FEh), obtemos 127 (07Fh), exatamente o esperado. Entretanto, se deslocarmos uma posição para a direita a representação binária de -2 (0FEh), obtemos 127 (07Fh), o que não está correto. Este problema ocorre porque estamos introduzindo um zero no bit sete. Se o bit sete contém 1 antes do deslocamento, indicativo de número negativo nos inteiros com sinal, e depois recebe zero, estamos alterando o sinal deste número (que passa de negativo para positivo). Como este não é o propósito da divisão... dá erro.
Para usar um shift para a direita como um operador de divisão, antes é preciso definir uma terceira operação de deslocamento: o deslocamento aritmético para a direita (arithmetic shift right). Um shift aritmético para a direita funciona como o shift para a direita normal, com uma diferença: ao invés de deslocar o bit sete para a posição seis, este bit é deixado intacto, ou seja, o bit sete não é zerado.
Isto geralmente produz o resultado esperado. Por exemplo, fazendo um shift aritmético para a direita com -2 (0FEh), obtemos -1 (0FFh). Uma coisa, no entanto, não pode ser esquecida: esta operação sempre arredonda os números para o inteiro que seja menor ou igual ao resultado, ou seja, arredonda para baixo. Um shift artimético para a direita com 5 dá como resultado 2. Mas preste atenção. Um shift aritmético para a direita com -1 dá como resultado -1, e não zero! O arredondamento se faz na direção do menor valor e -1 é menor do que 0. Este não é um "bug" no shift aritmético para a direita, é apenas como a divisão de inteiros foi definida.
Outra operação muito útil é a rotação para a esquerda e para a direita. Estas operações se comportam como os deslocamentos, com uma diferença importante: o bit que sai numa extremidade entra na extremidade oposta.
Campos Bit e Dados Compactados
Apesar do 80x86 operar eficientemente com tipos de dado byte, word e double word, ocasionalmente teremos que trabalhar com tipos de dado que usam um número de bits diferente do habitual (8, 16, 32 ou 64). Por exemplo, imagine uma data na forma "2/4/98". São três valores numéricos que representam uma data: valores para o dia, para o mês e para o ano. Os dias variam de 1 a 31, o que consome 5 bits (valor máximo de 32) para representar a entrada dos dias. Os meses variam de 1 a 12. Portanto, 4 bits são suficientes (valor máximo de 16). O valor para o ano, imaginando que trabalhemos com valores variando de 0 a 99, precisam de 7 bits (que podem representar até o valor 128). Somando os bits necessários, chega-se a 16 bits, o mesmo que 2 bytes. Em outras palavras, podemos acomodar nossos dados de data em dois bytes ao invés de três se usássemos um byte para cada um dos campos dia, mês e ano. Isto economiza um byte de memória para cada data armazenada, o que pode representar uma economia substancial se tivermos que armazenar zilhões de datas. O arranjo dos bits pode ser visto na figura acima.
DDDDD representam os 5 bits reservados para o valor do dia, MMMM representam os quatro bits para armazenar o mês e YYYYYYY são os sete bits reservados para o valor do ano. Cada coleção de bits representando um item de data é denominado campo bit. 2 de Abril de 1998 seria representado, respectivamente, pelos bits 00010 0100 1100010, ou seja, 1262h.
DDDDD MMMM AAAAAAA 00010 0100 1100010 ou 1262h 2 4 98
Apesar dos valores compactados serem muito eficientes para economizar espaço (isto é, eficientes em termos de uso de memória), computacionalmente são muito ineficientes. A razão é que dependem de instruções extras para descompactar os dados dos diversos campos bit. Estas operações adicionais consomem tempo e bytes adicionais para armazenar as instruções. Portanto, o planejamento prévio e o custo/benefício precisam ser bem analisados.
Existem inúmeros exemplos de tipos de dados compactados. Pode-se compactar oito valores booleanos num único byte, pode-se acomodar dois dígitos BCD num byte, etc.
O conjunto de caracteres ASCII
O conjunto dos caracteres ASCII (excluindo-se os caracteres expandidos definidos pela IBM) é dividido em quatro grupos de 32 caracteres. O primeiro grupo, códigos ASCII de 0 a 1Fh (31), formam um conjunto especial de caracteres não imprimíveis chamados de caracteres de controle. Recebem este nome porque realizam várias operações de controle de impressão/display ao invés de mostrarem símbolos. Exemplos incluem o retorno de carro (carriage return), que posiciona o cursor no lado esquerdo da linha atual, avanço de linha (line feed), que move o cursor uma linha para baixo no dispositivo de saída e o retorno (back space), que move o cursor uma posição para a esquerda. Infelizmente os caracteres de controle realizam operações diferentes dependendo do dispositivo de saída. A padronização entre os dispositivos de saída é precária. Para saber exatamente como se comporta determinado caracter de controle em relação a um determinado dispositivo de saída é preciso consultar o manual do dispositivo.
O segundo grupo de 32 códigos de caracteres ASCII inclui vários símbolos de pontuação, caracteres especiais e algarismos. Os caracteres mais conhecidos deste grupo são o espaço (código ASCII 20h) e os algarismos (códigos ASCII de 30h a 39h). Lembre-se de que os algarismos diferem dos seus valores numéricos apenas no nibble mais significativo. Se subtrairmos 30h do código ASCII de qualquer um dos dígitos, obtemos o equivalente numérico deste dígito.
O terceiro grupo de caracteres ASCII é reservado para os caracteres alfabéticos maiúsculos. Os códigos ASCII para os caracteres de A a Z ficam no intervalo 41h a 5Ah (65 a 90). Como só existem 26 caracteres alfabéticos diferentes, os seis códigos restantes são de vários símbolos especiais.
Finalmente, o quarto grupo de 32 códigos de caracteres ASCII são reservdos para os símbolos alfabéticos minúsculos, cinco símbolos adicionais especiais e outro caracter de controle (delete). Observe que os símbolos dos caracteres minúsculos usam os códigos ASCII de 61h a 7Ah. Se convertermos os códigos dos caracteres maiúsculos e minúsculos para binário, é possível verificar que os símbolos maiúsculos diferem dos seus correspondentes minúsculos em apenas um bit (veja ao lado os códigos para os caracteres "E" e "e").
A única diferença entre estes dois códigos reside no bit 5. Caracteres maiúsculos sempre possuem 0 no bit cinco, os minúsculos sempre possuem 1 no bit cinco. Podemos usar esta característica para converter rapidamente maiúsculas em minúsculas e vice versa. Se o caracter for maiúsculo, para forçá-lo para minúsculo basta setar o bit cinco para 1. Se o caracter for minúsculo, para forçá-lo para maiúsculo basta setar o bit cinco para 0.
Na realidade, os bits cinco e seis determinam o grupo ao qual o caracter ASCII pertence:
Bit 6 Bit 5 Grupo 0 0 Caracteres de controle 0 1 Dígitos e caracteres de pontuação 1 0 Maiúsculos e especiais 1 1 Minúsculos e especiais
Podemos, por exemplo, transformar qualquer caracter maiúsculo ou minúsculo (ou especial) no seu caracter de controle equivalente apenas zerando os bits cinco e seis.
Agora observe os códigos ASCII para os caracteres dos dígitos numéricos. A representação decimal destes códigos ASCII não esclarece grande coisa, mas a representação hexadecimal revela algo muito importante - o nibble menos significativo do código ASCII é o equivalente binário do número representado. Zerando o nibble mais significativo, converte-se o código do caracter para a sua representação binária. Inversamente, é possível converter um valor binário do intervalo de 0 a 9 para a sua representação ASCII simplesmente setando o nibble mais significativo em três. Note que é possível usar a operação lógica AND para forçar os bits mais significativos para zero e usar a operação lógica OR para forçar os bits mais significativos para 0011 (três).
Caractere Decimal Hexa 0 48 30h 1 49 31h 2 50 32h 3 51 33h 4 52 34h 5 53 35h 6 54 36h 7 55 37h 8 56 38h 9 57 39h
Lembre-se de que não é possível transformar uma string de caracteres numéricos na sua representação binária correspondente simplesmente zerando o nibble mais significativo de cada dígito da string. Transformando 123 (31h 32h 33h) desta maneira resulta em três bytes: 010203h, e não no valor correto que é 7Bh. Transformar uma string de dígitos requer um pouco mais de sofisticação. A transformação explicada acima só serve para dígitos únicos.
O bit sete no ASCII padrão é sempre zero. Isto significa que o conjunto de caracteres ASCII utiliza apenas a metade dos códigos possíveis num byte de oito bits. A IBM usa os 128 códigos restantes para vários caracteres especiais (caracteres com acento, etc), símbolos matemáticos e caracteres de desenho de linhas. Deve ficar claro que estes caracteres extras são uma extensão não padronizada do conjunto de caracteres ASCII. É claro que o nome IBM tem peso e a maioria dos computadores baseados no 80x86 e as impressoras acabaram incorporando os caracteres adicionais da IBM.
Apesar do fato de que sejam um padrão, codificar dados usando simplesmente caracteres padrão ASCII não garante a compatibilidade entre os sistemas, se bem que, hoje em dia, dificilmente encontraremos problemas. Como usaremos com frequência os caracteres ASCII em Assembly, seria interessante guardar de cabeça alguns códigos ASCII importantes, como o do "A", do "a" e do "0".
Palpites da vó
Existem três aplicativos escritos em Object Pascal (Delphi) pelo Randall Hyde que traduzi para o Português e recompilei. Um deles é uma calculadora de operações lógicas (procure por LOGICAP na seção de downloads/tutoriais/AoA), o outro faz extensões com sinal e com zeros (SIGNEXTP na seção de downloads/tutoriais/AoA). O terceiro é para campos bit. Adaptei-o para o exemplo acima e recompilei. É um compactador de dados para datas no formato dia/mês/ano. Você pode fazer o download do executável com código fonte em downloads/tutoriais/AoA.
Treine exaustivamente a notação decimal, binária e hexadecimal. Faça conversões de binário para hexadecimal e de hexadecimal para binário. Além disso, complete a tabela construída com valores decimais e binários com os respectivos valores hexadecimais. Faça complemento de dois de vários valores e teste os menores números negativos de 8, 16 e 32 bits. Faça extensões e contrações de valores com e sem sinal. E quer saber mais? Faça tudo usando lápis e papel - calculadora só para conferir os resultados. Esta história de bits precisa ser incorporada!
Fonte
- Art of Assembly de Randall Hyde.
- Tradução meio que livre da vovó Vicki.