Este código C ofuscado pretende ejecutarse sin un main (), pero ¿qué hace realmente?

84
#include <stdio.h>
#define decode(s,t,u,m,p,e,d) m##s##u##t
#define begin decode(a,n,i,m,a,t,e)

int begin()
{
    printf("Ha HA see how it is?? ");
}

¿Esto llama indirectamente main? ¿cómo?

Rajeev Singh
fuente
146
Las macros definidas para expandir comienzan a decir "principal". Es solo un truco. Nada interesante.
rghome
10
Su cadena de herramientas debe tener una opción para dejar el código preprocesado en un archivo, el archivo real que está compilado, donde lo verá, de hecho, tiene un main ()
@rghome ¿Por qué no publicar como respuesta? Y es claramente interesante, dada la cantidad de votos a favor.
Matsemann
3
@Matsemann ¡Guau! No me di cuenta de los votos a favor. Podría cambiarlo a una respuesta, y si los votos a favor de los comentarios fueran votos a favor de la respuesta, sería de lejos mi mejor puntuación, pero ya hay una respuesta detallada. Creo que el punto de mi comentario es que no es realmente interesante y, por lo tanto, actúa como una alternativa para las personas que no quieren votar a favor de la respuesta. Sin embargo, gracias por señalarlo.
rghome
Chicos, depende del enlazador como herramienta del sistema operativo establecer el punto de entrada, y no el idioma en sí. ¡Incluso puede establecer nuestro propio punto de entrada y puede crear una biblioteca que también sea ejecutable! unix.stackexchange.com/a/223415/37799
Ho1

Respuestas:

193

El lenguaje C define el entorno de ejecución en dos categorías: independiente y alojado . En ambos entornos de ejecución, el entorno llama a una función para el inicio del programa.
En un entorno independiente , la función de inicio del programa se puede definir por implementación, mientras que en el entorno alojado debería serlo main. Ningún programa en C puede ejecutarse sin la función de inicio del programa en los entornos definidos.

En su caso, mainestá oculto por las definiciones del preprocesador. begin()se expandirá a lo decode(a,n,i,m,a,t,e)que más se expandirá main.

int begin() -> int decode(a,n,i,m,a,t,e)() -> int m##a##i##n() -> int main() 

decode(s,t,u,m,p,e,d)es una macro parametrizada con 7 parámetros. La lista de reemplazo para esta macro es m##s##u##t. m, s, uy tson 4 º , 1 st , 3 rd y 2 nd parámetro utilizado en la lista de reemplazo.

s, t, u, m, p, e, d
1  2  3  4  5  6  7

El descanso no sirve de nada ( solo para ofuscar ). El argumento que se pasa a decodees " a , n , i , m , a, t, e", por lo que los identificadores m, s, uy tse reemplazan con argumentos m, a, iy n, respectivamente.

 m --> m  
 s --> a 
 u --> i 
 t --> n
haccks
fuente
11
@GrijeshChauhan todos los compiladores de C procesan las macros, es requerido por todos los estándares de C desde C89.
jdarthenay
17
Eso es claramente incorrecto. En Linux puedo usar _start(). O incluso en un nivel más bajo, puedo intentar alinear el inicio de mi programa con la dirección a la que se establece la IP después del inicio. main()es la biblioteca C estándar . C en sí mismo no impone restricciones sobre esto.
ljrk
1
@haccks La biblioteca estándar define un punto de entrada. Al idioma en sí no le importa
ljrk
3
¿Puede explicar cómo se decode(a,n,i,m,a,t,e)convierte m##a##i##n? ¿Reemplaza a los personajes? ¿Puede proporcionar un enlace a la documentación de la decodefunción? Gracias.
AL
1
@AL First beginse define para ser reemplazado por lo decode(a,n,i,m,a,t,e)que se define antes. Esta función toma los argumentos s,t,u,m,p,e,dy los concatena en esta forma m##s##u##t( ##significa concatenar). Es decir, ignora los valores de p, e y d. A medida que "llama" decodecon s = a, t = n, u = i, m = m, efectivamente reemplaza begincon main.
ljrk
71

Intente usar gcc -E source.c, la salida termina con:

int main()
{
    printf("Ha HA see how it is?? ");
}

Entonces, una main()función es realmente generada por el preprocesador.

jdarthenay
fuente
37

El programa en cuestión hace la llamada main()debido a la expansión de la macro, pero su suposición es errónea - que no tiene que llamar main()a todos!

Estrictamente hablando, puede tener un programa en C y poder compilarlo sin tener un mainsímbolo. maines algo a lo que c libraryespera saltar, después de que haya terminado su propia inicialización. Por lo general, salta maindesde el símbolo libc conocido como _start. Siempre es posible tener un programa muy válido, que simplemente ejecute ensamblado, sin tener un main. Mira esto:

/* This must be compiled with the flag -nostdlib because otherwise the
 * linker will complain about multiple definitions of the symbol _start
 * (one here and one in glibc) and a missing reference to symbol main
 * (that the libc expects to be linked against).
 */

void
_start ()
{
    /* calling the write system call, with the arguments in this order:
     * 1. the stdout file descriptor
     * 2. the buffer we want to print (Here it's just a string literal).
     * 3. the amount of bytes we want to write.
     */
    asm ("int $0x80"::"a"(4), "b"(1), "c"("Hello world!\n"), "d"(13));
    asm ("int $0x80"::"a"(1), "b"(0)); /* calling exit syscall, with the argument to be 0 */
}

Compile lo anterior con gcc -nostdlib without_main.c, y véalo imprimiendo Hello World!en la pantalla simplemente emitiendo llamadas al sistema (interrupciones) en ensamblado en línea.

Para obtener más información sobre este problema en particular, consulte el blog de ksplice

Otro tema interesante, es que también puedes tener un programa que compile sin que el mainsímbolo corresponda a una función C. Por ejemplo, puede tener lo siguiente como un programa en C muy válido, que solo hace que el compilador gime cuando sube el nivel de Advertencias.

/* These values are extracted from the decimal representation of the instructions
 * of a hello world program written in asm, that gdb provides.
 */
const int main[] = {
    -443987883, 440, 113408, -1922629632,
    4149, 899584, 84869120, 15544,
    266023168, 1818576901, 1461743468, 1684828783,
    -1017312735
};

Los valores de la matriz son bytes que corresponden a las instrucciones necesarias para imprimir Hello World en la pantalla. Para obtener una descripción más detallada de cómo funciona este programa específico, eche un vistazo a esta publicación de blog , que es donde también la leí primero.

Quiero hacer un último aviso sobre estos programas. No sé si se registran como programas C válidos de acuerdo con la especificación del lenguaje C, pero compilarlos y ejecutarlos es ciertamente muy posible, incluso si violan la especificación en sí.

NocheNFotis
fuente
1
¿El nombre de _startparte de un estándar definido o es solo una implementación específica? Ciertamente, su "principal como matriz" es específico de la arquitectura. También es importante que no sea irrazonable que su truco "main as an array" falle en tiempo de ejecución debido a restricciones de seguridad (aunque eso sería más probable si no usara el constcalificador, y aún así muchos sistemas lo permitirían).
mah
1
@mah: _startno está en el estándar ELF, aunque el AMD64 psABI contiene una referencia a _startal 3.4 proceso de inicialización . Oficialmente, ELF solo conoce la dirección e_entryen el encabezado ELF, _startes solo un nombre que eligió la implementación.
ninjalj
1
@mah También es importante, no sería irrazonable que su truco "principal como matriz" fallara en tiempo de ejecución debido a restricciones de seguridad (aunque eso sería más probable si no usara el calificador const, y aún así muchos sistemas lo permitirían eso). Solo si el ejecutable final es de alguna manera distinguible como algo inseguro: un ejecutable binario es un ejecutable binario sin importar cómo llegó allí. Y constno importará ni un bit, el nombre del símbolo en ese archivo ejecutable binario es main. Ni mas ni menos. constes una construcción de C que no significa nada en el momento de la ejecución.
Andrew Henle
1
@ Stewart: ciertamente falla en ARMv6l (falla de segmentación). Pero debería funcionar en cualquier arquitectura x86-64.
izquierda rotonda sobre el
@AndrewHenle un ejecutable binario es un ejecutable binario sin importar cómo llegó allí , no es exactamente cierto. Un ejecutable binario no es un solo blob de instrucciones ejecutables, es un blob de particiones cuidadosamente mapeadas, algunas de las cuales son instrucciones, algunas de las cuales son datos de solo lectura y algunas de las cuales son datos que se inicializan en datos de lectura y escritura. (Algunas) MMU de hardware de seguridad pueden evitar la ejecución de páginas no marcadas como tales, y esta es una buena característica para evitar, por ejemplo, desbordamientos de pila que llevan a ejecutar código en la pila, pero lamentablemente eso a veces es legítimo o, a menudo, no está habilitado.
mah
30

Alguien está tratando de actuar como un mago. Cree que puede engañarnos. Pero todos sabemos que la ejecución del programa c comienza con main().

El int begin()será reemplazado decode(a,n,i,m,a,t,e)por una pasada de la etapa de preprocesador. Luego, nuevamente, decode(a,n,i,m,a,t,e)será reemplazado por m ## a ## i ## n. Como por asociación posicional de llamada macro, stendrá un valor de carácter a. Asimismo, userá reemplazado por 'i' y tserá reemplazado por 'n'. Y así es como m##s##u##tse convertirámain

En cuanto al ##símbolo en la expansión macro, es el operador de preprocesamiento y realiza el pegado de tokens. Cuando se expande una macro, los dos tokens a cada lado de cada operador '##' se combinan en un solo token, que luego reemplaza el '##' y los dos tokens originales en la expansión de macro.

Si no me cree, puede compilar su código con -Eflag. Detendrá el proceso de compilación después del preprocesamiento y podrá ver el resultado de pegar el token.

gcc -E FILENAME.c
abhiarora
fuente
11

decode(a,b,c,d,[...])mezcla los primeros cuatro argumentos y los une para obtener un nuevo identificador, en el orden dacb. (Los tres argumentos restantes se ignoran). Por ejemplo, decode(a,n,i,m,[...])proporciona el identificador main. Tenga en cuenta que así beginse define la macro.

Por lo tanto, la beginmacro se define simplemente como main.

Frxstrem
fuente
2

En su ejemplo, la main()función está realmente presente, porque begines una macro que el compilador reemplaza con una decodemacro que a su vez reemplaza por la expresión m ## s ## u ## t. Usando la expansión macro ##, llegará a la palabra mainde decode. Este es un rastro:

begin --> decode(a,n,i,m,a,t,e) --> m##parameter1##parameter3##parameter2 ---> main

Es solo un truco main(), pero usar el nombre main()de la función de entrada del programa no es necesario en el lenguaje de programación C. Depende de sus sistemas operativos y del vinculador como una de sus herramientas.

En Windows, no siempre se utiliza main(), pero en lugar WinMainowWinMain , aunque se puede utilizar main(), incluso con cadena de herramientas de Microsoft . En Linux, se puede usar _start.

Depende del vinculador como herramienta del sistema operativo establecer el punto de entrada, y no el idioma en sí. ¡Incluso puede establecer nuestro propio punto de entrada y puede crear una biblioteca que también sea ejecutable !

Ho1
fuente
@vaxquis Tienes razón, pero esta es una respuesta parcial que escribí para complementar / corregir la primera respuesta que vincula la main()función al lenguaje de programación C, lo cual no es correcto.
Ho1
@vaxquis Supuse que explicar "la función main () no es esencial en los programas C" sería una respuesta parcial. He añadido un párrafo para completar la respuesta. - Ho1 Hace 16 minutos
Ho1