物力维艰 - AI错误逻辑的应对及处置

从知乎直答推出后,我就放弃了chatgpt,看一个deepseek被自己的思路绕晕的例子:

1.源码 - 用来进行字符串检索替换,将key 替换为value.

//字符串关键字替换
int gp_str_replace(char *tgt, const char *key, const char *value, size_t tgt_len) {
    if (!tgt || !key) return -1; // 校验空指针
    if(0==tgt_len) return 0;

    size_t key_len = strlen(key);
    if(0==key_len) return 0;
    
    size_t value_len = 0;
    if(!value) value_len = 0;
    else{
        value_len = strlen(value);
        if(0 == strcmp(key,value)) return 0;
    }
    
    char *start_pos = NULL;
    do
    {
        start_pos = strstr(tgt, key);
        if (!start_pos) return 0; // no key left, leave.

        size_t front_len = start_pos - tgt; //before key
        size_t back_len = strlen(start_pos + key_len); // back value
        size_t new_len = front_len + value_len + back_len + 1;
        if (tgt_len < new_len) return -2; // no room, return

        memmove(start_pos + value_len, start_pos + key_len, back_len + 1); //tail move back
        if(0 == value_len) continue;
        memcpy(start_pos, value, value_len); // value in
        tgt += front_len+value_len;
    } while (*tgt != 0);
    return 0;
}

2.发给AI的问题

 针对上述代码,给我一个可能的失效案例,like this :char buf[32] = "hello world"; assert(gp_str_replace(buf, "world", "C", sizeof(buf)) == 0); // 正常替换

之所以这样提问,是发现AI始终有一些错误的思路,比如,如果直接让他分析代码,它始终纠结于一些不存在的问题:

1. 循环逻辑缺陷

  • 问题:do-while 循环条件 while (*tgt != 0) 错误,可能导致死循环或越界。

    • 每次替换后 tgt 指针被修改(tgt += front_len+value_len),但循环条件仍检查原始 tgt 的终止符,替换后的新内容可能包含新的 key 未被处理。
  • 风险:若替换后的字符串尾部仍有 key(如 tgt="aa", key="a", value="bb"),循环会遗漏后续匹配。

⚠️ 2. 指针更新逻辑错误

  • 问题:替换后更新 tgt = start_pos + front_len + value_len 不合理。

    • start_pos 是匹配位置的指针,但 memmove 和 memcpy 已修改该位置之后的内存,继续用 start_pos 计算偏移会导致后续操作偏离实际字符串。

所以,我只能让它拿出一个证据,于是AI卡死了: 

3.AI的死循环 

我们分析用户提供的gp_str_replace函数,目的是找出一个可能的失效案例。函数的功能是替换字符串中的关键字。函数逻辑如下:

  1. 检查空指针和长度,如果key长度为0或value等于key则直接返回0(不替换)。

  2. 循环查找key,每次找到一个key后,计算替换后新字符串的长度,检查缓冲区是否足够(tgt_len>=new_len)。

  3. 然后移动后面的字符串(memmove)腾出位置,再复制value(如果value_len>0)。

  4. 更新tgt指针(tgt += front_len+value_len)以便继续查找后面的部分。

注意:在循环中,每次替换后,tgt指针被更新为:tgt += front_len+value_len。这个操作是为了移动查找起点,避免重复替换已经替换过的部分,同时继续查找后面是否还有key。

但是,这里有一个关键点:在循环的下一轮,start_pos = strstr(tgt, key) 是从新的tgt位置开始查找。但是,在替换后,字符串的内容发生了改变,而且tgt指针被移动到了当前替换部分的后面(即已经跳过了替换后的value部分)。

然而,我们需要注意,替换操作可能会在移动内存时覆盖掉后面的内容,而且tgt指针的移动是基于替换前的偏移量,这可能导致后续查找出现问题吗?

我们考虑一种情况:替换后的字符串中,在刚刚替换的value后面紧接着又出现了key(可能是value本身包含key,也可能是移动后的字符串形成了新的key)。

例如:原始字符串是 "hello world world",我们替换第一个"world"为"worl",那么替换后变成"hello worl world"。如果我们希望替换所有的"world",但是第二次查找是从第一个替换后的位置开始,即从第一个替换后的"worl"后面开始,所以第二个"world"仍然会被找到并替换。

但是,这里我们要找的是失效案例,即函数不能按预期工作的情况。

观察循环条件:while (*tgt != 0)。这个条件表示只要当前tgt指向的字符不是'\0'就继续循环。但是,在循环内部,如果替换后,字符串的结尾已经被移动,并且我们更新tgt指针后,它指向的位置可能已经超过了字符串的结尾?或者,更关键的是,我们每次替换后,tgt指针被更新为:tgt += front_len+value_len。注意,front_len是本次替换开始位置相对于本次查找起点的偏移(实际上,这个front_len是相对于本次查找起点tgt的偏移)。但是,在第一次替换时,tgt是原始字符串的起始地址,front_len就是start_pos(第一个key的位置)到原始字符串起始地址的偏移。然后tgt被更新为:tgt = tgt + front_len + value_len。这样,下一次查找就从替换后的value后面开始。

然而,这里隐藏了一个问题:在更新tgt指针后,tgt指向的位置是替换后的value的后面。但是,这个位置并不一定是整个字符串的起始位置,所以我们不能简单地用这个指针去作为下一次查找的起点(因为strstr是从tgt开始直到字符串结束)。看起来逻辑是正常的。

但是,我们考虑一种特殊情况:当替换的value中包含key,而且这个key出现在value的末尾,那么替换后,在value的末尾和原来被移动的字符串开头部分可能形成一个新的key。

例如: char buf[64] = "aba"; gp_str_replace(buf, "a", "a", sizeof(buf)); // 用"a"替换"a",这本身会被跳过(因为value等于key,函数开头有判断:if(0 == strcmp(key,value)) return 0;),所以不会进行替换。

那我们换一个:替换的value是key的一部分,且替换后形成新的key。

另一种情况:替换的value中包含key,且后面紧跟着原来字符串中key后面的部分又组成了key。

但是,函数中更新tgt指针的方式可能会导致跳过一些位置。我们看一个具体的例子:

假设原始字符串:buf = "abcabcd" 我们要替换"abc"为"x"(value_len=1,key_len=3)

第一次查找:start_pos指向第一个'abc'(位置0),front_len=0,back_len=4(因为start_pos+key_len指向'd',后面字符串"abcd"的长度为4,注意:这个back_len是包括后面整个字符串直到'\0')。 然后移动内存:memmove(start_pos+value_len, start_pos+key_len, back_len+1) -> 也就是把位置3开始的5个字符("abcd\0"共5个)移动到位置1(因为value_len=1)。移动后,字符串变成:'a'(原来是第一个'a')+'x'(我们会覆盖)?不对,我们先移动再拷贝value。

步骤:

1. 先移动:将start_pos+key_len(即位置3)开始的back_len+1(5)个字节移动到start_pos+value_len(0+1=1)的位置。所以,移动后,从位置1开始是"abcd\0",此时字符串变为:a a b c d \0  (注意:第一个位置还是'a',然后后面是"abcd\0")

2. 然后拷贝value:memcpy(start_pos, value, value_len) -> 在位置0拷贝'x',所以字符串变成:'x'然后位置1开始是"abcd\0",即字符串为"xabcd"。

3. 然后更新tgt指针:tgt += front_len+value_len -> tgt原来指向0,加上0+1=1,所以tgt指向位置1(即'a'的位置)。

下一次循环:从tgt(位置1)开始查找"abc",在字符串"abcd"中查找,找到位置1开始的"abc"吗?字符串是"abcd",所以从位置1开始是"abc"?实际上,位置1是'a',位置2是'b',位置3是'c',所以strstr("abcd"+1, "abc") -> 在"bcd"中找"abc",找不到。所以函数返回0,替换了第一个。

但是,我们预期是替换所有的"abc"。现在字符串变成了"xabcd",里面还有一个"abc"(位置1到3)没有被替换。所以,这里没有替换第二个。

因此,这个函数并不能替换所有的匹配,因为每次替换后,下一次查找从替换后的value后面开始,而替换后的value后面可能是原来字符串的一部分,可能包含key,但是由于跳过了这部分,所以不会被替换。

所以,失效案例是:当一次替换后,在替换位置后面仍然可能存在key,但是函数不会回头检查,而是从替换后的value后面继续查找,从而漏掉了一些重叠的key。

但是,我们上面的例子中,替换后形成的key是在替换位置后面的,但是我们的查找起点已经跳过了这个key的开始位置(因为tgt指向了替换后的value的下一个位置,而新的key的开始位置在这个位置之前?不对,新的key开始位置在位置1,而tgt也指向位置1,所以下一次查找是从位置1开始,应该能找到位置1开始的"abc"啊?为什么没找到?因为字符串已经变成了"xabcd",所以从位置1开始的字符串是"abcd",而"abc"是位置1开始的子串(位置1: 'a', 位置2: 'b', 位置3: 'c'),所以应该找到。

重新检查:在第一次替换后,字符串变为"xabcd",然后tgt指向位置1(即'a'的位置)。然后执行strstr(tgt, key) -> 在"abcd"中查找"abc",结果是能查找到,因为"abcd"的前三个字符就是"abc"。

为什么上面说找不到?实际上,strstr("abcd", "abc")是可以找到的,返回位置1(相对于整个字符串的地址是位置1)。所以第二次循环会替换这个"abc"。

第二次替换: start_pos指向位置1(即'a'的位置) front_len = start_pos - tgt = 1-1=0(因为tgt此时也是位置1) back_len = strlen(start_pos+key_len)-> key_len=3,所以start_pos+3指向'd',后面的字符串是"d\0",长度为1。 new_len = 0+1+1+1=3,缓冲区足够。 移动:memmove(位置1+1, 位置1+3, 1+1) -> 将位置4(即'\0')开始的2个字节(实际上只有一个字符'd'和'\0')移动到位置2。移动后,从位置2开始是"d\0"。注意:移动前,位置1开始的字符串是"abcd\0",移动后,位置1+1(即位置2)开始变成"d\0"。所以位置1后面的内容变成了:'a'(位置1)后面是'd'(位置2)和'\0'(位置3)。但是,注意,在移动前,我们还没有修改位置1的内容。 然后,拷贝value:在位置1拷贝'x',所以字符串变成:位置0是'x',位置1是'x',位置2是'd',位置3是'\0'。所以整个字符串是"xxd"。 然后更新tgt:tgt += front_len+value_len = 1+0+1=2(因为front_len=0, value_len=1) -> tgt指向位置2。 下一次循环:从位置2开始查找,字符串是"d\0",找不到,结束。

所以最后字符串变成了"xxd",替换了两次。这符合预期。

那么,问题在哪里?

我们注意到,在替换过程中,我们移动后面的字符串时,可能会覆盖掉后面的key吗?或者,另一种情况:替换的value中包含key,且后面紧接着又有key,这种情况可能会造成无限循环?

举例:原始字符串:buf = "aaa" 替换:将"a"替换为"aa"

第一次查找:在位置0找到"a" front_len=0, back_len=strlen(位置0+1)=strlen("aa")=2 new_len = 0+2+2+1=5,缓冲区足够(假设buf足够大) 移动:将位置1开始的3个字节("aa\0")移动到位置0+2=2的位置。移动后,字符串变为:位置0:'a'(还没被覆盖),位置1:'a',位置2:'a',位置3:'a',位置4:'\0'?不对,移动后,从位置2开始存放"aa\0",所以字符串暂时是:位置0:'a',位置1:'a',位置2:'a'(原来的第一个a),位置3:'a'(原来的第二个a),位置4:'\0'?不对,移动的源位置是位置1(即第二个字符),源字符串是"aa\0"(三个字节),目标位置是位置2,所以移动后,位置2,3,4分别是'a','a','\0'。然后,我们拷贝value:"aa"到位置0(覆盖前两个字符)。所以替换后,字符串变成"aa"(位置0和1)加上位置2的'a'和位置3的'a',即"aaaa"(但注意,我们只拷贝了两个字符,所以位置0:'a',1:'a',然后位置2开始是"aa\0",所以字符串是"aaaa\0"?不,位置2是移动过来的第一个'a',位置3是第二个'a',位置4是原来的'\0'(移动后变成了位置4)?不对,移动时,我们移动了3个字节(包括'\0'),所以移动后,位置2,3,4分别是'a','a','\0'。所以整个字符串是:位置0:'a'(value的第一个字符),位置1:'a'(value的第二个字符),位置2:'a'(移动过来的第一个字符),位置3:'a'(移动过来的第二个字符),位置4:'\0'。所以字符串是"aaaa\0",但有效长度是4。

然后更新tgt:tgt+=0+2=2(指向位置2)。 下一次循环:从位置2开始查找"a",找到位置2的'a'(注意,这个位置2的'a'实际上是原始字符串的第二个字符,现在变成了第三个字符?)。所以找到,然后再次替换: 此时,start_pos指向位置2。 front_len=0(相对于tgt位置2,start_pos也是位置2,所以偏移0) back_len=strlen(位置2+1)=strlen("a\0")=1 -> 注意,位置2+1是位置3,字符串是"a\0",长度为1。 new_len=0+2+1+1=4,缓冲区足够。 移动:将start_pos+key_len=2+1=3开始的2个字节("a\0")移动到start_pos+value_len=2+2=4。移动后,位置4和5:'a','\0'。 然后拷贝value:"aa"到位置2和3。字符串变成:位置0,1:'a','a';位置2,3:'a','a';位置4:'a';位置5:'\0'。所以是"aaaaa\0"?(位置0到4:五个a,位置5结束)不对:位置0,1是原来的value,位置2,3是新的value,位置4是移动过来的'a',位置5是'\0'。所以整个字符串是"aaa"(位置0-2)?不对,重新写: 初始:位置0:'a',1:'a',2:'a',3:'a',4:'\0'(第一次替换后) 移动:将位置3(start_pos+key_len=2+1=3)开始的两个字节(位置3和4)移动到位置4(目标位置)。所以位置4和5:原来位置3是'a',位置4是'\0',移动后,位置4得到'a',位置5得到'\0'(注意,移动前,位置4是'\0',移动后,位置4变成了'a',位置5变成了'\0')。 然后拷贝value:"aa"到位置2和3。所以位置2变成'a',位置3变成'a'。所以整个字符串:位置0:'a',1:'a',2:'a',3:'a',4:'a',5:'\0' -> "aaaaa\0" 然后更新tgt:tgt += 0+2=2+2=4(指向位置4)。 第三次循环:在位置4开始查找,字符串位置4是'a',然后下一个字符是'\0',所以strstr会返回位置4(作为匹配,因为"a"匹配)。然后再替换: start_pos=位置4 front_len=0 back_len=strlen(位置4+1)=strlen("")=0 new_len=0+2+0+1=3 移动:将位置4+1=5(即'\0')开始的1个字节(即'\0')移动到位置4+2=6。但是,注意,移动的字节数是back_len+1=0+1=1,所以移动后位置6得到'\0'(原来位置5的'\0'被移动到位置6?但位置5和6可能超出缓冲区?因为缓冲区大小是64,所以位置5和6在范围内)。 然后拷贝value:"aa"到位置4和5。所以位置4和5变成'a','a',然后位置6是'\0'。字符串变成"aaaaaaa"?不对,之前位置0-4已经是5个'a',然后再在位置4和5写两个'a',位置6结束:所以位置0-5都是'a',共6个,位置6结束。所以是"aaaaaa\0"。 tgt更新为4+2=6(指向位置6)。 第四次循环:从位置6开始,查找"a",找到'\0',结束。

所以一共替换了三次,最终字符串变成6个'a'。但是原始的"aaa"中有三个'a',每个'a'都被替换成"aa",所以应该是6个,符合预期。

那么,到底有什么问题?

观察函数中的循环条件:while (*tgt != 0)。这个条件在tgt指向的位置不是'\0'时就继续循环。但是,在替换过程中,我们可能会把tgt更新到字符串的中间,然后下一次循环时,即使后面的字符串还有内容,我们也能继续查找。但是,如果我们替换后,字符串的结尾已经移动,并且tgt指针已经指向了字符串的末尾(即'\0'),那么循环结束。所以这个循环条件看起来是合理的。

然而,我们考虑一种情况:替换后,字符串变短了(比如用空字符串替换一个非空字符串)。那么,在替换后,字符串的结束符会前移。但是,我们的tgt指针更新为:tgt += front_len+value_len。注意,front_len是本次替换的开始位置相对于本次查找起点(即当前的tgt)的偏移。而value_len可能为0(如果用空字符串替换)。那么,tgt指针移动后,可能指向一个非结束符的位置,但此时整个字符串的结束符已经前移,所以从tgt往后直到结束符的长度可能很短。

但是,这并不会引起错误,因为strstr在遇到结束符时会停止。

另一种可能的失效案例:缓冲区溢出检查错误。

函数中,计算新长度:new_len = front_len + value_len + back_len + 1; 其中,front_len是替换位置之前的长度,back_len是替换关键字之后部分的长度(不包括key本身)。所以整个新字符串的长度应该是:front_len(key前面的部分)+ value_len(替换value的长度)+ back_len(key后面的部分)+1(结束符)。这个计算正确。

但是,有一种情况:当替换发生在字符串的末尾时,back_len为0(因为key后面是结束符),所以new_len=front_len+value_len+0+1。但是,front_len=start_pos-tgt,这里tgt是当前查找的起始位置,而start_pos是相对于整个字符串的起始地址的?不对,start_pos是相对于整个字符串的起始地址(即最初传入的tgt)的指针,而front_len=start_pos-tgt,这个tgt在循环过程中已经被修改过,它指向的是当前查找的起始位置(可能是原始字符串中间的位置)。所以,这里的front_len实际上是当前查找起点到本次匹配点的长度,而不是整个字符串开头到匹配点的长度!这就会导致计算new_len时,front_len只是从当前tgt到start_pos的长度,而不是从整个字符串开头到start_pos的长度。

例如: 假设原始字符串:char buf[32] = "abcabc"; tgt初始指向0位置。 第一次替换:找到第一个"abc"(位置0),front_len=0(因为start_pos-tgt=0-0=0),back_len=strlen(start_pos+key_len)=strlen("abc")=3(实际是3,因为后面还有"abc"和一个结束符,所以从位置3开始到结束的长度是3(包括"abc\0",strlen是3))。 然后替换,假设用"x"替换,则new_len=0+1+3+1=5,缓冲区足够。 替换后,移动:将位置3(start_pos+3=位置3)开始的back_len+1=4个字节(包括结束符)移动到位置0+1=1。移动后,从位置1开始是"abc\0"。然后拷贝value:位置0写入'x'。所以字符串变成"xabc"。 更新tgt:tgt += front_len+value_len=0+1=1(指向位置1)。 第二次查找:从位置1开始,strstr("abc", "abc"),找到位置1的"abc"。 此时,front_len=start_pos-tgt=1-1=0(因为start_pos=1,tgt=1) back_len=strlen(start_pos+key_len)=strlen(1+3=位置4)? 但是,注意,第一次替换后,字符串是"xabc",所以位置1是'a',位置2是'b',位置3是'c',位置4是'\0'。所以back_len=strlen(位置4)=0。 new_len=0+1+0+1=2。缓冲区足够。 然后移动:memmove(1+1, 1+3, 0+1) -> 将位置4('\0')开始的1个字节(即'\0')移动到位置2。 然后拷贝value:位置1写入'x'。所以替换后,字符串变成:位置0:'x',位置1:'x',位置2:'\0'。所以字符串是"xx"。 然后更新tgt:tgt +=0+1=1+1=2(指向位置2)。 循环条件:*tgt='\0',结束。

看起来没问题。

但是,注意front_len的计算:front_len=start_pos-tgt。在第一次替换后,tgt变成了1,而第二次查找的start_pos是1,所以front_len=0。这个front_len并不是整个字符串开头到start_pos的长度(整个字符串开头到start_pos的长度应该是1),但是计算new_len时我们并不需要整个字符串开头的长度,因为移动时我们只关心从start_pos+key_len开始的后面部分,而前面部分已经不用管了(因为value会覆盖从start_pos开始的key,而front_len只是当前tgt到start_pos的部分,这部分我们并不需要移动,所以只需要计算start_pos之后的部分)。所以,这里front_len=0是合理的,因为tgt和start_pos相同,所以start_pos之前的部分(相对于tgt)就是0。

因此,front_len的计算没有问题,它表示本次查找起点到本次匹配位置的偏移,而我们只需要移动匹配位置后面的部分。

那么,真正的失效案例是什么?

考虑:替换后,字符串的结束符前移,但函数在计算back_len时,strlen(start_pos+key_len)会从key后面开始计算直到结束符。但是,如果替换后,结束符被前移,那么下一次循环时,字符串的长度已经变短了,所以不会越界。

我们考虑一个边界情况:缓冲区刚好够用,但替换后字符串长度不变,但是内容变化导致下一次替换需要更多的空间?

但是,new_len的计算是基于当前的字符串状态的,每次替换前都计算,所以每次替换都会检查缓冲区是否足够。

另一个问题:内存重叠。memmove可以处理内存重叠,所以移动是安全的。

还有一种情况:value等于空字符串(即删除key)。此时,value_len=0。

例如:char buf[32] = "hello world"; gp_str_replace(buf, "world", "", sizeof(buf)); // 删除"world"

第一次查找:找到"world",front_len=6("hello "的长度为6,包括空格),back_len=strlen("world"+5)=strlen("")=0。 new_len=6+0+0+1=7,缓冲区足够。 移动:memmove(start_pos+0, start_pos+5, 0+1) -> 将start_pos+5(即'\0')移动1个字节到start_pos(即'w'的位置)。所以,移动后,start_pos位置变成'\0'。然后,因为value_len=0,所以不拷贝。 字符串变成"hello "(注意,空格后面直接是结束符)。 更新tgt:tgt += front_len+value_len = 6+0=6(指向空格后面的'\0')。 下一次循环:因为*tgt='\0',所以结束。

这样正常。

但是,如果删除多个字符,比如: char buf[32] = "aabbcc"; gp_str_replace(buf, "a", "", sizeof(buf)); // 删除所有的'a'

第一次:在位置0找到'a',front_len=0,back_len=strlen("abbcc")=5(实际是5?不对,strlen(start_pos+1) -> start_pos+key_len=0+1=1,字符串是"abbcc",长度为5?不对,整个字符串是"aabbcc",start_pos+1指向第二个字符'a',所以strlen(start_pos+1)=5(包括后面的"abbcc\0",实际长度是5,因为字符串是"abbcc\0")?不对,strlen计算的是从该位置到结束符的长度,所以从位置1开始是"abbcc",长度5(包括后面直到结束符)。

new_len=0+0+5+1=6,缓冲区足够。 移动:memmove(0,1,5+1)=6个字节 -> 将位置1开始的6个字节("abbcc\0")移动到位置0。所以字符串变成"abbcc\0"。 更新tgt:tgt+=0+0=0(指向开头)。 第二次:从位置0开始查找,找到第一个字符'a'(已经被移动了,现在位置0是'a'吗?原来位置1是'a',移动后位置0变成了'a'(原来位置1的字符),位置1变成'b'。所以字符串是"abbcc\0")。查找'a',在位置0又找到'a'(因为移动后的第一个字符是'a')。 然后重复:front_len=0, back_len=strlen(0+1)=strlen("bbcc")=4。 new_len=0+0+4+1=5,缓冲区足够。 移动:memmove(0,1,4+1) -> 将位置1开始的5个字节("bbcc\0")移动到位置0,变成"bbcc\0"。 更新tgt:tgt=0+0=0。 第三次:在位置0查找,找不到'a'(因为第一个字符是'b'),结束。

所以最后是"bbcc",删除了两个'a'。符合预期。

那么,有什么失效案例呢?

实际上,我们发现函数中更新tgt指针为:tgt += front_len+value_len; 这个更新使得下一次查找从本次替换的value的后面开始。但是,如果替换的value中包含key,并且key在value的末尾,那么value后面紧接着的字符可能和value的末尾组成key,而由于tgt跳过了value,就会导致这个重叠的key被错过。

例如:用"a"替换"a"(这被开头的判断过滤了,不会执行替换)。所以换一个:用"ba"替换"x"。

原始字符串:"xx" 替换:将"x"替换为"ba"

第一次:在位置0找到'x' front_len=0 back_len=strlen(0+1)=strlen("x")=1 new_len=0+2+1+1=4,缓冲区足够。 移动:memmove(0+2, 0+1, 1+1) -> 将位置1开始的2个字节('x'和'\0')移动到位置2。移动后,位置2是'x',位置3是'\0'。 拷贝value:位置0和1写成'b','a'。所以字符串变成"ba"后面跟着'x'和'\0',即"bax\0"。 更新tgt:tgt+=0+2=2(指向'x'的位置) 第二次:从位置2开始查找'x',找到位置2的'x',替换: front_len=0(因为从tgt=2开始,start_pos=2) back_len=strlen(2+1)=strlen("")=0 new_len=0+2+0+1=3 移动:memmove(2+2, 2+1, 0+1) -> 将位置3的'\0'移动到位置4(缓冲区足够吗?假设缓冲区有4个字节,那么位置4可能越界?因为buf初始大小为4("xx\0"共3个字节,缓冲区大小为4),当我们第一次替换时,移动目标位置是2,源位置是1,移动2个字节,这是安全的。然后第二次替换,移动目标位置是4,但是缓冲区大小为4,那么位置4在缓冲区之外?因为缓冲区只有4字节(0,1,2,3)。所以访问位置3是合法的(最后一个字节),但位置4超出了缓冲区。

所以,这是一个缓冲区溢出的案例吗?

函数中,在替换前检查:if (tgt_len < new_len) return -2; 第一次替换前,new_len=4,tgt_len=4(sizeof(buf)=4),所以tgt_len>=new_len,通过。 第二次替换前,new_len=3,tgt_len=4,也通过。 但是,移动时:memmove(4, 3, 1),目标位置4超出了缓冲区(0-3),所以会覆盖缓冲区外的内存。

所以,失效案例是:连续替换导致缓冲区边界被突破。

但是,我们如何构造这个案例?第一次替换后,字符串使用了4个字节(包括结束符),所以缓冲区满了(4字节)。第二次替换需要移动一个结束符到位置4,但位置4在缓冲区外。

因此,我们构造如下:

char buf[4] = "xxx"; // 注意,字符串初始为"xxx\0",占用4个字节 gp_str_replace(buf, "x", "ba", sizeof(buf));

第一次替换: key="x", value="ba", key_len=1, value_len=2 start_pos在位置0 front_len=0 back_len=strlen(0+1)=strlen("xx")=2 -> 注意,字符串是"xxx\0",所以位置0+1=1,字符串是"xx\0",长度是2。 new_len=0+2+2+1=5,但是tgt_len=4,所以返回-2(缓冲区不足),不会替换。

所以,初始缓冲区必须足够大,才能进行第一次替换。我们加大缓冲区,但保证第二次替换时溢出:

char buf[5] = "xxx"; // 缓冲区大小为5,字符串初始为"xxx\0"(4字节) gp_str_replace(buf, "x", "ba", sizeof(buf));

第一次替换: new_len=0+2+2+1=5,tgt_len=5,所以通过。 移动:memmove(0+2, 0+1, 2+1) -> 移动3个字节:位置1,2,3 -> 位置2,3,4。移动后,位置2,3,4分别是'x','x','\0'(原来位置1,2,3是"xx\0")。 然后拷贝"ba"到位置0,1:所以缓冲区变成:位置0:'b',1:'a',2:'x',3:'x',4:'\0' -> "baxx\0" 更新tgt:tgt=0+2=2(指向位置2) 第二次替换:在位置2找到'x' front_len=0(start_pos-tgt=2-2=0) back_len=strlen(2+1)=strlen("x")=1(因为位置3是'x',位置4是'\0',所以从位置3开始,strlen=1) new_len=0+2+1+1=4,tgt_len=5,足够。 移动:memmove(2+2, 2+1, 1+1) -> 目标位置4,源位置3,移动2个字节('x'和'\0')。所以,将位置3的'x'和位置4的'\0'移动到位置4和5。但是,缓冲区只有5个字节(0-4),位置5超出了缓冲区!所以,memmove会越界写入。

所以,这个案例会导致缓冲区溢出。

因此,失效案例可以是:

char buf[5] = "xxx"; assert(gp_str_replace(buf, "x", "ba", sizeof(buf)) == 0); // 第一次替换后,字符串变成"baxx",然后第二次替换时,移动操作会越界,但由于memmove不检查边界,所以运行时错误。

但是,函数在第二次替换前检查了new_len=4,而tgt_len=5,所以不会返回-2,然后执行移动,移动2个字节到位置4,目标地址是4和5,5超出边界。

然后:

这种对AI的潜在弱点的反制,可能是有意义的。比如一种通用的Yolo分类欺骗,在战场上可能是非常有效的。你们发现过什么可用的手法吗?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

子正

thanks, bro...

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值