📜  在C / C++中执行main()–幕后

📅  最后修改于: 2021-05-25 20:19:32             🧑  作者: Mango

如何编写不带main()函数的C程序来打印“ Hello world”?
首先,在没有main()函数的情况下执行程序似乎是不切实际的,因为main()函数是任何程序的入口点。

首先让我们了解在Linux系统中执行C程序时,幕后情况,如何调用main()以及如何在没有main()的情况下执行程序。

该演示将考虑以下设置。

  • Ubuntu 16.4 LTS操作系统
  • GCC 5.4.0编译器
  • objdump实用程序

从C / C++编程角度看,程序入口点是main()函数。但是,从程序执行的角度来看,事实并非如此。在执行流程到达main()之前,将进行对其他几个函数的调用,这些函数会设置参数,为程序执行准备环境变量等。

编译C源代码后创建的可执行文件是可执行和可链接格式(ELF)文件。
每个ELF文件都有一个ELF头,其中有一个e_entry字段,该字段包含程序存储器地址,从该地址开始执行可执行程序。该内存地址指向_start()函数。
加载程序后,加载程序会从ELF文件头中查找e_entry字段。可执行和可链接格式(ELF)是UNIX系统中用于可执行文件,目标代码,共享库和核心转储的一种通用标准文件格式。

让我们来看一个例子。我正在创建一个example.c文件来演示这一点。

int main()
{
   return(0);
}

现在使用以下命令进行编译

gcc -o example example.c

现在创建了一个示例可执行文件,让我们使用objdump实用工具进行检查

objdump -f example

这将输出以下有关我的机器上可执行文件的重要信息。看看下面的起始地址,这是指向_start()函数的地址。

example:     file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x00000000004003e0

我们可以通过反汇编可执行文件来交叉检查该地址,输出很长,所以我只粘贴显示该地址0x00000000004003e0指向的输出

objdump --disassemble  example

输出 :

00000000004003e0 <_start>:
  4003e0:    31 ed                    xor    %ebp,%ebp
  4003e2:    49 89 d1                 mov    %rdx,%r9
  4003e5:    5e                       pop    %rsi
  4003e6:    48 89 e2                 mov    %rsp,%rdx
  4003e9:    48 83 e4 f0              and    $0xfffffffffffffff0,%rsp
  4003ed:    50                       push   %rax
  4003ee:    54                       push   %rsp
  4003ef:    49 c7 c0 60 05 40 00     mov    $0x400560,%r8
  4003f6:    48 c7 c1 f0 04 40 00     mov    $0x4004f0,%rcx
  4003fd:    48 c7 c7 d6 04 40 00     mov    $0x4004d6,%rdi
  400404:    e8 b7 ff ff ff           callq  4003c0 
  400409:    f4                       hlt    
  40040a:    66 0f 1f 44 00 00        nopw   0x0(%rax,%rax,1)

正如我们可以清楚地看到的那样,这指向_start()函数。

_start()函数

_start()函数为另一个函数_libc_start_main()准备输入参数,该函数随后将被调用。这是_libc_start_main()函数的原型。在这里,我们可以看到_start()函数准备的参数。

int __libc_start_main(int (*main) (int, char * *, char * *), /* address of main function*/
int argc, /* number of command line args*/
char ** ubp_av, /* command line arg array*/
void (*init) (void), /* address of init function*/
void (*fini) (void), /* address of fini function*/
void (*rtld_fini) (void), /* address of dynamic linker fini function */
void (* stack_end) /* end of the stack address*/
);

_libc_start_main()函数

_libc_start_main()函数如下–

  • 准备环境变量以执行程序
  • 调用_init()函数,该函数在main()函数启动之前执行初始化。
  • 注册_fini()_rtld_fini()函数以在程序终止后执行清理
      完成所有先决条件操作后,_libc_start_main()调用main()函数。

      编写没有main()的程序

      现在我们知道如何对main()进行调用了。为了明确起见,main()只是启动代码的约定术语。对于启动代码,我们可以使用任何名称,而不必一定是“ main”。由于_start()函数默认调用main(),因此如果要执行我们的自定义启动代码,则必须对其进行更改。我们可以重写_start()函数,使其调用自定义启动代码而不是main()。让我们举个例子,将其另存为nomain.c

      #include
      #include
      void _start()
      {
          int x = my_fun(); //calling custom main function
          exit(x);
      }
        
      int my_fun() // our custom main function
      {
          printf("Hello world!\n");
          return 0;
      }
      

      现在我们必须强制编译器不要使用它自己的_start()实现。在GCC中,我们可以使用-nostartfiles实现

    gcc -nostartfiles -o nomain nomain.c
    

    执行可执行文件nomain

    ./nomain
    

    输出:

    Hello world!
    

    参考

    • http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html
    • Milan Stevanovic的高级C / C++编译
    要从最佳影片策划和实践问题去学习,检查了C++基础课程为基础,以先进的C++和C++ STL课程基础加上STL。要完成从学习语言到DS Algo等的更多准备工作,请参阅“完整面试准备课程”