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以外のポインタです。変数last
とnext
はタイプuint32_t
です。Segfaultは、2番目の関数呼び出しで発生しますisalnum
。そして、最も重要なことは、musl libcではなく、glibcを使用した場合にのみ再現可能です。コードを数回読み直す必要がある場合は、あなただけではありません:segfaultをトリガーするものは何もありません。
すべてがglibcライブラリにあることがわかっていたので、私はそのソースを入手して実装を探し始め
isalnum
、愚かながらくたに直面する準備をしました。しかし、私が愚かながらくたにたどり着く前に、つまり、私をまとめて信じる前に、まず良い選択肢を簡単に見てみましょう。これは、関数がisalnum
musllibcに実装される方法です。
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なしで機能します。これは、なぜisalnum
segfaultをスローする必要があるのでしょうか。
では、これを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マクロの値と等しくなければなりません。
これで、問題を修正する方法が明らかになりました。私の関節。
isalnum
0x30-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に感謝します。