O copo tá meio cheio ou meio vazio?

Well… O objetivo aqui é explicar um cadinho sobre o flag overflow que você encontra em todas as implementações de microprocessadores e microcontroladores. Mas, para isso vou ter que explicar rapidamente o significado do flag de carry… Tá bom, é be-a-bá, mas tenha paciência, ok?

Em decimal, quando você soma dois algarismos e o resultado não cabe num único algarismo (por exemplo, 8+3), o que você faz? A tia Maria, lá do pré-primário, te ensinou que você faz a conta, coloca o algarismo mais à direita no lugar dele e acontece um “vai um” para o lado esquerdo… 8+3 é 1 e “vai um”, lembra disso? Esse “vai um” é o carry.

No contexto da aritmética inteira isso acontece quando você soma dois números inteiros sem sinal. Como esses números têm tamanho fixo (um int tem 32 bits de tamanho das arquiteturas i386 e x86-64, por exemplo), então o “vai um” que escapa além do MSB (Most Significant Bit) é colocado num lugar especial: No flag de carry (ou CF). Isso é útil porque indica que o resultado está além da capacidade de representação de um valor inteiro sem sinal. Nesse sentido, um carry também é um overflow.

Overflow, em inglês, significa “transbordar” e carry, “transporte”. A idéia é que o bit adicional da operação é “transportado” para o CF (Carry Flag) e o verbo transbordar nos diz que alguma coisa passou da medida. Mas, se o CF é um overflow, para que serve esse flag OF (Overflow Flag)?

Duas confusões que o newbie faz a respeiro de processadores:

A primeira é acreditar que processadores sejam capazes de fazer qualquer outra operação artimética elementar além da adição… Claro, existem instruções que realizam subtrações, multiplicações e divisões… Mas, todas essas operações usam adições!

Não acredita? Bem… Para subtrair dois valores basta que você inverta o sinal do segundo termo e faça uma adição: 3 - 2 = 3 + (-2). Multiplicações são adições sucessivas: 3 \cdot 4 = 3 + 3 + 3 + 3, ou seja, “o que dá se eu somar 3 com ele mesmo 4 vazes?”. E divisões nos dão a contagem de subtrações sucessivas entre o dividendo e o divisor (“Quantas vezes eu consigo subtrair o divisor do dividendo?”)… Acontece que sabemos que subtrações são adições disfarçadas e, portanto, as divisões também usam adições… Com isso em mente, os construtores da lógica interna dos processadores pode usar apenas um circuito para fazer adições e terá todas as outras três operações fundamentais da aritmética!

A segunda confusão é acreditar que os processadores sejam capazes de lidar com valores negativos! Não são! Um inteiro é um conjunto de bits que não têm sinal! Mas, existe um macete que nos permite interpretar um valor inteiro como se ele fosse negativo… Chama-se complemento 2.

A coisa funciona assim: Se um valor tem o seu bit MSB setado, podemos “fazer de conta” que o valor é negativo e descobrirmos qual número negativo está sendo representado invertendo todos os bits e adicionarmos 1… Para exemplificar, com o tipo char, em C, temos um inteiro sinalizado de 8 bits. Assim, o valor binário 11111111(2) (255, em decimal) é uma representação de um valor negativo, já que o bit 7 é 1. Invertendo todos os seus bits obtemos 00000000(2) e adicionando 1 obtemos 00000001(2). Ou seja: 11111111(2) representa o valor -1.

A diferença entre overflow e carry:

Toda essa explicação ai em cima é para te dizer que CF indica um “overflow” de valores sem sinal e OF indica overlow de valores com sinal.

Obvserve o que acontece no código abaixo:

    mov  ah,127
    add ah,3
    ; Qual é o estado de CF e OF aqui?

Óbviamente o resultado da adição será 130. O CF estará zerado (o resultado não é maior que 255), mas OF estará setado, já que num valor de 8 bits sinalizando não podemos representar valores positivos maiores que 127… Ou seja, o valor 130 não pode ser expresso num char! Ainda, se formos encarar esse resultado de 8 bits como tendo sinal ele representará o valor -126 (130 é 100000102 em binário, com o bit 7 setado e, portanto, representa um valor negativo). Isso é fácil de comprovar:

$ cat test.c
#include <stdio.h>

void main(void)
{
  char a = 127;

  a += 3;

  printf("%hd\n", a);
}
$ gcc -o test test.c
$ ./test
-126

Infelizmente não adianta muito tentar compilar esse pequeno exemplo com as opções “-g -O0” e usar o GDB para ajustar um breakpoint na linha seguinte ao “a += 3” para verificar a alegação feita acima. Já que estamos compilando para um ambiente de 32 bits (ou 64), o compilador tende a realizar operações aritméticas em 32 bits de qualquer maneira, e isso não vai alterar os flags do jeito que esperamos. O fragmento da sessão de debugging, abaixo, mostra isso:

(gdb) disas
Dump of assembler code for function main:
   0x0000000000400506 <+0>:     push   rbp
   0x0000000000400507 <+1>:     mov    rbp,rsp
   0x000000000040050a <+4>:     sub    rsp,0x10
   0x000000000040050e <+8>:     mov    BYTE PTR [rbp-0x1],0x7f
   0x0000000000400512 <+12>:    movzx  eax,BYTE PTR [rbp-0x1]
   0x0000000000400516 <+16>:    add    eax,0x3
   0x0000000000400519 <+19>:    mov    BYTE PTR [rbp-0x1],al
=> 0x000000000040051c <+22>:    movsx  eax,BYTE PTR [rbp-0x1]
   0x0000000000400520 <+26>:    mov    esi,eax
   0x0000000000400522 <+28>:    mov    edi,0x4005c4
   0x0000000000400527 <+33>:    mov    eax,0x0
   0x000000000040052c <+38>:    call   0x4003e0 <printf@plt>
   0x0000000000400531 <+43>:    leave  
   0x0000000000400532 <+44>:    ret    
End of assembler dump.
(gdb) info reg eflags
eflags         0x216  [ PF AF IF ]

Note que a adição é feita com eax (e ‘a’, que esá em [rbp-1], é copiado para eax via instrução movzx). Mas, se vocẽ alterar o código em C para usar o tipo int e o valor inicial 2147483647 (ou 0x7FFFFFFF), que é o maior valor inteiro sinalizado e positivo de 32 bits, então teremos:

$ gcc -g -O0 -o test test.c
$ gdb test
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
Reading symbols from test...done.
(gdb) l
1 #include <stdio.h>
2 
3 void main(void)
4 {
5   int a = 2147483647; 
6 
7   a += 3;
8 
9   printf("%d\n", a);
10  }
(gdb) b 9
Breakpoint 1 at 0x400519: file test.c, line 9.
(gdb) r
Starting program: ~/test 
Breakpoint 1, main () at test.c:9
9   printf("%d\n", a);
(gdb) disas
Dump of assembler code for function main:
   0x0000000000400506 <+0>:   push   rbp
   0x0000000000400507 <+1>:   mov    rbp,rsp
   0x000000000040050a <+4>:   sub    rsp,0x10
   0x000000000040050e <+8>:   mov    DWORD PTR [rbp-0x4],0x7fffffff
   0x0000000000400515 <+15>:  add    DWORD PTR [rbp-0x4],0x3
=> 0x0000000000400519 <+19>:  mov    eax,DWORD PTR [rbp-0x4]
   0x000000000040051c <+22>:  mov    esi,eax
   0x000000000040051e <+24>:  mov    edi,0x4005b4
   0x0000000000400523 <+29>:  mov    eax,0x0
   0x0000000000400528 <+34>:  call   0x4003e0 <printf@plt>
   0x000000000040052d <+39>:  leave  
   0x000000000040052e <+40>:  ret    
End of assembler dump.
(gdb) info reg eflags
eflags         0xa92  [ AF SF IF OF ]
(gdb) x/x $rbp-4
0x7fffffffe0bc:	0x80000002

Olha ai o OF e o SF indicando que houve, no resultado da adição, um overflow e um sinal negativo, mas não houve carry! E, por último, obtemos o conteúdo da memória apontada por [rbp-4] e vemos que ‘a’ é 0x80000002, como esperado.

Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s