是否可以避免 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 程序中的入口点(main)。在下面的代码中,是否可以在func()
不通过main()
下面的程序调用的情况下调用调用?如果是,如何做,什么时候需要,为什么要给出这样的规定?
int func(void)
{
printf("This is func \n");
return 0;
}
int main(void)
{
printf("This is main \n");
return 0;
}
如果您使用的是 gcc,我发现一个线程说您可以使用-e
命令行参数指定不同的入口点;因此您可以将func
其用作您的入口点,这将保持main
未使用状态。
请注意,这实际上并不允许您调用另一个例程而不是main
. 相反,它允许您调用另一个例程而不是_start
libc 启动例程——它进行一些设置,然后调用main
. 因此,如果您这样做,您将丢失一些内置在运行时库中的初始化代码,其中可能包括解析命令行参数之类的内容。在使用它之前阅读这个参数。
如果您使用的是其他编译器,则可能有也可能没有此参数。
在构建直接从 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 具有命名函数的 EXPORTS 部分foo
,则可以编写类似的实用程序来加载和调用它。当然,您不需要对 做任何特别的事情main
,因为这将是自然的切入点。当然,在您的实用程序中初始化的 C 运行时可能与与可执行文件链接的 C 运行时不同。(谷歌提示“DLL Hell”。)在这种情况下,您的实用程序可能需要更智能。例如,它可以充当调试器,加载带有断点的 EXE main
,运行到该断点,然后将 PC 更改为指向或进入foo
并从那里继续。
在 Linux 上可能会出现某种类似的诡计,因为 .so 文件在某些方面也与真正的可执行文件相似。当然,可以使像调试器一样工作的方法起作用。
一个经验法则是系统提供的加载程序将始终运行 main。有了足够的权限和能力,理论上你可以编写一个不同的加载器来做其他事情。
将 main 重命名为 func 并将 func 重命名为 main 并从名称中调用 func。
如果您可以访问源代码,则可以执行此操作,而且很容易。
如果您使用诸如 GCC 之类的开源编译器或针对嵌入式系统的编译器,您可以修改 C 运行时启动 (CRT) 以在您需要的任何入口点启动。在 GCC 中,此代码位于 crt0.s 中。通常,此代码部分或全部在汇编程序中,对于大多数嵌入式系统编译器将提供示例或默认启动代码。
然而,一种更简单的方法是简单地“隐藏” main() 在链接到代码的静态库中。如果 main() 的实现看起来像:
int main(void)
{
func() ;
}
然后它将查看所有意图和目的,就好像用户入口点是 func()。这是有多少具有除 main() 之外的入口点的应用程序框架工作。请注意,因为它位于静态库中,所以 main() 的任何用户定义都将覆盖该静态库版本。
解决方案取决于您使用的编译器和链接器。Always is that not main
是应用程序的真正入口点。真正的入口点进行一些初始化和调用,例如main
。如果您使用 Visual Studio 为 Windows 编写程序,则可以使用链接器的 /ENTRY 开关覆盖默认入口点mainCRTStartup
并调用func()
而不是main()
:
#ifdef NDEBUG
void mainCRTStartup()
{
ExitProcess (func());
}
#endif
如果您编写最小的应用程序,那么如果是标准做法。在这种情况下,您将收到使用 C-Runtime 函数的限制。您应该使用 Windows API 函数而不是 C-Runtime 函数。例如,printf("This is func \n")
您应该使用OutputString(TEXT("This is func \n"))
where OutputString
are 仅针对WriteFile
or实现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));
}
这实际上取决于您如何调用二进制文件,并且将合理地针对平台和环境。最明显的答案是简单地将“main”符号重命名为其他符号并将“func”称为“main”,但我怀疑这不是你想要做的。