Conceito por trás dessas quatro linhas de código C complicado

384

Por que esse código fornece a saída C++Sucks? Qual é o conceito por trás disso?

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

int main() {
    m[1]--?m[0]*=2,main():printf((char*)m);    
}

Teste aqui .

codeslayer1
fonte
11
@BoBTFish tecnicamente, sim, mas ele é executado todos o mesmo em C99: ideone.com/IZOkql
nijansen
12
@ Nurettin Eu tive pensamentos semelhantes. Mas não é culpa do OP, são as pessoas que votam nesse conhecimento inútil. Admitido, esse material de ofuscação de código pode ser interessante, mas digite "ofuscação" no Google e você obtém vários resultados em todos os idiomas formais em que pode pensar. Não me interpretem mal, acho que é bom fazer essa pergunta aqui. É apenas uma questão superestimada, porque não é muito útil.
TobiMcNamobi
6
@ detonator123 "Você deve ser novo aqui" - se você observar o motivo do fechamento, poderá descobrir que não é esse o caso. O entendimento mínimo necessário está claramente ausente da sua pergunta - "Eu não entendo isso, explique" não é algo bem-vindo no Stack Overflow. Caso você tenha tentado algo a si mesmo em primeiro lugar, que a questão não foram fechados. É trivial para o Google "dupla representação C" ou algo parecido.
42
Minha máquina PowerPC big-endian é impressa skcuS++C.
Adam Rosenfield
27
Minha palavra, eu odeio perguntas planejadas como esta. É um pequeno padrão na memória que é o mesmo que uma string boba. Não serve a nenhum propósito útil a ninguém e, no entanto, ganha centenas de pontos de repetição para o interlocutor e o respondente. Enquanto isso, perguntas difíceis que podem ser úteis para as pessoas ganham talvez alguns pontos, se houver. Esse é um tipo de pôster do que há de errado com o SO.
amigos estão

Respostas:

494

O número 7709179928849219.0tem a seguinte representação binária como um de 64 bits double:

01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011
+^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- --------

+mostra a posição do sinal; ^do expoente e -da mantissa (ou seja, o valor sem o expoente).

Como a representação usa expoente binário e mantissa, dobrar o número incrementa o expoente em um. Seu programa faz isso precisamente 771 vezes; portanto, o expoente iniciado em 1075 (representação decimal de 10000110011) se torna 1075 + 771 = 1846 no final; representação binária de 1846 é 11100110110. O padrão resultante é assim:

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011
-------- -------- -------- -------- -------- -------- -------- --------
0x73 's' 0x6B 'k' 0x63 'c' 0x75 'u' 0x53 'S' 0x2B '+' 0x2B '+' 0x43 'C'

Esse padrão corresponde à sequência que você vê impressa, apenas ao contrário. Ao mesmo tempo, o segundo elemento da matriz torna-se zero, fornecendo terminador nulo, tornando a cadeia adequada para a passagem printf().

dasblinkenlight
fonte
22
Por que a corda está ao contrário?
Derek
95
@Derek x86 é little-endian
Angew não está mais orgulhoso de SO
16
@Derek Isso é por causa do específico da plataforma endianness : os bytes do abstrato IEEE 754 representação são armazenados na memória em endereços diminuindo, assim que a corda impresso corretamente. Em hardware com grande endianness, seria necessário começar com um número diferente.
precisa saber é o seguinte
14
@AlvinWong Você está correto, o padrão não requer o IEEE 754 ou qualquer outro formato específico. Este programa é sobre como não-portátil quanto ele ganha, ou muito próximo a ele :-)
dasblinkenlight
10
@GrijeshChauhan Usei uma calculadora IEEE754 de precisão dupla : colei o 7709179928849219valor e recuperei a representação binária.
precisa saber é o seguinte
223

Versão mais legível:

double m[2] = {7709179928849219.0, 771};
// m[0] = 7709179928849219.0;
// m[1] = 771;    

int main()
{
    if (m[1]-- != 0)
    {
        m[0] *= 2;
        main();
    }
    else
    {
        printf((char*) m);
    }
}

Recursivamente chama main()771 vezes.

No início, m[0] = 7709179928849219.0que está para C++Suc;C. Em cada chamada, m[0]é duplicado, para "reparar" as duas últimas letras. Na última chamada, m[0]contém representação de caracteres ASCII de C++Suckse m[1]contém apenas zeros, portanto, possui um terminador nulo para a C++Suckssequência. Tudo sob a suposição de que m[0]é armazenado em 8 bytes, então cada caractere leva 1 byte.

Sem recursão e main()chamada ilegal , ficará assim:

double m[] = {7709179928849219.0, 0};
for (int i = 0; i < 771; i++)
{
    m[0] *= 2;
}
printf((char*) m);
Adam Stelmaszczyk
fonte
8
É decremento pós-fixado. Por isso, será chamado 771 vezes.
Jack Aidley
106

Isenção de responsabilidade: Esta resposta foi postada na forma original da pergunta, que mencionava apenas C ++ e incluía um cabeçalho C ++. A conversão da pergunta para C puro foi feita pela comunidade, sem a contribuição do autor original.


Formalmente, é impossível argumentar sobre esse programa porque ele é mal formado (ou seja, não é C ++ legal). Ele viola o C ++ 11 [basic.start.main] p3:

A função main não deve ser usada dentro de um programa.

Além disso, ele se baseia no fato de que, em um computador de consumo típico, a doublepossui 8 bytes de comprimento e usa uma certa representação interna conhecida. Os valores iniciais da matriz são calculados para que, quando o "algoritmo" for executado, o valor final do primeiro doubleseja tal que a representação interna (8 bytes) seja o código ASCII dos 8 caracteres C++Sucks. O segundo elemento da matriz é então 0.0, cujo primeiro byte está 0na representação interna, tornando-a uma seqüência de caracteres válida no estilo C. Este é então enviado para a saída usandoprintf() .

A execução disso no HW, onde algumas das opções acima não são válidas, resultaria em texto inválido (ou talvez até mesmo um acesso fora dos limites).

Angew não está mais orgulhoso de SO
fonte
25
Devo acrescentar que isso não é uma invenção do C ++ 11 - C ++ 03 também tinha basic.start.main3.6.1 / 3 com a mesma redação.
Sharptooth
11
O objetivo deste pequeno exemplo é ilustrar o que pode ser feito com C ++. Amostra mágica usando truques de UB ou pacotes de software enormes de código "clássico".
SChepurin 01/08/13
11
Obrigado por adicionar isso. Eu não quis dizer o contrário, apenas citei o padrão que eu usei.
Angew não está mais orgulhoso de SO
@ Angew: Sim, eu entendo isso, só queria dizer que a redação é bastante antiga.
Sharptooth # 01/08
11
@ JimBalter Observe que eu disse "formalmente falando, é impossível argumentar", não "é impossível argumentar formalmente". Você está certo de que é possível argumentar sobre o programa, mas você precisa conhecer os detalhes do compilador usado para fazer isso. Seria totalmente dentro dos direitos de um compilador simplesmente eliminar a chamada main()ou substituí-la por uma chamada de API para formatar o disco rígido ou o que for.
Angew não está mais orgulhoso de SO
57

Talvez a maneira mais fácil de entender o código seja trabalhar de maneira inversa. Começaremos com uma string para imprimir - para o equilíbrio, usaremos "C ++ Rocks". Ponto crucial: assim como o original, tem exatamente oito caracteres. Como vamos fazer (aproximadamente) o original e imprimi-lo na ordem inversa, começaremos colocando-o na ordem inversa. Para nossa primeira etapa, veremos esse padrão de bits como um doublee imprimiremos o resultado:

#include <stdio.h>

char string[] = "skcoR++C";

int main(){
    printf("%f\n", *(double*)string);
}

Isso produz 3823728713643449.5. Então, queremos manipular isso de alguma maneira que não seja óbvio, mas que seja fácil de reverter. Escolho semi-arbitrariamente a multiplicação por 256, o que nos dá 978874550692723072. Agora, basta escrever um código oculto para dividir por 256 e imprimir os bytes individuais em ordem inversa:

#include <stdio.h>

double x [] = { 978874550692723072, 8 };
char *y = (char *)x;

int main(int argc, char **argv){
    if (x[1]) {
        x[0] /= 2;  
        main(--x[1], (char **)++y);
    }
    putchar(*--y);
}

Agora, temos muitos lançamentos, passando argumentos para (recursivos) mainque são completamente ignorados (mas a avaliação para obter o incremento e o decréscimo é absolutamente crucial) e, é claro, esse número de aparência completamente arbitrário para encobrir o fato de que estamos fazendo é realmente bem direto.

É claro que, como todo o ponto é ofuscação, se quisermos, também podemos dar mais passos. Apenas por exemplo, podemos tirar proveito da avaliação de curto-circuito, para transformar nossa ifdeclaração em uma única expressão, para que o corpo de main seja assim:

x[1] && (x[0] /= 2,  main(--x[1], (char **)++y));
putchar(*--y);

Para qualquer pessoa que não esteja acostumada com código ofuscado (e / ou código de golfe), isso começa a parecer bastante estranho - computar e descartar a lógica andde algum número de ponto flutuante sem sentido e o valor de retorno demain , que nem sequer está retornando um valor. Pior, sem perceber (e pensar) como funciona a avaliação em curto-circuito, pode até não ser imediatamente óbvio como evita a recursão infinita.

Nosso próximo passo provavelmente seria separar a impressão de cada caractere da localização desse caractere. Podemos fazer isso com bastante facilidade, gerando o caractere certo como valor de retorno maine imprimindo o quemain retorna:

x[1] && (x[0] /= 2,  putchar(main(--x[1], (char **)++y)));
return *--y;

Pelo menos para mim, isso parece ofuscado o suficiente, então vou deixar por isso mesmo.

Jerry Coffin
fonte
11
Adoro a abordagem forense.
ryyker
24

É apenas a construção de uma matriz dupla (16 bytes) que - se interpretada como uma matriz de caracteres - cria os códigos ASCII para a string "C ++ Sucks"

No entanto, o código não está funcionando em cada sistema, ele conta com alguns dos seguintes fatos indefinidos:

DR
fonte
12

O código a seguir é impresso C++Suc;C, portanto, toda a multiplicação é apenas para as duas últimas letras

double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);
Servir Laurijssen
fonte
11

Os outros explicaram a questão minuciosamente, gostaria de acrescentar uma observação de que esse é um comportamento indefinido de acordo com o padrão.

C ++ 11 3.6.1 / 3 Função principal

A função main não deve ser usada dentro de um programa. A ligação (3.5) do main é definida pela implementação. Um programa que define main como excluído ou que declara main como inline, estático ou constexpr está mal formado. O nome main não está reservado de outra forma. [Exemplo: funções-membro, classes e enumerações podem ser chamadas de principais, assim como as entidades em outros espaços para nome. Exemplo final]

Yu Hao
fonte
11
Eu diria que é até mal formado (como fiz na minha resposta) - viola um "deve".
Angew não está mais orgulhoso de SO
9

O código pode ser reescrito assim:

void f()
{
    if (m[1]-- != 0)
    {
        m[0] *= 2;
        f();
    } else {
          printf((char*)m);
    }
}

O que está fazendo é produzir um conjunto de bytes na doublematrizm que correspondam aos caracteres 'C ++ Sucks' seguidos por um terminador nulo. Eles ofuscaram o código escolhendo um valor duplo que, quando duplicado, 771 vezes produz, na representação padrão, esse conjunto de bytes com o terminador nulo fornecido pelo segundo membro da matriz.

Observe que esse código não funcionaria sob uma representação endian diferente. Além disso, a chamada main()não é estritamente permitida.

Jack Aidley
fonte
3
Por que seu fretorno é int?
usar o seguinte comando
11
Er, porque eu estava sem cérebro copiando o intretorno na pergunta. Deixe-me consertar isso.
Jack Aidley
1

Primeiro, devemos lembrar que números duplos de precisão são armazenados na memória em formato binário da seguinte maneira:

i) 1 bit para o sinal

(ii) 11 bits para o expoente

iii) 52 bits para a magnitude

A ordem dos bits diminui de (i) para (iii).

Primeiro, o número fracionário decimal é convertido em número binário fracionário equivalente e, em seguida, é expresso como forma de ordem de magnitude em binário.

Assim, o número 7709179928849219.0 se torna

(11011011000110111010101010011001010110010101101000011)base 2


=1.1011011000110111010101010011001010110010101101000011 * 2^52

Agora, considerando os bits de magnitude 1., é negligenciado, pois todo o método da ordem de magnitude deve começar com 1.

Então a parte da magnitude se torna:

1011011000110111010101010011001010110010101101000011 

Agora, a potência de 2 é 52 , precisamos adicionar um número de polarização como 2 ^ (bits para o expoente -1) -1 ou seja, 2 ^ (11 -1) -1 = 1023 , para que nosso expoente se torne 52 + 1023 = 1075

Agora, nosso código multiplica o número com 2 , 771 vezes, o que faz com que o expoente aumente 771

Portanto, nosso expoente é (1075 + 771) = 1846, cujo equivalente binário é (11100110110)

Agora nosso número é positivo, então nosso bit de sinal é 0 .

Portanto, nosso número modificado se torna:

bit de sinal + expoente + magnitude (concatenação simples dos bits)

0111001101101011011000110111010101010011001010110010101101000011 

como m é convertido em ponteiro de char, dividiremos o padrão de bits em pedaços de 8 do LSD

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 

(cujo equivalente hexadecimal é :)

 0x73 0x6B 0x63 0x75 0x53 0x2B 0x2B 0x43 

GRÁFICO ASCII Qual do mapa de caracteres, como mostrado, é:

s   k   c   u      S      +   +   C 

Agora que isso foi feito, m [1] é 0, o que significa um caractere NULL

Agora, supondo que você execute este programa em uma máquina little-endian (o bit de ordem inferior é armazenado no endereço inferior), então o ponteiro m aponta para o bit de endereço mais baixo e, em seguida, prossegue pegando bits em pedaços de 8 (como o tipo convertido para char * ) e o printf () para quando contado 00000000 no último chunck ...

Este código, no entanto, não é portátil.

Abhishek Ghosh
fonte