11 运行哭

时间:2024-05-22 18:40:21

  • 一个程序比作一个世界,那么程序启动无疑就是“创世”。
  • 本章从程序的创世开始,接触到在程序背后另一类默默服务的团体。
  • 它们能够使得程序正常地启动,能够使得各种我们熟悉的函数发挥作用,它们就是应用程序的运行库。

11.1入口函数和程序初始化

11.1.1程序从main开始吗

  • 程序从main函数开始。
  • 真是如此吗?如果你善于观察,
  • 发现当程序执行到main函数的第一行时,很多事情都已经完成:
11 运行哭

  • 程序刚刚执行到main时,全局变量的初始化过程已经结束了(a已经确定),
  • main函数的两个参数(argc和argv)也被正确传了进来。
  • 堆和栈的初始化悄悄地完成了,一些系统O也被初始化了,
  • 因此可以放心用printf和 malloc

11 运行哭
  • 对象v的构造函数,用于初始化全局变量g的函数foo都在main前调

11 运行哭

  • 操作系统装载程序之后,首先运行的代码并不是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实现:
11 运行哭

  • 省略一些不重要代码,_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)。
11 运行哭

是不是很好奇,为啥刚开始环境变量和参数就在栈上啦,这是谁干得啊!!答案:程序执行前装载器把用户的参数和环境变量压入栈,

  • pop %esi将argc存入esi,
  • movl %esp、%ecx将栈顶(argv和环境变量数组的起始地址)传给%ecx。
  • %esi指向argc,%ecx指向argv及环境变量数组

11 运行哭

意思就是:这个环境变量表要在__libc_start_main里面用到啊,所以要从argv里提取出来啊

  • 环境变量是存在于系统中的一些公用数据,任何程序都可以访问。
  • 环境变量存储的都是一些系统的公共信息,
    • 系统搜索路径,
    • 当前OS版本。
  • 环境变量的格式
  • key= value的字符串,C用geten函数来获取环境变量信息。
  • Windows里,可以直接在控制面板→系统→高级→环境变量查阋当前的环境变量
  • Linux下,直接在命令行里输入 export

  • 实际执行代码是
  • __libc_start_main,
  • 很长

11 运行哭

  • _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 运行哭

  • 图11-2就是根据从_start源代码分析得到的機布局,让 environ指针指向紧跟在argv数组之后的环境变量数组。

11 运行哭

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

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标准库进行
    了一些扩展

11.3运行库与多线程

11.4C++全局构造与析构

11.5 fread实现

11.6本章小结