SIGSEGV. Durch die Untersuchung des Problems konnte ich einen hervorragenden Vergleich zwischen musl libcund anstellen glibc. Schauen wir uns zunächst die Stapelverfolgung an:
==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)
Der Quellcode in dieser Zeile lautet wie folgt:
if (!isalnum(last) || ((p->flags & FORMAT_UNDERLINE) && !isalnum(next))) {
Hinweis: Dies
pist ein gültiger Zeiger ungleich Null. Variablen lastund nextsind vom Typ uint32_t. Segfault tritt beim zweiten Funktionsaufruf auf isalnum. Und vor allem nur mit glibc reproduzierbar, nicht mit musl libc. Wenn Sie den Code mehrmals neu lesen müssen, sind Sie nicht allein: Es gibt einfach nichts, was einen Segfault auslösen könnte.
Da bekannt war, dass sich das Ganze in der glibc-Bibliothek befand, holte ich mir die Quellen und suchte nach einer Implementierung
isalnum, um mich auf einen dummen Mist vorzubereiten. Aber bevor ich zu dem dummen Mist komme, der, glauben Sie mir, in großen Mengen ist , werfen wir zunächst einen kurzen Blick auf eine gute Option. So wird die Funktion isalnumin musl libc implementiert:
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;
}
Wie erwartet
cfunktioniert die Funktion für jeden Wert ohne Segfault, denn warum zum Teufel isalnumsollte überhaupt ein Segfault ausgelöst werden?
Okay, jetzt vergleichen wir dies mit der glibc-Implementierung . Sobald Sie den Titel öffnen, werden Sie mit typischem GNU-Unsinn begrüßt, aber lassen Sie uns ihn überspringen und versuchen, ihn zu finden
isalnum.
Das erste Ergebnis ist folgendes:
enum
{
_ISupper = _ISbit (0), /* UPPERCASE. */
_ISlower = _ISbit (1), /* lowercase. */
// ...
_ISalnum = _ISbit (11) /* Alphanumeric. */
};
Es sieht aus wie ein Implementierungsdetail. Fahren wir fort.
__exctype (isalnum);
Aber was ist das
__exctype? Wir gehen ein paar Zeilen zurück ...
#define __exctype(name) extern int name (int) __THROW
Okay, anscheinend ist dies nur ein Prototyp. Es ist jedoch nicht klar, warum hier ein Makro benötigt wird. Weiter suchen ...
#if !defined __NO_CTYPE
# ifdef __isctype_f
__isctype_f (alnum)
// ...
Das sieht also schon nach etwas Nützlichem aus. Was ist das
__isctype_f? Aufschütteln ...
#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
Nun, es beginnt ... Okay, zusammen werden wir es irgendwie herausfinden. Anscheinend ist
__isctype_fdies eine Inline-Funktion ... stop, alles befindet sich im else-Block der Präprozessoranweisung #ifndef __cplusplus. Sackgasse. Wo isalnum, ihre Mutter, ist eigentlich definiert? Weiter suchen ... Vielleicht ist es das?
#if !defined __NO_CTYPE
# ifdef __isctype_f
__isctype_f (alnum)
// ...
# elif defined __isctype
# define isalnum(c) __isctype((c), _ISalnum) // <-
Hey, das ist das "Implementierungsdetail", das wir zuvor gesehen haben. Merken?
enum
{
_ISupper = _ISbit (0), /* UPPERCASE. */
_ISlower = _ISbit (1), /* lowercase. */
// ...
_ISalnum = _ISbit (11) /* Alphanumeric. */
};
Versuchen wir, dieses Makro schnell auszuwählen:
# 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
Was zum Teufel ist das? Okay, lassen Sie uns weitermachen und bedenken, dass dies nur eine magische Konstante ist. Ein anderes Makro heißt
__isctype, ähnlich dem, das wir kürzlich gesehen haben __isctype_f. Werfen wir noch einen Blick auf die Branche #ifndef __cplusplus:
#ifndef __cplusplus
# define __isctype(c, type) \
((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type)
#elif defined __USE_EXTERN_INLINES
// ...
#endif
Äh ...
Nun, zumindest haben wir eine Zeiger-Dereferenzierung gefunden, die den Segfault erklären könnte. Was ist das
__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__));
Wie cool von dir, glibc! Ich liebe es einfach , mit Locales umzugehen. Wie auch immer, gdb ist mit meiner abgestürzten Anwendung verbunden, und mit all den Informationen, die ich erhalten habe, schreibe ich dieses Elend:
(gdb) print ((unsigned int **(*)(void))__ctype_b_loc)()[next]
Cannot access memory at address 0x11dfa68
Segfault gefunden. In dem Kommentar stand eine Zeile dazu: "ISO C erfordert ctype-Funktionen, um mit Werten wie" unsigned char "und EOF zu arbeiten." Wenn wir dies in der Spezifikation finden, sehen wir:
In allen Implementierungen [der in ctype.h deklarierten Funktionen] lautet das Argument int, dessen Wert in ein vorzeichenloses Zeichen passen muss oder gleich dem Wert des EOF-Makros ist.
Jetzt wird klar, wie das Problem behoben werden kann. Mein Gelenk. Es stellt sich heraus, dass ich kein
isalnumbeliebiges UCS-32-Zeichen eingeben kann, um dessen Auftreten in den Bereichen 0x30-0x39, 0x41-0x5A und 0x61-0x7A zu überprüfen.
Aber hier werde ich mir
isalnumerlauben, vorzuschlagen : Vielleicht sollte die Funktion überhaupt keinen Segfault auslösen, unabhängig davon, was sie bekommt? Vielleicht bedeutet es nicht, dass es so gemacht werden sollte, selbst wenn die Spezifikation es zulässt ? Vielleicht sollte das Verhalten dieser Funktion nicht nur fünf Makros enthalten, sondern auch die Verwendung des C ++ - Compilers überprüfen, von der Bytereihenfolge Ihrer Architektur, der Nachschlagetabelle, den Stream-Gebietsschemadaten und der Dereferenzierung von zwei Zeigern abhängen.
Schauen wir uns zur kurzen Erinnerung noch einmal die Muss-Version an:
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;
}
Das sind die Kuchen.
Anmerkung des Übersetzers: Vielen Dank an MaxGraey für die Verknüpfung mit dem Original.