3

我正在编写一个解析器(用于 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);
}

段错误发生在 line 上if (!strcasecmp(args[0] + 1, nmea_parsers[i].type)) {,对 args 的索引操作尝试遵循空指针。

通过手动编辑程序集或添加printf("")对函数中任何位置的调用来增加堆栈的大小,使其不再出现段错误,就像使args数组更大(例如,在 中添加一个num_args)。

总之,以下任何一项都可以防止段错误:
- 使用 clang 10 以外的编译器
- 修改程序集以使动态分配之前的堆栈大小为 80 字节或更多(编译为 64)
- 使用奇数的输入字符串标记
- 分配args为具有正确数量的标记(或更多)的固定长度数组
- 分配args为具有至少num_args + 1元素的可变长度数组
请注意,在 Linux 上使用 clang 7 编译时,动态分配之前的堆栈大小仍然是 64 字节,但代码没有段错误。

我希望有人能够解释为什么会发生这种情况,如果有什么方法可以让这个代码用 clang 10 正确编译。

4

1 回答 1

4

当各种几乎不相关的因素(例如编译器的特定版本)似乎有所不同时,这是一个非常确定的迹象,表明您在某处有未定义的行为。

您正确计算逗号以预先确定字段的确切数量,num_args. 您分配的数组刚好容纳这些字段:

char *args[num_args];

但是然后你运行这个循环:

for (uint16_t i = 0; (args[i] = strsep(&line, ",")) != NULL; i++);

在此循环中将有num_args多次行程,其中返回填充到throughstrsep的非 NULL 指针,这是您想要的,这很好。但是还有一个对 的调用,即返回 NULL 并终止循环的调用——但该空指针也被存储到数组中,特别是存储到最后一个单元格中。换句话说,数组溢出。args[0]args[num_args-1]strsepargsargs[num_args]

有两种方法可以解决此问题。您可以使用额外的变量,以便在将其存储到数组中之前strsep捕获和测试的返回值:args

char *p;
for (uint16_t i = 0; (p = strsep(&line, ",")) != NULL; i++)
    args[i] = p;

这还有一个额外的好处,那就是你有一个更传统的循环,有一个实际的身体。

或者,您可以将args数组声明为比严格要求的大一,这意味着它有空间存储最后一个 NULL 指针args[num_args]

char *args[num_args+1];

这有一个附带好处,即您始终将“NULL 终止数组”传递给解析函数,这对它们来说很方便(并且最终匹配,碰巧,方式main被调用)。

于 2019-07-12T04:52:04.517 回答