读书笔记--程序员的自我修养-入口函数和程序初始化

来源:互联网 发布:vue.js 2.0视频 编辑:程序博客网 时间:2024/05/21 07:12

        程序肯定不是从main开始执行的,进入main函数之前,全局变量的初始化已经结束,全局变量如果是某个函数的返回值,那么这个函数也已经被调用过;main函数的两个参数(int argc:参数个数和char * argv[]:参数列表)已经被传递进来;堆和栈也已经被初始化(所以可以使用malloc:从堆中获取空间,alloca函数:从栈中获取空间);系统I/o也被初始化了,所以可以使用printf函数。atexit(*p(void));在main函数结束之后调用函数P。

        main函数之前和之后的代码,称为入口函数或入口点。他们负责准备main函数执行需要的环境并调用main函数,这时,main函数中就可以申请内存、使用系统调用、触发异常、访问I/O等。

        操作系统在创建进程后,控制权交给程序的入口函数,这个入口一般是运行库中的函数,入口函数会对程序运行环境初始化,包括堆、I/O、线程、全局变量构造等。入口函数初始化之后调用main函数,知道main函数返回到入口函数,入口函数在进行清理,包括全局变量析构、堆销毁、关闭I/O等,然后系统调用结束进程。

一、linux环境下glibc

       linux环境下的glibc库有静态glib和动态glibc,可执行文件和共享库(.so文件),有四种组合情况;这里讨论静态glibc库作用于可执行文件的情况,入口函数_start:

在调用_start之前装载器会把用户的参数和环境变量压入栈中:

_start:

     1、ebp清0;

     2、pop获得argc;

     3、将当前栈顶指针esp保存;

     4、参数入栈,调用__libc_start_main:需要参数有main,argc,argv数组指针(esp的值),__libc_csu_init、__libc_cus_fini、edx,栈顶指针(esp)自右向左的顺序入栈。

__libc_start_main:

    参数包括main函数指针(调用main函数)

     参数包括__libc_csu_init:调用main函数之前的初始化工作

     参数包括__libc_cus_fini:调用main函数之后的清理工作

    参数包括:edx(rtld_fini?)动态加载有关的清理工作

exit

    主要是逐个调用有cxa_atexit(*q)和atexit(*p)注册的函数链表,保证所有退出函数p和q等都被执行。

    hlt:保证进程的强制退出,一般exit会退出,所有hlt不会被执行。

 

二、MSVC的入口函数。CRT(C RunTime)

     MSVC的CRT入口函数名为mainCTRStartup

     1、初始化和OS版本相关的全局变量,这里使用alloca函数在栈中(堆并未初始化,不可使用)开辟一段空间,通过GetVersionExA函数获得版本信息。

     2、初始化堆_heap_init(0),失败则程序直接退出。

     3、使用__try{}  __except{}结构。初始化IO;_ioinit();

     4、在try中通过GetCommandLineA函数获得main函数的参数信息并且通过_setargv设置argv;

     5、通过crtGetEnvironmentStringsA获得环境变量并且通过_setenvp设置环境变量;

     6、_cinit()用来设置C库。

     7、调用main函数并记录返回值,(名字不重要可以是winMain等)

     8、检查错误并将main函数的返回值返回__except{}块的功能。

 

三、堆的初始化:和内存管理相关

        linux下,是有系统库从操作系统请求一大片的空间,然后由应用程序一点点的申请,不用频繁的进行操作系统的系统调用,加速程序运行。

        windows下,初始化堆仅调用HeapCreat函数,malloc函数直接调用HeapAlloc这个API,堆得管理直接交给操作系统。

四、I/O初始化:

     在linux和windows下,将具有I/O概念的实体包括磁盘、设备、命令行等统称为文件。C库中的文件操作时通过一个FILE结构的指针来进行的,fopen函数返回一个FILE结构的指针,fwrite和fread的文件操作函数通过这个指针操作文件。

     在操作系统的层面上,也有类似于FILE的概念,linux下叫做文件描述符(File Descriptor),windows里叫做句柄(handle),用户通过某个函数打开文件获得句柄,此后用户通过句柄操作文件。句柄是和内核的文件对象相关联的,内核可以通过句柄计算文件对象的地址,而用户不可以,用户只有访问句柄的权力。这样防止了用户随意读写操作系统内核的文件对象。

     linux下,值为0、1、2的fd分别代表标准输入、标准输出、标准错误输出。程序中打开的文件的fd从3开始增长。在内核中,每个进程都有自己的打开文件对象列表,fd就是这列表的下标,此列表处于内核,所以用户只能通过系统调用访问。在C语言里,操作文件通过FILE,这个必然和fd有一一对应的关系,每个FILE结构都会记录自己唯一的fd,stdin、stdout、stderr都是FILE结构的指针。windows中句柄和fd类似,只是非下标,需要线性变换。

     有了以上的知识,那么I/O的初始化不过是在用户空间中建立stdin、stdout、stderr及其对应的FILE结构,使得程序进入main之后可以直接使用printf和scanf等函数。

     MSVC的CRT中,句柄信息由数据结构ioinfo表示,在ioinit.c文件中定义了一个二维的指针数组 ioinfo *__pininfo[64]  == ioinfo __pioinfo[64][32],不明白为什么???指针的好处就是可以随用随创建,而不是每次都要初始化这么大的空间。

     二维数组,所以要线性变换才能得到内核的地址,与linux下不同。#define _osfhnd(i) (_pioinfo(i) ->osfhnd)  //得到osfhand值 #define _pioinfo (__pioinfo[(i)>>5]+((i) & ((1<<5)-1)))

     _ioinit函数主要是初始化这个二维数组的第一维,1、将父进程的打开文件句柄继承下来 2、标准输入输出句柄(如果父进程没有的话,概率比较小)。

    GetStartupInfo函数可以继承父进程的打开句柄,参数传递回来一个指针,指向一块内存区域,第一个字表示传递句柄的数量N,后面的[4-3+n]个字节指示每个句柄的属性,4+n之后的是句柄的值。

     接着要把父进程的打开句柄填充到打开文件表中,如果打开的文件表不够,则需呀重新申请第二行,第三行(总共64行*32个句柄)等等。

     接着初始化标准输入输出,检验是否已经继承,如果无效,即父进程没有,则使用GetStdHandle函数获得默认的标准输入输出句柄,GetFileType函数获得默认句柄类型。

    所以I/O初始化主要进行1、建立打开文件表。2、继承父进程句柄。3、初始化标准输入输出。

 

五、C语言标准库stdio.h 标准输入输出,文件操作;ctype.h字符操作;string.h字符串操作;math.h数学函数;stdlib.h资源管理格式转换;time.h时间;assert.h断言;limits.h和float.h类型转换的;stdarg.h变长参数;setjmp.h非局部跳转。

1、变长参数:

     int printf(const char * format,...);如此声明,表示其后可以追加任意数量、任意类型的参数。可变参数的实现基于默认的cdecl的调用惯例,即从右向左压栈传递参数,由调用方负责清除堆栈。

     printf函数的实现是在字符串中指定了每个参数的格式,即其在栈中所占空间,因而调用方可以据此从栈中获得参数值及类型。如果是统一格式,可以由开始指定参数的数目如:int sum(int num ,。。。)。由num指定参数个数,类型固定。

2、非局部跳转:是实现C++中catch throw的基础。longjmp可以让程序跳转到setjmp返回的时刻。绝对不是结构化的变成方法。

 

gcc有一些平台相关的链接文件,MSVC的CRT根据属性的不同也有好些版本:libcmt.lib(默认的运行库)、msvcrt.lib、libcmtd.lib、msvcrtd.lib。。。。。这些信息会在目标文件的.drectve段中指示出来,供链接时使用。

原创粉丝点击