pbn/curso/aula-09.org
2025-06-07 12:52:07 -03:00

47 KiB
Raw Blame History

9 Conversão de strings numéricas para inteiros

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, 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:

; ------------------------------------------------------
; 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 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:

	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 com 10: 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 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:

#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

  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.