文章目录
- 一个程序比作一个世界,那么程序启动无疑就是“创世”。
- 本章从程序的创世开始,接触到在程序背后另一类默默服务的团体。
- 它们能够使得程序正常地启动,能够使得各种我们熟悉的函数发挥作用,它们就是应用程序的运行库。
11.1入口函数和程序初始化
11.1.1程序从main开始吗
- 程序从main函数开始。
- 真是如此吗?如果你善于观察,
- 发现当程序执行到main函数的第一行时,很多事情都已经完成:
- 程序刚刚执行到main时,全局变量的初始化过程已经结束了(a已经确定),
- main函数的两个参数(argc和argv)也被正确传了进来。
- 堆和栈的初始化悄悄地完成了,一些系统O也被初始化了,
- 因此可以放心用printf和 malloc
- 对象v的构造函数,用于初始化全局变量g的函数foo都在main前调
- 操作系统装载程序之后,首先运行的代码并不是main的第一行,
- 而是某些别的代码,这些代码负责准备好main函数执行所需要的环境,且负责调用main函数,这时候你才可以在main函数里放心大胆地写各种代码:申请内存、使用系统调用、触发异常、访问IO。
- main返回后,它会记录main的返回值,调用atexit注册的函数,然后结束进程
- 运行这些代码的函数称为入口函数或入口点,视平台的不同而有不同的名字。
- 程序的入口点实际上是一个程序的初始化和结束部分,它往往是运行库的一部分
- 一个典型的程序运行步骤大致如下
- OS在创建进程后,把控制权交到了程序入口,这个入口往往是运行库中的某个入口函数。
- 入口函数对运行库和程序运行环境进行初始化,包括堆、IO、线程、全局变量构造,等
- 入口函数在完成初始化之后,调main函数,正式开始执行程序主体部分。
- main函数执行完后,返回到入口函数,入口函数清理工作,包括全局变量析构、堆销毁、关闭O等,然后进行系统调用结東进程。
11.1.2入口函数如何实现
- 大部分程序员在平时都接触不到入口函数,为了对入口函数进行详细的了解,本节深入剖析 glibc和MSVC的入口函数实现。
GLIBC入口函数
- glibc的启动过程在不同的情况下差别很大,
- 静态的 glibc和动态的 glibc
- glibc用于可执行文件和用于共享库的差别,
- 可组合出4种情况,
- 这里只选取最简单的静态
- glibc用于可执行文件的时候作例子,
- 其他情况诸如共享库的全局对象构造和析构跟例子中稍有出入,
- 本书中不羊述了,自己阅读 glibc和gcc的源代码
- 下面所有关于 Glibc和MsVC CRT的代码分析在不额外说明的情况下,都默认为静态/可执行文件链接的情况
- 免费下载Linux下glibc源码,
- libc/csu里,有关于程序启动的代码。
- glibc的程序入口为_start(由ld链接器默认的链接脚本所指定,也可设定自己的入口)。
- _start由汇编实现,且和平台相关,
- 看i386的_start实现:
- 省略一些不重要代码,_star函数最终调用__lib_start_main
函数。 - 加粗是对该函数的完整调用过程,
- 7个压栈指令用于给函数传参。
- 在最开始的地方还有3条指令,作用分别为
异或,同为零,
- xor %ebp,%ebp:
- ebp寄存器清零
- 目的表明当前是程序的最外层函数。
- ebp设为0体现出这个最外层函数的尊贵地位。
- pop %esi及 movl %esp,%ecx:
- 调 _start前,装载器会把用户的参数和环境变量入栈
- 按其压栈的方法,实际上栈顶的元素是argc,而接着其下就是argv和环境变量的数组
- 图11-1为此时的栈布局,
- 虚线箭头是执行pop %esi前的機顶(%esp),
- 而实线箭头是执行之后的栈顶(%esp)。
是不是很好奇,为啥刚开始环境变量和参数就在栈上啦,这是谁干得啊!!答案:程序执行前装载器把用户的参数和环境变量压入栈,
- pop %esi将argc存入esi,
- movl %esp、%ecx将栈顶(argv和环境变量数组的起始地址)传给%ecx。
- %esi指向argc,%ecx指向argv及环境变量数组
意思就是:这个环境变量表要在__libc_start_main里面用到啊,所以要从argv里提取出来啊
- 环境变量是存在于系统中的一些公用数据,任何程序都可以访问。
- 环境变量存储的都是一些系统的公共信息,
- 系统搜索路径,
- 当前OS版本。
- 环境变量的格式
- key= value的字符串,C用geten函数来获取环境变量信息。
- Windows里,可以直接在控制面板→系统→高级→环境变量查阋当前的环境变量
- Linux下,直接在命令行里输入 export
- 实际执行代码是
- __libc_start_main,
- 很长
- _star函数里的调用一致,
- 7参
- main由第一个参数传入,
- argc和argv(这里称ubp_av,还包含环境变量表)。
- 除main的函数指针之外,
- 还要传3个函数指针
- init:main调用前的初始化工作
- fini:main结束后的收尾工作。
- rtld_fini:和动态加载有关的收尾工作,
- runtime loader
- stack_end标明栈底的地址,即最高的栈地址。
- GCC支持 bounded类型指针( 用bounded关键字标出,若默认为
bounded指针,则普通指针用 unbounded标出),这种指针占用3个指针的空间,第一个空间里存储原指针的值,第二个空间里存储下限值,第三个空间里存储上限值。 - __ptrvalue、 __ptrlow、__ptrhigh返回这3个值,有了3个值以后,内存越界错误便很容易查出。
- 且要定义 BOUNDED_ POINTERS_这个宏才有作用否则这3个宏定义是空的。
- 尽管 bounded指针看上去似平很有用,在2003年被去掉
- 现在所有关于 bounded指针的关键字其实都是一个空的宏。
- 接下来在讨论libc代码时都默认不用bounded指钆
- 即不定义_ BOUNDED_POINTERS__
- 图11-2就是根据从_start源代码分析得到的機布局,让 environ指针指向紧跟在argv数组之后的环境变量数组。
- 另外这段代码还将栈底地址存储在一个全局变量里,以留作它用。
11.2C/C++运行库
11.2.3 glibc与 MSVC CRT
- 运行库是平台相关的,因为它与OS结合非常紧密。
- C的运行库从某种程度上来讲是C的程序和不同操作系统平台之间的抽象层,它将不同的操作系统API抽象成相同的库函数。
- 可在不同的操作系统平台下使用 fread来读取文件,
- 事实上fread在不同的操作系统平合下的实现是不同
- 但作为运行库的使用者我们不需要关心
- 各平台下的C运行库提供了很多功能,但很多时候它们毕竟有限,
- 用户的权限控制、OS线程创建等都不属标准的C语言运行库
- 不得不通过其他办法,
- 如绕过C运行库直接调用操作系统API或用其他的库
- Linux和Windows下的两个主要C运行库
- glibc( GNU C Library)
- MSVCRT( Microsoft Visual C Run-time),
- 分别介绍
- 像线程操作这样的功能并不是标准的C语言运行库的一部分,但是 glibc和MSVCRT都包含线程操作的库函数。
- glibc有一个可选的 pthread库中的pthread_create函数可以用来创建线程;
- MSVCRT中可用_beginthread创建线程。
- glibc和 MSVCRT事实上是标准C运行库的超集,各自对C标准库进行
了一些扩展