c庫(上)----小話c語言(17)

[Win7  vs2010]


Q: C庫和系統api之間是什麼關係?

A: 如下圖,簡單示意:


可以看出,C庫一部分是使用系統api實現自身功能(比如文件操作),另一部分並不會直接依賴系統api,單獨實現功能(比如字符串處理)。另外,對於驅動模塊,按照不同的理解,也可以放入操作系統內部或者操作系統下層;如果把操作系統看成隱形的CPU和內存的驅動,那麼它也可以看成和常規意義的硬件驅動是平級的。而,C庫,從理論上來說,沒必要和驅動有依賴關係。當然,訪問操作系統的方式不僅僅是它提供的api,也是可以通過其它方式來訪問。


Q:c庫和多線程到底什麼關係?

A: 多線程在操作系統上的運用導致了很多庫,包括之前的單線程版本的c庫,也必須做出相應修改,才能保證運行不會出現問題。例如,如下是vs2010附帶的fgets.c中部分源代碼:

_TSCHAR * __cdecl _fgetts (
        _TSCHAR *string,
        int count,
        FILE *str
        )
{
    REG1 FILE *stream;
    REG2 _TSCHAR *pointer = string;
    _TSCHAR *retval = string;
    int ch;

    _VALIDATE_RETURN(( string != NULL ) || ( count == 0 ), EINVAL, NULL);
    _VALIDATE_RETURN(( count >= 0 ), EINVAL, NULL);
    _VALIDATE_RETURN(( str != NULL ), EINVAL, NULL);

    if (count == 0)
    {
        return NULL;
    }

    /* The C Standard states the input buffer should remain
    unchanged if EOF is encountered immediately. Hence we
    do not blank out the input buffer here */

    /* Init stream pointer */
    stream = str;

    _lock_str(stream);
    __try {
#ifndef _UNICODE
        _VALIDATE_STREAM_ANSI_SETRET(stream, EINVAL, retval, NULL);
#endif  /* _UNICODE */
        if (retval!=NULL)
        {
            while (--count)
            {
                if ((ch = _fgettc_nolock(stream)) == _TEOF)
                {
                    if (pointer == string) {
                                    retval=NULL;
                                    goto done;
                    }

                    break;
                }

                if ((*pointer++ = (_TSCHAR)ch) == _T('\n'))
                    break;
            }

            *pointer = _T('\0');
        }


/* Common return */
done: ;
    }
    __finally {
        _unlock_str(stream);
    }

    return(retval);
}
可以看出,它的調用過程中會先調用_lock_str:
#define _lock_str(s)            _lock_file(s)
_lock_file的內部實現:
void __cdecl _lock_file (
        FILE *pf
        )
{
        /*
         * The way the FILE (pointed to by pf) is locked depends on whether
         * it is part of _iob[] or not
         */
        if ( (pf >= _iob) && (pf <= (&_iob[_IOB_ENTRIES-1])) )
        {
            /*
             * FILE lies in _iob[] so the lock lies in _locktable[].
             */
            _lock( _STREAM_LOCKS + (int)(pf - _iob) );
            /* We set _IOLOCKED to indicate we locked the stream */
            pf->_flag |= _IOLOCKED;
        }
        else
            /*
             * Not part of _iob[]. Therefore, *pf is a _FILEX and the
             * lock field of the struct is an initialized critical
             * section.
             */
            EnterCriticalSection( &(((_FILEX *)pf)->lock) );
}

對於_lock函數:

void __cdecl _lock (
        int locknum
        )
{

        /*
         * Create/open the lock, if necessary
         */
        if ( _locktable[locknum].lock == NULL ) {

            if ( !_mtinitlocknum(locknum) )
                _amsg_exit( _RT_LOCK );
        }

        /*
         * Enter the critical section.
         */

        EnterCriticalSection( _locktable[locknum].lock );
}

可以看出,不管加鎖的方式如何,它實際上還是會調用系統提供的原始api來進行排他訪問,從而避免多線程訪問共享資源可能導致讀或者寫出錯的問題。


Q: 那麼如何設置將使用單線程版本的c庫或者多線程版本的c庫?

A: 如下圖,是vs2010設置使用多線程版本c庫的截圖:

按照微軟的說法,從vs2005開始,單線程版本的C庫就已經被移除,所以可以不用擔心使用單線程版本C庫導致問題了。如果使用的是VC6,依然可以設置使用單線程版本C庫。如果使用IDE工具沒找到,可以使用命令行工具尋找相關選項:

當然,grep需要cygwin的支持。


Q: printf函數,它可以處理變參,內部會如何處理呢?

A: 參數入棧的原則很好地支撐了變參的處理。也就是說,當確定了參數中一個的地址,那麼其他參數的地址是隨着這個地址按照類型大小變動即可。如下:

#define va_start _crt_va_start

#define va_end _crt_va_end

_crt_va_start和_crt_va_end的聲明如下:
#define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )

#define _crt_va_end(ap)      ( ap = (va_list)0 )
可以看出,va_start也就是獲取了變參的首地址,而va_end也就是將操作變參的數據設置爲0,來結束處理。當然,上面的宏定義是一種平臺下特殊情況,不同平臺下的定義會有所不同。順便將_ADDRESSOF宏和_INTSIZEOF宏列出:
#ifdef  __cplusplus
#define _ADDRESSOF(v)   ( &reinterpret_cast<const char &>(v) )
#else
#define _ADDRESSOF(v)   ( &(v) )
#endif
#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
上面的這個宏表示以sizeof(int)對齊的大小。


Q: printf函數內部究竟調用了什麼操作系統API?

A: 從表面分析,應該會調用系統控制檯輸出的API; 如下是用ida工具對於如下代碼hello.c生成的可執行文件的分析:

#include <stdio.h>
#include <wchar.h>

int main()
{
  char ch = 'a';
  wchar_t wch = (wchar_t)ch;
  printf("%C\n", wch);
  return 0; 
}
使用cl  hello.c編譯成hello.exe.


可以確切看到內部調用的系統API名稱。當然,使用hook WriteConsoleW函數的方式同樣可以得出結論。

Q: 經常看到關於文件輸入輸出的符號stdin, stdout, stderr,它們究竟是什麼?

A: 它們是FILE *類型的變量。如下定義:

#define stdin  (&__iob_func()[0])
#define stdout (&__iob_func()[1])
#define stderr (&__iob_func()[2])
__iob_func定義如下:

/*
 * FILE descriptors; preset for stdin/out/err (note that the __tmpnum field
 * is not initialized)
 */
FILE _iob[_IOB_ENTRIES] = {
        /* _ptr, _cnt, _base,  _flag, _file, _charbuf, _bufsiz */

        /* stdin (_iob[0]) */

        { _bufin, 0, _bufin, _IOREAD | _IOYOURBUF, 0, 0, _INTERNAL_BUFSIZ },

        /* stdout (_iob[1]) */

        { NULL, 0, NULL, _IOWRT, 1, 0, 0 },

        /* stderr (_iob[3]) */

        { NULL, 0, NULL, _IOWRT, 2, 0, 0 },

};


/* These functions are for enabling STATIC_CPPLIB functionality */
_CRTIMP FILE * __cdecl __iob_func(void)
{
    return _iob;
}
不同平臺,甚至相同平臺下不同環境,對於描述符、句柄、指針之類的名稱含義理解不盡一致。所以,這裏使用英文的方式來說明。FILE *是C語言抽象出來的文件操作指針,而對於FILE結構內部的_file成員可以被看成是handle,它是整形數據,正如stdin對應於file handle 0, stdout對應於file handle 1,stderr對應於file handle 2.

對於FILE *和file handle, 可以使用fileno和fdopen來互相獲取對應的數值。

另外,上面的代碼是從_file.c頭文件中摘錄,stderr (_iob[3])應該是它的錯誤,應該爲stderr (_iob[2]).


Q: 對於字符串處理函數strtok, 它是如何保存中間狀態的?

A: 單線程版本的strtok函數,可以通過static變量保存中間處理的位置信息,使得後來的調用可以繼續工作。對於多線程版本的strtok函數,這樣就不行了。實際上,它是採用了TLS的方式來保存這些中間數據。如下:

struct _tiddata {
    unsigned long   _tid;       /* thread ID */


    uintptr_t _thandle;         /* thread handle */

    int     _terrno;            /* errno value */
    unsigned long   _tdoserrno; /* _doserrno value */
    unsigned int    _fpds;      /* Floating Point data segment */
    unsigned long   _holdrand;  /* rand() seed value */
    char *      _token;         /* ptr to strtok() token */
    wchar_t *   _wtoken;        /* ptr to wcstok() token */
    unsigned char * _mtoken;    /* ptr to _mbstok() token */

    /* following pointers get malloc'd at runtime */
    char *      _errmsg;        /* ptr to strerror()/_strerror() buff */
    wchar_t *   _werrmsg;       /* ptr to _wcserror()/__wcserror() buff */
    char *      _namebuf0;      /* ptr to tmpnam() buffer */
    wchar_t *   _wnamebuf0;     /* ptr to _wtmpnam() buffer */
    char *      _namebuf1;      /* ptr to tmpfile() buffer */
    wchar_t *   _wnamebuf1;     /* ptr to _wtmpfile() buffer */
    char *      _asctimebuf;    /* ptr to asctime() buffer */
    wchar_t *   _wasctimebuf;   /* ptr to _wasctime() buffer */
    void *      _gmtimebuf;     /* ptr to gmtime() structure */
    char *      _cvtbuf;        /* ptr to ecvt()/fcvt buffer */
    unsigned char _con_ch_buf[MB_LEN_MAX];
                                /* ptr to putch() buffer */
    unsigned short _ch_buf_used;   /* if the _con_ch_buf is used */

    /* following fields are needed by _beginthread code */
    void *      _initaddr;      /* initial user thread address */
    void *      _initarg;       /* initial user thread argument */

    /* following three fields are needed to support signal handling and
     * runtime errors */
    void *      _pxcptacttab;   /* ptr to exception-action table */
    void *      _tpxcptinfoptrs; /* ptr to exception info pointers */
    int         _tfpecode;      /* float point exception code */

    /* pointer to the copy of the multibyte character information used by
     * the thread */
    pthreadmbcinfo  ptmbcinfo;

    /* pointer to the copy of the locale informaton used by the thead */
    pthreadlocinfo  ptlocinfo;
    int         _ownlocale;     /* if 1, this thread owns its own locale */

    /* following field is needed by NLG routines */
    unsigned long   _NLG_dwCode;

    /*
     * Per-Thread data needed by C++ Exception Handling
     */
    void *      _terminate;     /* terminate() routine */
    void *      _unexpected;    /* unexpected() routine */
    void *      _translator;    /* S.E. translator */
    void *      _purecall;      /* called when pure virtual happens */
    void *      _curexception;  /* current exception */
    void *      _curcontext;    /* current exception context */
    int         _ProcessingThrow; /* for uncaught_exception */
    void *              _curexcspec;    /* for handling exceptions thrown from std::unexpected */
#if defined (_M_IA64) || defined (_M_AMD64)
    void *      _pExitContext;
    void *      _pUnwindContext;
    void *      _pFrameInfoChain;
    unsigned __int64    _ImageBase;
#if defined (_M_IA64)
    unsigned __int64    _TargetGp;
#endif  /* defined (_M_IA64) */
    unsigned __int64    _ThrowImageBase;
    void *      _pForeignException;
#elif defined (_M_IX86)
    void *      _pFrameInfoChain;
#endif  /* defined (_M_IX86) */
    _setloc_struct _setloc_data;

    void *      _reserved1;     /* nothing */
    void *      _reserved2;     /* nothing */
    void *      _reserved3;     /* nothing */
#ifdef _M_IX86
    void *      _reserved4;     /* nothing */
    void *      _reserved5;     /* nothing */
#endif  /* _M_IX86 */

    int _cxxReThrow;        /* Set to True if it's a rethrown C++ Exception */

    unsigned long __initDomain;     /* initial domain used by _beginthread[ex] for managed function */
};

typedef struct _tiddata * _ptiddata;
可以看到,裏面有strtok函數中間狀態需要保存的token指針位置;而,對於strtok的實現代碼也能看出:

#ifdef _SECURE_VERSION
#define _TOKEN *context
#else  /* _SECURE_VERSION */
#define _TOKEN ptd->_token
#endif  /* _SECURE_VERSION */

#ifdef _SECURE_VERSION
char * __cdecl strtok_s (
        char * string,
        const char * control,
        char ** context
        )
#else  /* _SECURE_VERSION */
char * __cdecl strtok (
        char * string,
        const char * control
        )
#endif  /* _SECURE_VERSION */
{
        unsigned char *str;
        const unsigned char *ctrl = control;

        unsigned char map[32];
        int count;

#ifdef _SECURE_VERSION

        /* validation section */
        _VALIDATE_RETURN(context != NULL, EINVAL, NULL);
        _VALIDATE_RETURN(string != NULL || *context != NULL, EINVAL, NULL);
        _VALIDATE_RETURN(control != NULL, EINVAL, NULL);

        /* no static storage is needed for the secure version */

#else  /* _SECURE_VERSION */

        _ptiddata ptd = _getptd();

#endif  /* _SECURE_VERSION */

        /* Clear control map */
        for (count = 0; count < 32; count++)
                map[count] = 0;

        /* Set bits in delimiter table */
        do {
                map[*ctrl >> 3] |= (1 << (*ctrl & 7));
        } while (*ctrl++);

        /* Initialize str */

        /* If string is NULL, set str to the saved
         * pointer (i.e., continue breaking tokens out of the string
         * from the last strtok call) */
        if (string)
                str = string;
        else
                str = _TOKEN;

        /* Find beginning of token (skip over leading delimiters). Note that
         * there is no token iff this loop sets str to point to the terminal
         * null (*str == '\0') */
        while ( (map[*str >> 3] & (1 << (*str & 7))) && *str )
                str++;

        string = str;

        /* Find the end of the token. If it is not the end of the string,
         * put a null there. */
        for ( ; *str ; str++ )
                if ( map[*str >> 3] & (1 << (*str & 7)) ) {
                        *str++ = '\0';
                        break;
                }

        /* Update nextoken (or the corresponding field in the per-thread data
         * structure */
        _TOKEN = str;

        /* Determine if a token has been found. */
        if ( string == str )
                return NULL;
        else
                return string;
}
函數的最後, _TOKEN = str; 正是操作了tiddata.

如果希望得到tiddata是如何初始化的,查看_beginthreadex函數的源代碼(部分):

 /*
         * Allocate and initialize a per-thread data structure for the to-
         * be-created thread.
         */
        if ( (ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL )
                goto error_return;

        /*
         * Initialize the per-thread data
         */

        _initptd(ptd, _getptd()->ptlocinfo);

        ptd->_initaddr = (void *) initialcode;
        ptd->_initarg = argument;
        ptd->_thandle = (uintptr_t)(-1);

Q: 斷言判斷的代碼應該怎麼寫?

A: 它的核心就在於如何輸出錯誤信息和結束程序。

#define assert(_Expression) (void)( (!!(_Expression)) || (_wassert(_CRT_WIDE(#_Expression), _CRT_WIDE(__FILE__), __LINE__), 0) )

在_wassert中將會組裝錯誤信息並輸出,並進入結束程序狀態。


xichen

2012-5-29  16:54:53


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章