`
lobin
  • 浏览: 430874 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

C: 入口函数

 
阅读更多

入口函数

C的入口函数是main,C++的入口函数也是这个。这是相对于C程序来说,对于C/C++运行时来说,入口函数其实是mainCRTStartup函数,main实际上应该称为user entry函数。

 

入口函数形式

入口函数名称指定为main,main函数形式有多种:主要表现在main函数参数以及返回值类型

 

函数参数和返回值类型可以任意组合。但参数要么没有,要么是int argc, char** argv形式。返回值类型要么是void,要么是int类型。

 

无参无返回

void main() {

 

}

 

无参返回int类型

int main(){

 

}

 

有参无返回

void main(int argc, char** argv) {

 

}

 

有参返回int类型

int main(int argc, char** argv) {

 

}

 

以下面的例子为例来讲解入口函数,采用gcc编译器:

#include <stdio.h>

void main()
{
  printf("hello, c!\n");
}

这是一段简单的C程序,只有一个入口函数main函数。

 

编译链接:

>gcc maintest.c -o maintest

>maintest

hello, c!

 

指定入口函数

C程序必须要有入口函数,这个函数就是main函数。但实际上这个说法是不准确的,C程序是必须要有入口函数,但并不非得是main函数。

 

可以通过-e main(main为入口函数名)或者--entry=main(main为入口函数名,这种方式其实已经没用了。gcc: extraneous argument to '--entry' option)来指定入口函数

 

# gcc -nostdlib maintest.c -o maintest

/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 00000000080480b8

/tmp/ccJd078d.o: In function `main':

maintest.c:(.text+0x11): undefined reference to `puts'

collect2: ld returned 1 exit status

 

# gcc -nostartfiles maintest.c -o maintest

/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 00000000080481ec

 

这里在指定-nostartfiles后编译链接有个警告提示,这个警告提示可以通过指定-e main(main为入口函数名)或者--entry=main(main为入口函数名,这种方式其实已经没用了。gcc: extraneous argument to '--entry' option)来消除

 

# gcc -nostartfiles -e main maintest.c -o maintest

 

# ./maintest

hello, c!

Segmentation fault

执行的时候有正常输出,但最后报错:段错误(Segmentation fault)。程序还是有问题。

 

 

下面是在Windows Cygwin下的编译,编译结果表现不一样:

 

>gcc -nostdlib maintest.c -o maintest

/cygdrive/c/DOCUME~1/ADMINI~1/LOCALS~1/Temp/cclEIYe9.o:maintest.c:(.te

xt+0xa): undefined reference to `___main'

/cygdrive/c/DOCUME~1/ADMINI~1/LOCALS~1/Temp/cclEIYe9.o:maintest.c:(.te

xt+0x16): undefined reference to `_puts'

collect2: ld returned 1 exit status

 

>gcc -nostartfiles maintest.c -o maintest

这里在指定-nostartfiles后编译链接没报什么错。

 

>.\maintest

运行没有任何输出。

 

上面的程序例子在执行的时候会报一个段错误(Segmentation fault),这里稍微对这个程序改一下:

#include <stdio.h>
#include <stdlib.h>

void main()
{
  printf("hello, c!\n");

  exit(0);
}

 

编译链接:

# gcc mainexittest.c -o mainexittest

# ./mainexittest 

hello, c!

 

# gcc -nostdlib mainexittest.c -o mainexittest

/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 00000000080480b8

/tmp/ccXCYx2g.o: In function `main':

mainexittest.c:(.text+0x11): undefined reference to `puts'

mainexittest.c:(.text+0x1d): undefined reference to `exit'

collect2: ld returned 1 exit status

 

# gcc -nostartfiles mainexittest.c -o mainexittest

/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 000000000804821c

 

这里在指定-nostartfiles后编译链接有个警告提示,这个警告提示可以通过指定-e main(main为入口函数名)或者--entry=main(main为入口函数名,这种方式其实已经没用了。gcc: extraneous argument to '--entry' option)来消除

 

# gcc -nostartfiles -e main mainexittest.c -o mainexittest

 

# ./maintest

hello, c!

这里就没有再报段错误(Segmentation fault)了。

 

我们可以不在程序中编写main入口函数,使用自己编写的一个函数作为入口函数。在下面这个程序中没有入口函数main函数:

#include <stdio.h>
#include <stdlib.h>

int start()
{
  printf("hello, c!\n");
  exit(0);
}

我们也可以将它编译出可执行程序。在编译链接的时候通过指定-nostartfiles,在程序链接的时候不使用标准系统启动文件(standard system startup files)

写道
-nostartfiles
Do not use the standard system startup files when linking. The standard system libraries are used normally, unless -nostdlib, -nolibc, or -nodefaultlibs is used.

同时指定-e start来指定入口函数:

 

写道

 

-e entry
--entry=entry
Specify that the program entry point is entry. The argument is interpreted by the linker; the GNU linker accepts either a symbol name or an address.

编译链接:

# gcc maintest2.c -o maintest2

/usr/lib/gcc/i686-redhat-linux/4.4.7/../../../crt1.o: In function `_start':

(.text+0x18): undefined reference to `main'

collect2: ld returned 1 exit status

因为没有入口函数main函数,这里编译链接的时候失败了。

 

# gcc -nostartfiles maintest2.c -o maintest2

/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 000000000804821c

 

这里在指定-nostartfiles后编译链接有个警告提示,这个警告提示可以通过指定-e start(start为入口函数名)或者--entry=start(start为入口函数名,这种方式其实已经没用了。gcc: extraneous argument to '--entry' option)来消除

 

# gcc -nostartfiles -e start maintest2.c -o maintest2

 

# ./maintest2

hello, c!

 

下面是在Windows Cygwin下的编译,编译结果表现不一样:

 

>gcc -nostartfiles maintest2.c -o maintest2

这里编译链接没报什么错。

 

>.\maintest2

运行没有任何输出。

 

入口函数main编译后的汇编代码

 

void main() 
{

}

>gcc -S stacktestmain.c -o stactestmain.S

 

 

	.file	"stacktestmain.c"
	.text
.globl main
	.type	main, @function
main:
	pushl	%ebp
	movl	%esp, %ebp
	popl	%ebp
	ret
	.size	main, .-main
	.ident	"GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-23)"
	.section	.note.GNU-stack,"",@progbits

下面是在Windows Cygwin下gcc反编译出来的汇编代码:

 

	.file	"stacktestmain.c"
	.def	___main;	.scl	2;	.type	32;	.endef
	.text
.globl _main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	pushl	%ebp
	movl	%esp, %ebp
	andl	$-16, %esp
	call	___main
	movl	%ebp, %esp
	popl	%ebp
	ret

其中andl $-16, %esp这条汇编指令是干什么的?

 

 

#include <stdio.h>

int main()
{
  printf("Hello, World\n");
  return 0;
}
$ gcc -E main.c -o main.i

 

也可以使用cpp(C PreProcessor预处理器命令)执行预处理:

$ cpp main.c -o main.i

预处理后的结果如下:

 

# 1 "main.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 330 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "main.c" 2
... ...
... ...
int printf(const char * restrict, ...) __attribute__((__format__ (__printf__, 1, 2)));
... ...
int puts(const char *);
... ...
... ...
extern int __vsnprintf_chk (char * restrict, size_t, int, size_t,
       const char * restrict, va_list);
# 499 "/usr/include/stdio.h" 2 3 4
# 2 "main.c" 2

int main()
{
  printf("Hello, World\n");
  return 0;
}
上面预处理后的程序经过了些裁剪,只保留了一些主要的信息。

 

$ gcc -S main.i -o main.S

编译后的汇编代码如下:

 

	.section	__TEXT,__text,regular,pure_instructions
	.macosx_version_min 10, 12
	.globl	_main
	.align	4, 0x90
_main:                                  ## @main
	.cfi_startproc
## BB#0:
	pushq	%rbp
Ltmp0:
	.cfi_def_cfa_offset 16
Ltmp1:
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
Ltmp2:
	.cfi_def_cfa_register %rbp
	subq	$16, %rsp
	leaq	L_.str(%rip), %rdi
	movl	$0, -4(%rbp)
	movb	$0, %al
	callq	_printf
	xorl	%ecx, %ecx
	movl	%eax, -8(%rbp)          ## 4-byte Spill
	movl	%ecx, %eax
	addq	$16, %rsp
	popq	%rbp
	retq
	.cfi_endproc

	.section	__TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
	.asciz	"Hello, World\n"


.subsections_via_symbols
$ gcc -c main.S -o main.o

 

汇编之后生成对象文件,到这里就生成了最终代码。也可以通过as汇编器来进行汇编:

$ as main.S -o main.o

 

$ gcc main.o -o main

链接生成可执行程序,到这里就生成了最终的程序,可以执行。

$ ./main

Hello, World

链接的时候也可以通过ld链接器完成链接

$ ld -o main main.o

ld: warning: -macosx_version_min not specified, assuming 10.10

ld: warning: object file (main.o) was built for newer OSX version (10.12) than being linked (10.10)

Undefined symbols for architecture x86_64:

  "_printf", referenced from:

      _main in main.o

  "start", referenced from:

     implicit entry/start for main executable

ld: symbol(s) not found for inferred architecture x86_64

这里在链接的时候出错了,没有找到_printf和start符号。程序中调用了printf函数,这里的_printf就是调用printf函数的时候要找到的符号,这个可以理解。至于start符号就不太好理解了,我们在程序中并没有用到呀。先一个个来看。

首先看printf,这个是标准库实现的函数,如libc、glibc。对应的标准库就是libc.dylib,在/usr/lib目录下。

$ ld -o main main.o /usr/lib/libc.dylib 

ld: warning: -macosx_version_min not specified, assuming 10.10

ld: warning: object file (main.o) was built for newer OSX version (10.12) than being linked (10.10)

Undefined symbols for architecture x86_64:

  "start", referenced from:

     implicit entry/start for main executable

ld: symbol(s) not found for inferred architecture x86_64

这样就没有报没有找到_printft符号的错误了。

至于start,在程序中并没有用到,这个其实是编译器提供的,属于运行时库的部分。类似用来引导到main函数执行的,不然程序怎么找到main函数并从main函数开始执行,程序必要要有一个入口函数,这个入口函数默认就是main函数,尽管我们可以指定一个入口函数,这个入口函数并非就一定是main,入口函数名可以是任意的。

 

start函数可以在crt1.o中找到,这是个对象文件,我们可以像main.o那样直接链接进来:

$ ld -o main main.o /usr/lib/crt1.o /usr/lib/libc.dylib

ld: warning: -macosx_version_min not specified, assuming 10.10

ld: warning: object file (main.o) was built for newer OSX version (10.12) than being linked (10.10)

这里出现了两个警告,但可以运行了

$ ./main

 

Hello, World

我们可以指定macosx支持的最低版本-macosx_version_min 10.12.0来消除这个警告信息。这个是系统支持的最低版本。

$ ld -macosx_version_min 10.12.0 -o main main.o /usr/lib/crt1.o /usr/lib/libc.dylib

这样就不会出现警告了。至于这个版本为什么是10.12.0,我们可以通过gcc编译的时候指定-v可以看到在链接的时候指定的macosx支持的最低版本,也就是使用的是当前系统的版本。

$ gcc main.c -o main -v

 

这个其实也不一定就得是10.12.0,得看兼容情况。而且这个10.12.0也不是非的10.12.0,指定成10.12也行。

$ ld -macosx_version_min 10.12 -o main main.o /usr/lib/crt1.o /usr/lib/libc.dylib

根据上面的警告信息,因为没有指定-macosx_version_min,链接的时候它默认的是10.10,我们在指定-macosx_version_min的时候指定为10.10也行,只要兼容就行。

$ ld -macosx_version_min 10.10 -o main main.o /usr/lib/crt1.o /usr/lib/libc.dylib

ld: warning: object file (main.o) was built for newer OSX version (10.12) than being linked (10.10)

 

也可以以库的形式链接进来,对应的库为libclang_rt.osx.a,这个库的完整路径我这里是/Users/admin/Downloads/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../lib/clang/8.0.0/lib/darwin/libclang_rt.osx.a,如下:

$ ld -arch x86_64 -o main main.o /Users/admin/Downloads/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../lib/clang/8.0.0/lib/darwin/libclang_rt.osx.a /usr/lib/libc.dylib 

ld: warning: -macosx_version_min not specified, assuming 10.10

ld: warning: object file (main.o) was built for newer OSX version (10.12) than being linked (10.10)

注意这里需要指定-arch x86_64。

 

start函数

以下代码来自clang编译器提供的运行时库,libc/loader/linux/x86_64,参考LLVM项目,这里我参考的是llvm-project-llvmorg-12.0.0。

extern "C" void _start() {
  uintptr_t *frame_ptr =
      reinterpret_cast<uintptr_t *>(__builtin_frame_address(0));

  // This TU is compiled with -fno-omit-frame-pointer. Hence, the previous value
  // of the base pointer is pushed on to the stack. So, we step over it (the
  // "+ 1" below) to get to the args.
  Args *args = reinterpret_cast<Args *>(frame_ptr + 1);

  // After the argv array, is a 8-byte long NULL value before the array of env
  // values. The end of the env values is marked by another 8-byte long NULL
  // value. We step over it (the "+ 1" below) to get to the env values.
  uint64_t *env_ptr = args->argv + args->argc + 1;
  uint64_t *env_end_marker = env_ptr;
  while (*env_end_marker)
    ++env_end_marker;

  // After the env array, is the aux-vector. The end of the aux-vector is
  // denoted by an AT_NULL entry.
  Elf64_Phdr *programHdrTable = nullptr;
  uintptr_t programHdrCount;
  for (AuxEntry *aux_entry = reinterpret_cast<AuxEntry *>(env_end_marker + 1);
       aux_entry->type != AT_NULL; ++aux_entry) {
    switch (aux_entry->type) {
    case AT_PHDR:
      programHdrTable = reinterpret_cast<Elf64_Phdr *>(aux_entry->value);
      break;
    case AT_PHNUM:
      programHdrCount = aux_entry->value;
      break;
    case AT_PAGESZ:
      app.pageSize = aux_entry->value;
      break;
    default:
      break; // TODO: Read other useful entries from the aux vector.
    }
  }

  for (uintptr_t i = 0; i < programHdrCount; ++i) {
    Elf64_Phdr *phdr = programHdrTable + i;
    if (phdr->p_type != PT_TLS)
      continue;
    // TODO: p_vaddr value has to be adjusted for static-pie executables.
    app.tls.address = phdr->p_vaddr;
    app.tls.size = phdr->p_memsz;
    app.tls.align = phdr->p_align;
  }

  __llvm_libc::initTLS();

  __llvm_libc::syscall(SYS_exit,
                       main(args->argc, reinterpret_cast<char **>(args->argv),
                            reinterpret_cast<char **>(env_ptr)));
}

这里先主要看最后一条代码:

__llvm_libc::syscall(SYS_exit,

                       main(args->argc, reinterpret_cast<char **>(args->argv),

                            reinterpret_cast<char **>(env_ptr)));

入口main函数就是从这里调用的。这条代码的意思是调用main函数,函数返回之后调用syscall系统调用,这个系统就是SYS_exit,类似调用exit函数,main函数返回值作为参数传给syscall。程序终止,终止返回的值就是main函数返回的值。

 

initTLS函数代码如下:

void initTLS() {
  if (app.tls.size == 0)
    return;

  // We will assume the alignment is always a power of two.
  uintptr_t tlsSize = (app.tls.size + app.tls.align) & -app.tls.align;

  // Per the x86_64 TLS ABI, the entry pointed to by the thread pointer is the
  // address of the TLS block. So, we add more size to accomodate this address
  // entry.
  size_t tlsSizeWithAddr = tlsSize + sizeof(uintptr_t);

  // We cannot call the mmap function here as the functions set errno on
  // failure. Since errno is implemented via a thread local variable, we cannot
  // use errno before TLS is setup.
  long mmapRetVal = __llvm_libc::syscall(
      mmapSyscallNumber, nullptr, tlsSizeWithAddr, PROT_READ | PROT_WRITE,
      MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
  // We cannot check the return value with MAP_FAILED as that is the return
  // of the mmap function and not the mmap syscall.
  if (mmapRetVal < 0 && static_cast<uintptr_t>(mmapRetVal) > -app.pageSize)
    __llvm_libc::syscall(SYS_exit, 1);
  uintptr_t *tlsAddr = reinterpret_cast<uintptr_t *>(mmapRetVal);

  // x86_64 TLS faces down from the thread pointer with the first entry
  // pointing to the address of the first real TLS byte.
  uintptr_t endPtr = reinterpret_cast<uintptr_t>(tlsAddr) + tlsSize;
  *reinterpret_cast<uintptr_t *>(endPtr) = endPtr;

  __llvm_libc::memcpy(tlsAddr, reinterpret_cast<const void *>(app.tls.address),
                      app.tls.size);
  if (__llvm_libc::syscall(SYS_arch_prctl, ARCH_SET_FS, endPtr) == -1)
    __llvm_libc::syscall(SYS_exit, 1);
}

 

 

__llvm_libc::syscall的代码可以参考libc/config/linux/x86_64/syscall.h.inc

__attribute__((always_inline)) inline long syscall(long __number, long __arg1) {
  long retcode;
  LIBC_INLINE_ASM("syscall"
                  : "=a"(retcode)
                  : "a"(__number), "D"(__arg1)
                  : SYSCALL_CLOBBER_LIST);
  return retcode;
}

_start函数是由loader加载器调用的。

 

 

 

0
1
分享到:
评论

相关推荐

    嵌入式学习一阶段-C语言:函数

    4. **主函数`main()`**:每个C程序都有一个特殊的函数`main()`,它是程序的入口点。所有程序的执行都从`main()`开始。 5. **局部变量与全局变量**:局部变量在函数内部定义,只在该函数内可见;全局变量在整个程序...

    入口函数( main、WinMain)

    在C/C++编程中,入口函数是程序执行的起点,它是程序执行的首个被执行的代码块。最常见的入口函数是`main`函数,它的标准形式如下: ```cpp int main(int argc, char* argv[]) { // 程序代码 return 0; } ``` 在...

    汇编入口点 汇编 入口 函数汇编 入口 函数

    在汇编语言中,入口函数通常由一系列特定的指令组成,这些指令包括但不限于: 1. **栈初始化**:设置栈指针寄存器(如x86架构下的ESP/RSP),为程序的局部变量和调用函数预留空间。 2. **数据段初始化**:如果程序...

    C高级语言程序设计:08函数简介.pptx

    本文将围绕《C高级语言程序设计:08函数简介》这一主题展开详细论述,深入探讨C语言中的函数概念及其应用。通过理解函数的基本定义、作用、分类以及如何在实际编程中合理运用函数,读者将能够更好地掌握C语言的模块...

    DLL函数查看器,查看DLL文件入口函数

    本篇文章将深入探讨如何使用DLL函数查看器来查看C语言编写的DLL文件的入口函数。 首先,我们要理解DLL文件的结构。在C语言中,DLL的入口点通常是`DllMain`函数,这是DLL加载和卸载时被系统调用的地方。`DllMain`...

    让易语言的DLL不依赖任何插件都能拥有完整的入口函数(DllMain)

    本篇文章将深入探讨如何使易语言编译的DLL不依赖任何插件就能拥有完整的入口函数DllMain。 首先,我们来理解DllMain函数。DllMain是DLL的核心入口点,当DLL被加载到进程地址空间或卸载时,操作系统会调用这个函数。...

    C语言:指针函数和函数指针

    每个函数在编译时都有一个唯一的入口地址,函数指针可以存储这个地址。定义一个函数指针变量时,我们需要指定它所指向的函数的参数类型和返回类型。例如: ```c float fun(int a, int b) { return (float)(a + b);...

    关于c语言函数大全总结

    例如,计算阶乘的递归函数: ```c int factorial(int n) { if (n == 0 || n == 1) return 1; else return n * factorial(n - 1); } ``` 7. **局部变量和全局变量**: - 局部变量仅在函数内部可见,而全局...

    c语言学习笔记劫持函数劫持函数.docx

    "C语言学习笔记劫持函数" C语言学习笔记劫持函数是关于使用C语言实现函数劫持的笔记。函数劫持是指在程序运行时,动态地替换或修改函数的行为,以达到特定的目的。在这个笔记中,我们将使用Detours库来实现函数劫持...

    C语言基础知识函数学习教案.pptx

    `main()`函数是特殊的,它是程序的入口点,可以调用其他所有函数,但不能被其他函数调用。 4. **函数类型与返回值**:每个函数都有一个特定的返回值类型,定义了函数执行完毕后返回的数据类型。如果函数没有返回值...

    常用C语言函数程序大全

    1. `abort` 函数:这个函数用于异常终止当前进程,通常在遇到无法恢复的错误时使用。调用 `abort()` 会导致程序立即停止执行并生成核心转储(如果系统设置允许)。 2. `abs` 函数:这是一个用于计算整数绝对值的...

    uboot中C语言代码入口函数(start_armboot)的注释.docx

    在U-Boot(通用引导加载程序)中,`start_armboot`函数是C语言代码的入口点,负责启动过程中的核心任务。理解`start_armboot`的运作方式,需要先了解一些关键的数据结构,如`gd_t`和`bd_t`,以及初始化函数列表。 1...

    C语言程序设计函数图文PPT学习教案.pptx

    5. 递归函数: - 函数可以调用自身,这就是递归。递归在解决某些问题时非常有效,如计算阶乘或遍历树结构。但要注意防止无限递归。 6. 内联函数: - 内联函数是为了提高效率而设计的,它试图避免函数调用的开销。...

    初学C语言:01C语言概述

    - **函数定义**:C程序由一个或多个函数组成,其中`main`函数是程序的入口点。 - **语句**:包括控制语句(如`if`,`for`,`while`)和表达式语句(如函数调用,赋值操作)。 - **注释**:使用`/*`和`*/`或`//`进行...

    C语言:基于C实现的辗转相除法

    2. main函数:C语言程序的入口点是main()函数。程序的执行总是从main函数开始,结束于main函数的末尾。 3. 数据类型int:int是C语言中的整数类型,用于存储整数值。在辗转相除法的实现中,变量u和v用int类型表示,...

    c语言函数查询软件

    5. **主函数**:`main()`函数是C程序的入口点,所有程序执行始于`main()`。`int main()`通常是标准的主函数声明,返回一个整数值表示程序的退出状态。 6. **库函数**:C语言标准库提供了一组预定义的函数,如数学...

    C语言编写S函数模版

    C语言编写S函数时,通常会遵循MATLAB提供的特定模板,包括几个关键的函数定义,如`simulink.c`或`ssFunction.c`。 1. **初始化函数**:这是S函数的入口点,通常命名为`sfuntmpl_init()`。在这个函数中,我们会设置S...

    学生信息管理系统C语言版.doc

    学生信息管理系统C语言版 学生信息管理系统是使用C语言开发的一款学生信息管理软件,能够实现学生信息的添加、修改...18. 主函数main():该函数是程序的入口点,用于加载学生信息、显示菜单、处理用户输入和退出程序。

    c语言第八章函数学习教案.pptx

    - C程序通常由多个函数组成,其中`main`函数是程序的入口点,每个程序至少有一个`main`函数。 - 程序可以包含任意数量的自定义函数,这些函数可以互相调用,但`main`函数不能被其他函数直接调用。 - 程序的执行...

    C语言函数习题及答案.doc

    第4题:在一个 C 程序中,main 函数可以在任何地方出现,但它必须是程序的入口点。 第5题:若在 C 语言中未说明函数的类型,则系统默认该函数的数据类型是int类型。 第6题:函数未被调用时,系统将不为形参分配...

Global site tag (gtag.js) - Google Analytics