16

Es posible evitar el punto de entrada (principal) en un programa C. En el siguiente código, ¿es posible invocar la llamada func() sin llamar a través del main() en el siguiente programa? En caso afirmativo, ¿cómo hacerlo y cuándo se requeriría y por qué se da tal disposición?Evitar el principal (punto de entrada) en un programa C

int func(void) 
{ 
    printf("This is func \n"); 
    return 0; 
} 

int main(void) 
{ 
    printf("This is main \n"); 
    return 0; 
} 
+9

¿Por qué necesitarías hacer eso alguna vez? – Oded

+2

En C++, un ctor de un objeto estático global puede ejecutarse antes que main(). – seand

+0

Para reescribir la pregunta de Oded: díganos qué quiere * lograr * y le diremos cómo lograrlo, probablemente sin eludir 'principal'. (Más específicamente: algo así como. Mi falta de conocimiento en C me impide ayudarlo.) – MvanGeest

Respuesta

22

Si está utilizando gcc, encontré un hilo que decía que puede usar el -e command-line parameter para especificar un punto de entrada diferente; por lo que podría usar func como punto de entrada, lo que dejaría sin usar main.

Tenga en cuenta que esto realmente no le permite llamar a otra rutina en lugar de main. En su lugar, le permite llamar a otra rutina en lugar de _start, que es la rutina de inicio de libc: realiza algunas configuraciones y luego llamadas main. Por lo tanto, si hace esto, perderá parte del código de inicialización que está integrado en su biblioteca de tiempo de ejecución, que puede incluir aspectos como el análisis de argumentos de la línea de comandos. Lea sobre este parámetro antes de usarlo.

Si está utilizando otro compilador, puede haber o no un parámetro para esto.

+0

Información interesante. + 1 por eso. ¿También proporciona una disposición para determinar la presencia de diferentes puntos de entrada? –

+0

Probablemente no, ya que ese parámetro afecta al enlazador, no al compilador. Pero ¿por qué necesitarías detectarlo? Tú eres el que está compilando tu aplicación, para que sepas si la estás construyendo de esta manera o no. –

0

Esto depende realmente cómo está invocando el binario, y va a ser razonablemente plataforma y entorno específico. La respuesta más obvia es simplemente cambiar el nombre del símbolo "principal" a otra cosa y llamar "func" "principal", pero sospecho que eso no es lo que estás tratando de hacer.

5

Una regla de oro sería que el cargador suministrado por el sistema sería siempre ejecutar principal. Con suficiente autoridad y competencia, teóricamente podrías escribir un cargador diferente que hiciera otra cosa.

3

Si está utilizando un compilador de código abierto como GCC o un compilador dirigido a sistemas integrados, puede modificar el inicio de ejecución de C (CRT) para comenzar en cualquier punto de entrada que necesite. En GCC este código está en crt0.s. En general, este código es parcial o totalmente en ensamblador, para la mayoría de los sistemas incorporados se proporcionará un ejemplo de compiladores o un código de inicio predeterminado.

Sin embargo, un enfoque más simple es simplemente 'ocultar' main() en una biblioteca estática que enlaza con su código. Si que la aplicación de main() se parece a:

int main(void) 
{ 
    func() ; 
} 

entonces se verá a todos los efectos como si el punto de entrada de usuario es func(). Esta es la cantidad de marcos de aplicación con puntos de entrada distintos de main(). Tenga en cuenta que, como está en una biblioteca estática, cualquier definición de usuario de main() anulará esa versión de biblioteca estática.

16

Al construir el firmware de los sistemas integrados para que se ejecute directamente desde la ROM, a menudo evitaré nombrar el punto de entrada main() para enfatizar a un revisor de código la naturaleza especial del código. En estos casos, estoy suministrando una versión personalizada del módulo de inicio C runtime, por lo que es fácil reemplazar su llamada a main() con otro nombre como BootLoader().

I (o mi proveedor) casi siempre tenemos que personalizar el inicio del tiempo de ejecución de C en estos sistemas porque no es inusual que la RAM requiera un código de inicialización para que comience a funcionar correctamente. Por ejemplo, los chips DRAM típicos requieren una cantidad sorprendente de configuración de su hardware de control, y a menudo requieren un retraso sustancial (miles de ciclos de bus) antes de que sean útiles.Hasta que eso se complete, puede que ni siquiera haya un lugar para colocar la pila de llamadas, por lo que es posible que el código de inicio no pueda llamar a ninguna función. Incluso si los dispositivos RAM están operativos al encenderse, casi siempre hay cierta cantidad de hardware de selección de chips o un FPGA o dos que requieren inicialización antes de que sea seguro dejar que el tiempo de ejecución de C inicie su inicialización.

Cuando un programa escrito en C carga e inicia, algunos componentes son responsables de que exista el entorno en el que se llama main(). En Unix, Linux, Windows y otros entornos interactivos, gran parte de ese esfuerzo es una consecuencia natural del componente del sistema operativo que carga el programa. Sin embargo, incluso en estos entornos hay una cierta cantidad de trabajo de inicialización antes de llamar al main(). Si el código es realmente C++, puede haber una cantidad sustancial de trabajo que incluye llamar a los constructores para todas las instancias de objetos globales.

Los detalles de todo esto son manejados por el enlazador y sus archivos de configuración y control. El enlazador ld (1) tiene un archivo de control muy elaborado que le dice exactamente qué segmentos incluir en la salida, en qué direcciones y en qué orden. Encontrar el archivo de control del enlazador que está usando implícitamente para su cadena de herramientas y leerlo puede ser instructivo, al igual que el manual de referencia para el enlazador mismo y el estándar ABI que sus ejecutables deben seguir para poder ejecutar.

Editar: Para responder más directamente a la pregunta como se pregunta en un contexto más común: "¿Se puede llamar a foo en lugar de main?" La respuesta es "Tal vez, pero solo por ser complicado".

En Windows, un archivo ejecutable y una DLL tienen casi el mismo formato de archivo. Es posible escribir un programa que carga una DLL arbitraria nombrada en tiempo de ejecución, y localiza una función arbitraria dentro de ella, y la llama. Uno de estos programas en realidad se envía como parte de una distribución estándar de Windows: rundll32.exe.

Dado que un archivo .EXE puede cargarse e inspeccionarse mediante las mismas API que manejan archivos .DLL, en principio, si .EXE tiene una sección de EXPORTACIONES que nombra la función foo, se podría escribir una utilidad similar para cargar y invocarlo. No necesita hacer nada especial con main, por supuesto, ya que ese será el punto de entrada natural. Por supuesto, el tiempo de ejecución de C que se inicializó en su utilidad puede no ser el mismo tiempo de ejecución de C que se vinculó con su ejecutable. (Google para "DLL Hell" para la sugerencia.) En ese caso, su utilidad podría necesitar ser más inteligente. Por ejemplo, podría actuar como un depurador, cargar el EXE con un punto de interrupción en main, ejecutar hasta ese punto de interrupción, luego cambiar la PC para apuntar ao en foo y continuar desde allí.

Algún tipo de engaño similar podría ser posible en Linux ya que los archivos .so también son similares en algunos aspectos a los ejecutables verdaderos. Ciertamente, el enfoque de actuar como un depurador podría funcionar.

+0

¡Quizás más que el OP siempre quiso saber, pero lectura fascinante en cualquier caso! –

+0

@Carl, incluso si nunca necesita utilizar el conocimiento, puede ser útil mirar de vez en cuando ... y tratar de implementar realmente un enlazador es un ejercicio extremadamente instructivo ;-) – RBerteig

+0

¡Oh, absolutamente! De hecho, hice algo como esto para un sistema integrado hace unos 20 años, la reubicación personalizada del código compilado de Turbo C para un sistema integrado. Aún así, estoy contento de no tener que hacer eso más :) –

1

La solución depende del compilador y del vinculador que utilice. Siempre es que nomain es el punto de entrada real de la aplicación. El punto de entrada real realiza algunas inicializaciones y llama por ejemplo main. Si escribir programas para Windows utilizando Visual Studio, puede utilizar/interruptor de entrada de los enlazador para sobrescribir el punto mainCRTStartup entrada por defecto y llamar func() en lugar de main():

#ifdef NDEBUG 
void mainCRTStartup() 
{ 
    ExitProcess (func()); 
} 
#endif 

Si es una práctica habitual si se escribe la mayor parte pequeña aplicación. En el caso, recibirá restricciones en el uso de las funciones de C-Runtime. Debería usar la función API de Windows en lugar de la función C-Runtime.Por ejemplo en lugar de printf("This is func \n") se debe utilizar OutputString(TEXT("This is func \n")) donde OutputString se implementan sólo respecto de WriteFile o WriteConsole:

static HANDLE g_hStdOutput = INVALID_HANDLE_VALUE; 
static BOOL g_bConsoleOutput = TRUE; 

BOOL InitializeStdOutput() 
{ 
    g_hStdOutput = GetStdHandle (STD_OUTPUT_HANDLE); 
    if (g_hStdOutput == INVALID_HANDLE_VALUE) 
     return FALSE; 

    g_bConsoleOutput = (GetFileType (g_hStdOutput) & ~FILE_TYPE_REMOTE) != FILE_TYPE_DISK; 
#ifdef UNICODE 
    if (!g_bConsoleOutput && GetFileSize (g_hStdOutput, NULL) == 0) { 
     DWORD n; 

     WriteFile (g_hStdOutput, "\xFF\xFE", 2, &n, NULL); 
    } 
#endif 

    return TRUE; 
} 

void Output (LPCTSTR pszString, UINT uStringLength) 
{ 
    DWORD n; 

    if (g_bConsoleOutput) { 
#ifdef UNICODE 
     WriteConsole (g_hStdOutput, pszString, uStringLength, &n, NULL); 
#else 
     CHAR szOemString[MAX_PATH]; 
     CharToOem (pszString, szOemString); 
     WriteConsole (g_hStdOutput, szOemString, uStringLength, &n, NULL); 
#endif 
    } 
    else 
#ifdef UNICODE 
     WriteFile (g_hStdOutput, pszString, uStringLength * sizeof (TCHAR), &n, NULL); 
#else 
    { 
     //PSTR pszOemString = _alloca ((uStringLength + sizeof(DWORD))); 
     CHAR szOemString[MAX_PATH]; 
     CharToOem (pszString, szOemString); 
     WriteFile (g_hStdOutput, szOemString, uStringLength, &n, NULL); 
    } 
#endif 
} 

void OutputString (LPCTSTR pszString) 
{ 
    Output (pszString, lstrlen (pszString)); 
} 
4

Cambiar el nombre principal para ser func y func siendo el principal y llamar func del nombre.

Si tiene acceso a la fuente, puede hacerlo y es fácil.

Cuestiones relacionadas