仅在使用 clang 10 编译时出现 strsep 之后的段错误
Segfault after strsep only when compiling with clang 10
我正在编写一个解析器(用于 NMEA 句子),它使用 strsep 在逗号上拆分字符串。当使用 clang(Apple LLVM 版本 10.0.1)编译时,代码在拆分具有偶数个标记的字符串时会出现段错误。当在 Linux 上使用 clang(版本 7.0.1)或 gcc(9.1.1)编译时,代码工作正常。
出现问题的代码的精简版本如下:
#include <stdio.h>
#include <stdint.h>
#include <string.h>
static void gnss_parse_gsa (uint8_t argc, char **argv)
{
}
/**
* Desciptor for a NMEA sentence parser
*/
struct gps_parser_t {
void (*parse)(uint8_t, char**);
const char *type;
};
/**
* List of avaliable NMEA sentence parsers
*/
static const struct gps_parser_t nmea_parsers[] = {
{.parse = gnss_parse_gsa, .type = "GPGSA"}
};
static void gnss_line_callback (char *line)
{
/* Count the number of comma seperated tokens in the line */
uint8_t num_args = 1;
for (uint16_t i = 0; i < strlen(line); i++) {
num_args += (line[i] == ',');
}
/* Tokenize the sentence */
char *args[num_args];
for (uint16_t i = 0; (args[i] = strsep(&line, ",")) != NULL; i++);
/* Run parser for received sentence */
uint8_t num_parsers = sizeof(nmea_parsers)/sizeof(nmea_parsers[0]);
for (int i = 0; i < num_parsers; i++) {
if (!strcasecmp(args[0] + 1, nmea_parsers[i].type)) {
nmea_parsers[i].parse(num_args, args);
break;
}
}
}
int main (int argc, char **argv)
{
char pgsa_str[] = "$GPGSA,A,3,02,12,17,03,19,23,06,,,,,,1.41,1.13,0.85*03";
gnss_line_callback(pgsa_str);
}
段错误发生在第 if (!strcasecmp(args[0] + 1, nmea_parsers[i].type)) {
行,对 args 的索引操作试图引用空指针。
通过手动编辑程序集或在函数中的任何位置添加对 printf("")
的调用来增加堆栈的大小,使其不再出现段错误,使 args
数组更大(例如,将 1 添加到 num_args
).
总而言之,以下任何一项都可以防止段错误:
- 使用 clang 10
以外的编译器
- 修改程序集使动态分配前的堆栈大小为 80 字节或更多(编译为 64)
- 使用带有奇数个标记的输入字符串
- 将 args
分配为具有正确数量(或更多)令牌的固定长度数组
- 将 args
分配为至少包含 num_args + 1
个元素的可变长度数组
请注意,当在 Linux 上使用 clang 7 编译时,动态分配之前的堆栈大小仍为 64 字节,但代码不会出现段错误。
我希望有人能够解释为什么会发生这种情况,如果有任何方法我可以让这段代码用 clang 10 正确编译。
当编译器的特定版本等各种 barely-relevant 因素似乎有所不同时,这是一个非常明确的迹象,表明您在某处存在未定义的行为。
您正确地计算了逗号数以预先确定字段的确切数量,num_args
。您分配的数组 勉强 足以容纳这些字段:
char *args[num_args];
但是你 运行 这个循环:
for (uint16_t i = 0; (args[i] = strsep(&line, ",")) != NULL; i++);
此循环将有 num_args
次行程,其中 strsep
returns non-NULL 指针通过 args[0]
填充args[num_args-1]
,这正是您想要的,这很好。但是随后又调用了一个 strsep
,那个 return 为 NULL 并终止循环的调用 -- 但是那个空指针也被存储到 args
数组中,特别是 args[num_args]
,这是最后一个单元格。数组溢出,换句话说。
有两种方法可以解决这个问题。您可以使用额外的变量,以便在将 strsep
的 return 值 存储到 args
数组之前捕获和测试 :
char *p;
for (uint16_t i = 0; (p = strsep(&line, ",")) != NULL; i++)
args[i] = p;
这还有一个附带的好处,那就是你有一个更传统的循环,有一个实际的主体。
或者,您可以将 args
数组声明为比严格要求的数组大一个,这意味着它有空间用于存储在 args[num_args]
:
中的最后一个 NULL 指针
char *args[num_args+1];
这有一个附带的好处,就是你总是将 "NULL terminated array" 传递给解析函数,这对它们来说很方便(最终匹配,碰巧的是 main
得到的方式称为)。
我正在编写一个解析器(用于 NMEA 句子),它使用 strsep 在逗号上拆分字符串。当使用 clang(Apple LLVM 版本 10.0.1)编译时,代码在拆分具有偶数个标记的字符串时会出现段错误。当在 Linux 上使用 clang(版本 7.0.1)或 gcc(9.1.1)编译时,代码工作正常。
出现问题的代码的精简版本如下:
#include <stdio.h>
#include <stdint.h>
#include <string.h>
static void gnss_parse_gsa (uint8_t argc, char **argv)
{
}
/**
* Desciptor for a NMEA sentence parser
*/
struct gps_parser_t {
void (*parse)(uint8_t, char**);
const char *type;
};
/**
* List of avaliable NMEA sentence parsers
*/
static const struct gps_parser_t nmea_parsers[] = {
{.parse = gnss_parse_gsa, .type = "GPGSA"}
};
static void gnss_line_callback (char *line)
{
/* Count the number of comma seperated tokens in the line */
uint8_t num_args = 1;
for (uint16_t i = 0; i < strlen(line); i++) {
num_args += (line[i] == ',');
}
/* Tokenize the sentence */
char *args[num_args];
for (uint16_t i = 0; (args[i] = strsep(&line, ",")) != NULL; i++);
/* Run parser for received sentence */
uint8_t num_parsers = sizeof(nmea_parsers)/sizeof(nmea_parsers[0]);
for (int i = 0; i < num_parsers; i++) {
if (!strcasecmp(args[0] + 1, nmea_parsers[i].type)) {
nmea_parsers[i].parse(num_args, args);
break;
}
}
}
int main (int argc, char **argv)
{
char pgsa_str[] = "$GPGSA,A,3,02,12,17,03,19,23,06,,,,,,1.41,1.13,0.85*03";
gnss_line_callback(pgsa_str);
}
段错误发生在第 if (!strcasecmp(args[0] + 1, nmea_parsers[i].type)) {
行,对 args 的索引操作试图引用空指针。
通过手动编辑程序集或在函数中的任何位置添加对 printf("")
的调用来增加堆栈的大小,使其不再出现段错误,使 args
数组更大(例如,将 1 添加到 num_args
).
总而言之,以下任何一项都可以防止段错误:
- 使用 clang 10
以外的编译器
- 修改程序集使动态分配前的堆栈大小为 80 字节或更多(编译为 64)
- 使用带有奇数个标记的输入字符串
- 将 args
分配为具有正确数量(或更多)令牌的固定长度数组
- 将 args
分配为至少包含 num_args + 1
个元素的可变长度数组
请注意,当在 Linux 上使用 clang 7 编译时,动态分配之前的堆栈大小仍为 64 字节,但代码不会出现段错误。
我希望有人能够解释为什么会发生这种情况,如果有任何方法我可以让这段代码用 clang 10 正确编译。
当编译器的特定版本等各种 barely-relevant 因素似乎有所不同时,这是一个非常明确的迹象,表明您在某处存在未定义的行为。
您正确地计算了逗号数以预先确定字段的确切数量,num_args
。您分配的数组 勉强 足以容纳这些字段:
char *args[num_args];
但是你 运行 这个循环:
for (uint16_t i = 0; (args[i] = strsep(&line, ",")) != NULL; i++);
此循环将有 num_args
次行程,其中 strsep
returns non-NULL 指针通过 args[0]
填充args[num_args-1]
,这正是您想要的,这很好。但是随后又调用了一个 strsep
,那个 return 为 NULL 并终止循环的调用 -- 但是那个空指针也被存储到 args
数组中,特别是 args[num_args]
,这是最后一个单元格。数组溢出,换句话说。
有两种方法可以解决这个问题。您可以使用额外的变量,以便在将 strsep
的 return 值 存储到 args
数组之前捕获和测试 :
char *p;
for (uint16_t i = 0; (p = strsep(&line, ",")) != NULL; i++)
args[i] = p;
这还有一个附带的好处,那就是你有一个更传统的循环,有一个实际的主体。
或者,您可以将 args
数组声明为比严格要求的数组大一个,这意味着它有空间用于存储在 args[num_args]
:
char *args[num_args+1];
这有一个附带的好处,就是你总是将 "NULL terminated array" 传递给解析函数,这对它们来说很方便(最终匹配,碰巧的是 main
得到的方式称为)。