linux入口函数和程序初始化.docx

上传人:b****8 文档编号:23657661 上传时间:2023-05-19 格式:DOCX 页数:9 大小:21.34KB
下载 相关 举报
linux入口函数和程序初始化.docx_第1页
第1页 / 共9页
linux入口函数和程序初始化.docx_第2页
第2页 / 共9页
linux入口函数和程序初始化.docx_第3页
第3页 / 共9页
linux入口函数和程序初始化.docx_第4页
第4页 / 共9页
linux入口函数和程序初始化.docx_第5页
第5页 / 共9页
点击查看更多>>
下载资源
资源描述

linux入口函数和程序初始化.docx

《linux入口函数和程序初始化.docx》由会员分享,可在线阅读,更多相关《linux入口函数和程序初始化.docx(9页珍藏版)》请在冰豆网上搜索。

linux入口函数和程序初始化.docx

linux入口函数和程序初始化

linux:

入口函数和程序初始化

整理自P57官方网站[]

如果把一个程序比作一个世界,那么程序的启动无疑就是“创世”。

在本章里,我们将从程序的创世开始,接触到在程序背后另一类默默服务的团体。

它们能够使得程序正常地启动,能够使得各种我们熟悉的函数发挥作用,它们就是应用程序的运行库。

  11.1入口函数和程序初始化

  11.1.1程序从main开始吗

  正如基督徒认为世界的诞生起于7天创世一样,任何一个合格的C/C++程序员都应该知道一个事实:

程序从main函数开始。

但是事情的真相真是如此吗?

如果你善于观察,就会发现当程序执行到main函数的第一行时,很多事情都已经完成了:

 

  从代码中我们可以看到,在程序刚刚执行到main的时候,全局变量的初始化过程已经结束了(a的值已经确定),main函数的两个参数(argc和argv)也被正确传了进来。

此外,在你不知道的时候,堆和栈的初始化悄悄地完成了,一些系统I/O也被初始化了,因此可以放心地使用printf和malloc。

  

   在这里,对象v的构造函数,以及用于初始化全局变量g的函数foo都会在main之前调用。

  【铁证3】atexit也是一个特殊的函数。

atexit接受一个函数指针作为参数,并保证在程序正常退出(指从main里返回或调用exit函数)时,这个函数指针指向的函数会被调用。

例如:

  voidfoo(void)

  {

  printf("bye!

\n");

  }

  intmain()

  {

  atexit(&foo);

  printf("endofmain\n");

  }

  用atexit函数注册的函数的调用时机是在main结束之后,因此这段代码的输出是:

  endofmain

  bye!

  所有这些事实都在为“main创论”提供不利的证据:

操作系统装载程序之后,首先运行的代码并不是main的第一行,而是某些别的代码,这些代码负责准备好main函数执行所需要的环境,并且负责调用main函数,这时候你才可以在main函数里放心大胆地写各种代码:

申请内存、使用系统调用、触发异常、访问I/O。

在main返回之后,它会记录main函数的返回值,调用atexit注册的函数,然后结束进程。

  运行这些代码的函数称为入口函数或入口点(EntryPoint),视平台的不同而有不同的名字。

程序的入口点实际上是一个程序的初始化和结束部分,它往往是运行库的一部分。

一个典型的程序运行步骤大致如下:

  l操作系统在创建进程后,把控制权交到了程序的入口,这个入口往往是运行库中的某个入口函数。

  l入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造,等等。

  l入口函数在完成初始化之后,调用main函数,正式开始执行程序主体部分。

  lmain函数执行完毕以后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程。

  11.1.2入口函数如何实现

  大部分程序员在平时都接触不到入口函数,为了对入口函数进行详细的了解,本节我们将深入剖析glibc和MSVC的入口函数实现。

  GLIBC入口函数

  glibc的启动过程在不同的情况下差别很大,比如静态的glibc和动态的glibc的差别,glibc用于可执行文件和用于共享库的差别,这样的差别可以组合出4种情况,这里只选取最简单的静态glibc用于可执行文件的时候作为例子,其他情况诸如共享库的全局对象构造和析构跟例子中稍有出入,我们在本书中不一一详述了,有兴趣的读者可以根据这里的介绍自己阅读glibc和gcc的源代码,相信能起到举一反三的效果。

下面所有关于Glibc和MSVCCRT的相关代码分析在不额外说明的情况下,都默认为静态/可执行文件链接的情况。

  读者可以免费下载到Linux下glibc的源代码,在其中的子目录libc/csu里,有关于程序启动的代码。

glibc的程序入口为_start(这个入口是由ld链接器默认的链接脚本所指定的,我们也可以通过相关参数设定自己的入口)。

_start由汇编实现,并且和平台相关,下面可以单独看i386的_start实现:

   这里省略了一些不重要的代码,可以看到_start函数最终调用了名为__lib_start_main的函数。

加粗部分的代码是对该函数的完整调用过程,其中开始的7个压栈指令用于给函数传递参数。

在最开始的地方还有3条指令,它们的作用分别为:

  lxor%ebp,%ebp:

这其实是让ebp寄存器清零。

xor的用处是把后面的两个操作数异或,结果存储在第一个操作数里。

这样做的目的表明当前是程序的最外层函数。

  ebp设为0正好可以体现出这个最外层函数的尊贵地位J。

lpop%esi及mov%esp,%ecx:

在调用_start前,装载器会把用户的参数和环境变量压入栈中,按照其压栈的方法,实际上栈顶的元素是argc,而接着其下就是argv和环境变量的数组。

图11-1为此时的栈布局,其中虚线箭头是执行pop%esi之前的栈顶(%esp),而实线箭头是执行之后的栈顶(%esp)。

   pop%esi将argc存入了esi,而mov%esp、%ecx将栈顶地址(此时就是argv和环境变量(env)数组的起始地址)传给%ecx。

现在%esi指向argc,%ecx指向argv及环境变量数组。

  综合以上分析,我们可以把_start改写为一段更具有可读性的伪代码:

  void_start()

  {

  %ebp=0;

  intargc=popfromstack

  char**argv=topofstack;

  __libc_start_main(main,argc,argv,__libc_csu_init,__libc_csu_fini,

  edx,topofstack);

  }

  其中argv除了指向参数表外,还隐含紧接着环境变量表。

这个环境变量表要在__libc_start_main里从argv内提取出来。

  环境变量

  环境变量是存在于系统中的一些公用数据,任何程序都可以访问。

通常来说,环境变量存储的都是一些系统的公共信息,例如系统搜索路径,当前OS版本等。

环境变量的格式为key=value的字符串,C语言里可以使用getenv这个函数来获取环境变量信息。

  在Windows里,可以直接在控制面板→系统→高级→环境变量查阅当前的环境变量,而在Linux下,直接在命令行里输入export即可。

  实际执行代码的函数是__libc_start_main,由于代码很长,下面我们一段一段地看:

这是__libc_start_main的函数头部,可见和_start函数里的调用一致,一共有7个参数,其中main由第一个参数传入,紧接着是argc和argv(这里称为ubp_av,因为其中还包含了环境变量表)。

除了main的函数指针之外,外部还要传入3个函数指针,分别是:

  linit:

main调用前的初始化工作。

  lfini:

main结束后的收尾工作。

  lrtld_fini:

和动态加载有关的收尾工作,rtld是runtimeloader的缩写。

  最后的stack_end标明了栈底的地址,即最高的栈地址。

  boundedpointer

  GCC支持bounded类型指针(bounded指针用__bounded关键字标出,若默认为bounded指针,则普通指针用__unbounded标出),这种指针占用3个指针的空间,在第一个空间里存储原指针的值,第二个空间里存储下限值,第三个空间里存储上限值。

__ptrvalue、__ptrlow、__ptrhigh分别返回这3个值,有了3个值以后,内存越界错误便很容易查出来了。

并且要定义__BOUNDED_POINTERS__这个宏才有作用,否则这3个宏定义是空的。

  不过,尽管bounded指针看上去似乎很有用,但是这个功能却在2003年被去掉了。

因此现在所有关于bounded指针的关键字其实都是一个空的宏。

鉴于此,我们接下来在讨论libc代码时都默认不使用bounded指针(即不定义__BOUNDED_POINTERS__)。

  接下来的代码如下:

  char**ubp_ev=&ubp_av[argc+1];

  INIT_ARGV_and_ENVIRON;

  __libc_stack_end=stack_end;

  INIT_ARGV_and_ENVIRON这个宏定义于libc/sysdeps/generic/bp-start.h,展开后本段代码变为:

  char**ubp_ev=&ubp_av[argc+1];

  __environ=ubp_ev;

  __libc_stack_end=stack_end;

  图11-2实际上就是我们根据从_start源代码分析得到的栈布局,让__environ指针指向原来紧跟在argv数组之后的环境变量数组。

   图11-2中实线箭头代表ubp_av,而虚线箭头代表__environ。

另外这段代码还将栈底地址存储在一个全局变量里,以留作它用。

  为什么要分两步赋值给__environ呢?

这又是为了兼容bounded惹的祸。

实际上,INIT_ARGV_and_ENVIRON根据bounded支持的情况有多个版本,以上仅仅是假定不支持bounded的版本。

  接下来有另一个宏:

  DL_SYSDEP_OSCHECK(__libc_fatal);

这是用来检查操作系统的版本,宏的具体内容就不列出了。

接下来的代码颇为繁杂,我们过滤掉大量信息之后,将一些关键的函数调用列出:

  __pthread_initialize_minimal();

  __cxa_atexit(rtld_fini,NULL,NULL);

  __libc_init_first(argc,argv,__environ);

  __cxa_atexit(fini,NULL,NULL);

  (*init)(argc,argv,__environ);

  这一部分进行了一连串的函数调用,注意到__cxa_atexit函数是glibc的内部函数,等同于atexit,用于将参数指定的函数在main结束之后调用。

所以以参数传入的fini和rtld_fini均是用于main结束之后调用的。

在__libc_start_main的末尾,关键的是这两行代码:

  result=main(argc,argv,__environ);

  exit(result);

  }

  在最后,main函数终于被调用,并退出。

然后我们来看看exit的实现:

   其中__exit_funcs是存储由__cxa_atexit和atexit注册的函数的链表,而这里的这个while循环则遍历该链表并逐个调用这些注册的函数,由于其中琐碎代码过多,这里就不具体列出了。

最后的_exit函数由汇编实现,且与平台相关,下面列出i386的实现:

  _start->__libc_start_main->exit->_exit:

  _exit:

  movl4(%esp),%ebx

  movl$__NR_exit,%eax

  int$0x80

  hlt

  可见_exit的作用仅仅是调用了exit这个系统调用。

也就是说,_exit调用后,进程就会直接结束。

程序正常结束有两种情况,一种是main函数的正常返回,一种是程序中用exit退出。

在__libc_start_main里我们可以看到,即使main返回了,exit也会被调用。

exit是进程正常退出的必经之路,因此把调用用atexit注册的函数的任务交给exit来完成可以说万无一失。

   我们看到在_start和_exit的末尾都有一个hlt指令,这是作什么用的呢?

在Linux里,进程必须使用exit系统调用结束。

一旦exit被调用,程序的运行就会终止,因此实际上_exit末尾的hlt不会执行,从而__libc_start_main永远不会返回,以至_start末尾的hlt指令也不会执行。

_exit里的hlt指令是为了检测exit系统调用是否成功。

如果失败,程序就不会终止,hlt指令就可以发挥作用强行把程序给停下来。

而_start里的hlt的用处也是如此,但是为了预防某种没有调用exit(这里指的不是exit系统调用)就回到_start的情况(例如有人误删了__libc_main_start末尾的exit)。

  MSVCCRT入口函数

  相信读者对glibc的入口函数已经有了一些了解。

但可惜的是glibc的入口函数书写得不是非常直观。

事实上,我们也没从glibc的入口函数了解到多少内容。

为了从另一面看世界,我们再来看看Windows下的运行库的实现细节。

下面是MicrosoftVisualStudio2003里crt0.c(位于VC安装目录的crt\src)的一部分。

这里也删除了一些条件编译的代码,留下了比较重要的部分。

MSVC的CRT默认的入口函数名为mainCRTStartup:

   被赋值的这些变量,是VC7里面预定义的一些全局变量,其中_osver和_winver表示操作系统的版本,_winmajor是主版本号,更具体的可以查阅MSDN。

这段代码通过调用GetVersionExA(这是一个WindowsAPI)来获得当前的操作系统版本信息,并且赋值给各个全局变量。

  为什么这里为posvi分配内存不使用malloc而使用alloca呢?

是因为在程序的一开始堆还没有被初始化,而alloca是唯一可以不使用堆的动态分配机制。

alloca可以在栈上分配任意大小的空间(只要栈的大小允许),并且在函数返回的时候会自动释放,就好像局部变量一样。

  由于没有初始化堆,所以很多事情没法做,当务之急是赶紧把堆先初始化了:

  if(!

_heap_init(0))

  fast_error_exit(_RT_HEAPINIT);

  这里使用_heap_init函数对堆(heap)进行了初始化,如果堆初始化失败,那么程序就直接退出了。

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高中教育 > 语文

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1