47 KiB
9 – Conversão de strings numéricas para inteiros
- Objetivos
- Todos os dados externos são recebidos como caracteres
- Conversão para inteiros sem sinal em alto nível
- Conversão para inteiros sem sinal em baixo nível
- Conversão para inteiros com sinal
- Exercícios propostos
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: exemplo.c
#include <stdio.h>
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;
}
Compilação e execução:
:~$ gcc -g exemplo.c :~$ ./a.out Inteiro a: 123 String b: 123 Inteiro c: 3355185
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
:
:~$ 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)
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):
(gdb) x /1wx &a 0x7fffffffdecc: 0x0000007b (gdb) x /1wx b 0x555555556004: 0x00333231 (gdb) x /1wx c 0x555555556004: 0x00333231
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:
(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
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:
(gdb) x /4bx c 0x555555556004: 0x31 0x32 0x33 0x00 (gdb) x /1wx c 0x555555556004: 0x00333231
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 |
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
.
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:
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
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:
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
Ou, de uma forma mais regular:
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
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:
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;
}
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:
unsigned int str_to_uint(char *str, int *err);
Deste modo, a função poderia ser utilizada assim:
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...
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:
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;
}
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):
// 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;
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:
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
A ideia é que os dois primeiros casos ("abc123"
e ""
) retornem 0
om estado
de término 0
(erro), portanto…
// 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;
Nós podemos, inclusive, combinar as duas verificações numa só:
// 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;
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:
valor = (valor * 10) + dígito;
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:
valor * 10 + dígito < UINT_MAX valor * 10 < UINT_MAX - dígito valor < (UINT_MAX - dígito) / 10
No código, isso pode ser escrito no loop de conversão como:
// 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;
Implementação final em C
Aqui está a implementação final, com as verificações e os procedimentos atualizados:
#include <limits.h> // 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;
}
Exemplo de uso
Arquivo: uint.c
/*
* Arquivo : uint.c
* Compilação: gcc -Wall uint.c -o uintc
*/
#include <stdio.h>
#include <limits.h> // 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;
}
Compilação e testes:
:~$ 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!
Conversão para inteiros sem sinal em baixo nível
Em Assembly, o procedimento de conversão ainda é o mesmo:
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
Que pode ser implementado, de forma genérica, com uma sub-rotina assim:
_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
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:
unsigned int str_to_uint(char *str, int *err /* nullable */);
A ABI determina que o segundo argumento deve ser recebido em rdx
, então a
assinatura da sub-rotina passaria a ser:
_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)
; ------------------------------------------------------
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:
if (err) *err = 0; // Estado padrão é de erro (0 = falso)!
// ...
if (err) *err = 1; // Altera estado para sucesso (1 = verdadeiro)
return conv;
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:
_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
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:
; ------------------------------------------------------
; Teste de ponteiro nulo...
; ------------------------------------------------------
test rsi, rsi ; verifica se enderçeo é nulo (0)
jz .error ; se for, termina retornando -1 (erro)
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:
; ------------------------------------------------------
; 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
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:
(UINT_MAX - dígito) / 10
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
, daglibc
. - É 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 erdx
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 emrcx
como0xffffffff
, o maior número sem sinal que pode ser escrito com 4 bytes. - Os registradores
r8
er10
podem ser usados para salvar e restaurar os dados emrax
erdx
. - O registrador
r9
pode ser usado para auxiliar nos cálculos.
Portanto, esta é uma implementação possível:
; ------------------------------------------------------
; 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)
Implementação final em Assembly
Com alguns ajustes estéticos e na documentação, aqui está a implementação completa…
Arquivo: 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
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:
; ----------------------------------------------------------
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]
Montagem e execução no GDB:
:~$ 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)
Execução e verificações antes da chamada:
(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 <num_str>: 0x00333231 (gdb) x /1s &num_str 0x402000 <num_str>: "123" (gdb) x /1wx &num_ No symbol "num_" in current context. (gdb) x /1wx &num 0x402004 <num>: 0x00000000 (gdb) x /1wx &status 0x402008 <status>: 0x00000000
Execução e verificações após a chamada:
(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 <num>: 0x0000007b (gdb) print (int)num $1 = 123 (gdb) x /1wx &status 0x402008 <status>: 0x00000001
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:
00000000 00000000 00000000 00000101 = 5 (0x0005) NOT -> 11111111 11111111 11111111 11111010 = -6 (0xFFFA) +1 -> 11111111 11111111 11111111 11111011 = -5 (0xFFFB)
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á:
10000000 00000000 00000000 00000000 = 0x80 00 00 00 = 2.147.483.648
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:
mov eax, 0x80000000
cmp eax, 0
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 com1
porque o MSB do resultado é1
(negativo). - Flag
OF
: como não houve overflow na operação, seu valor foi definido como0
. - 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 deeax
), seu valor foi definido como0
.
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:
mov eax, 0x80000000
cmp eax, 0 ; resulta 0x80000000 -> SF=1 e OF=0
jl .menor ; Se SF != OF, o salto acontece
; ...
.menor:
; ...
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?
mov eax, 0x80000000
cmp eax, 0 ; resulta 0x80000000 -> SF=1 e OF=0
jb .menor ; Se CF = 1, o salto acontece
; ...
.menor:
; ...
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
…
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:
; ...
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
?
mov al, 10 ; 10 = 1010 = 0x0A
cmp al, 14 ; 14 = 1110 = 0xFE
A instrução cmp
fará a subtração…
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
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:
CF=1 ↓ (1)0110 - 1110 ------ 1100 = -4 ← ZF=0 (não é zero) ↑ SF=1 OF=0 (é possível escrever em 4 bits)
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 com10
:1010+0010 = 1100 (-4)
.
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…
11111111 11111111 11111111 11111111 (0xFFFFFFFF) NOT -> 00000000 00000000 00000000 00000000 +1 -> 00000000 00000000 00000000 00000001 => (1)
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
:
Maior byte positivo: 0x7f = 0111 1111 = 127
Se somarmos 1
a 0x7F
, nós chegaremos a 0x80
(128), que será interpretado como
um número negativo:
0x7F -> 0111 1111
+1 -> 1000 0000 = 0x80 = -128
↑ ↑
SF=1 -------------´
Então, se o inteiro com sinal tiver 1 byte, seus limites serão:
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
Transpondo o princípio para o tamanho de um inteiro na arquitetura x86_64, os limites são:
0x7FFFFFFF -> 2.147.483.647 0x80000000 -> -2.147.483.648
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
e0x80000000
); - 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çãoneg
.
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:
#include <limits.h> // 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;
}
}
Agora cabe a você, como parte dos exercícios propostos, elaborar e executar os testes e os ajustes, se forem necessários.
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:
; ----------------------------------------------------------
_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
Também faz parte dos exercícios propostos, elaborar e executar os testes e os ajustes, se forem necessários.
Exercícios propostos
- Altere o valor em
num_str
, nos testes da sub-rotina_str_to_uint
, e analise os resultados com o GDB. - Em C, crie as funções
str_to_ulong
(string para inteiros longos sem sinal) estr_to_slong
(string para inteiros longos com sinal). - Em Assembly, crie as sub-rotinas
_str_to_ulong
e_str_to_slong
.