Ir para conteúdo

fredericopissarra

Membros
  • Postagens

    420
  • Registro em

  • Última visita

  • Dias Ganhos

    160

Posts postados por fredericopissarra

  1. Em 06/02/2024 em 09:55, teuzero disse:

    default rel  não entendi.

    " mov ebx,[eax]  ; RBX inteiro é inicializado e EAX está errado aqui."

    eax seria, algo como dword [rax+offset]?

    prefixo R é melhor ne ai não fica dados no registrador.

    Coisas como:

      section .bss
    var: resd 1
    
      section .text
    ...
      mov eax,[var]

    Aqui var é um offset no endereço efetivo [var]. Como o NASM está usando um endereçamento não relativo a RIP o linker precisará colocar um fixup, uma entrada de relocação. Um jeito de evitar isso no modo x86-64 é usando mov eax,[rel val], dizendo ao NASM que o offset é relativo a RIP. Mas é melhor dizer a ele que TODOS os endereços desse tipo são relativos a RIP via diretiva default rel.

    A vantagem do modo x86-64 e os endereços relativos a RIP é que bem menos entradas na tabela de relocação são necessárias no executável final...

    Note que endereços relativos a RIP têm apenas o offset, nunca outro registrador... Algo como [rax+4] ou [rbx+4*rax+4] não são relativos a RIP.

    Com relação ao mov ebx,[eax], note que no modo x86-64 os endereços têm sempre 64 bits de tamanho. Ao usar [eax] você está, explicitamente, extirpando os 32 bits superiores de um endereço. Endereços devem usar registradores R??, assim, o correto ali seria mov ebx,[rax]. Note que, aqui, RBX será inicializado e os 32 bits superiores serão, automaticamente, zerados, tornando o xor rbx,rbx anterior desnecessário.

    Além disso, ao usar EAX num endereço efetivo o NASM é obrigado e acrescentar um prefixo 0x67 na sua instrução. Evite usar endereços de 32 bits num código de 64. Mesmo que não seja para endereços... Por exemplo, suponha que eu queira multiplicar EAX (de 32 bits) por 5. Isso pode ser facilmente feito com lea eax,[rax+4*rax]. Note que o destino é EAX, mas o endereço efetivo é RAX+4*RAX. O endereço efetivo é calculado em 64 bits (como deve ser), mas a atribuição final estirpará os 32 bits superiores (e os zerará).

    PS: Yep... multiplicar EAX por 10 é tão simples quanto:

      lea eax,[rax+4*rax]    ; ou '5*rax', o NASM entende que isso é 'rax+4*rax'
      add eax,eax

    Desde que estejamos trabalhando com valores sem sinal.

    • Agradecer 1
  2. Entendi que a intenção é ofuscar o código ao máximo, mas, mesmo assim, acho que têm algumas coisas que podem ser melhoradas:

    Em primeiro lugar, se for criar sessões que não sejam "de sistema" como ".bss" (e não ".BSS"), ".data", ".rdata" e ".text", não use o "." inicial. Sessões que não sejam "de sistema" não deveriam ter esse "." inicial.

    Constantes, ou seja, valores em memória que não serão modificados no decorrer do código, deveriam estar na sessão ".rdata" e não em ".data".

    Se for usar estruturas (struc) e blocos de dados inicializados com essas estruturas, os elementos não especificados são, automaticamente, inicializados com zeros. Por exemplo, o label ctx poderia ser definido como:

      ; Coloque em .bss para não colocar dados da imagem binária do executável final.
      ; O loader automaticamente zera toda a sessão .bss.
      section .bss
    
    ctx:
      istruc CONTEXT  ; Aqui, TODOS os membros serão zerados.
      iend

    A mesma coisa (zerar automaticamente) acontece com "variáveis" comuns... Por exemplo:

      section .bss   ; De novo, colocando em .bss para evitar fazer uma grande imagem binária...
    
    ;  addressAlloc  times 8  dq 0
    ;  TamArqProgram times 8 dq 0
    ;  TamArqTarget  times 8 dq 0
    
      ; Serão zerados, automaticamente.
      addressAlloc:  resq 8
      TamArqProgram: resq 8
      TamArqTarget:  resq 8

    Para zerar um registrador R?? qualquer não é necessário usar R?? na instrução, mas apenas a versão E??. Como em:

      xor edx, edx   ; Zera todo o RDX (sem o prefixo REX).

    A mesma coisa acontece se tivermos que inicializar um registrador com valores menores que 2³²-1:

      mov eax,1  ; Opcode: B8 01 00 00 00
      mov rax,1  ; Opcode: 48 B8 01 00 00 00 00 00 00 00

    O NASM tende a otimizar isso, nesse caso específico, mas é bom sempre usar E?? ao invés de R??, se for o caso.

    Um endereço efetivo (no formato [offset]), numa instrução, é ABSOLUTO e exige um fixup para relocação. No entanto, no modo x86-64, temos endereçamentos relativos ao registrador RIP, que elimina a relocação. Adicione a diretiva default rel no seu código.

    Evite recarregar o mesmo dado mais de uma vez, como em:

      mov rcx,[alloc]
      mov rax,[alloc]

    Mesmo que:

      mov rcx,[alloc]
      mov rax,rcx

    Insira uma dependência no stream das instruções (a inicialização de RAX depende de RCX)... Isso ainda é mais rápido (e menor) que DOIS acessos à memória.

    Pra quê tanta manipulação da pilha se funções da Win32 API para x86-64 tendem a NÃO usar a pilha para receber argumentos? Tenha em mente, também, que RBX, RBP e de R12 até R15 são preservados pela função chamada (no caso da Win32 API) e suas funções deveriam preservá-las também...

    Lembre-se que todo endereço efetivo no modo x86-64 é de 64 bits. Essas instruções estão erradas:

      xor rbx,rbx    ; Pra quê?
      mov ebx,[eax]  ; RBX inteiro é inicializado e EAX está errado aqui.
    

    Chamadas indiretas, como em call r12, sofrem do problema de branch misprediction de forma muito mais fácil do que uma chamada direta. O processador mantém a estatística das chamadas... se a chamada anterior for "para frente" e R12 aponta "para trás", alguns ciclos de penalidade serão usados para recarregar o cache L1I...

    []s
    Fred

    • Agradecer 1
  3. Depende do que você quer fazer, é claro, mas eu sugeriria, se for o caso de ter uma única interface, que 3 funções fossem criadas. Por exemplo:
     

    enum kind_e { PRODUCT, CONSUMER };
    
    _Bool saveProduct( Product *p ) {  ... }
    _Bool saveConsumer( Consumer *p )  {  ... }
    
    _Bool save( void *p, enum kind_e kind )
    {
      switch ( kind )
      {
        case PRODUCT: return saveProduct( p );
        case CONSUMER: return saveConsumer( p );
      }
    
      return 0;
    }

     

  4. Não está perfeito e nem mesmo sei se está "certo"... O detalhe é que entrada formatada por std::basic_istream não funciona como um scanf, você é quem tem que verificar o formato. Eis um exemplo rápido pra sua avaliação:

    #include <cstdlib>
    #include <iostream>
    #include <string>
    #include <sstream>
    #include <iomanip>
    #include <exception>
    
    struct horario
    {
      int hora;
      int minuto;
    
      // Só para garantir valores válidos.
      horario ( int h = 0, int m = 0 )
        : hora ( h ), minuto ( m ) {}
    
      bool operator<( const horario& );
    
      // Crie o seu operador - aqui para retornar um horario como diferença.
    };
    
    bool horario::operator<( const horario& rhs )
    { int m = hora*60 + minuto; return m < ( rhs.hora*60 + rhs.minuto ); }
    
    static std::ostream& operator<<( std::ostream& os, const horario& h )
    {
      std::stringstream ss;
    
      ss << std::setw(2) << std::setfill('0') << h.hora << ':' << h.minuto;
      os << ss.str();
      return os;
    }
    
    static std::istream& operator>>( std::istream& is, horario& h )
    {
      std::string s;
      std::string::size_type n;
    
      is >> s;
    
      n = s.find( ':' );
    
      if ( n == std::string::npos )
        throw std::range_error( "Invalid format" );
    
      h.hora = std::stoi( s.substr( 0, n ) );
      h.minuto = std::stoi( s.substr( n + 1 ) );
    
      // Porque 60 no limite superior? Porque leap seconds devem ser considerados em horários!
      if ( h.minuto < 0 || h.minuto > 60 )
        throw std::range_error( "Invalid format" );
    
      return is;
    }
    
    // Escolhi que o operator -, quando usando duas referências a horario, deve retornar uma string.
    static std::string operator-( const horario& lhs, const horario& rhs )
    {
      int h, m;
      static const char *sigs[2] = { "", "-" };
      bool negative = false;
    
      h = lhs.hora - rhs.hora;
      m = lhs.minuto - rhs.minuto;
    
      m += 60*h;
      if ( m < 0 )
        negative = true;
    
      h = abs( m / 60 );
      m = abs( m % 60 );
    
      std::stringstream ss;
    
      ss << sigs[negative] << std::setw(2) << std::setfill('0') << h << ':' << m;
    
      return ss.str();
    }
    
    int main()
    {
      horario h1, h2;
    
      std::cin >> h1;
      std::cin >> h2;
    
      if ( h2 < h1 )
      {
        std::cerr << "Primeiro horário precisa ser menor ou igual ao segundo.\n";
        return 1;
      }
    
      std::cout << "Diferença: " << h2 - h1 << '\n';
    
      return 0;
    }

    []s
    Fred

  5. Qualquer coisa pode acontecer, inclusive um segmentation fault.

    ISO 9899 7.21.5.2 § 2:
    If stream points to an output stream or an update stream in which the most recent operation was not input, the fflush function causes any unwritten data for that stream to be delivered to the host environment to be written to the file; otherwise, the behavior is undefined.

    • Curtir 1
  6. Em 26/12/2022 em 16:33, eric samuel disse:

    programa que cria um vetor em ordem decrescente a partir de um numero digitado pelo usuario. caso o usuario digite um numero negativo o seu program deve exibir a seguinte mensagem "numero negativo" e o vetor nao deve conter nenhum elemento.

    Exemplo:usuario digita o número 10.
    O seu vetor deve conter 10 posições da seguinte forma:
    9,8,7,6,5,4,3,2,1,0.

    Existem diversas maneiras de fazer isso... Não acho que alguém vai fazer por você.
    Coloque sua tentativa por aqui e talvez tenha alguma resposta.

    []s
    Fred

    • Curtir 1
  7. idade é a idade lida (óbvio, né?), mas você quer a quantidade (e, portanto, quer contar) quantos são menores de 18. E também quer acumular (somar) as idades dos menores de 18 para calcular a média depois (que precisa da contagem). Eis a modificação, para seu estudo:
     

    #include <stdio.h>
    
    int main( void )
    {
      // Dica: Evite criar objetos globais
      int idade;
      unsigned int quantidade;
      double soma;
    
      // Começamos com a soma e quantidade zeradas.
      soma = 0.0;
      quantidade = 0;
    
      for ( int i = 1; i <= 10; i++ )
      {
        printf ( "Digite a idade do aluno:" );
        scanf ( "%d", &idade );
    
        // Só acumula e conta a quantidade se a idade for < 18.
        if ( idade < 18 )
        {
          soma += idade;
          quantidade++;
        }
      }
    
      printf ( "\nA quantidade de alunos menores de 18 anos é: %u\n"
               "A média de idade dos menores de 18: %.1f\n",
               quantidade, soma / quantidade );
    
      return 0;
    }

    Esse exemplo tem alguns problemas:

    1 - E se o usuário entrar com uma idade negativa ou 0?
    2 - E se ele entrar com alguma coisa que não seja um número?

    Veja se consegue tratar isso para o programinha ficar correto...

    • Agradecer 1
  8. One comment of mine on sprintf(), the way it was used above: Notice strftime() will write a 20 chars, including the NUL char, to buffer (which is defined as an array of 80 chars - too much unnecessary chars), and the sprintf(), just below, will write the 19 chars from buffer plus 5 chars (including NUL char), so a 24 chars buffer and currentTime are sufficient... Or, if you want some space to spare, 32, each. This way sprintf() isn't "insecure", and a little bit faster than snprintf(), because the sizes are known.

    Why the original code using a double as UNIX epoch timestamp is beyond my comprehension ! time_t (defined as a long long int) is more precise (nowadays) than double. To avoid dealing with 2 buffers I would encode the function get_time() as:

    // Assumes p points to a buffer 24 chars long or more.
    void get_time_str( char *p, time_t t, int fraction )
    {
      struct tm tm;
    
      // thread safe version of localtime() - available o glibc.
      localtime_r( &t, &tm );
    
      // 4+1+2+1+2+1+2+1+2+1+2+1+3+1= 24 chars!
      sprintf( p, "%04d-%02d-%02d %02d:%02d:%02d.%03d",
        tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
        tm.tm_hour, tm.tm_min, tm.tm_sec, fraction );
    }

    With "d" conversions there is always the possibility we get more then the minimum with or precision given on printf's format string. "%04d" is a MINIMUM of 4 chars long filled with zeros on the left, but if the integer is > 9999, printf will print 5 chars or more! Of course we can avoid this calculating the reminders, as in:

      sprintf( p, "%04d-%02d-%02d %02d:%02d:%02d.%03d",
        (tm.tm_year + 1900) % 10000, tm.tm_mon + 1, tm.tm_mday,
        tm.tm_hour, tm.tm_min, tm.tm_sec, abs(fraction % 1000) );

    And we have to make sure the buffer is, at least 25 chars long (because the year component can be negative). Notice I forced fraction to be passed as an absolute (positive) value to avoid adding a '-' char, if, by accident, it is negative.

    If you don't know how many chars will be "printed" at memory, then sprintf() is, indeed, "unsafe".

    PS: Since every width and precision with 'd' conversion is a minimal size, the maximum size is of an int, 11 chars for the year (which can be negative) and 10 for the other 6 components, plus se separators and a NUL char: 67 chars (less then 80 or 84).

    PS2: Fernando is absolutely corrent saying that mixing C with C++ isn't a good idea. And notice the equivalent of string.h is cstring, time.h is ctime, stdio.h is cstdio and stdlib.h is cstdlib, if you insistir on mixing C++ code with C like functions. It is good to know that modern C++ STL (Since C++11) have a namespace std::chrono with templates and functions to deal with date/time. Take a look at cppreference site.

    []s
    Fred

  9. Alguns detalhes:

    1. O código não tem nada dependente de localização, então usar setlocale() ali é supérfluo;
    2. Busca binária (binary search) exige que o array esteja em ordem... Seu array, obviamente, não está;
    3. C (e, de lambuja C++) já possui a rotina bsearch();
    4. C++20 possui o algoritmo std::binary_search().
    • Curtir 1
  10. Ainda de mais nada, essas duas coisas estão erradas:

    setlocale( LC_ALL, "portuguese" );
    fflush( stdin );

    Se setlocale() "ajusta (set) o local (locale)", então, existe um lugar no mundo chamado "português"?

    fflush() "despeja" (flushes) streams de saída apenas.

    Quanto ao resto... um pouco de atenção ao que foi feito seria interessante!

  11. Se não há um padrão, não vejo problemas. Eu só reescreveria a coisa um pouquinho diferente:
     

    // test.c
    #include <stdio.h>
    #include <stdlib.h>
    
    // Note que isolei as rotinas de entrada e cálculo!
    
    // Obtém tipo e quantidade. Retorna 1 se ok, 0 em caso de erro.
    static _Bool getTypeQuant( unsigned int *type, unsigned int *quant )
    {
      // Por quê fputs? Porquê é mais rápido do que printf() -- em alguns compiladores.
      fputs( "Tipo Quarto: ", stdout );
      fflush( stdout ); // stdout é "line buffered". Flushing pode ser necessário sem o '\n' final.
    
      // Note que verifico quantas conversões foram feitas aqui!
      if ( scanf( "%u", type ) != 1 )
        return 0;
    
      fputs( "Quantidade: ", stdout );
      fflush( stdout ); // stdout é "line buffered". Flushing pode ser necessário sem o '\n' final.
    
      if ( scanf( "%u", quant ) != 1 )
        return 0;
    
      return 1;
    }
    
    // Calcula a conta com base no tipo e quantidade.
    // Retorna -1 se não pode calcular.
    static double conta ( unsigned int type, unsigned int quant )
    {
      double val = -1.0;  // Valor default, se não selecionado abaixo.
    
      switch ( type )
      {
        // Se não há um padrão nos fatores de escala, isso é um bom
        // jeito de fazer...
        case 101: val = 3.50 * quant; break;
        case 102: val = 5.00 * quant; break;
        case 103: val = 7.80 * quant; break;
        case 104: val = 5.00 * quant; break;
        case 105: val = 8.20 * quant; break;
        case 106: val = 6.40 * quant; break;
        case 107: val = 4.75 * quant; break;
        case 108: val = 3.60 * quant; break;
        case 109: val = 6.00 * quant; break;
        case 110: val = 4.30 * quant;
      }
    
      return val;
    }
    
    int main ( void )
    {
      unsigned int type, quant;
    
      // Por quê 'double' ao invés de 'float'?
      // O formato %f de printf exige 'double'. 'float' é
      // automaticamente convertido para 'double'. Então evito essa
      // conversão automática usando o tipo certo.
      double val;
    
      // Note que passo os ponteiros aqui, mas não declaro objetos dentro de getTypeQuant().
      if ( ! getTypeQuant( &type, &quant ) )
      {
        fputs( "\nERRO ao tentar obter tipo e quantidade!\n"
               "Processo abortado.\n", stdout );
    
        return EXIT_FAILURE;
      }  
    
      val = conta ( type, quant );
    
      // Já que -1.0 é condição de erro...
      if ( val < 0.0 )
      {
        fputs ( "\nERRO ao calcular total da conta para tipo e quantidade fornecidos.\n", stderr );
    
        return EXIT_FAILURE;
      }
    
      printf ( "\nValor total da conta: R$ %.2f", val );
    
      return EXIT_SUCCESS;
    }

    []s
    Fred

  12. Qual é a mensagem de erro que dá? (PS: Use formatação, o botão <> aqui em cima.)

    Anyway... Recomendo que você não use arquivos de cabeçalho (header files) para definir funções, apenas protótipos, macros e declarações de externs. Por exemplo:
     

    /* main.c */
    #include "functions.h"
    
    int main( void )
    { imprimeOi(); }

    Eis o header functions.h:

    /* functions.h */
    #ifndef FUNCTIONS_H_
    #define FUNCTIONS_H_
    
    void imprimeOi(void);
    
    #endif

    E a função é definida em functions.c:

    /* functions.c */
    #include <stdio.h>
    
    // Incluído para "bater" o protótipo com a definição abaixo.
    #include "functions.h"
    
    void imprimeOi( void )
    {
      puts( "Oi" );
    }

    Agora é só compilar:

    $ cc -o test main.c functions.c
    $ ./test
    Oi

    PS: Incluir arquivos com "" significa procurar apenas no diretório corrente. Incluir com <> significa procurar por todos os diretórios "padrão" do compilador, inclusive os informados pelas opções -I. No caso acima poderíamos substituir o #include "functions.h" por #include <functions.h> mas teríamos que incluir a opção -I. (repare o ponto) na linha de comando do cc.

    • Curtir 1
  13. O que acontece se o preço de venda for informado como 0 (zero)?
    O que acontece se inserir mais que 4 "produtos"?
    O que acontece se tiver um produto que custe R$ 16777217,00?
    O que acontece se, ao invés do usuário informar um valor numérico, informar uma string inválida (tipo `abc' para venda)?
    Por que você deixou o '\n' na string do nome do produto (fgets() deixa o '\n' final lá!)?
    E se o nome do produto tiver mais que 60 chars?

    PS: int main() não é C, mas C++. fflush(stdin) está errado (fflush() deve ser usado apenas em streams de saída).

    • Curtir 1
  14. Para tentar ajudar, eis um método de implementar lista encadeada dupla circular... começamos com uma estrutura do nó "cabeça":

    struct list_head {
      struct list_head *prev_, *next_;
    };

    Uma lista vazia tem sempre esse nó e os links prev e next apontando para ele mesmo. Podemos criar um macro para isso:

    #define LIST_HEAD_INIT(list__) { &(list_), &(list__) }

    Podemos ter, também, uma inicialização em runtime:

    static inline void list_init( struct list_head *list )
    { list->prev_ = list->next_ = list; }

    Uma lista vazia poderia ser declarada (em tempo de compilação) como:

    struct list_head mylist = LIST_HEAD_INIT(mylist);

    Para facilitar a criação de nós "polimórficos" usarei o seguinte macro:

    #define LIST_HEAD_PTRS struct list_head *prev_, *next_

    Ou, em runtime, como:

    {
      struct list_head list;
    
      list_init( &list );
      ...
    }

    O nosso nó pode ser declarado como:

    struct music_entry {
      LIST_HEAD_PTRS;
      
      char *name;  // Nome da música (usarei como "chave").
      // ... outros campos aqui.
    };

    Com isso struct music_entry tem exatamente a mesma estrutura que struct list_head, nos dois primeiros membros (os links). De fato, qualquer estrutura que declare LIST_HEAD_PTRS como seu primeiro membro pode ser usada como nó para uma lista. Assim, todas as nossas rotinas para manipulação de lista podem, simplesmente, usar struct list_head como nó base.

    Primeira rotina: Adicionar um nó na lista... De posse do ponteiro de um nó qualquer, podemos adicionar um nó depois ou antes desse:
     

    // Rotina "privada" (repare o underscore no nome).
    // Passamos o elemento a inserir na lista e os ponteiros prev e next
    // que serão usados.
    static inline void list_add_(struct list_head *element,
                                 struct list_head *prev, struct list_head *next )
    {
      element->prev_ = prev;
      element->next_ = next;
      prev->next_ = element;
      next->prev_ = element;
    }
    
    // Adiciona um elemento depois de um nó:
    static inline void list_add_after( struct list_head *element,
                                       struct list_head *node )
    { list_add_( element, node, node->next_ ); }
    
    // Adiciona um elemento antes de um nó:
    static inline void list_add_before( struct list_head *element,
                                        struct list_head *node )
    { list_add_( element, node->prev, node ); }	

    Segunda rotina: Apagar um nó.
     

    // Rotina "privada". Dados os links prev e next, retira o elemento do meio.
    static inline void list_del_( struct list_head *prev,
                                  struct list_head *next )
    { prev->next_ = next; next->prev_ = prev; }
    
    // Retira um elemento da lista.
    static inline void list_del( struct list_head *element )
    { list_del_( element->prev, element->next ); }

    Note que, se a lista estiver vazia e passarmos o ponteiro da "cabeça" nada é, de fato, feito (essa é a vantagem de usar esse nó "sentinela").

    Uma outra rotina útil é a determinação se uma lista está vazia ou não... para isso precisamos usar SEMPRE o nó "cabeça". Algo semelhante é a determinação se estamos ou não na cabeça:

    static inline int list_is_empty( struct list_head *head )
    { return head == head->next_; }

    Para percorrermos todos os elementos da lista podemos usar um macro assim:

    // iterção "para frente"
    #define list_for_each( iter__, head__ ) \
      for ( iter__ = (head__)->next_; iter__ != (head__); iter__ = iter__->next_ )
    
    // iteração "para trás"
    #define list_for_earch_prev( iter__, head__ ) \
      for ( iter__ = (head__)->prev_; iter__ != (head__); iter__ = iter__->prev_ )
    
    // iterção segura "para frente" (para o caso de queremos modificar
    // um elemento na iterção). Usa um ponteiro "temporário" extra.
    #define list_for_each_safe( iter__, tmp__, head__ ) \
      for ( iter__ = (head__)->next_, tmp__ = (iter__)->next_; \
            iter__ != (head__); \
            iter__ = tmp__, tmp__ = (iter__)->next_ )
    
    // iterção segura "para trás" (para o caso de queremos modificar
    // um elemento na iterção). Usa um ponteiro "temporário" extra.
    #define list_for_each_prev_safe( iter__, tmp__, head__ ) \
      for ( iter__ = (head__)->prev_, tmp__ = (iter__)->prev_; \
            iter__ != (head__); \
            iter__ = tmp__, tmp__= (iter__)->prev_ )

    Assim, considere se tivermos uma lista list e queremos procurar pela música "Owner of a lonely heart":

    struct list_head *i;
    struct music_entry *p;
    
    list_for_each( i, &list )
    {
      p = (struct music_entry *)i;
    
      if ( ! strcasecmp( p->name, "Owner of a lonely heart" ) )
        break;
    }
    
    // Não estamos na cabeça, entao achamos.
    if ( i != &list )
      printf( "found '%s'.", p->name );

    Acredito que, com isso, a implementação do programinha fique mais simples, huh?

    PS: Alguns perceberão que essa implementação é praticamente idêntica a do Linux, mas, na verdade, esse método é o de Robert Sedgewick.

    • l33t 1
×
×
  • Criar Novo...