Jump to content

fredericopissarra

Membros
  • Content Count

    301
  • Joined

  • Last visited

  • Country

    Brazil

Posts posted by fredericopissarra


  1. Sempre topo com essa questão em foruns e mídias como o Discord: Como usar funções da biblioteca padrão de C em códigos puramente escritos em assembly? É “fácil”, mas eu não recomendo. Primeiro mostrarei como, usando o NASM e o GCC e depois digo o por quê da não recomendação. Eis um “hello, world” simples:

    ; test.asm
    ; Compile com:
    ;   nasm -felf64 -o test.o test.asm
     
    bits 64
     
    ; A glibc exige "RIP relative addressing". 
    ; De qualquer maneira, isso é sempre uma boa 
    ; prática no modo x86-64.
    default rel
     
    section .rodata
     
    msg:
      db    `hello, world`,0
     
    section .text
     
      ; Este símbolo está na glibc!
      extern puts
     
      global main
    main:
      ; Chama puts(), passando o ponteiro da string.
      lea   rdi,[msg]
      call  puts wrt ..plt
     
      ; return 0;
      xor   eax,eax
      ret

    O código acima é diretamente equivalente a:

    #include <stdio.h>
     
    int main( void )
    {
      puts( "hello, world" );
      return 0;
    }

    Para compilar, linkar e testar:

    $ nasm -felf64 -o test.o test.asm
    $ gcc -o test test.o
    $ ./test
    hello, world

    Simples assim. O gcc deve ser usado como linker porque ele sabe onde a libc está.

    O wrt ..plt é um operador do NASM chamado With Reference To. Ele diz que o símbolo está localizado em uma section específica. No caso a section ..plt, que contém os saltos para as funções da libc (late binding). Isso é necessário, senão obterá um erro de linkagem porque o linker não encontrará o símbolo… PLT é a sigla de “Procedure Linkage Table“. É uma tabela com ponteiros para as funções carregadas dinamicamente da libc.

    Eis o porque de minha não recomendação: O compilador nem sempre usa o nome “oficial” da função, que pode ser um #define num header e, ás vezes, existem argumentos adicionais “invisíveis” para certas funções “intrínsecas”, mas o principal argumento contra essa prática é que seu código não ficará menor ou mais rápido só porque você está usando assembly. Por exemplo, ambos os códigos em C e Assembly, acima, fazem exatamente a mesma coisa e têm o mesmíssimo tamanho final (6 KiB, extirpando os códigos de debbugging e stack protection, no caso de C).

    De fato, é provável que você crie um código em assembly pior do que o compilador C criaria…

    Addedum: Não recomendo, particularmente, que se tente escrever códigos em assembly que usem a libc no ambiente Windows. O problema é que Windows coloca algum código adicional nas funções. Por exemplo, A pilha deve ser alterada para acomodar SEH (Structured Exception Handling), no caso de main, é necessário chamar uma função chamada _main… Coisas assim são problemáticas para o novato.

    Fonte: Lá no #BitIsMyth: https://wp.me/pudbF-1RF

    • Curtir 5

  2. Recomendo algo assim:
     

    #include <unistd.h>
    
    void print_decimal( long n )
    {
      char buffer[24];   // buffer local que conterá a string.
      char *p;           // ponteiro para o buffer.
      unsigned long m;   // O valor "sem sinal" de n.
      unsigned long r;   // resto.
      _Bool signaled;
      size_t size;
    
      p = buffer + sizeof buffer - 1; // aponta para o fim do buffer.
    
      // Verifica sinal.
      signaled = 0;
      m = n;
    
      // O caso de n==0 falha, então precisamos disso.
      if ( ! n )
      {
        size = 1;
        *p-- = '0';
        goto print;
      }
      
      if ( n < 0 )
      {
        signaled = !signaled;
        m = -n;
      }
    
      size = 0;
      while ( m )
      {
        r = m % 10;
        m /= 10;
        *p-- = '0' + r;
        size++;
      }
    
      if ( signaled )
      {
        *p-- = '-';
        size++;
      }
    
    print:
      write( STDOUT_FILENO, p+1, size );
    }
    
    int main( void )
    {
      print_decimal(-12);
      putchar('\n');
    }

    Convertido para asm:

    ; NASM code.
    bits 64
    default rel
    
    section .text
    
      global print_decimal
    print_decimal:
      sub   rsp, 40                     ; Aloca espaço na pilha para o buffer.
    
      test  rdi, rdi                    ; testa se n é 0 ou negativo.
      je    .zero_case
      js    .negative
    
      xor   r9d, r9d                    ; Usa R9 como booleano (signaled).
    .prepare:
      xor   ecx, ecx                    ; RCX é size
      lea   rsi, [rsp+23]               ; Aponta para o final do buffer.
      mov   r8, -3689348814741910323    ; Constante usada no "macete" da multiplicação por 1/10, abaixo.
      jmp   .divide
    .loop:
      mov   rcx, rdx
    .divide:
      mov   rax, rdi
    
      sub   rsi, 1
    
      mul   r8
    
      shr   rdx, 3              ; Obtém o quociente da divisão por 10 em RAX
      mov   rax, rdx
    
      lea   rdx, [rdx+rdx*4]    ; Calcula o resto em RDX.
      add   rdx, rdx
      sub   rdi, rdx
      mov   rdx, rdi
    
      mov   rdi, rax            ; O quociente é o novo dividendo.
    
      add   edx, '0'
      mov   [rsi+1], dl
      lea   rdx, [rcx+1]
    
      test  rax, rax            ; Dividendo é 0?
      jne   .loop               ; Não? permanece no loop.
      test  r9d, r9d            ; Tem sinal?
      je    .print              ; Não? salta para impressão.
    
      mov   rax, rsi
      lea   rdx, [rcx+2]
      sub   rsi, 1
      mov   BYTE [rax], '-'
    
    .print:
      add   rsi, 1              ; de volta para o primeiro caracter do buffer.
      mov   edi, 1              ; STDOUT_FILENO.
      mov   eax, edi            ; SysV ABI write syscall
      syscall
    
      add   rsp, 40             ; Dealloca espaço para o buffer.
      ret
    
    .zero_case:
      mov   BYTE [rsp+23], '0'
      mov   edx, 1
      lea   rsi, [rsp+22]
      jmp   .print
    
    .negative:
      neg   rdi
      mov   r9d, 1
      jmp   .prepare

    Dá pra melhorar? Yep... meu ponto é que criar uma rotina em ASM não necessariamente a faz mais rápida (essa ai é mais rápida que a sua - medida!), nem menor.

    • Curtir 2

  3. A ideia de empilhar os restos foi interessante, mas tem um problema: Vocẽ está empilhando QWORDs, não BYTES.
    Pra quê alocar o "tamanho" na pilha... Existem 16 registradores de uso geral disponíveis.
    Existem alguns "acertos" a serem feitos no código... Por exemplo:
    XOR EDX,EDX (sim EDX, não RDX) é mais performático e menor que MOV RDX,0, mas, como o @Felipe.Silva disse, estender o sinal é melhor.
    E você acabará com um problema com valores negativos (acho que não testou esses, né?)... -12, por exemplo, resultará num resto de -2 na primeira divisão, o que empilhará '0'-2 ou o caracter '.'.

    Ainda... multiplicações são sempre mais rápidas que divisões. Veja isso: https://is.gd/rB9Vlb

    Como calcular o resto... Ora... se tem o quociente Q, o resto de A/10 é A - (Q*10)

    "Q*10" pode ser fácilmente calculado com 'LEA RDX,[RAX+RAX*4] / ADD EDX,EDX' (unsigned).

    • Curtir 1

  4. 1 hora atrás, dudsdev disse:

    Feedback muito bacana galera do bem!

    Mais aí, alguém conseguiu compilar algum dos códigos das POCs do corkam?

    Como ele monta todo o header "na mão", o formato para compilação com o NASM (ele não usa MASM) deve ser bin:

    nasm -fbin mini.asm -o mini.exe

     

    • Haha 1

  5. 10 horas atrás, Fabiano Furtado disse:

    Fred... tudo bem? Achei interessante a idéia do IPv6! Quem sabe depois da implementação do payload?

    Bem, estou começando a estudar o código para implementar o payload nos protocolos e acredito que tenha encontrado um bug no ICMP...

    Comecei pelo ICMP por se tratar do menor código existente no T50, ou seja, mais fácil de se entender e modificar.

    Executei um

    # t50 --protocol ICMP --threshold 2 127.0.0.1

    e capturei os pacotes no Wireshark. No Wireshark, o campo do checksum estava errado, e o mesmo mostrava os bytes invertidos.

    Mensagem do WS: "Checksum: 0xbbaa incorrect, should be 0xaabb"

    Retirei a função htons no checksum do módulo icmp.c e funcionou.

    
    /* Computing the checksum. */
      icmp->checksum = co->bogus_csum ? RANDOM() :
                      cksum ( icmp, sizeof ( struct icmphdr ) );

    Isso é um bug?

    Desde já, agradeço.

    Estranho... o código está certo e o checksum PRECISA estar em BIG ENDIAN... vou investigar...
    Thanks.

    • Curtir 1

  6. @mandelacruz, lembro a você que "herrar é umano" e, assim como Fernando disse, a iniciativa de aprender é sempre louvável!
    Meu comentário deve-se apenas ao fato de que algumas informações são facilmente encontradas...

    [[]]ão
    Fred

    • Curtir 2

  7. PS: "CAST" é uma daquelas palavras em inglês difíceis de fixar... "CAST" significa elenco (teatro), mas também "jogar" (como em "cast a spell" ou "jogar um feitiço"). Mas, também é o nome que se dá ao suporte usado quando quebramos um braço ou uma perna (um "molde")... Esse último parece ser o neologismo para "casting", no contexto de C...

    • Curtir 1

  8. 8 horas atrás, unc4nny disse:

    ...Minha duvida eh, como que funciona o casting da funcao para uma string?...

    De novo, a dúvida ainda está confusa... "casting de função para string"? Em primeiro lugar, não existem "strings" em C (mas sim arrays de chars,  onde o último char é um '\0')... Em segundo lugar, o identificador de uma função é um ponteiro, bem como o identificador de um array é convertido para um ponteiro.

    No seu exemplo, eu suponho que a função crc32() tenha o protótipo:

    uint32_t crc32( unsigned char *ptr, size_t size );

    Quando o argumento ptr poderia ser, muito bem, definido como um ponteiro para void e, neste caso, qualquer casting de ponteiro na passagem deste argumento para a função seria supérfluo (void * aponta para qualquer coisa).

    Este é o mesmo caso de chamadas à função malloc(), por exemplo:

    char *p;
    
    p = malloc( 16 );  // aloca 16 bytes e devolve o ponteiro.

    Casting aqui é desnecessário porque malloc() tem o protótipo:

    void *malloc( size_t size );

    Como o @Marioh disse ai em cima, casting é uma maneira de reinterpretar um tipo. Por exemplo:

    int x = 1, y = 2;
    float z;
    
    // O casting aqui é "obrigatório" porque senão a divisão será feita
    // com INTEIROS, resultando num INTEIRO que depois será convertido para
    // float... Com o casting, a divisão será entre valores em PONTO FLUTUANTE.
    z = (float) x / y;  // z = 0.5;
    
    z = x / y;   // isso resulta em 0.0!

    Casting pode ser usado para forçar a barra da atualização de um ponteiro também:

    char *p;
    ...
    // inicializa p em algum lugar...
    ...
    *(int *)p++ = 1;  // aqui p[0]=1, p[1] até p[3]=0 (little endian).
    // mas, p será avançado em 4 posições... (sizeof(int)).

    Quando fazemos a conversão de tipos de menor precisão para maior, não precisamos de castring também. A promoção de tipos funciona direitinho... Para fazer a "demoção" de tipos o castring pode ser necessário para evitar avisos do compilador:

    char a;
    int b = 10;
    
    a = (char)b;  // casting para evitar (?) warnings... nem sempre evita!
    b = a;  // castring desnecessário.

    Agora... no seu exemplo, você está passando um ponteiro para uma função (checa) para a função crc32(). O que é válido, mas LER a memória onde a função foi codificada é um "comportamento indefinido" (undefined behavior) - escrever nessa memória, provavelmente causará um SEGMENTATION FAULT - um erro.

    Note que, no seu caso, o identificador checa equivale ao ponteiro:

    _Bool (*checa)( char *, int );

    Se seu crc32() for definido como:

    uint32_t crc32( unsigned char *, size_t );

    Então o pontiero terá que sofrer casting para evitar avisos... Note que isso pode não funcionar em todas as plataformas. Algumas assumem ponteiros para região de código com tamanho diferente da ponteiros para região de dados... Outras não permitem isso...

    • Curtir 1
    • l33t 1

  9. Prefira sempre compilar seus códigos com a opção -O2 (pelo menos). Isso criará código mais "enxuto". A função checa(), por exemplo, ficará assim:
     

    ; Entrada: RDI = nome, ESI = serial
    ; Saída AL (0 = false, 1 = true).
    checa:
    	movsx	eax, BYTE PTR [rdi]   ; Lê o primeiro byte.
    
    	xor	edx, edx              ; edx = serialcorreto = 0
    
    	test	al, al                ; fim de string?
    	je	.endOfString       ; sim, salta para a comparação.
    
    .loop:
    	add	rdi, 1                  ; aponta para o próximo byte
    	add	edx, eax                         ; acumula o checksum.
    	movsx	eax, BYTE PTR [rdi]   ; lê o próximo byte.
    	test	al, al                          ; fim de string?
    	jne	.loop                              ; não, volta ao loop.
    
    .endOfString:
    	cmp	esi, edx         ; serial == serialcorreto?
    	sete	al                    ; ajusta AL.
    	ret

    No entanto, não entendi qual é a dúvida, especificamente...

    • Curtir 2

  10. Exemplo:

    #include <stdio.h>
    #include <string.h>
    
    int main( void )
    {
      char v, *p;
      char buffer[16];
    
      // Lê 1 char de stdin, mas ignora todos os "separadores" (isspace()) subsequentes...
      scanf( "%c ", &v );
    
      // Lê 16 chars à partir do primeiro caracter válido do stream stdin.
      fgets( buffer, sizeof buffer, stdin );
    
      // Livra-se do '\n' no final da string, se houver.
      if ( p = strchr( buffer, '\n' ) )
        *p = '\0';
    
      printf( "'%c' : \"%s\"\n", v, buffer );
    }

    Testando:

    Untitled.png.5f1bf3a7575b85e98d05b56e153d3e31.png

    Ahhhhhhhhhh... NÃO use gets()... é uma função obsoleta!

    • Curtir 2

  11. 23 horas atrás, Fabiano Furtado disse:

    Frederico e Fernando... será que eu poderia me aventurar a implementar esse suporte a payload de usuário no T50?

    Para mim seria um bom desafio em C... acho que aprenderia muito nessa implementação. Claro, preciso do apoio de vocês durante o desenvolvimento pois certamente terei dúvidas.

    Sou aberto a críticas e sugestões.

    O que vcs acham?

    Valeu!

    Manda ver... mas, ainda não vejo muita utilidade em adicionar payloads no T50...
    Como o Nelson insiste: Acho que seria mais útil adicionar protocolos...
    Aliás... algo que dará trabalho para fazer: Adicionar suporte a IPv6!


  12. Sorry... só vi agora. Fiquei internado num hospital na última semana (não... não foi o coronga!)...

    O detalhe é que scanf varre um stream em busca do formato desejado, mas deixa para trás o restante, uma vez atendido o formato. Ao usar "%c" você pede a conversão de um único caractere, todos os demais que o seguem, "ficam para trás"...  Acontece que stdin normalmente precisa de um '\n', quando o redirecionamento é feito para o teclado e '\n' é o final de linha interpretado pelo fgets() também...

    Não... usar fflush com streams de entrada não resolve, uma vez que, como @Marioh apontou corretamente, isso é undefined behavior. De fato, o comportamento, em alguns OSs, é simplesmente ignorar a ordem de "despejo".

    Você pode tentar um scanf() diferente:

    scanf("%c ", &value_menu);

    Note o espaço depois do "%c". Isso faz com que quaisquer caracteres "separadores" sejam varridos, mas ignorados.

    • l33t 1

  13. O problema com rand() é o mesmo problema com a maioria dos PRNG (Pseudo Random Number Generators)... Eles não são, de fato, aleatórios. A não ser por efeitos físicos, como decaimento radioativo, flutuações quânticas, etc,  não há como obter valores realmente aleatórios - pode-se até mesmo usar meios físicos não tão especializados como contar a quantidade de batimentos de asas de um colibri se alimentando em 1 segundo ou alguma outra grandeza que pareça aleatória (algumas certificate authorities usam meios assim!)...

    rand() geralmente é implementado usando-se o Linear Congruential Generator, que nada mais é do que uma equação linear:

    png.latex.png.a0086b813d7ce6c67fb1229754218ecf.png

    Onde X0 é a "semente" informada via função srand(). Mas, esse esquema tem um problema... Se m for descartado ou for o tamanho da "palavra", então os bits inferiores tendem a ser menos aleatórios que os bits superiores (veja Knuth)... Felizmente a glibc tenta resolver isso com uma série de equações lineares e uma "mistura" binária "esperta":

    // rand_r() é a versão therad-safe de rand() e a base do mesmo.
    int rand_r (unsigned int *seed)
    {
      unsigned int next = *seed;
      int result;
    
      next *= 1103515245;
      next += 12345;
      result = (unsigned int) (next / 65536) % 2048;
    
      next *= 1103515245;
      next += 12345;
      result <<= 10;
      result ^= (unsigned int) (next / 65536) % 1024;
    
      next *= 1103515245;
      next += 12345;
      result <<= 10;
      result ^= (unsigned int) (next / 65536) % 1024;
    
      *seed = next;
    
      return result;
    }

    Aqui temos 3 valores pseudo aleatórios obtidos com uma única semente que são "misturadas" com outro método PRNG chamado linar feedback shift registers. O quão aleatório isso é, não faço ideia... Mas, o problema permanece: Se eu sei qual é a semente inicial e a equação que é usada para obter os valores subsequentes, eles deixam de ser pseudo aleatórios para serem previsíveis!

    Geralmente escolhemos uma semente que também pareça aleatória. Uma das formas é usar o epoch timestamp, obtidas em qualquer sistema baseado em UNIX com a função time():

    // time() retorna o número de segundos ocorridos
    // desde 1º de janeiro de 1970, às 0:00, UTC.
    srand( time( NULL ) );

    Mas um sujeito chato e metódico pode obter esse valor de antemão... Uma solução é confiar na "aleatoriedade" de certas APIs e dispositivos contidos no seu sistema operacional para obter uma semente aleatória e depois usar rand() para obter os demais valores mais rapidamente:

    // Inicializa a semente com um valor "aleatório" no UNIX:
    void setup_seed( void )
    {
      unsigned int seed;
      int fd;
      ssize_t size;
    
      if ( ( fd = open( "/dev/urandom", O_RDONLY ) ) < 0 )
      {
        perror( "open" );
        exit( EXIT_FAILURE );
      }
    
      errno = 0;
      size = read( fd, &seed, sizeof seed );
      if ( errno || size < sizeof seed)
      {
        fputs( "read error\n", stderr );
        close( fd );
        exit( EXIT_FAILURE );
      }
      
      srand( seed );
      
      close( fd );
    }

    Ler /dev/urandom é, com toda certeza, mais lento do que usar rand(), mas resolve o problema da semente original. Não resolve o problema da aleatóriedade previsível do LCG ou qualquer outro PRNG, mas minimiza a coisa toda nos dando valores que "parecem" aleatórios.

    • Agradecer 2

  14. Yep... C++11 tem uma série de templates para "uniformização" de distribuições de inteiros e um monte de templates com "random engines". Mas, se você não souber usá-los, vai piorara a coisa... E mais: EU NÃO GOSTO DE C++!


  15. Pode-se imaginar uma maneira, menos eficiente em termos de performance, para eliminar o problema da distribuição não uniforme trabalhando com ponto flutuante. Infelizmente isso tem alguns problemas...

    Se RAND_MAX tem 31 bits de tamanho, usaremos o tipo double, que tem 53, para garantir a precisão e fazer algo assim:

    #define RANGE 6
    
    int r;
    
    // r estará entre 0 e (RANGE - 1).
    r = rand() / ( ( double )RAND_MAX / RANGE + 1 );

    A distribuição dos valores resultantes estará em "clusters"... No caso, cada um dos 6 valores estará dentro de uma faixa de 0.1666 (1/6)... 0 está na faixa [0;0.1666...); 1, [0.1666...;0.33333...); ... Acontece que rand() pode devolver qualquer valor entre 0 e RAND_MAX, ou seja, valores entre [0; 0.1666...*RAND_MAX), neste exemplo, serão necessariamente 0... Ou seja, qualquer valor de rand() retornado entre 0 e 357913941 será zero!

    Você acaba de obter um RNG bem menos aleatório que o original.


  16. Um exemplo de geração de ticket para MEGA SENA tendando corrigir a discrepância da não uniformidade de distribuição, por rejeição:

    /* Sorteio MEGA SENA, corrigindo a distribuição não uniforme. */
    #include <stdio.h>
    #include <stdlib.h>
    #if defined(__x86_64) || defined(__i386)
      #include <immintrin.h>
    #endif
    
    // Usar rand() não é uma solução ideal, mas pode ser preciso,
    // se __RDRND__ não estiver definido.
    #ifndef __RDRND__
      #include <unistd.h>
      #include <fcntl.h>
      #include <time.h>
    #endif
    
    /* Para obter aleatoriedade uniforme e garantir que
       o valor seja verdadeiramente aleatório, uso a instrução
       RDRAND, disponível nos processadores Intel/AMD. Usei
       _rdrand16_step() na esperança que a entropia não seja
       prejudicada entre chamadas.
    
       Note, também, que essa rotina retorna o resto da divisão por
       64 (6 bits). Isso garante que a distribuição seja uniforme
       e, mais adiante, simplesmente discarto os valores maiores que
       60. */
    #ifdef __RDRND__
    unsigned short get_random_64( void )
    {
      unsigned short r;
      unsigned int retries;
    
      retries = 10;
      do
        if ( _rdrand16_step( &r ) )
          return r & 0x3f;  // o mesmo que 'r % 64'.
      while ( --retries );
    
      fputs( "\n\033[31;1mERROR\033[m: Could not get random value.\n", stderr );
      exit( EXIT_FAILURE );
    }
    #else
    // Usa rand(), se RDRAND não estiver disponível.
    unsigned short get_random_64( void )
    {
      return rand() % 0x3f;
    }
    
    // Obtem uma semente mais aleatória que time(NULL)...
    // Não funciona no Windows!
    unsigned int get_random_seed( void )
    {
      int fd;
      unsigned int r;
    
      fd = open( "/dev/urandom", O_RDONLY );
      if ( fd < 0 )
      {
        fputs( "\n\033[31;1mERROR\033[m: Cannot open random device!\n", stderr );
        exit( EXIT_FAILURE );
      }
    
      if ( read( fd, &r, sizeof r ) != sizeof r )
      {
        close( fd );
        fputs( "\n\033[31;1mERROR\033[m: Error reading from random device.\n", stderr );
        exit( EXIT_FAILURE );
      }
    
      close( fd );
    
      return r;
    }
    #endif
    
    int main( void )
    {
      /* LEMBRE-SE: O array começa no índice 0! */
      _Bool values[60] = {0};
      int count;
      unsigned int r;
    
      /* Alimenta a semente, se __RDRND__ não estiver definido. */
      #ifndef __RDRND__
        srand( get_random_seed() );
      #endif
    
      fputs( "Discarded: ", stdout );
    
      count = 6;
      do {
        r = get_random_64();
    
        // Somente entre 0 e 59 são aceitos.
        // NÃO é uma solução ideal o descarte dos valores
        // do último bloco, incompleto, mas é uma solução!
        if ( r > 59 )
        {
          printf( "%2u ", r );
          continue;
        }
    
        // Se o valor já foi sorteado, tenta de novo...
        if ( values[r] )
          continue;
    
        // Marca como sorteado.
        values[r] = 1;
    
        // Obtivemos o valor, continua enquanto ainda precisamos
        // de mais...
        count--;
      } while ( count );
    
      // Apresenta o ticket de maneira ordenada.
      fputs( "\nTicket:    ", stdout );
      r = 0;
      while ( r < sizeof values / sizeof values[0] )
      {
        if ( values[r] ) 
          printf( "%2u ", r + 1 );
    
        ++r;
      }
      putchar('\n');
    
      return EXIT_SUCCESS;
    }

    Funciona em Linux (qualquer plataforma), mas para melhores resultados na aleatoriedade, compile para Intel/AMD com:
     

    $ gcc -O2 -march=native -o mega mega.c

     


  17. Outro detalhe importante para se lembrar sobre aleatoriedade é que ela não significa que você não possa obter um determinado valor mais vezes do que outros. Se fato, uma sequência do tipo (1,2,2,3,1,5,4) continua sendo aleatória, mesmo que 1 e 2 sejam repetidos... O problema todo está na distribuição das chances de obtenção dos valores...

    Outro exemplo é a MEGA SENA... todo mundo faz jogos com valores "aparentemente" aleatórios, mas se esquecem que, por exemplo, a sequência (1,2,3,4,5,6) tem a mesma chance de ser sorteada que qualquer outra, se considerarmos a aleatoriedade perfeita (uniformemente distribuída).

×
×
  • Create New...