O grande problema do sistema binário é sua verbosidade. Para representar o valor decimal 202, de apenas três casas, 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: criar um sistema cuja base fosse a mesma do tipo mais usado nos computadores. Como já vimos no módulo anterior, 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 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 (lembre-se de que a potência é indicada por ^):
dígitos hexadecimais 1 2 3 4 numeração 3 2 1 0 potência de 16 16^3 16^2 16^1 16^0 ou seja (1 x 16^3) + (2 x 16^2) + (3 x 16^1) + (4 x 16^0) = 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:
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. Além disso, as conversões são fáceis. Veja a seguinte tabela que fornece toda a 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 dígito 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
À 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! Seu 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.
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.
A tradução de AND é E. Meu menumô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 é 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 é sempre zero, não importando o valor do outro operando. Na tabela acima, por exemplo, a linha que 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.
A operação lógica OR (cuja tradução é OU) também é uma operação diádica. Meu mneumô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; do 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.
A tradução de XOR (exclusive OR) é OU exclusivo (ou excludente). Esta operação lógica, como as outras, também é diádica. A 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 for ao supermercado (1) e for ao cinema (1), não decidi nada (0) porque não posso ir aos dois lugares ao mesmo tempo. Em outras palavras, "se um dos operandos for 1, então o resultado é 1; caso contrário, o resultado é 0".
Se os operandos forem iguais, o resultado é 1. Se os operando forem diferentes, o resultado é zero. Esta característica permite inverter os valores numa sequência de bits e é uma mão na roda.
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 |
Como foi visto acima, as funções lógicas funcionam apenas com operandos de bit único. Uma vez que o 80x86 usa grupos de oito, dezesseis ou trinta e dois 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
Como resultado desta operação, "ligamos" os bits onde ambos 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 ;))))
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á.
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 é muito grande). 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:
No seguinte exemplo faremos a conversão de +5 para -5 usando o complemto de dois usando apenas 8 bits:
decimal 5 binário 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 binário 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 de 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 verde é o bit que excedeu o comprimento de oito bits, chamado de carry (o excedente). Se ignorarmos o carry, o resultado está absolutamente correto, pois 5 + (-5) = 0. E é exatamente assim que o processador opera.
Não custa repetir que, os dados representado 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 mantendo sua consistência.
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 basta fazer 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 perderíamos o valor original. 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 |
Antes de começar os exercícios propostos, leia o texto adicional Asm - Lógica Booleana, que é uma outra abordagem das operações lógicas com bits. Existem dois 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 (download do executável com código fonte) e o outro faz extensões com sinal e com zeros (download do executável com código fonte).
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!