2つの標準Cライブラリの歴史

今日、私は、scdocユーティリティにいくつかのでたらめを与えてそれを入手したDebianユーザーからバグレポートを受け取りましたSIGSEGV問題を調査することは、私が間に優れ比較するために許可されるmusl libcとしますglibcまず、スタックトレースを見てみましょう。



==26267==ERROR: AddressSanitizer: SEGV on unknown address 0x7f9925764184
(pc 0x0000004c5d4d bp 0x000000000002 sp 0x7ffe7f8574d0 T0)
==26267==The signal is caused by a READ memory access.
    0 0x4c5d4d in parse_text /scdoc/src/main.c:223:61
    1 0x4c476c in parse_document /scdoc/src/main.c
    2 0x4c3544 in main /scdoc/src/main.c:763:2
    3 0x7f99252ab0b2 in __libc_start_main
/build/glibc-YYA7BZ/glibc-2.31/csu/../csu/libc-start.c:308:16
    4 0x41b3fd in _start (/scdoc/scdoc+0x41b3fd)


この行のソースコードは次のように述べています。



if (!isalnum(last) || ((p->flags & FORMAT_UNDERLINE) && !isalnum(next))) {


ヒント:これpは有効なnull以外のポインタです。変数lastnextはタイプuint32_tです。Segfaultは、2番目の関数呼び出しで発生しますisalnumそして、最も重要なことは、musl libcではなく、glibcを使用した場合にのみ再現可能です。コードを数回読み直す必要がある場合は、あなただけではありません:segfaultをトリガーするものは何もありません。



すべてがglibcライブラリにあることがわかっていたので、私はそのソースを入手して実装を探し始めisalnum、愚かながらくたに直面する準備をしました。しかし、私が愚かながらくたにたどり着く前に、つまり、私をまとめて信じる前に、まず良い選択肢を簡単に見てみましょう。これは、関数がisalnummusllibcに実装される方法です



int isalnum(int c)
{
	return isalpha(c) || isdigit(c);
}

int isalpha(int c)
{
	return ((unsigned)c|32)-'a' < 26;
}

int isdigit(int c)
{
	return (unsigned)c-'0' < 10;
}


予想どおり、どの値でも、c関数はsegfaultなしで機能します。これは、なぜisalnumsegfaultをスローする必要があるのでしょうか。



では、これをglibcの実装と比較してみましょうタイトルを開くとすぐに、典型的なGNUナンセンスで迎えられますが、それをスキップして見つけてみましょうisalnum



最初の結果はこれです:



enum
{
  _ISupper = _ISbit (0),        /* UPPERCASE.  */
  _ISlower = _ISbit (1),        /* lowercase.  */
  // ...
  _ISalnum = _ISbit (11)        /* Alphanumeric.  */
};


実装の詳細のように見えます。次に進みましょう。



__exctype (isalnum);


しかし、それは何__exctypeですか?数行前に戻ります...



#define __exctype(name) extern int name (int) __THROW


さて、どうやらこれは単なるプロトタイプです。ただし、ここでマクロが必要な理由は明確ではありません。さらに見て...



#if !defined __NO_CTYPE
# ifdef __isctype_f
__isctype_f (alnum)
// ...


だから、これはすでに何か便利なもののように見えます。それはなん__isctype_fですか?揺さぶる..。



#ifndef __cplusplus
# define __isctype(c, type) \
  ((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type)
#elif defined __USE_EXTERN_INLINES
# define __isctype_f(type) \
  __extern_inline int                                                         \
  is##type (int __c) __THROW                                                  \
  {                                                                           \
    return (*__ctype_b_loc ())[(int) (__c)] & (unsigned short int) _IS##type; \
  }
#endif


さて、それは始まります...さて、一緒に私たちは何とかそれを理解します。どうやら、__isctype_fこれはインライン関数です...停止、すべて#ifndef__cplusplusプリプロセッサ命令のelseブロック内にあります。デッドエンド。isalnum彼女の母親は実際にどこ定義されていますか?さらに見て...多分これはそれですか?



#if !defined __NO_CTYPE
# ifdef __isctype_f
__isctype_f (alnum)
// ...
# elif defined __isctype
# define isalnum(c)     __isctype((c), _ISalnum) // <-  


ねえ、これは前に見た「実装の詳細」です。覚えていますか?



enum
{
  _ISupper = _ISbit (0),        /* UPPERCASE.  */
  _ISlower = _ISbit (1),        /* lowercase.  */
  // ...
  _ISalnum = _ISbit (11)        /* Alphanumeric.  */
};


このマクロをすばやく選択してみましょう。



# include <bits/endian.h>
# if __BYTE_ORDER == __BIG_ENDIAN
#  define _ISbit(bit)   (1 << (bit))
# else /* __BYTE_ORDER == __LITTLE_ENDIAN */
#  define _ISbit(bit)   ((bit) < 8 ? ((1 << (bit)) << 8) : ((1 << (bit)) >> 8))
# endif


これは一体何なの?さて、先に進んで、これは単なる魔法の定数であると考えてみましょう。もう1つのマクロはと呼ばれ__isctype、最近見たものと似てい__isctype_fます。ブランチをもう一度見てみましょう#ifndef __cplusplus



#ifndef __cplusplus
# define __isctype(c, type) \
  ((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type)
#elif defined __USE_EXTERN_INLINES
// ...
#endif


ええと...



まあ、少なくとも私たちは、segfaultを説明するかもしれないポインタの逆参照を見つけました。それはなん__ctype_b_locですか?



/*      ctype-info.c.
          localeinfo.h.

     ,   , (. `uselocale'  <locale.h>)
        ,  .
    ,   -,   
    ,    ,   .

        384 ,    
     `unsigned char' [0,255];   EOF (-1);  
    `signed char' value [-128,-1).  ISO C ,   ctype 
      `unsigned char'  EOF;    
    `signed char'      .
          `int`,
     `unsigned char`,   `tolower(EOF)'   EOF,   
       `unsigned char`.     - , 
         .  */
extern const unsigned short int **__ctype_b_loc (void)
     __THROW __attribute__ ((__const__));
extern const __int32_t **__ctype_tolower_loc (void)
     __THROW __attribute__ ((__const__));
extern const __int32_t **__ctype_toupper_loc (void)
     __THROW __attribute__ ((__const__));


なんてクールなんだ、glibc!私は大好きロケールを扱います。とにかく、gdbはクラッシュしたアプリケーションに接続されており、受け取ったすべての情報を念頭に置いて、次のように書いています。



(gdb) print ((unsigned int **(*)(void))__ctype_b_loc)()[next]
Cannot access memory at address 0x11dfa68


Segfaultが見つかりました。コメントには、これに関する行がありました:「ISOCでは、「unsignedchar」やEOFなどの値を処理するためにctype関数が必要です」。仕様でこれを見つけると、次のようになります。



[ctype.hで宣言された関数の]すべての実装では、引数はintであり、その値はunsigned charに収まるか、EOFマクロの値と等しくなければなりません。



これで、問題を修正する方法が明らかになりました。私の関節。isalnum0x30-0x39、0x41-0x5A、0x61-0x7Aの範囲で発生するかどうかを確認するために任意のUCS-32文字をフィードできないことがわかりました。



しかし、ここで私は提案の自由を取ります:多分isalnumそれが何を得るかに関係なく、関数はセグフォールトをまったく投げるべきではありませんか?仕様で許可されていてもこのようにする必要があるという意味ではありませんか?たぶん、おかしな考えと同じように、この関数の動作には5つのマクロが含まれていてはならず、C ++コンパイラの使用を確認し、アーキテクチャのバイト順序、ルックアップテーブル、ストリームロケールデータに依存し、2つのポインタを逆参照しますか?



簡単なリマインダーとして、muslバージョンをもう一度見てみましょう。



int isalnum(int c)
{
	return isalpha(c) || isdigit(c);
}

int isalpha(int c)
{
	return ((unsigned)c|32)-'a' < 26;
}

int isdigit(int c)
{
	return (unsigned)c-'0' < 10;
}


これらはパイです。



翻訳者注:オリジナルにリンクしてくれたMaxGraeyに感謝します。



All Articles