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 .
skcuS++C
.
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 .
skcuS++C
.
Respostas:
O número 7709179928849219.0
tem 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()
.
7709179928849219
valor e recuperei a representação binária.
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.0
que 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++Sucks
e m[1]
contém apenas zeros, portanto, possui um terminador nulo para a C++Sucks
sequê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);
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 double
possui 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 double
seja 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á 0
na 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).
basic.start.main
3.6.1 / 3 com a mesma redação.
main()
ou substituí-la por uma chamada de API para formatar o disco rígido ou o que for.
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 double
e 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) main
que 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 if
declaraçã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 and
de 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 main
e 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.
É 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:
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);
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]
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 double
matrizm
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.
f
retorno é int
?
int
retorno na pergunta. Deixe-me consertar isso.
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
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.