首页
论坛
课程
招聘

[部分原创] CRT启动函数学习笔记

2009-5-22 12:40 9671

[部分原创] CRT启动函数学习笔记

2009-5-22 12:40
9671
CRT运行库启动函数分析:
在进入main函数之前,系统会调用CRT运行库的启动函数,做如下工作:
全局变量已完成初始化,
堆的初始化,
I/O也完成了初始化,
Main调用

1. *****CRTStartUp()的框架:

******CRTStartUp()
{
        /*初始化一些操作系统版本的全局变量*/
        _osver   =   GetVersion();   
   
        _winminor   =   (_osver   >>   8)   &   0x00FF   ;   
        _winmajor   =   _osver   &   0x00FF   ;   
        _winver     =   (_winmajor   <<   8)   +   _winminor;   
        _osver      =   (_osver   >>   16)   &   0x00FFFF   ;   

        /*初始化堆*/
        if   (   !_heap_init(1)   )
                        ……………..

/*初始化I/O ,这样在main函数中才能直接使用printf 之类的函数,使用windows的SHE机制*/

try {
        _ioinit();  

}__except (_XcptFilter(GetExceptionCode(),   GetExceptionInformation()) ){
                _exit(  GetExceptionCode() );
}

/*取得命令行参数*/
_wcmdln   =   (wchar_t   *)__crtGetCommandLineW();   
_wenvptr   =   (wchar_t   *)__crtGetEnvironmentStringsW();  

/*初始化main函数的argv参数*/
_wsetargv();   
/*初始化环境变量*/
_wsetenvp();   

/*初始化一些C数据,进行C库设置*/
_cinit();

/*调用main函数*/
mainret   =   main(__argc,   __argv,   _environ);   

/*等待main函数返回,然后退出进程*/
        exit(mainret);   
}
--------------------------------------------------------------------------------------------------------------------
下面逐一分析各个阶段:
2. 初始化堆
调用的是_heap_init, 分析该函数:
int __cdecl _heap_init (   int mtflag  /*多线程标志*/  )
{
        /*调用HeapCreate 创见进程堆,*/
        if ( (_crtheap = HeapCreate( mtflag ? 0 : HEAP_NO_SERIALIZE,
                                     BYTES_PER_PAGE, 0 )) == NULL )
            return 0;

        // Pick a heap, any heap
        __active_heap = __heap_select();

        return 1;
}

初始化堆是非常紧急的事情,否则其他的很多事情都做不了,如果堆初始化失败,那么进程就直接退出了。

--------------------------------------下面的就是转载的  程序员的修养
  懒得写了 --------------------------------------------------------------------------------


3. I/O的初始化
首先I/O初始化函数需要在用户空间中建立stdin、stdout、stderr及其对应的FILE结构,使得程序进入main之后可以直接使用printf、scanf等函数。(其实printf和scanf操作的是FILE结构。)
在linux中 stdin stdout stderr 的fd 分别为1,2,3 进程打开的文件fd从4开始。
MSVC中FILE文件结构:
struct _iobuf {
    char *_ptr;
    int   _cnt;
    char *_base;
    int   _flag;
    int   _file;           //通过该变量得到内部二维句柄表数组的两个下标,从而找到句柄
    int   _charbuf;
    int   _bufsiz;
    char *_tmpfname;
    };
typedef struct _iobuf FILE;

在MSVC的CRT中,已经打开的文件句柄的信息使用数据结构ioinfo来表示:
typedef struct {
    intptr_t osfhnd;    //文件句柄
    char osfile;
    char pipech;
}   ioinfo;

在crt/src/ioinit.c中,有一个数组:
int _nhandle;
ioinfo * __pioinfo[64]; // 等效于ioinfo __pioinfo[64][32];
这就是每个进程用户态的打开文件表

通过FILE结构的 _file 计算出二维数组下标,然后 取得 osfhnd 便可以得到句柄。

计算二维数组下标的宏(CRT内部使用):
#define _osfhnd(i)  ( _pioinfo(i)->osfhnd )
其中宏函数_pioinfo的定义是:
#define _pioinfo(i) ( __pioinfo[(i) >> 5] + ((i) & ((1 << 5) -  1)) )
FILE:_file的第5位到第10位是第一维坐标(共6位),_file的第0位到第4位是第二维坐标(共5位)。

MSVC的I/O初始化就是要构造这个二维的打开文件表。MSVC的I/O初始化函数_ioinit定义于crt/src/ioinit.c中。首先,_ioinit函数初始化了__pioinfo数组的第一个二级数组:
mainCRTStartup -> _ioinit():
if ( (pio = _malloc_crt( 32 * sizeof(ioinfo) ))   //现在可以从对堆中分配内存了
             == NULL )
{
    return -1;
}
__pioinfo[0] = pio;
_nhandle = 32;
for ( ; pio < __pioinfo[0] + 32 ; pio++ ) {
    pio->osfile = 0;
    pio->osfhnd = (intptr_t)INVALID_HANDLE_VALUE;
    pio->pipech = 10;
}
在这里_ioinit初始化了的__pioinfo[0]里的每一个元素为无效值,其中 INVALID_ HANDLE_VALUE是Windows句柄的无效值,值为-1。接下来,_ioinit的工作是将一些预定义的打开文件给初始化,这包括两部分:
(1)   从父进程继承的打开文件句柄,当一个进程调用API创建新进程的时候,可以选择继承自己的打开文件句柄,如果继承,子进程可以直接使用父进程的打开文件句柄。
(2)   操作系统提供的标准输入输出。
应用程序可以使用API GetStartupInfo来获取继承的打开文件,GetStartupInfo的参数如下:
void GetStartupInfo(STARTUPINFO* lpStartupInfo);
STARTUPINFO是一个结构,调用GetStartupInfo之后,该结构就会被写入各种进程启动相关的数据。在该结构中,有两个保留字段为:
typedef struct _STARTUPINFO {
    ……
    WORD cbReserved2;
    LPBYTE lpReserved2;
    ……
} STARTUPINFO;
这两个字段的用途没有正式的文档说明,但实际是用来传递继承的打开文件句柄。当这两个字段的值都不为0时,说明父进程遗传了一些打开文件句柄。操作系统是如何使用这两个字段传递句柄的呢?首先lpReserved2字段实际是一个指针,指向一块内存,这块内存的结构如下:
l           字节[0,3]:传递句柄的数量n。
l           字节[4, 3+n]:每一个句柄的属性(各1字节,表明句柄的属性,同ioinfo结构的_osfile字段)。
l           字节[4+n之后]:每一个句柄的值(n个intptr_t类型数据,同ioinfo结构的_osfhnd字段)。
_ioinit函数使用如下代码获取各个句柄的数据:
cfi_len = *(__unaligned int *)(StartupInfo.lpReserved2);
posfile = (char *)(StartupInfo.lpReserved2) + sizeof( int );
posfhnd = (__unaligned intptr_t *)(posfile + cfi_len);
其中__unaligned关键字告诉编译器该指针可能指向一个没有进行数据对齐的地址,编译器会插入一些代码来避免发生数据未对齐而产生的错误。这段代码执行之后,lpReserved2指向的数据结构会被两个指针分别指向其中的两个数组,如图11-6所示。
图11-6  句柄属性数组和句柄数组
接下来_ioinit就要将这些数据填入自己的打开文件表中。当然,首先要判断直接的打开文件表是否足以容纳所有的句柄:
cfi_len = __min( cfi_len, 32 * 64 );
然后要给打开文件表分配足够的空间以容纳所有的句柄:
for ( i = 1 ; _nhandle < cfi_len ; i++ ) {
    if ( (pio = _malloc_crt( 32 * sizeof(ioinfo) )) == NULL )
    {
        cfi_len = _nhandle;
        break;
    }
    __pioinfo[i] = pio;
    _nhandle += 32;
    for ( ; pio < __pioinfo[i] + 32 ; pio++ ) {
        pio->osfile = 0;
        pio->osfhnd = (intptr_t)INVALID_HANDLE_VALUE;
        pio->pipech = 10;
    }
}
在这里,nhandle总是等于已经分配的元素数量,因此只需要每次分配一个第二维的数组,直到nhandle大于cfi_len即可。由于__pioinfo[0]已经预先分配了,因此直接从__pioinfo[1]开始分配即可。分配了空间之后,将数据填入就很容易了:
for ( fh = 0 ; fh < cfi_len ; fh++, posfile++, posfhnd++ )
{
    if ( (*posfhnd != (intptr_t)INVALID_HANDLE_VALUE) &&
               (*posfile & FOPEN) &&
               ((*posfile & FPIPE) ||
               (GetFileType( (HANDLE)*posfhnd ) !=
            FILE_TYPE_UNKNOWN)) )
    {
        pio = _pioinfo( fh );
        pio->osfhnd = *posfhnd;
        pio->osfile = *posfile;
    }
}
在这个循环中,fh从0开始递增,每次通过_pioinfo宏来转换为打开文件表中连续的对应元素,而posfile和posfhnd则依次递增以遍历每一个句柄的数据。在复制的过程中,一些不符合条件的句柄会被过滤掉,例如无效的句柄,或者不属于打开文件及管道的句柄,或者未知类型的句柄。
这段代码执行完成之后,继承来的句柄就全部复制完毕。接下来还须要初始化标准输入输出。当继承句柄的时候,有可能标准输入输出(fh=0,1,2)已经被继承了,因此在初始化前首先要先检验这一点,代码如下:
for ( fh = 0 ; fh < 3 ; fh++ )
{
    pio = __pioinfo[0] + fh;
    if ( pio->osfhnd == (intptr_t)INVALID_HANDLE_VALUE )
    {
        pio->osfile = (char)(FOPEN | FTEXT);
        if ( ((stdfh = (intptr_t)GetStdHandle( stdhndl(fh) ))
                != (intptr_t)INVALID_HANDLE_VALUE)
                && ((htype =GetFileType( (HANDLE)stdfh ))
                != FILE_TYPE_UNKNOWN) )
        {
            pio->osfhnd = stdfh;
            if ( (htype & 0xFF) == FILE_TYPE_CHAR )
                pio->osfile |= FDEV;
            else if ( (htype & 0xFF) == FILE_TYPE_PIPE )
                pio->osfile |= FPIPE;
        }
        else {
            pio->osfile |= FDEV;
        }
    }
    else  {
        pio->osfile |= FTEXT;
    }
}
如果序号为0、1、2的句柄是无效的(没有继承自父进程),那么_ioinit会使用GetStdHandle函数获取默认的标准输入输出句柄。此外,_ioinit还会使用GetFileType来获取该默认句柄的类型,给_osfile设置对应的值。
在处理完标准数据输出的句柄之后,I/O初始化工作就完成了。我们可以看到,MSVC的I/O初始化主要进行了如下几个工作:
l           建立打开文件表。
l           如果能够继承自父进程,那么从父进程获取继承的句柄。
l           初始化标准输入输出。
在I/O初始化完成之后,所有的I/O函数就都可以自由使用了

[推荐]看雪企服平台,提供项目众包、渗透测试、安全分析、定制项目开发、APP等级保护等安全服务!

最新回复 (5)
coolboy 2009-5-23 18:36
2
0
学习~~~~~~~~~~~~~~~~
frozenrain 2009-5-23 18:50
3
0
不错,学习
lizichuan 2009-6-18 11:25
4
0
谢谢
分享
rsa 2009-6-18 14:11
5
0
很不错的文章啊
Atlone 2009-7-14 11:59
6
0
这个正找资料看呢,认真翻翻书看了
游客
登录 | 注册 方可回帖
返回