我正在编写一个解析器(用于解析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
数组更大(例如将num_args
加一)来实现。
总之,以下任何一项都可以避免段错误:
- 使用除clang 10以外的编译器
- 修改汇编代码,使动态分配之前的堆栈大小为80字节或更多(编译结果为64个字节)
- 使用具有奇数个标记的输入字符串
- 将args
作为具有正确数量的标记(或更多)的定长数组进行分配
- 将args
作为至少具有num_args + 1
个元素的可变长度数组进行分配
请注意,在Linux上使用clang 7编译时,动态分配之前的堆栈大小仍为64字节,但是代码不会出现段错误。
我希望有人能够解释为什么会发生这种情况,以及是否有任何方法可以使这段代码在clang 10上编译正确。
*args[num_args+1];
吗?精确性是好的,但我发现为自己留出一点余地通常可以节省时间,以防我在某个地方出现了一个单元错误。实际上我很确定你有一个单元错误:在通过strsep
循环进行 N 次(即num_args
次)之后,你开始制作第 N+1 次循环,也就是你发现strsep
返回 NULL 的那一次循环。但是你已经将该 NULL 指针存储在args [num_args]
中,从而导致了溢出。 - Steve Summit