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مؤشر صالح وغير فارغ. المتغيرات lastومن nextالنوع uint32_t. يحدث Segfault في استدعاء الوظيفة الثاني isalnum. والأهم من ذلك ، يمكن استنساخه فقط عند استخدام glibc ، وليس musl libc. إذا كان عليك إعادة قراءة الكود عدة مرات ، فأنت لست وحدك: ببساطة لا يوجد شيء لتشغيل segfault.
نظرًا لأنه كان معروفًا أن الأمر كله يتعلق بمكتبة glibc ، فقد حصلت على مصادرها وبدأت في البحث عن تطبيق
isalnum، والاستعداد لمواجهة بعض الهراء الغبي. لكن قبل أن أصل إلى الحماقة الغبية ، والتي ، صدقوني ، بكميات كبيرة ، دعونا أولاً نلقي نظرة سريعة على خيار جيد. هذه هي الطريقة التي يتم بها isalnumتنفيذ الوظيفة في musl libc:
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 . بمجرد أن تفتح العنوان ، سيتم استقبالك بهراء نموذجي من غنو ، لكن دعنا نتخطاه ونحاول العثور عليه
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هذه وظيفة مضمنة ... توقف ، كلها داخل كتلة else لتعليمات المعالج المسبق #ifndef __cplusplus. نهاية. أين 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
ما هذا بحق الجحيم؟ حسنًا ، دعنا ننتقل ونعتبر أن هذا مجرد ثابت سحري. يسمى ماكرو آخر
__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__));
كم هو رائع منك ، غليبك! أنا فقط أحب التعامل مع المواقع. على أي حال ، فإن gdb متصل بتطبيقي المعطل ، ومع كل المعلومات التي تلقيتها في الاعتبار ، أكتب هذا القذارة:
(gdb) print ((unsigned int **(*)(void))__ctype_b_loc)()[next]
Cannot access memory at address 0x11dfa68
تم العثور على Segfault. كان هناك سطر حول هذا في التعليق: "تتطلب ISO C دوال ctype للعمل مع قيم مثل" حرف غير موقعة و EOF ". إذا وجدنا هذا في المواصفات ، نرى:
في جميع عمليات التنفيذ [للوظائف المعلنة في ctype.h] ، تكون الوسيطة عبارة عن int ، يجب أن تتناسب قيمتها مع حرف غير موقعة ، أو مساوية لقيمة ماكرو EOF.
الآن أصبح من الواضح كيفية حل المشكلة. مفاصلي. اتضح أنني لا أستطيع إطعام
isalnumحرف UCS-32 تعسفيًا للتحقق من حدوثه في النطاقات 0x30-0x39 و 0x41-0x5A و 0x61-0x7A.
لكن هنا سأستفيد من اقتراح: ربما
isalnumلا يجب أن تؤدي الوظيفة إلى إجراء segfault على الإطلاق ، بغض النظر عما تحصل عليه؟ ربما حتى لو سمحت المواصفات بذلك ، فهذا لا يعني أنه يجب القيام بذلك بهذه الطريقة ؟ ربما ، كفكرة مجنونة ، لا ينبغي أن يحتوي سلوك هذه الوظيفة على خمسة وحدات ماكرو ، والتحقق من استخدام مترجم C ++ ، وتعتمد على ترتيب البايت الخاص بالعمارة ، وجدول البحث ، وبيانات لغة الدفق ، وإشارة مرجعية مؤشرين؟
دعنا نلقي نظرة أخرى على نسخة الموسل كتذكير سريع:
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 على الارتباط بالأصل.