仅在使用 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 得到的方式称为)。