【问题标题】:Avoiding the main (entry point) in a C program避免 C 程序中的 main(入口点)
【发布时间】:2011-03-23 16:37:57
【问题描述】:

是否可以避免 C 程序中的入口点(main)。在下面的代码中,是否可以在下面的程序中调用func() 而无需通过main() 调用?如果是,如何做,什么时候需要,为什么要给出这样的规定?

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

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

【问题讨论】:

  • 你为什么需要这样做?
  • 在 C++ 中,全局静态对象的 ctor 可能在 main() 之前运行。
  • 改写 Oded 的问题:告诉我们您想要实现什么,我们会告诉您如何实现它,可能不会绕过main。 (更具体地说:一些 SOer 是。我缺乏 C 知识使我无法帮助您。)
  • 这是我在讨论各种棘手的 C 问题时遇到的一个问题 :-) 我也想知道它的必要性和用途。
  • 在 C 中 - 不。一些编译器/平台可能会提供实现它的方法。您有什么特别的平台吗?

标签: c compiler-construction function operating-system entry-point


【解决方案1】:

如果您使用 gcc,我发现一个线程说您可以使用 -e command-line parameter 指定不同的入口点;所以你可以使用func 作为你的入口点,这将使main 未被使用。

请注意,这实际上并不允许您调用另一个例程而不是 main。相反,它允许您调用另一个例程而不是 _start,这是 libc 启动例程——它进行一些设置,然后 it 调用 main。因此,如果您这样做,您将丢失一些内置在运行时库中的初始化代码,其中可能包括解析命令行参数等内容。使用前请阅读此参数。

如果您使用的是其他编译器,则可能有也可能没有此参数。

【讨论】:

  • 有趣的信息。 + 1 为此。它是否还提供了确定不同入口点存在的规定?
  • 可能不会,因为该参数会影响链接器,而不是编译器。但是为什么你需要检测它呢?你是编译你的应用程序的人,所以你会知道你是否以这种方式构建它。
  • 您还可以在链接器中使用 ENTRY 命令。见ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_node/ld_24.html
【解决方案2】:

在构建直接从 ROM 运行的嵌入式系统固件时,我通常会避免将入口点命名为 main(),以向代码审查者强调代码的特殊性质。在这些情况下,我提供了 C 运行时启动模块的定制版本,因此很容易将其对 main() 的调用替换为另一个名称,例如 BootLoader()

我(或我的供应商)几乎总是必须在这些系统中自定义 C 运行时启动,因为 RAM 需要初始化代码才能开始正确运行并不罕见。例如,典型的 DRAM 芯片需要对其控制硬件进行大量配置,并且通常需要大量(数千个总线时钟周期)延迟才能使用。在完成之前,甚至可能没有放置调用堆栈的地方,因此启动代码可能无法调用任何函数。即使 RAM 设备在通电时运行,也几乎总是有一定数量的片选硬件或一两个 FPGA 需要初始化,然后才能安全地让 C 运行时开始其初始化。

当一个用 C 编写的程序加载并启动时,某些组件负责使调用main() 的环境存在。在 Unix、Linux、Windows 和其他交互式环境中,大部分工作都是加载程序的操作系统组件的自然结果。然而,即使在这些环境中,在调用main() 之前也需要进行一些初始化工作。如果代码真的是 C++,那么可能会有大量工作,包括调用所有全局对象实例的构造函数。

所有这些的细节都由链接器及其配置和控制文件处理。链接器 ld(1) 有一个非常精细的控制文件,它准确地告诉它要在输出中包含哪些段、在哪些地址以及以什么顺序。找到您隐式用于工具链的链接器控制文件并阅读它可能具有指导意义,链接器本身的参考手册和可执行文件必须遵循的 ABI 标准才能运行。

编辑:更直接地回答在更常见的上下文中提出的问题:“你可以调用 foo 而不是 main 吗?”答案是“也许可以,但只能通过狡猾”。

在 Windows 上,可执行文件和 DLL 几乎是相同的文件格式。可以编写一个程序,在运行时加载任意命名的 DLL,并在其中定位任意函数并调用它。一个这样的程序实际上是作为标准 Windows 发行版的一部分提供的:rundll32.exe

由于可以通过处理 .DLL 文件的相同 API 加载和检查 .EXE 文件,原则上如果 .EXE 有一个将函数命名为 foo 的 EXPORTS 部分,则可以编写类似的实用程序来加载并调用它。当然,您不需要对main 做任何特别的事情,因为这将是自然的切入点。当然,在您的实用程序中初始化的 C 运行时可能与与可执行文件链接的 C 运行时不同。 (谷歌提示“DLL Hell”。)在这种情况下,您的实用程序可能需要更智能。例如,它可以充当调试器,加载带有断点 main 的 EXE,运行到该断点,然后将 PC 更改为指向或指向 foo 并从那里继续。

由于 .so 文件在某些​​方面与真正的可执行文件相似,因此在 Linux 上可能会出现某种类似的诡计。当然,像调试器一样工作的方法是可行的。

【讨论】:

  • 也许比 OP 想知道的要多,但无论如何读起来都很有趣!
  • @Carl,即使您从不需要使用这些知识,偶尔也可以窥探一下内部情况......并且尝试实际实现链接器是一项非常有启发性的练习 ;-)
  • 哦,绝对!实际上,大约 20 年前,我为嵌入式系统做了类似的事情,为嵌入式系统自定义重新定位 Turbo C 编译代码。不过,我很高兴我不必再这样做了:)
  • 构建加载程序以将 EXE 放入 ROM 中也是一项有趣的练习。在这一点上,我讨厌所有的“老笨蛋”,但有一种诱惑是抱怨这些年轻人如何永远不必真正看到他们正在使用的比特。然后是关于从前面板开关手动加载引导加载程序的故事,以便纸带阅读器可以加载真正的程序.... ;-)
【解决方案3】:

根据经验,系统提供的加载程序将始终运行 main。如果有足够的权限和能力,理论上你可以编写一个不同的加载器来做其他事情。

【讨论】:

    【解决方案4】:

    将 main 重命名为 func,将 func 重命名为 main,并从 name 中调用 func。

    如果您可以访问源代码,则可以这样做,而且很容易。

    【讨论】:

      【解决方案5】:

      如果您使用的是 GCC 等开源编译器或针对嵌入式系统的编译器,您可以修改 C 运行时启动 (CRT) 以在您需要的任何入口点启动。在 GCC 中,此代码位于 crt0.s 中。通常此代码部分或全部在汇编程序中,对于大多数嵌入式系统编译器将提供示例或默认启动代码。

      然而,一种更简单的方法是简单地“隐藏”main() 到您链接到代码的静态库中。如果 main() 的实现看起来像:

      int main(void)
      {
          func() ;
      }
      

      然后它将查看所​​有意图和目的,就好像用户入口点是 func()。这是有多少具有除 main() 之外的入口点的应用程序框架工作。请注意,因为它位于静态库中,所以 main() 的任何用户定义都将覆盖该静态库版本。

      【讨论】:

        【解决方案6】:

        解决方案取决于您使用的编译器和链接器。始终不是 main 是应用程序的真正入口点。真正的入口点进行一些初始化并调用例如main。如果您使用 Visual Studio 为 Windows 编写程序,您可以使用链接器的 /ENTRY 开关覆盖默认入口点 mainCRTStartup 并调用 func() 而不是 main()

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

        如果您编写最小的应用程序,这是一种标准做法。在这种情况下,您将收到使用 C-Runtime 函数的限制。您应该使用 Windows API 函数而不是 C-Runtime 函数。例如,您应该使用OutputString(TEXT("This is func \n")) 而不是printf("This is func \n"),其中OutputString 仅针对WriteFileWriteConsole 实现:

        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));
        }
        

        【讨论】:

          【解决方案7】:

          这实际上取决于您如何调用二进制文件,并且将合理地针对特定平台和环境。最明显的答案是简单地将“main”符号重命名为其他符号并将“func”称为“main”,但我怀疑这不是你想要做的。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-10-14
            • 2012-11-26
            • 1970-01-01
            • 1970-01-01
            • 2011-01-22
            • 1970-01-01
            相关资源
            最近更新 更多