#+title: 9 -- Conversão de strings numéricas para inteiros #+author: Blau Araujo #+email: cursos@blauaraujo.com #+options: toc:3 * Objetivos - Diferenciar as expressões numéricas em texto dos números que representam. - Conhecer a relação entre tipos numéricos e tamanhos de dados. - Criar sub-rotinas reaproveitáveis para conversões de caracteres para números. * Todos os dados externos são recebidos como caracteres Se consideramos como /dados externos/: - Argumentos de linha de comando; - Variáveis exportadas para o ambiente do processo; - Dados digitados pelo usuário; - Dados lidos por redirecionamentos e pipes; - Dados lidos de arquivos em geral... Todos eles têm em comum o fato de serem recebidos nos nossos programas como cadeias de caracteres, mesmo que representem números. Consequentemente, para utilizá-los em operações numéricas, será necessário convertê-los nos valores que representam. ** Entendendo o problema Para entendermos melhor o problema, vamos analisar o que acontece no programa abaixo (=exemplo.c=) com o GDB. Arquivo: [[exemplos/09/exemplo.c][exemplo.c]] #+begin_src c :tangle exemplos/09/exemplo.c #include int main(void) { int a = 123; char *b = "123"; int *c = (int *)b; // Casting do valor no endereço 'b' para inteiro printf("Inteiro a: %d\n", a); printf("String b: %s\n", b); printf("Inteiro c: %d\n", *c); // Imprime o valor no endereço 'c' return 0; } #+end_src *Compilação e execução:* #+begin_example :~$ gcc -g exemplo.c :~$ ./a.out Inteiro a: 123 String b: 123 Inteiro c: 3355185 #+end_example Note que o ponteiro =c=, que recebeu endereço dos bytes da string no ponteiro =b= (="123"=), aponta para um valor inteiro totalmente diferente do que está representado pelos dígitos da string (o inteiro =3355185=). *Abertura e execução com o GDB:* Para examinar os conteúdo da memória em hexadecimal, nós vamos marcar a unção =main= como ponto de parada, executar o programa e avançar a execução em 3 linhas, para que as variáveis sejam inciadas, o nos levará ao ponto anterior à chamada da função =printf=: #+begin_example :~$ gdb a.out Reading symbols from a.out... (gdb) break main Breakpoint 1 at 0x1141: file exemplo.c, line 5. (gdb) run Starting program: /home/blau/git/pbn/curso/exemplos/09/a.out [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, main () at exemplo.c:5 warning: Source file is more recent than executable. 5 int a = 123; (gdb) next 3 9 printf("Inteiro a: %d\n", a); (gdb) #+end_example O que queremos observar é a diferença entre os valores numéricos dos três dados na memória, como se todos eles fossem inteiros (formato =w= no GDB): #+begin_example (gdb) x /1wx &a 0x7fffffffdecc: 0x0000007b (gdb) x /1wx b 0x555555556004: 0x00333231 (gdb) x /1wx c 0x555555556004: 0x00333231 #+end_example Veja que os bytes da string apontada por 'b' são, como esperado, os mesmos do inteiro apontado por 'c', mas ambos são totalmente diferentes do valor em hexadecimal do inteiro em =a= (=123=). ** A ordem dos bytes Nós também podemos examinar o conteúdo byte a byte desses dados a partir de seus endereços iniciais: #+begin_example (gdb) x /4bx &a 0x7fffffffdecc: 0x7b 0x00 0x00 0x00 (gdb) x /4bx b 0x555555556004: 0x31 0x32 0x33 0x00 (gdb) x /4bx c 0x555555556004: 0x31 0x32 0x33 0x00 #+end_example Isso ajuda a demonstrar que os bytes de inteiros, como os de qualquer outro tipo diferente de =char=, são escritos em ordem inversa (/little endian/) na memória. Portanto, quando os bytes da string foram interpretados como os 4 bytes de um inteiro, eles também foram invertidos: #+begin_example (gdb) x /4bx c 0x555555556004: 0x31 0x32 0x33 0x00 (gdb) x /1wx c 0x555555556004: 0x00333231 #+end_example O tipo =char= é composto por apenas um byte e a ordem /little endian/ só afeta a disposição de bytes: é por isso que somente os dados de tipos com dois ou mais bytes são invertidos. Além disso, na montagem dos dados na memória, o que importa é o tamanho de cada dado, não o que eles formam. Então, como strings são formadas por cadeias de caracteres terminadas com o caractere nulo (='\0'=), a ordem de seus bytes não é invertida. ** Dígitos na tabela ASCII Os valores dos bytes da string ="123"= (além do terminador nulo ='\0'=, de valor =0x00=) foram determinados pela convenção de representação de caracteres da tabela ASCII: | Caractere | Hexadecimal | Octal | Decimal | |-----------+-------------+-------+---------| | =\0= | 0x00 | 0 | 0 | | =0= | 0x30 | 060 | 48 | | =1= | 0x31 | 061 | 49 | | =2= | 0x32 | 062 | 50 | | =3= | 0x33 | 063 | 51 | | =4= | 0x34 | 064 | 52 | | =5= | 0x35 | 065 | 53 | | =6= | 0x36 | 066 | 54 | | =7= | 0x37 | 067 | 55 | | =8= | 0x38 | 070 | 56 | | =9= | 0x39 | 071 | 57 | Em comparação aos valores dos caracteres em outras bases, a representação em hexadecimal é bastante conveniente, pois basta subtrair =0x30= para saber qual é o dígito correspondente. ** Considerações sobre os tipos numéricos Em baixo nível, nós trabalhamos diretamente com tamanhos de dados. Os tipos das linguagens de alto nível, como a linguagem C, são apenas abstrações para ajudar a definir o tamanho dos dados e as regras de seus usos. Sendo assim, existe uma equivalência entre as formas de representar tamanhos de dados em Assembly com os tipos, por exemplo, da linguagem C. Considerando a arquitetura x86_64: | Tipo em C | Tamanho | Tamanho em Assembly | |-----------+---------+---------------------| | =char= | 1 byte | /byte/ | | =short= | 2 bytes | /word/ | | =int= | 4 bytes | /double-word/ | | =long= | 8 bytes | /quad-word/ | | =float= | 4 bytes | /double-word/ | | =double= | 8 bytes | /quad-word/ | #+begin_quote Esses tamanhos são especificados na ABI do System V AM64, usada pelo Linux em arquitetura x86_64 e garantidos, na prática, pelo GCC e em sistemas baseados na =glibc=. #+end_quote Em Assembly, portanto, definir uma lista de /double-words/ (=dd=) tem o mesmo efeito de definir um vetor de inteiros na linguagem C, inclusive na ordem em que os bytes são dispostos na memória. Ou então, o dado em um registrador de 64 bits, se tomado como um todo, terá o tamanho de uma /quad-word/ e será equivalente ao tipo =long= da linguagem C. ** Pesos posicionais Falando apenas de inteiros sem sinal por enquanto, cada byte de uma string numérica decimal representa um algarismo do número multiplicado pelo seu /peso posicional/, por exemplo: #+begin_example 0x31 0x32 0x33 => '1' '2' '3' 0x31 - 0x30 * 100 = 100 (centenas) 0x32 - 0x30 * 10 = 20 (dezenas) 0x33 - 0x30 * 1 = 3 (unidades) 100 + 20 + 3 = 123 #+end_example Manualmente, este é um procedimento de conversão bastante simples, mas a sua implementação em uma rotina de baixo nível não é tão intuitiva, embora também não seja nada muito complicado. O /"pulo do gato"/ está em alcançar os pesos posicionais de cada algarismo progressivamente: #+begin_example 031 0x32 033 => '1' '2' '3' val = 0x31 - 0x30 = 1 val = (val * 10) + (0x32 - 0x30) = 10 + 2 = 12 val = (val * 10) + (0x33 - 0x30) = 120 + 3 = 123 #+end_example Ou, de uma forma mais regular: #+begin_example val = 0 val = (val * 10) + (0x31 - 0x30) = 0 + 1 = 1 val = (val * 10) + (0x32 - 0x30) = 10 + 2 = 12 val = (val * 10) + (0x33 - 0x30) = 120 + 3 = 123 #+end_example Esse procedimento, facilmente implementável em Assembly, contempla tudo que vimos sobre o problema: - Percorre os bytes da string na ordem em que estão na memória; - Converte os dígitos ASCII para os valores que representam; - Aplica os pesos posicionais corretos a cada algarismo encontrado. Com isso, nós já podemos escrever uma rotina de conversão para strings que representam números decimais sem sinal de todos os tipos compatíveis com =int= (=char=, =short= e =long=, por exemplo). * Conversão para inteiros sem sinal em alto nível Em linguagem C, sem utilizar as funções especializadas da biblioteca padrão e com base no procedimento que elaboramos, nós poderíamos implementar uma função de conversão para inteiros sem sinal deste modo: #+begin_src c unsigned int str_to_uint(char *str) { int dig; // Recebe o dígito convertido unsigned int conv = 0; // Recebe a parcial da conversão for (int i = 0; str[i] != '\0'; i++) { if (str[i] < '0' || str[i] > '9') break; dig = str[i] - '0'; conv = (conv * 10) + dig; } return conv; } #+end_src Embora funcional para a maioria dos casos, essa abordagem tem alguns problemas: - Não verifica se o valor convertido excede o limite máximo para inteiros sem sinal da plataforma; - Não verifica se o ponteiro para a string é nulo (=NULL=); - Retorna =0= tanto para a conversão da string ="0"= quanto para uma string sem dígitos nos primeiros caracteres. Todos esses problemas poderiam ser solucionados com verificações e o retorno de um valor de erro: mas que valor seria esse? Tipicamente, utilizaríamos =-1=, mas o retorno da função tem que ser =unsigned=. Também poderíamos sacrificar um número válido, como o valor do próprio limite máximo de inteiros sem sinal, definido na =glibc= em =UINT_MAX=, no cabeçalho =limits.h=, mas isso requereria uma nota sobre essa limitação. Uma terceira opção, que me parece bem mais razoável, seria incluir um segundo parâmetro para receber o estado de término da função: #+begin_src c unsigned int str_to_uint(char *str, int *err); #+end_src Deste modo, a função poderia ser utilizada assim: #+begin_src c char *str_num = "123"; // String a ser convertida. int status; // Recebe o estado de término da função. unsigned int num = str_to_uint(str_num, &status); if (!status) ...; // Consequências do erro... #+end_src Para os casos de uso onde o tratamento de erros não for importante, nós podemos dar a opção de passar =NULL= como argumento de =*err= e trabalhar a implementação para definir um estado padrão =0= (erro), condicionado ao ponteiro recebido, e alterá-lo para =1= somente antes de terminar com sucesso: #+begin_src c unsigned int str_to_uint(char *str, int *err /* nullable */) { // Inicia a função com estado de erro, mas só se '*err' não for NULL... if (err) *err = 0; // Outras verificações e rotina de conversão... // Altera o estado antes de terminar com sucesso (1)... if (err) *err = 1; return conv; } #+end_src Assim, caso =*err= seja =NULL=, nenhum estado de término será definido. ** Erro de ponteiro nulo Caso o endereço da string (=*str=) seja passada como um ponteiro nulo, nós simplesmente terminaremos a função com retorno =0= sem alterar o valor de =*err=, que também é =0= (erro): #+begin_src c // Inicia a função com estado de erro, mas só se '*err' não for NULL... if (err) *err = 0; // Termina com erro (0) e valor 0 se str==NULL... if (str == NULL) return 0; #+end_src ** Erro de byte inicial fora faixa de dígitos Apesar de ser uma verificação redundante, por ser repetida mais adiante no código, ela é necessária para determinar quando a função retorna =0= por ter convertido apenas uma string ="0"= ou devido a não haver dígitos para converter no início da string, ou seja: #+begin_example String: "abc123" --> Converte para 0 com estado 1 String: "" --> Converte para 0 com estado 1 String: "0" --> Converte para 0 com estado 1 Erros --> Retornam conversão para 0, mas com estado 0 #+end_example A ideia é que os dois primeiros casos (="abc123"= e =""=) retornem =0= om estado de término =0= (erro), portanto... #+begin_src c // Inicia a função com estado de erro, mas só se '*err' não for NULL... if (err) *err = 0; // Termina com erro (0) e valor 0 se str==NULL... if (str == NULL) return 0; // Termina com erro (0) e valor 0 se primeiro byte não for um dígito... if (str[0] < '0' || str[0] > '9') return 0; #+end_src Nós podemos, inclusive, combinar as duas verificações numa só: #+begin_src c // Inicia a função com estado de erro, mas só se '*err' não for NULL... if (err) *err = 0; // Termina com erro (0) e valor 0 se str==NULL // ou se o primeiro byte não for um dígito... if (str == NULL || str[0] < '0' || str[0] > '9') return 0; #+end_src ** Erro de estouro do limite do tipo Nós podemos utilizar o valor de =UINT_MAX= como referência para determinar se o próximo valor parcial do ciclo de conversões excederá ou não o limite máximo para números inteiros sem sinal. A cada ciclo, a conversão é feita com: #+begin_example valor = (valor * 10) + dígito; #+end_example Para determinar se isso excederá o valor de =UNIT_MAX=, basta calcular qual seria o valor corrente máximo para que a próxima conversão resulte em algo dentro do limite: #+begin_example valor * 10 + dígito < UINT_MAX valor * 10 < UINT_MAX - dígito valor < (UINT_MAX - dígito) / 10 #+end_example No código, isso pode ser escrito no loop de conversão como: #+begin_src c // Converte o dígito corrente... dig = str[i] - '0'; // Termina com erro e valor 0 se a próxima conversão exceder UINT_MAX... if (conv > (UINT_MAX - dig) / 10) return 0; // Processa a parcial da conversão... conv = (conv * 10) + dig; #+end_src ** Implementação final em C Aqui está a implementação final, com as verificações e os procedimentos atualizados: #+begin_src c #include // Requerido para obter UINT_MAX da plataforma unsigned int str_to_uint(char *str, int *err /* nullable */) { if (err) *err = 0; // Estado padrão é de erro (0 = falso)! // Termina com erro e valor 0 se str==NULL ou se '0'>str[0]>'9'... if (str == NULL || str[0] < '0' || str[0] > '9') return 0; int dig; // Recebe o dígito convertido unsigned int conv = 0; // Recebe a parcial da conversão for (int i = 0; str[i] != '\0'; i++) { // Se o caractere não for um dígito, termina a conversão... if (str[i] < '0' || str[i] > '9') break; // Converte o dígito corrente... dig = str[i] - '0'; // Termina com erro e valor 0 se a próxima conversão exceder UINT_MAX... if (conv > (UINT_MAX - dig) / 10) return 0; // Processa a parcial da conversão... conv = (conv * 10) + dig; } if (err) *err = 1; // Altera estado para sucesso (1 = verdadeiro) return conv; } #+end_src ** Exemplo de uso Arquivo: [[exemplos/09/uint.c][uint.c]] #+begin_src c :tangle exemplos/09/uint.c /* * Arquivo : uint.c * Compilação: gcc -Wall uint.c -o uintc */ #include #include // Requerido para obter UINT_MAX da plataforma unsigned int str_to_uint(char *str, int *err /* nullable */); int main(int argc, char **argv) { if (argc == 1) { fprintf(stderr, "Uso: %s NÚMERO\n", argv[0]); return 1; } int status; unsigned int num = str_to_uint(argv[1], &status); if (status) { printf("String: %s\nNúmero: %u\n", argv[1], num); } else { fprintf(stderr, "Erro de conversão!\n"); return 1; } return 0; } unsigned int str_to_uint(char *str, int *err /* nullable */) { if (err) *err = 0; // Estado padrão é de erro (0 = falso)! // Termina com erro e valor 0 se str==NULL ou se '0'>str[0]>'9'... if (str == NULL || str[0] < '0' || str[0] > '9') return 0; int dig; // Recebe o dígito convertido unsigned int conv = 0; // Recebe a parcial da conversão for (int i = 0; str[i] != '\0'; i++) { // Se o caractere não for um dígito, termina a conversão... if (str[i] < '0' || str[i] > '9') break; // Converte o dígito corrente... dig = str[i] - '0'; // Termina com erro e valor 0 se a próxima conversão exceder UINT_MAX... if (conv > (UINT_MAX - dig) / 10) return 0; // Processa a parcial da conversão... conv = (conv * 10) + dig; } if (err) *err = 1; // Altera estado para sucesso (1 = verdadeiro) return conv; } #+end_src Compilação e testes: #+begin_example :~$ gcc -Wall uint.c -o uintc $ ./uintc Uso: ./uintc NÚMERO :~$ ./uintc 123 String: 123 Número: 123 :~$ ./uintc 123abc String: 123abc Número: 123 :~$ ./uintc $((2**32)) Erro de conversão! :~$ ./uintc $((2**32 - 1)) String: 4294967295 Número: 4294967295 :~$ ./uintc '' Erro de conversão! :~$ ./uintc abc Erro de conversão! #+end_example * Conversão para inteiros sem sinal em baixo nível Em Assembly, o procedimento de conversão ainda é o mesmo: #+begin_example val = 0 val = (val * 10) + (0x31 - 0x30) = 0 + 1 = 1 val = (val * 10) + (0x32 - 0x30) = 10 + 2 = 12 val = (val * 10) + (0x33 - 0x30) = 120 + 3 = 123 #+end_example Que pode ser implementado, de forma genérica, com uma sub-rotina assim: #+begin_src asm _str_to_uitn: ; ------------------------------------------------------ ; Entrada: RSI = Endereço da string (char * str) ; Saída : RAX = Valor convertido (unsigned int) ; ------------------------------------------------------ ; Preparação... ; ------------------------------------------------------ push rbx ; Salva conteúdo de rbx xor rax, rax ; rax = 0 (acumula a conversão) xor rbx, rbx ; rbx = 0 (recebe os caracteres no loop) ; ------------------------------------------------------ .conv_loop: ; ------------------------------------------------------ ; Condições de término do loop... ; ------------------------------------------------------ mov bl, [rsi] ; Lê o primeiro byte test bl, bl ; Compara primeiro byte com o terminador '\0' je .done ; Se 0x00, termina o loop cmp bl, 0x30 ; Compara primeiro byte com '0' jl .done ; Se menor, termina o loop cmp bl, 0x39 ; Compara primeiro byte com '9' jg .done ; Se maior, termina o loop ; ------------------------------------------------------ ; Conversão... ; ------------------------------------------------------ sub rbx, 0x30 ; Subtrai '0' do byte (dígito convertido) imul rax, rax, 10 ; rax *= 10 (deslocamento do peso posicional) add rax, rbx ; rax += dígito convertido inc rsi ; Avança rdi para o próximo byte jmp .conv_loop ; Volta ao início do loop de conversão ; ------------------------------------------------------ .done: ; ------------------------------------------------------ pop rbx ; Restaura rbx ret #+end_src Isso equivale à primeira versão da função em C e traz os mesmos problemas: - Não verifica se o valor convertido excede o limite máximo para inteiros sem sinal da plataforma; - Não verifica se o ponteiro para a string é nulo (=NULL=); - Retorna =0= tanto para a conversão da string ="0"= quanto para uma string sem dígitos nos primeiros caracteres. Além disso, se quisermos adaptar a sub-rotina para uso em um programa em C (em um objeto compartilhado, por exemplo) ela precisa seguir as convenções da ABI. Do jeito que está, ela já poderia ser utilizada, porque recebe o primeiro argumento em =rdi= e retorna o valor convertido em =rax=, mas ainda precisamos decidir como implementar a sinalização de erros. Na função em C, isso é feito com um ponteiro opcional (/nullable/) para a variável que receberá o estado de erro: #+begin_src c unsigned int str_to_uint(char *str, int *err /* nullable */); #+end_src A ABI determina que o segundo argumento deve ser recebido em =rdx=, então a assinatura da sub-rotina passaria a ser: #+begin_src asm _str_to_uitn: ; ------------------------------------------------------ ; Entrada: ; RSI = Endereço da string (char * str) ; RDX = Endereço da variável de erro (int *err) ; Saída : ; RAX = Valor convertido (unsigned int) ; Se sucesso: *err = 1 (verdadeiro) ; Se erro : *err = 0 (falso) ; ------------------------------------------------------ #+end_src Ainda na função em C, nós decidimos definir o estado de =*err= logo no início e só alterá-lo antes do retorno com sucesso. Como o argumento pode receber um ponteiro nulo (=NULL=), o estado de término foi definido condicionalmente: #+begin_src c if (err) *err = 0; // Estado padrão é de erro (0 = falso)! // ... if (err) *err = 1; // Altera estado para sucesso (1 = verdadeiro) return conv; #+end_src Além disso, sempre que a função terminava com erro, o valor retornado era definido com =0=, e isso também deve ser implementado na sub-rotina: #+begin_src asm _str_to_uitn: ; ------------------------------------------------------ ; Entrada: ; RSI = Endereço da string (char * str) ; RDX = Endereço da variável de erro (int *err /* nullable */) ; Saída : ; RAX = Valor convertido (unsigned int) ; Se sucesso: *err = 1 (verdadeiro) ; Se erro : *err = 0 (falso) ; ------------------------------------------------------ ; Definição condicional do estado inicial de erro... ; ------------------------------------------------------ test rdx, rdx ; Se *err = NULL, rdx = 0 jz .skip_error ; Se for 0, pula a definição do erro mov dword [rdx], 0 ; *err = 0 => estado inicial é de erro (falso) ; ------------------------------------------------------ .skip_error: ; ------------------------------------------------------ ; ... ; ------------------------------------------------------ .error: ; ------------------------------------------------------ xor rax, rax ; Retorna 0 em caso de erro jmp .done ; ------------------------------------------------------ .success: ; ------------------------------------------------------ ; Redefinição condicional do estado final de sucesso... ; ------------------------------------------------------ test rdx, rdx ; Se *err = NULL, rdx = 0 jz .done ; Se for 0, pula a definição de sucesso mov dword [rdx], 1 ; *err = 1 => estado final é de sucesso (verdadeiro) ; ------------------------------------------------------ .done: ; ------------------------------------------------------ pop rbx ; Restaura rbx ret #+end_src Com o procedimento de tratamento de erros definido, nós podemos prosseguir com a implementação das validações necessárias. ** Erro de ponteiro nulo A primeira verificação que faremos é para o caso de =rsi= ter recebido um ponteiro nulo na chamada: #+begin_src asm ; ------------------------------------------------------ ; Teste de ponteiro nulo... ; ------------------------------------------------------ test rsi, rsi ; verifica se enderçeo é nulo (0) jz .error ; se for, termina retornando -1 (erro) #+end_src Deste modo, se =rsi= receber =0= (=NULL=, numa chamada em C), a sub-rotina salta para o rótulo local =.error= e termina retornando =0= em =rax= e =1= no endereço em =rdx=, se =*err= estiver definido. ** Erro de byte inicial fora da faixa de dígitos Para que o primeiro byte seja verificado, ele precisa ser lido. Portanto, a verificação da faixa de dígitos deve ser feita logo após a preparação dos registradores utilizados, antes do /loop/ de conversão: #+begin_src asm ; ------------------------------------------------------ ; Preparação... ; ------------------------------------------------------ push rbx ; salva rbx na pilha xor rax, rax ; rax = 0 (acumula o resultado) xor rbx, rbx ; rbx = 0 (recebe os caracteres no loop) mov bl, [rsi] ; lê o primeiro byte ; ------------------------------------------------------ ; Validação do primeiro byte... ; ------------------------------------------------------ cmp bl, 0x30 ; compara com menor byte válido jl .error ; se menor, termina retornando -1 cmp bl, 0x39 ; compara com maior byte válido jg .error ; se maior, termina retornando -1 #+end_src Assim, a sub-rotina salta para =.error= se o byte for menor que =0x30= (caractere ='0'=) ou maior que =0x39= (caractere ='9'=). ** Erro de estouro do limite do tipo Como vimos, o cálculo do maior valor que pode ser multiplicado por 10 na conversão é feito com: #+begin_example (UINT_MAX - dígito) / 10 #+end_example Contudo, essa verificação é um pouco mais complicada em Assembly, por alguns motivos: - Nós não temos um limite definido para a plataforma, como a constante simbólica =UINT_MAX=, da =glibc=. - É possível definir =UINT_MAX= com uma macro no Assembly, mas não podemos contar com isso no caso de reutilização da sub-rotina, a menos que ela esteja em uma biblioteca que também contenha definições de macros. - Também teremos que utilizar registradores auxiliares adicionais, porque, numa divisão, =rax= recebe o quociente e =rdx= o resto, e ambos estão em uso. Mas /"complicada"/ não é /"impossível"/, e nós podemos assumir alguns critérios: - A ABI define que inteiros terão 4 bytes (32 bits) em arquiteturas x86_64 e as implementações da linguagem C no GCC e na Glibc garantem isso. - O valor de =UINT_MAX= pode ser armazenado em =rcx= como =0xffffffff=, o maior número sem sinal que pode ser escrito com 4 bytes. - Os registradores =r8= e =r10= podem ser usados para salvar e restaurar os dados em =rax= e =rdx=. - O registrador =r9= pode ser usado para auxiliar nos cálculos. Portanto, esta é uma implementação possível: #+begin_src asm ; ------------------------------------------------------ ; Limite válido: conv < (UINT_MAX - dígito) / 10 ; ------------------------------------------------------ mov r8, rax ; Salva parcial da conversão em r8 mov r10, rdx ; Salva ponteiro de erro em r10 mov eax, -1 ; rax = 0x00000000ffffffff (UINT_MAX) sub eax, ebx ; rax = UINT_MAX - dígito (dividendo) xor rdx, rdx ; prepara rdx para receber o resto da divisão mov r9, 10 ; divisor div r9 ; limite(rax) = (UINT_MAX - dígito) / 10 cmp r8, rax ; se parcial > limite, teremos um estouro jg .error ; se maior, termina com erro mov rdx, r10 ; restaura rdx (pontiero para erros) mov rax, r8 ; restaura rax (parcial da conversão) #+end_src ** Implementação final em Assembly Com alguns ajustes estéticos e na documentação, aqui está a implementação completa... Arquivo: [[exemplos/09/uint.asm][uint.asm]] #+begin_src asm :tangle exemplos/09/uint.asm ; ---------------------------------------------------------- ; Arquivo : uint.asm ; Montagem: nasm -g -f elf64 uint.asm ; Ligação : ls uint.o -o uint ; ---------------------------------------------------------- section .data ; ---------------------------------------------------------- num_str db "123", 0 ; string numérica ; ---------------------------------------------------------- section .bss ; ---------------------------------------------------------- num resd 1 ; 4 bytes para o valor UINT32 status resd 1 ; 4 bytes para o estado de término ; ---------------------------------------------------------- section .text global _start ; ---------------------------------------------------------- _start: ; ---------------------------------------------------------- ; Teste de chamada (verificar com o GDB)... ; ---------------------------------------------------------- mov rsi, num_str ; copia endereço da string em rsi mov rdx, status ; copia o endereço do estado de erro em rdx call _str_to_uint ; chama a sub-rotina de conversão mov dword [num], eax ; copia o resultado como int para [num] ; ---------------------------------------------------------- _exit: ; ---------------------------------------------------------- mov rax, 60 mov rdi, 0 syscall ; ---------------------------------------------------------- ; Sub-rotinas... ; ---------------------------------------------------------- _str_to_uint: ; ---------------------------------------------------------- ; Converte string numérica em inteiro sem sinal (uint32_t). ; Entradas: ; RSI = Endereço da string (char *str) ; RDX = Endereço para estado de erro (int *err /* nullable */) ; Saída: ; RAX = valor convertido (uint32_t) ou 0, no caso de erro ; Sucesso: *rdx = 1 ; Erro: *rdx = 0 ; ---------------------------------------------------------- ; Definição condicional do estado inicial de erro... ; ------------------------------------------------------ test rdx, rdx ; Se *err = NULL, rdx = 0 jz .skip_error ; Se for 0, pula a definição do erro mov dword [rdx], 0 ; *err = 0 => estado inicial é de erro (falso) ; ------------------------------------------------------ .skip_error: ; ------------------------------------------------------ ; Teste de ponteiro nulo... ; ------------------------------------------------------ test rsi, rsi ; verifica se enderçeo é nulo (0) jz .error ; se for, termina retornando -1 (erro) ; ------------------------------------------------------ ; Preparação... ; ------------------------------------------------------ push rbx ; salva rbx na pilha xor rax, rax ; rax = 0 (acumula o resultado) xor rbx, rbx ; rbx = 0 (recebe os caracteres no loop) mov bl, [rsi] ; lê o primeiro byte ; ------------------------------------------------------ ; Validação do primeiro byte... ; ------------------------------------------------------ cmp bl, 0x30 ; compara com menor byte válido jl .error ; se menor, termina retornando -1 cmp bl, 0x39 ; compara com maior byte válido jg .error ; se maior, termina retornando -1 .conv_loop: ; ------------------------------------------------------ ; Condições de término da conversão ... ; ------------------------------------------------------ test bl, bl ; verifica se o byte é o terminador 0x00 je .success ; se for, termina com sucesso cmp bl, 0x30 ; compara o byte em rbx com o menor dígito jl .success ; se for menor, termina com sucesso cmp bl, 0x39 ; compara o byte em rbx com o maior dígito jg .success ; se for maior, termina com sucesso ; ------------------------------------------------------ ; Conversão do dígito corrente... ; ------------------------------------------------------ sub rbx, 0x30 ; converte o dígito para seu valor numérico ; ------------------------------------------------------ ; Limite válido: conv < (UINT_MAX - dígito) / 10 ; ------------------------------------------------------ mov r8, rax ; Salva parcial da conversão em r8 mov r10, rdx ; Salva ponteiro de erro em r10 mov eax, -1 ; eax = 0xffffffff (UINT_MAX - 32 bits) sub eax, ebx ; rax = UINT_MAX - dígito (dividendo) xor rdx, rdx ; prepara rdx para receber o resto da divisão mov r9, 10 ; divisor div r9 ; limite(rax) = (UINT_MAX - dígito) / 10 cmp r8, rax ; se parcial > limite, teremos um estouro jg .error ; se maior, termina com erro mov rdx, r10 ; restaura rdx (pontiero para erros) mov rax, r8 ; restaura rax (parcial da conversão) ; ------------------------------------------------------ ; Processa a parcial da conversão... ; ------------------------------------------------------ imul rax, rax, 10 ; rax *= 10 (deslocamento do peso posicional) add rax, rbx ; rax += novo algarismo inc rsi ; avança para o próximo byte mov bl, [rsi] ; carrega o dígito corrente em rbx jmp .conv_loop ; ------------------------------------------------------ .error: ; ------------------------------------------------------ xor rax, rax ; Retorna 0 em caso de erro jmp .done ; ------------------------------------------------------ .success: ; ------------------------------------------------------ ; Redefinição condicional do estado final de sucesso... ; ------------------------------------------------------ test rdx, rdx ; Se *err = NULL, rdx = 0 jz .done ; Se for 0, pula a definição de sucesso mov dword [rdx], 1 ; *err = 1 => estado final é de sucesso (verdadeiro) ; ------------------------------------------------------ .done: ; ------------------------------------------------------ pop rbx ; Restaura rbx ret #+end_src ** Teste com o GDB Nosso objetivo é testar se a string em =num_str= é convertida corretamente pela análise dos valores em =rax= e =rdx= (=status=) após a chamada da sub-rotina neste trecho do programa: #+begin_src asm ; ---------------------------------------------------------- section .data ; ---------------------------------------------------------- num_str db "123", 0 ; string numérica ; ---------------------------------------------------------- section .bss ; ---------------------------------------------------------- num resd 1 ; 4 bytes para o valor UINT32 status resd 1 ; 4 bytes para o estado de término ; ---------------------------------------------------------- section .text global _start ; ---------------------------------------------------------- _start: ; ---------------------------------------------------------- ; Teste de chamada (verificar com o GDB)... ; ---------------------------------------------------------- mov rsi, num_str ; copia endereço da string em rsi mov rdx, status ; copia o endereço do estado de erro em rdx call _str_to_uint ; chama a sub-rotina de conversão mov dword [num], eax ; copia o resultado como int para [num] #+end_src *Montagem e execução no GDB:* #+begin_example :~$ nasm -g -f elf64 uint.asm :~$ ld uint.o -o uint :~$ gdb ./uint Reading symbols from ./uint... (gdb) break _start Breakpoint 1 at 0x401000: file uint.asm, line 22. (gdb) run Starting program: /home/blau/git/pbn/curso/exemplos/09/uint Breakpoint 1, _start () at uint.asm:22 22 mov rsi, num_str ; copia endereço da string em rsi (gdb) #+end_example *Execução e verificações antes da chamada:* #+begin_example (gdb) n 2 24 call _str_to_uint ; chama a sub-rotina de conversão (gdb) info registers rax rdx rsi rax 0x0 0 rdx 0x402008 4202504 rsi 0x402000 4202496 (gdb) x /1wx &num_str 0x402000 : 0x00333231 (gdb) x /1s &num_str 0x402000 : "123" (gdb) x /1wx &num_ No symbol "num_" in current context. (gdb) x /1wx &num 0x402004 : 0x00000000 (gdb) x /1wx &status 0x402008 : 0x00000000 #+end_example *Execução e verificações após a chamada:* #+begin_example (gdb) n 25 mov dword [num], eax ; copia o resultado para [num] (gdb) n _exit () at uint.asm:29 29 mov rax, 60 (gdb) info registers rax rdx rsi rax 0x7b 123 rdx 0x402008 4202504 rsi 0x402003 4202499 (gdb) x /1wx &num 0x402004 : 0x0000007b (gdb) print (int)num $1 = 123 (gdb) x /1wx &status 0x402008 : 0x00000001 #+end_example * Conversão para inteiros com sinal No Assembly x86_64, números com sinal são representados usando o método do /complemento de dois/. Tomando um inteiro de 32 bits como exemplo, se o bit mais significativo (MSB, o bit mais a esquerda) for =0=, o número é interpretado como positivo ou zero; se for =1=, o número é negativo. Em outras palavras, o bit mais a esquerda de um número é o seu sinal. O complemento de dois de um número binário é obtido pela inversão de todos os seus bits (com uma operação =NOT= bit a bit) seguida da soma de 1: #+begin_example 00000000 00000000 00000000 00000101 = 5 (0x0005) NOT -> 11111111 11111111 11111111 11111010 = -6 (0xFFFA) +1 -> 11111111 11111111 11111111 11111011 = -5 (0xFFFB) #+end_example ** Interpretação do sinal Se o MSB de um número for =1=, o que acontecerá sempre que o byte (não bit) mais significativo de seu valor em hexadecimal iniciar com =8= ou mais, o número poderá ser interpretado como negativo ou positivo -- tudo depende do contexto. Por exemplo, o número 2.147.483.648, se escrito com 4 bytes em hexa, será =0x80000000= e seu equivalente binário será: #+begin_example 10000000 00000000 00000000 00000000 = 0x80 00 00 00 = 2.147.483.648 #+end_example Digamos que esse número seja escrito em =edx= (32 bits) e seja utilizado em uma chamada de função que espera um argumento inteiro sem sinal. Neste caso, o valor será tomado como positivo, mesmo que o binário inicie com =1=. Porém, considere esta situação: #+begin_src asm mov eax, 0x80000000 cmp eax, 0 #+end_src A instrução =cmp= calcula =eax - 0= e configura as flags de acordo com o valor em =eax= e o resultado da operação: - Flag =SF=: foi definida com =1= porque o MSB do resultado é =1= (negativo). - Flag =OF=: como não houve /overflow/ na operação, seu valor foi definido como =0=. - Flag =CF=: como a operação não provocou um "vai um" ou um empréstimo fora do limite representável em 32 bits (capacidade de =eax=), seu valor foi definido como =0=. Neste ponto, a interpretação do sinal do valor em =eax= vai depender do que faremos com o resultado da comparação. Observe esta possibilidade: #+begin_src asm mov eax, 0x80000000 cmp eax, 0 ; resulta 0x80000000 -> SF=1 e OF=0 jl .menor ; Se SF != OF, o salto acontece ; ... .menor: ; ... #+end_src A instrução =jl= (/jump if less/) avalia se as flags =SF= e =OF= são diferentes: se forem, o salto é feito. Portanto, =jl= levaria em conta o sinal do resultado da subtração e salto aconteceria porque =eax < 0=. Mas, e neste caso? #+begin_src asm mov eax, 0x80000000 cmp eax, 0 ; resulta 0x80000000 -> SF=1 e OF=0 jb .menor ; Se CF = 1, o salto acontece ; ... .menor: ; ... #+end_src Já a instrução =jb= (/jump if below/) só salta se =CF=1=, mas o resultado da subtração (=eax-0=) não provocou um "vai um" ou um empréstimo fora do limite do registrador: portanto, seu valor será =0= e o salto não acontecerá. Isso significa que, como =jb= não leva em conta o sinal, =0x80000000= foi interpretado como um valor acima de zero. Por outro lado, se utilizássemos =rax= em vez de =eax=... #+begin_src asm mov rax, 0x80000000 ; 0x0000000080000000 cmp rax, 0 ; CF=0 SF=0 e OF=0 jb .menor ; CF=0, não salta jl .menor ; SF=OF, não salta ; ... .menor: ; ... #+end_src Desta forma, não há ambiguidades quanto à interpretação do sinal. ** Uma nota sobre as flags É importante entender corretamente o conceito por detrás da /carry flag/, porque, em operações sem sinal, é ela que indica se um número é menor do que outro. Então, vejamos: =10= é menor do que =14=? #+begin_src asm mov al, 10 ; 10 = 1010 = 0x0A cmp al, 14 ; 14 = 1110 = 0xFE #+end_src A instrução =cmp= fará a subtração... #+begin_example Aqui, temos que fazer um empréstimo e o MSB de 10 será 0... ↓ 1010 - 1110 ------ 00 Agora é o MSB que requer um empréstimo! ↓ 0110 - 1110 ------ ?100 #+end_example Numa situação desas, onde não há mais de onde emprestar um bit =1= mais a esquerda, o processador efetua a subtração, como se houvesse de onde tirar o bit 1 de que precisa, sobe a flag =CF= (indicando que houve um empréstimo fora do limite dos operandos) e a flag =SF= (indicando que o resultado é negativo). Ao mesmo tempo, =OF= e =ZF= receberiam =0=, porque não houve estouro do limite do valor representável com sinal e o resultado não foi zero. Na operação acima, nós teríamos: #+begin_example CF=1 ↓ (1)0110 - 1110 ------ 1100 = -4 ← ZF=0 (não é zero) ↑ SF=1 OF=0 (é possível escrever em 4 bits) #+end_example #+begin_quote O processador não possui circuitos capazes de efetuar subtrações e, por isso, não executaria a operação desta forma, e sim calculando o complemento de dois de =-14= (=0010=) para, em seguida, somar com =10=: ~1010+0010 = 1100 (-4)~. #+end_quote ** Limites de inteiros com sinal Assumindo que inteiros são escritos com 4 bytes, o maior valor absoluto que podemos representar ainda é =0xFFFFFFFF=, mas ele teria que ser visto como um número negativo. Calculando seu complemento de dois... #+begin_example 11111111 11111111 11111111 11111111 (0xFFFFFFFF) NOT -> 00000000 00000000 00000000 00000000 +1 -> 00000000 00000000 00000000 00000001 => (1) #+end_example Portanto, levando em conta o sinal, =0xFFFFFFFF= é =-1=. Como o bit mais significativo representa o sinal, o primeiro dígito do byte mais significativo do maior número positivo que pode ser escrito em hexa tem que ser =7=: #+begin_example Maior byte positivo: 0x7f = 0111 1111 = 127 #+end_example Se somarmos =1= a =0x7F=, nós chegaremos a =0x80= (128), que será interpretado como um número negativo: #+begin_example 0x7F -> 0111 1111 +1 -> 1000 0000 = 0x80 = -128 ↑ ↑ SF=1 -------------´ #+end_example Então, se o inteiro com sinal tiver 1 byte, seus limites serão: #+begin_example 0x7F = 0111 1111 = +127 0x7E = 0111 1110 = +126 ... 0x01 = 0000 0001 = +1 0x00 = 0000 0000 = 0 0xFF = 1111 1111 = -1 ... 0x81 = 1000 0001 = -127 0x80 = 1000 0000 = -128 #+end_example Transpondo o princípio para o tamanho de um inteiro na arquitetura x86_64, os limites são: #+begin_example 0x7FFFFFFF -> 2.147.483.647 0x80000000 -> -2.147.483.648 #+end_example ** Procedimento de conversão para inteiros com sinal As diferenças em relação à conversão de strings para inteiros sem sinal são: - Antes da conversão, nós precisamos detectar a presença dos caracteres de sinal (=+= e =-=); - Depois do valor absoluto encontrado, nós teremos que verificar se ele está na faixa de valores inteiros com sinal (entre =0x7FFFFFFF= e =0x80000000=); - No final, se o número passar na verificação, nós teremos que calcular seu complemento de dois, o que pode ser feito multiplicando o valor por =-1= ou, de forma ainda mais compacta e eficiente, utilizando a instrução =neg=. Todo o restante do procedimento é idêntico, tanto que podemos reutilizar as mesmas funções de antes para a etapa de conversão. ** Conversão em alto nível Com base no procedimento geral, esta seria uma forma de implementar a conversão de inteiros com sinal em linguagem C reutilizando a função que criamos para converter inteiros sem sinal: #+begin_src c #include // Requerido para obter INT_MAX e INT_MIN da plataforma int str_to_sint(char *str, int *err) { if (err) *err = 0; // Flag de estado de término: 0 = erro if (str == NULL) return 0; // Teste do ponteiro para a string // Verificação e atrubuição do sinal da string... char sig = '+'; // Sinal padrão if (str[0] == '+' || str[0] == '-') { sig = str[0]; str++; // Avança o ponteiro da string } // Chama a função de conversão de inteiros sem sinal... unsigned conv = str_to_uint(str, err); if (!*err) return 0; // Verificação da faixa de inteiros com sinal... if (sig == '-') { // INT_MIN=-2147483648, que em unsigned é 2147483648 if (conv > (unsigned)INT_MAX + 1) return 0; if (err) *err = 1; // Flag de estado de término: 1 = sucesso return -(int)conv; } else { if (conv > INT_MAX) return 0; if (err) *err = 1; // Flag de estado de término: 1 = sucesso return (int)conv; } } #+end_src #+begin_quote Agora cabe a você, como parte dos exercícios propostos, elaborar e executar os testes e os ajustes, se forem necessários. #+end_quote ** Conversão em baixo nível Esta seria uma solução equivalente implementada como uma sub-rotina que depende de =_str_to_uint=, criada e testada anteriormente: #+begin_src asm ; ---------------------------------------------------------- _str_to_sint: ; ---------------------------------------------------------- ; Converte string numérica com sinal em inteiro (int32_t). ; Entradas: ; RSI = Endereço da string (char *str) ; RDX = Endereço para estado de erro (int *err /* nullable */) ; Saída: ; RAX = valor convertido (int32_t) ; Sucesso: *rdx = 1 ; Erro: *rdx = 0 ; ---------------------------------------------------------- push rbx ; salva rbx push rcx ; salva rcx xor rcx, rcx ; rcx = 0 (sinal: 0 = positivo, 1 = negativo) ; ------------------------------------------------------ ; Testa ponteiro nulo ; ------------------------------------------------------ test rsi, rsi jz .error ; ------------------------------------------------------ ; Verifica o sinal na string ; ------------------------------------------------------ mov bl, [rsi] cmp bl, '-' ; caractere de sinal negativo? jne .check_plus inc rcx ; rcx = 1 => negativo inc rsi ; avança o ponteiro jmp .convert .check_plus: cmp bl, '+' ; caractere de sinal positivo? jne .convert inc rsi ; ignora '+' .convert: ; ------------------------------------------------------ ; Chama _str_to_uint ; ------------------------------------------------------ call _str_to_uint ; RSI = str, RDX = err, RAX = resultado (uint32_t) ; ------------------------------------------------------ ; Verifica se _str_to_uint retornou erro ; ------------------------------------------------------ test rdx, rdx jz .check_sign cmp dword [rdx], 0 je .error .check_sign: test rcx, rcx ; se rcx = 1, é número negativo jz .check_range_pos ; ------------------------------------------------------ ; Verifica se valor cabe em int32_t negativo ; ------------------------------------------------------ cmp eax, 0x80000000 ja .error ; se maior que INT_MAX + 1 -> erro neg eax ; aplica o sinal negativo jmp .success .check_range_pos: ; ------------------------------------------------------ ; Verifica se valor cabe em int32_t positivo ; ------------------------------------------------------ cmp eax, 0x7fffffff ja .error ; maior que INT_MAX -> erro .success: test rdx, rdx jz .done mov dword [rdx], 1 ; *err = 1 jmp .done .error: xor eax, eax ; valor de erro = 0 test rdx, rdx jz .done mov dword [rdx], 0 ; *err = 0 .done: pop rcx pop rbx ret #+end_src #+begin_quote Também faz parte dos exercícios propostos, elaborar e executar os testes e os ajustes, se forem necessários. #+end_quote * Exercícios propostos 1. Altere o valor em =num_str=, nos testes da sub-rotina =_str_to_uint=, e analise os resultados com o GDB. 2. Em C, crie as funções =str_to_ulong= (string para inteiros longos sem sinal) e =str_to_slong= (string para inteiros longos com sinal). 3. Em Assembly, crie as sub-rotinas =_str_to_ulong= e =_str_to_slong=.