Jump to content
Sign in to follow this  
fredericopissarra

Aritmética inteira e de ponteiros

Recommended Posts

Posted (edited)

Aqui quero só apontar alguns conceitos que o estudante pode ter entendido mal. Um deles é a diferença entre tipos inteiros sinalizados e sem sinal. AMBOS são a mesma coisa, embora não pareçam... Existe, é claro uma diferença: A possibilidade de trabalhar com valores negativos. Mas, internamente, esses dois "tipos" comportam-se do mesmo jeito.

Por exemplo: Ao subtrair 1 de 0 obteremos, se estivermos trabalhando com tipos sinalizados, -1. Mas, para um tipo sem sinal, obteremos todos os bits do valor setados. Isso é fácil de ver:

#include <stdio.h>

void main ( void )
{
  int x = 0;
  unsigned int y = 0;

  x--;
  y--;
  printf ( "x=%1$d (%1$#x), y=%2$u (%2$#x)\n", x, y );
}

Isso imprimirá "x=-1 (0xffffffff), y=4294967295 (0xffffffff)", o que significa que int e unsigned int funcionam do mesmo jeito. De fato, ao subtrair 1 de 0, em binário, obteremos todos os bits setados e o flag CF também setado:

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

__attribute__((noinline))
int f(int x)
{
  __asm__ __volatile__(
    "subl $1,%0"
    : "=r" (x) : "0" (x)
  );

  return x;
}

void main ( void )
{
  printf ( "x=%1$d (%1$#x)\n", f(0) );
}

$ cc -g -O2 -o test test.c
$ gdb -q test
Reading symbols from test...done.
(gdb) b f
Breakpoint 1 at 0x6a0: file test.c, line 6.
(gdb) disas f
Dump of assembler code for function f:
   0x00000000000006a0 <+0>:	mov    %edi,%eax
   0x00000000000006a2 <+2>:	sub    $0x1,%eax
   0x00000000000006a5 <+5>:	retq   
End of assembler dump.
(gdb) r
Starting program: /mnt/vol2/Work/tmp/test 

Breakpoint 1, f (x=0) at test.c:6
6	  __asm__ __volatile__(
(gdb) n
12	}
(gdb) info reg
rax            0xffffffff	4294967295
rbx            0x0	0
rcx            0x5555555546b0	93824992233136
rdx            0x7fffffffdd08	140737488346376
rsi            0x7fffffffdcf8	140737488346360
rdi            0x0	0
rbp            0x5555555546b0	0x5555555546b0 <__libc_csu_init>
rsp            0x7fffffffdc08	0x7fffffffdc08
r8             0x7ffff7dd0d80	140737351847296
r9             0x7ffff7dd0d80	140737351847296
r10            0x2	2
r11            0x7	7
r12            0x555555554590	93824992232848
r13            0x7fffffffdcf0	140737488346352
r14            0x0	0
r15            0x0	0
rip            0x5555555546a5	0x5555555546a5 <f+5>
eflags         0x297	[ CF PF AF SF IF ]
cs             0x33	51
ss             0x2b	43
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) 

Troque 'int' por 'unsigned int' e você obterá a mesma coisa. O que o compilador faz é interpretar os tipos 'int' como representandos valores em complemento 2. Assim, o valor absoluto contido na variável tem apenas N-1 bits e depende do MSB (Bit de alta ordem)... Mas, repare, o valor contido no tipo continua sendo um grupo de N bits sem qualquer indicação de "sinal". O sinal aqui é uma representação, cuja semântica é obtida no programa e pelos flags.

Os flags, é claro, são usados para comparações. Com valores sinalizados usamos os flags SF e OF, bem como ZF. Com valores sem sinal, usamos os flags CF e ZF... No caso do x86, o flag SF é setado se o resultado da operação resultar num MSB=1 (como 0-1 resulta no bit MSB setado!), e o flag OF é uma indicação de overflow, ou seja, nos extremos das faixas sinalizadas... se passarmos de 0x7fffffff para 0x80000000, então OF=1 e o contrário também... Mas, note que isso não acontece se passarmos de 0 para 0xffffffff e vice versa (como pode ser visto acima). Isso seria um "underflow".

Toda comparação é feita por subtração. Se comparamos x com y, se x == y,  o ZF será 1, significando que a diferença entre eles é 0. Isso vale para os tipos sinalizados e não sinalizados, mas para comparações de inequalidades (menor que ou maior que), se os tipos forem sinalizados temos que usar AMBOS os flags SF e OF, e também ZF. Repare que, com 0-1 obtivemos SF=1, OF=0 e ZF=0. Uma comparação de "maior que" resulta sempre em SF!=OF e ZF=0. O processador tem um atalho para esse tipo de salto condicional: JL (Jump if less than)... Se quisermos comparar com "maior ou igual" a lógica fica SF!=OF ou ZF=1, que também existe um atalho: JLE. Para comparações "maior que", SF==OF.

No caso de valores sem sinal, precisamos apenas do CF e ZF. Como visto acima, se x < y, CF=1 e ZF=0, o processador tem um atalho: JB. Já se quisermos testar se x <= y podemos usar apenas CF, mesmo assim existe um "apelido" para JC: JBE.

Por que estou falando disso? Por causa de uma prática que vejo por ai com o uso de ponteiros... Alguns acreditam que existe uma diferença fundamental entre 'char' e 'unsigned char', onde o último representa 1 byte exatamente... AMBOS representam 1 byte exatamente, a diferença está apenas nas comparações entre variáveis desses tipos, mas a cópia e a aritmética inteira entre eles é exatamente a mesma. Declarar um ponteiro do tipo 'unsigned char *' ou 'char *', se não for fazer comparações entre os dados apontados, é exatamente a mesma coisa. O mesmo se aplica a 'short', 'int', 'long', 'long long'!

Repare que não há distinção, também, na aritmética com ponteiros:

char array[10];
char *p1 = array;
unsigned char *p2 = ( unsigned char * ) array;

p1++;  // aponta para array[1];
p2++  // também aponta para array[1]!

Nada te impede de fazer um casting na comparação, se precisar, quando tem arrays para char e quer tratar seus elementos como "bytes":

if ( ( unsigned )array[0] < array[1] )
...

Como a promoção de tipos encara o modificador 'unsigned' com maior prioridade do que o 'signed' (porque tem 1 bit de precisão extra), toda a expressão será promovida para 'unsigned'...

Outro detalhe é que ponteiros são, por definição, 'unsigned'. Ao comparar dois ponteiros o compilador preferirá usar instruções como JA, JB, JAE ou JBE, no caso de comparações de inequalidades (menor que, maior que e usando ou não o "ou igual"). Alguns podem pensar que a arquitetura x86-64 seja uma exceção a essa regra, já que os endereços têm que ser "canônicos" (ou seja, se o bit 47 do endereço for 1, todos os demais, até o MSB, tem que ser 1 também!). Mas, note que o endereço canônico não é uma representação em "complemento 2" e, por isso, as comparações continuam sendo feitas de maneira 'unsigned' (CF e ZF são usados, não SF e OF). Por exemplo:

$ cat test.c
int test( void *a, void *b ) { return a < b; }
$ cc -O2 -c -o test.o test.c
$ objdump -dM intel test.o
test.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <test>:
   0:	31 c0                	xor    eax,eax
   2:	48 39 f7             	cmp    rdi,rsi
   5:	0f 92 c0             	setb   al
   8:	c3                   	ret

Repare no 'setb'...

ATENÇÃO: Note que estou sendo enfático em dizer que a comparação entre tipos é diferente. Se você tentar comparar um tipo 'unsigned' com um 'signed', poderá obter resultados indesejados, se não souber o que está fazendo... Como eu disse antes, o compilador C promove o tipo de signed para unsigned... Assim, comparações como:

if ( x > -1 ) ...

Se x for 'unsigned', são perigosas... Esse -1 será promovido para 0xffffffff, que é o valor máximo de um 'int' (por exemplo) e x jamais poderá ser maior que isso... Ou seja, a expressão sempre será FALSA!

A indiferença dos tipos 'unsigned' e 'signed' aqui refere-se ao tamanho do armazenamento...

[]s
Fred

Edited by fredericopissarra
  • Curtir 1

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Sign in to follow this  

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...