Este código C ofuscado afirma ser executado sem um main (), mas o que ele realmente faz?

84

Isso chama indiretamente main? quão?

Rajeev Singh
fonte
146
As macros definidas expandem começam a dizer "principal". É apenas um truque. Nada interessante.
rghome
10
Seu conjunto de ferramentas deve ter uma opção para deixar o código pré-processado em um arquivo - o arquivo real que é compilado - onde você o verá, de fato, tem um main ()
@rghome Por que não postar como resposta? E é claramente interessante, dado o número de votos positivos.
Matsemann de
3
@Matsemann Uau! Eu não percebi os votos positivos. Eu poderia alterá-lo para uma resposta, e se os votos positivos de comentário fossem votos positivos de resposta, seria de longe minha melhor pontuação, mas já existe uma resposta detalhada. Acho que o ponto do meu comentário é que não é realmente interessante e, portanto, funciona como uma alternativa para as pessoas que não querem votar a favor da resposta. Obrigado por apontar isso.
rghome de
Pessoal, Cabe ao vinculador como ferramenta do sistema operacional definir o ponto de entrada, e não o idioma em si. Você pode até definir nosso próprio ponto de entrada e criar uma biblioteca que também seja executável! unix.stackexchange.com/a/223415/37799
Ho1

Respostas:

194

A linguagem C define o ambiente de execução em duas categorias: independente e hospedado . Em ambos os ambientes de execução, uma função é chamada pelo ambiente para a inicialização do programa.
Em um ambiente autônomo, a função de inicialização do programa pode ser definida pela implementação, enquanto no ambiente hospedado deveria ser main. Nenhum programa em C pode ser executado sem a função de inicialização do programa nos ambientes definidos.

No seu caso, mainestá oculto pelas definições do pré-processador. begin()irá expandir para o decode(a,n,i,m,a,t,e)qual ainda será expandido main.


decode(s,t,u,m,p,e,d)é uma macro parametrizada com 7 parâmetros. A lista de substituição para esta macro é m##s##u##t. m, s, ue tsão , , e parâmetros usados ​​na lista de substituição.

O descanso é inútil ( apenas para ofuscar ). O argumento passado para decodeé " a , n , i , m , a, t, e", portanto, os identificadores m, s, ue tsão substituídos pelos argumentos m, a, ie n, respectivamente.

haccks
fonte
11
@GrijeshChauhan todos os compiladores C processam as macros, isso é exigido por todos os padrões C desde C89.
jdarthenay
17
Isso é totalmente errado. Posso usar no Linux _start(). Ou ainda em um nível mais baixo, posso tentar apenas alinhar o início do meu programa com o endereço para o qual o IP é definido após a inicialização. main()é a biblioteca C Standard . O próprio C não impõe restrições sobre isso.
ljrk
1
@haccks A biblioteca padrão define um ponto de entrada. O idioma em si não importa
ljrk
3
Você pode explicar como decode(a,n,i,m,a,t,e)se tornou m##a##i##n? Substitui personagens? Você pode fornecer um link para a documentação da decodefunção? Obrigado.
AL
1
@AL First beginé definido para ser substituído pelo decode(a,n,i,m,a,t,e)que foi definido antes. Esta função pega os argumentos s,t,u,m,p,e,de os concatena nesta forma m##s##u##t( ##significa concatenar). Ou seja, ele ignora os valores de p, e e d. Conforme você "chama" decodecom s = a, t = n, u = i, m = m, ele efetivamente substitui beginpor main.
ljrk
71

Tente usar gcc -E source.c, a saída termina com:

Portanto, uma main()função é gerada pelo pré-processador.

Jdarthenay
fonte
37

O programa em questão faz chamada main()devido à expansão macro, mas o seu pressuposto é falho - que não tem que chamar main()em tudo!

A rigor, você pode ter um programa C e compilá-lo sem ter um mainsímbolo. mainé algo para o qual o c libraryespera saltar, depois de concluir sua própria inicialização. Normalmente, você salta para a mainpartir do símbolo libc conhecido como _start. É sempre possível ter um programa muito válido, que simplesmente execute assembly, sem ter um principal. Dê uma olhada neste:

Compile o acima com gcc -nostdlib without_main.ce veja a impressão Hello World!na tela apenas emitindo chamadas de sistema (interrupções) em assembly embutido.

Para obter mais informações sobre este problema específico, verifique o blog ksplice

Outra questão interessante, é que você também pode ter um programa que compila sem que o mainsímbolo corresponda a uma função C. Por exemplo, você pode ter o seguinte como um programa C muito válido, que só faz o compilador reclamar quando você sobe o nível de Avisos.

Os valores na matriz são bytes que correspondem às instruções necessárias para imprimir Hello World na tela. Para um relato mais detalhado de como esse programa específico funciona, dê uma olhada neste post do blog , que é onde eu também o li primeiro.

Quero fazer um último aviso sobre esses programas. Não sei se eles se registram como programas C válidos de acordo com a especificação da linguagem C, mas compilá-los e executá-los é certamente muito possível, mesmo que eles violem a própria especificação.

NlightNFotis
fonte
1
O nome faz _startparte de um padrão definido ou é apenas específico da implementação? Certamente o seu "principal como um array" é específico da arquitetura. Também importante, não seria irracional que seu truque "principal como um array" falhasse em tempo de execução devido a restrições de segurança (embora isso seja mais provável se você não usar o constqualificador, e muitos sistemas ainda permitiriam).
mah
1
@mah: _startnão está no padrão ELF, embora o AMD64 psABI contenha uma referência _startem 3.4 Inicialização do processo . Oficialmente, ELF só conhece o endereço em e_entryno cabeçalho ELF, _starté apenas um nome que a implementação escolheu.
ninjalj,
1
@mah Também importante, seria razoável que seu truque "principal como um array" falhasse em tempo de execução devido a restrições de segurança (embora isso fosse mais provável se você não usasse o qualificador const, e ainda assim muitos sistemas permitiriam isto). Somente se o executável final for de alguma forma distinguível como algo inseguro - um executável binário é um executável binário, não importa como ele foi parar lá. E constnão importa nem um pouco - o nome do símbolo nesse arquivo executável binário é main. Nem mais nem menos. consté uma construção C que não significa nada em tempo de execução.
Andrew Henle
1
@Stewart: certamente falha no ARMv6l (falha de segmentação). Mas deve funcionar em qualquer arquitetura x86-64.
esquerda por volta de
@AndrewHenle um executável binário é um executável binário, não importa como foi parar - não é exatamente verdade. Um executável binário não é um único blob de instruções executáveis, é um blob de partições cuidadosamente mapeado, algumas das quais são instruções, algumas das quais são dados somente leitura e outras são dados a serem inicializados em dados de leitura e gravação. (Alguns) MMUs de hardware de segurança podem impedir a execução de páginas não marcadas como tal, e este é um bom recurso para evitar, por exemplo, estouros de pilha que levam à execução de código na pilha, mas infelizmente isso às vezes é legítimo ou geralmente não habilitado.
maio
30

Alguém está tentando agir como mágico. Ele acha que pode nos enganar. Mas todos nós sabemos, a execução do programa c começa com main().

O int begin()será substituído decode(a,n,i,m,a,t,e)por uma passagem do estágio de pré-processador. Então, novamente, decode(a,n,i,m,a,t,e)será substituído por m ## a ## i ## n. Como por associação posicional de chamada de macro, svontade tem um valor de caráter a. Da mesma forma, userá substituído por 'i' e tserá substituído por 'n'. E é assim que m##s##u##tvai se tornarmain

Quanto ao ##símbolo na expansão da macro, é o operador de pré-processamento e realiza a colagem do token. Quando uma macro é expandida, os dois tokens de cada lado de cada operador '##' são combinados em um único token, que então substitui o '##' e os dois tokens originais na expansão da macro.

Se você não acredita em mim, você pode compilar seu código com -Eflag. Isso interromperá o processo de compilação após o pré-processamento e você poderá ver o resultado da colagem do token.

abhiarora
fonte
11

decode(a,b,c,d,[...])embaralha os primeiros quatro argumentos e os junta para obter um novo identificador, na ordem dacb. (Os três argumentos restantes são ignorados.) Por exemplo, decode(a,n,i,m,[...])fornece o identificador main. Observe que é assim que a beginmacro é definida.

Portanto, a beginmacro é simplesmente definida como main.

Frxstrem
fonte
2

Em seu exemplo, a main()função está realmente presente, porque beginé uma macro que o compilador substitui por decodemacro que por sua vez é substituída pela expressão m ## s ## u ## t. Usando expansão macro ##, você alcançará a palavra mainde decode. Este é um traço:

É apenas um truque para se ter main(), mas usar o nome main()para a função de entrada do programa não é necessário na linguagem de programação C. Depende de seus sistemas operacionais e do vinculador como uma de suas ferramentas.

No Windows, você nem sempre usa main(), mas sim WinMainouwWinMain , embora possa usar main(), mesmo com o conjunto de ferramentas da Microsoft . No Linux, pode-se usar _start.

Cabe ao vinculador, como ferramenta do sistema operacional, definir o ponto de entrada, e não o idioma em si. Você pode até definir nosso próprio ponto de entrada e criar uma biblioteca que também seja executável !

Ho1
fonte
@vaxquis Você está certo, mas esta é uma resposta parcial que escrevi para complementar / corrigir a primeira resposta que vincula a main()função à linguagem de programação C, o que não é correto.
Ho1 de
@vaxquis Presumi que explicar "a função main () não é essencial em programas C" seria uma resposta parcial. Eu adicionei um parágrafo para tornar a resposta completa. - Ho1 há 16 minutos
Ho1 de