C语言 Boyer摩尔算法的一个简单版本中的未定义行为

eiee3dmh  于 11个月前  发布在  其他
关注(0)|答案(3)|浏览(146)

我正在尝试写一个更简单的版本的Boyer摩尔算法,没有前缀函数。它必须打印所有与模式比较的符号的位置。并且在本地它通过了测试,但是当我提交到gitlab时它失败了。我不能在这里发现未定义的行为。我在最后有垃圾。

#include <stdio.h>

#define MAX_PATTERN_LEN 16
#define BUF_SIZE 69
#define ALPH_SIZE 128

int read_str(int max_len, unsigned char *str_place) {
    int count_read = 0;
    for (int i = 0, ch; i < max_len; i++) {
        if ((ch = getchar()) == '\n') {
            str_place[i] = '\0';
            break;
        }
        str_place[i] = (char)ch;
        count_read++;
    }
    return count_read;
}

void calculate_shifts(const unsigned char *str, int len_str, int *badchar) {
    for (int i = 0; i < ALPH_SIZE; i++)
        badchar[i] = len_str;
    for (int i = 0; i < len_str - 1; i++)
        badchar[str[i]] = len_str - 1 - i;
}

void search(const unsigned char *str, const unsigned char *patt, int len_patt, int len_str) {
    int badchar[ALPH_SIZE];
    calculate_shifts(patt, len_patt, badchar);
    int shift = 0;
    while (shift <= (len_str - len_patt)) {
        int j = len_patt - 1;
        for (; j >= 0 && patt[j] == str[shift + j]; j--)
            printf("%d ", shift + j + 1);
        if (j < 0) {
            shift += ((shift + len_patt) < len_str) ? badchar[patt[len_patt - 1]] : 1;
        } else {
            printf("%d ", shift + j + 1);
            int shift_addition = badchar[str[shift + j]];
            if ((shift_addition == len_patt) && (j < len_patt - 1) && (patt[len_patt - 1] == patt[0]))
                shift_addition--;
            shift += shift_addition;
        }
    }
}

int main(void) {
    unsigned char str[BUF_SIZE + 1];
    unsigned char patt[MAX_PATTERN_LEN + 1];
    int len_patt = read_str(MAX_PATTERN_LEN + 1, patt);
    int len_str = read_str(BUF_SIZE + 1, str);
    if (!len_patt || !len_str)
        return 0;
    search(str, patt, len_patt, len_str);
    return 0;
}

字符串
测试:

example
this is simple example


正确的输出:

7 14 13 12 11 10 20 22 21 20 19 18 17 16


实际输出:

7 14 13 12 11 10 20 22 21 20 19 18 17 16 28 ..

7rfyedvj

7rfyedvj1#

差异(输出末尾多了一个数字28)可能是由于输入的最后一行末尾缺少一个换行符(\n)造成的。我能够在本地复制你的两个输出(有28和没有28)。

$ gcc -g -O2 -W -Wall -o t t.c
$ printf 'example\nthis is simple example; \n' | ./t; echo
7 14 13 12 11 10 20 22 21 20 19 18 17 16
$ printf 'example\nthis is simple example; ' | ./t; echo
7 14 13 12 11 10 20 22 21 20 19 18 17 16 28

字符串

TL;DR通过将(ch = getchar()) == '\n'更改为(ch = getchar()) == EOF || ch == '\n',并将#define ALPH_SIZE 128更改为#define ALPH_SIZE 256来修复代码中的错误。

可能还有其他bug,我还没有检查。如果你遇到其他bug,请在StackOverflow上单独提问。
这个答案的其余部分解释了我是如何诊断这个bug的。
AddressSanitizergcc -fsanitize=address)在缺少换行符时显示内存访问问题:

$ gcc -g -O2 -W -Wall -fsanitize=address -o t t.c
$ printf 'example\nthis is simple example; \n' | ./t; echo
7 14 13 12 11 10 20 22 21 20 19 18 17 16 
$ printf 'example\nthis is simple example; ' | ./t; echo
ASAN:DEADLYSIGNAL
=================================================================
==19294==ERROR: AddressSanitizer: SEGV on unknown address 0x7ffe25fbe264 (pc 0x563d6542b204 bp 0x7ffe25fbe264 sp 0x7ffe9a1bbc70 T0)
==19294==The signal is caused by a READ memory access.
    #0 0x563d6542b203 in search /tmp/t.c:33
    #1 0x563d6542acb1 in main /tmp/t.c:54
    #2 0x7f617ff96c86 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21c86)
    #3 0x563d6542ad99 in _start (/tmp/t+0xd99)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /tmp/t.c:33 in search
==19294==ABORTING


请注意,我不得不重复多次以触发AddressSanitizer错误消息。
大多数情况下,这是由程序中的错误引起的,通常是未定义的行为。AddressSanitizer输出中的in search /tmp/t.c:33子句指示错误读取发生在源代码中的何处(即第33行,函数search)。
我在你的代码中添加了一些额外的printf s,看看第33行发生了什么。输出是:

$ printf 'example\nthis is simple example; ' | ./t; echo
patt[6] == 101
str[6] == 115
patt[6] == 101
str[13] == 101
patt[5] == 108
str[12] == 108
patt[4] == 112
str[11] == 112
patt[3] == 109
str[10] == 109
patt[2] == 97
str[9] == 105
patt[6] == 101
str[19] == 112
patt[6] == 101
str[21] == 101
patt[5] == 108
str[20] == 108
patt[4] == 112
str[19] == 112
patt[3] == 109
str[18] == 109
patt[2] == 97
str[17] == 97
patt[1] == 120
str[16] == 120
patt[0] == 101
str[15] == 101
patt[6] == 101
str[27] == 255
patt[6] == 101
str[-369011759] == ??
ASAN:DEADLYSIGNAL
=================================================================
==19906==ERROR: AddressSanitizer: SEGV on unknown address 0x7ffc6e5c0c71 (pc 0x5621e2363094 bp 0x0000ea0153d1 sp 0x7ffc845ab540 T0)
==19906==The signal is caused by a READ memory access.
    #0 0x5621e2363093 in gett /tmp/t.c:33
    #1 0x5621e236349b in search /tmp/t.c:42
    #2 0x5621e2362e61 in main /tmp/t.c:63
    #3 0x7f7063ee8c86 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21c86)
    #4 0x5621e2362f49 in _start (/tmp/t+0xf49)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /tmp/t.c:33 in gett
==19906==ABORTING


罪魁祸首是最后一次读访问:str[-369011759] == ??,这表明shift + j变成了-369011759,这显然是不正确的,因为正确的值是小的非负整数。
请注意,我不得不重复多次以触发Address Sanitizer错误消息,并且每次的数字-369011759都不同。
看起来j有一个正常的值6。要找出实际的bug,下一步是检查shift在哪里得到它的大负值。
在这一点上,输出行str[27] == 255对我来说是可疑的。我检查了您的代码是否正确处理了str中的如此大的值,然后我发现了一个bug。
修复这个新发现的bug的一种方法是将ALPH_SIZE128更改为256。(我已经验证了它会使地址消毒器错误消失。)原因如下。第39行包含badchar[str[shift + j]]的读取。要使其有效,我们需要0 <= str[shift + j] && str[shift + j] < ALPH_SIZE,因为ALPH_SIZEbadchar数组的大小。然而,str[...]是255(因为它是无符号字符),因此我们需要ALPH_SIZE >= 256
实际上,越界数组访问(读或写)在C中是未定义的行为。

printf("%d ", shift + j + 1); fprintf(stderr, "RBC %d == %d\n", shift + j, str[shift + j]);


,我还将第28行改为使用malloc:

int *badchar = malloc(sizeof(int) * ALPH_SIZE);


,之后我还添加了free(badchar)

$ gcc -g -O2 -W -Wall -include stdlib.h -fsanitize=address -o t t.c
$ printf 'example\nthis is simple example; \n' | ./t; echo
RBC 6 == 115
RBC 9 == 105
RBC 19 == 112
7 14 13 12 11 10 20 22 21 20 19 18 17 16 
$ printf 'example\nthis is simple example; ' | ./t; echo
RBC 6 == 115
RBC 9 == 105
RBC 19 == 112
RBC 27 == 255
=================================================================
==23463==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x61500000047c at pc 0x563915b3754a bp 0x7fff5f340710 sp 0x7fff5f340700
READ of size 4 at 0x61500000047c thread T0
    #0 0x563915b37549 in search /tmp/t.c:39
    #1 0x563915b36dc1 in main /tmp/t.c:54
    #2 0x7f1a63d6dc86 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21c86)
    #3 0x563915b36ea9 in _start (/tmp/t+0xea9)

Address 0x61500000047c is a wild pointer.
SUMMARY: AddressSanitizer: heap-buffer-overflow /tmp/t.c:39 in search
Shadow bytes around the buggy address:
  0x0c2a7fff8030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c2a7fff8040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c2a7fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c2a7fff8060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c2a7fff8070: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c2a7fff8080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa[fa]
  0x0c2a7fff8090: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c2a7fff80a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c2a7fff80b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c2a7fff80c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c2a7fff80d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==23463==ABORTING


实际上,AddressSanitizer的输出指出了第39行中的bug,而额外的fprintf调用的输出确认这是一个越界的数组访问:阅读数组badchar的元素255,该数组只有128个元素。
我们是怎么得到str[27] == 255的(因此阅读badchar[255])?这很简单:如果在阅读str时,在文件的末尾没有终止\n,则getchar()返回“0”,当分配给unsigned char时,“0”变为255。(如上所述,明显的修复方法是在x1处停止,并将ALPH_SIZE更改为256。
为什么AddressSanitizer没有报告读取越界数组索引问题(作为stack-buffer-overflow)在badchar之前的第39行被更改为使用malloc(报告为堆缓冲区溢出)?我还不太明白这一点。AddressSanitizer应该能够可靠地报告堆栈缓冲区溢出。可能这取决于GCC和Clang版本,因为将命令gcc更改为clang使堆栈缓冲区溢出错误可靠地出现。可能升级到最新的GCC和Clang会有所帮助。Valgrind在检测堆栈缓冲区溢出读取方面不如AddressSanitizer可靠。

b5buobof

b5buobof2#

你的read_str()函数在两个方面有缺陷:
1.它不识别或处理标准输入的文件结尾。当程序的标准输入连接到终端时,通常不会遇到这种情况,但如果输入从常规文件或管道(例如)重定向,则完全可能出现这种情况。这可能会导致字符串结尾处出现不需要的额外字符。
1.它不会向最大长度输入添加字符串结束符。
而这些合并,因为如果read_str()确实到达了文件结尾,那么它将始终表现得好像它收到了一个最大长度的输入,因此无法终止它。
终止失败将导致程序在其他地方表现出未定义的行为,但问题的根源是read_str()。我的第一个建议是完全转储read_str(),并使用标准fgets()

fgets(str, BUF_SIZE + 1, stdin);
    len_str = strcspn(str, "\n");
    str[len_str] = '\0';  // trim a trailing newline, if any

字符串
但是如果你坚持使用自己的read_str(),并且参数具有代码所隐含的语义,那么你需要纠正上面描述的两个问题。例如:

int read_str(int max_len, unsigned char *str_place) {
    int count_read = 0;
    int ch = EOF;
    for (int i = 0; i < max_len; i++) {
        ch = getchar();

        // break at newline OR EOF
        if (ch == '\n' || ch == EOF) {
            break;
        }

        str_place[i] = (char)ch;
        count_read++;
    }

    // If max_len characters were read without seeing end-of-line or end-of-file
    // then push the last back and adjust the number of characters accepted
    if (count_read >= max_len) {
        ungetc(ch, stdin);
        count_read--;
    }

    // Terminate the string after the last character accepted
    str_place[count_read] = '\0';
    return count_read;
}

hsvhsicv

hsvhsicv3#

存在多种问题:

  • ALPH_SIZE被定义为128,它不是unsigned char类型的范围。您应该使用(UCHAR_MAX+1),或256表示8位字节,以允许badchar数组处理字符串中超过127的字符,如UTF-8前导和连续字节或其他非ASCII内容。这是潜在的未定义行为。
  • 你将目标数组的长度传递给read_str作为参数max_len,如果你在到达max_len字符之前读取了一个换行符,那么你只会空终止这个数组。这是容易出错的:它可能不会在发布的代码中引起问题,但不推荐这样做,特别是当你显式地空终止较短的字符串时。
  • read_str中的阅读循环不会在文件结束时停止,因为您只测试'\n'。如果输入的最后一行没有以换行符结束,则循环将继续尝试从stdin读取,从getchar()接收EOF并存储'\377'(即:0xff255)到数组中,后来导致未定义的行为,因为数组badchar太短,无法处理这些字节值。

相关问题