cJSON_Delete函数底层原理探究

1. 释放cJSON结构体变量

见名知意, cJSON_Delete 函数用于释放一个cJSON结构体变量所申请的内存空间. 在阅读本章节内容之前, 我强烈推荐你先阅读 cJSON数据组装框架 章节, 这对你理解本节内容有很重要的帮助. 若你对JSON的某些语法或概念有些模糊了, 可以参考 JSON.org 。

先附上cJSON_Delele函数的内部代码实现:

void cJSON_Delete(cJSON *c)
    {
      cJSON *next; 
      while (c)
      {
        next=c->next;
        if (!(c->type&cJSON_IsReference/*256*/) && c->child) cJSON_Delete(c->child);
        if (!(c->type&cJSON_IsReference) && c->valuestring) cJSON_free(c->valuestring);
        if (!(c->type&cJSON_StringIsConst/*512*/) && c->string) cJSON_free(c->string);
        cJSON_free(c);
        c=next;
      }
    }
复制代码

此外, 在这里还需要你有掌握或了解过”递归”方面的知识. 因为cJSON_Delete函数内部采用了递归的方式, 不断递归释放cJSON对象或数组内部嵌套的其他对象或是数组, 直到最后的节点其成员child为空.

现在开始源码分析. while(c){} 循环能够保证即使是单个cJSON对象(即内部无嵌套其他类型数据)也能够成功被释放掉, 除非指针为空. cJSON *next(推荐定义的变量初始化, 改为cJSON *next = NULL; 尽管该代码中能够保证不会操作野指针.); 指针变量next用于指向cJSON中位于同级的cJSON对象、数组、或普通类型变量等. 第一次进来时候, next指针变量肯定是为空, 无论该cJSON对象内部是否嵌套其他数据类型. 因为cJSON的规则是:要么对象, 要么数组, 要么普通数据类型. 无法同时一个cJSON是这两种或多种数据类型的组合. 如下:

  1. cJSON是一个对象: {}
  2. cJSON是一个数组: []
  3. cJSON是一个字符串: ” “
  4. cJSON是一个数组: 0~N
  5. cJSON是一个空值: null(备: 小写), null是JSON中的一个特殊值, 可以对任何数据类型设置该值,包括数组、对象、数字和布尔类型. 若需了解更多, 可参考 处理JSON null 。
  6. cJSON是一个布尔值: false, true(备: 小写)

但是不会出现一个JSON对象同时是:对象{}, 又同时是数组的.

1 示例一

示例如下, 对于这样一个JSON对象类型, 首先, 该JSON对象的各成员参数值如下:{next = 0x0, prev = 0x0, child = 0x609060, type = 6, valuestring = 0x0, valueint = 0, valuedouble = 0, string = 0x0}. 所以第一次时候, cJSON*next = c->next为空.

{
    "name": "lixiaogang5",
    "company": "HIKVISION",
    "department": "R&D Center",
    "jobs": "Backend Engineer",
    "floor": 15
  }
复制代码

1.1. 示例1JSON对象在cJSON中的内部框架图

对于上面的这个JSON对象, 在cJSON内部的框架布局结构如下图所示.

图片

▲ cJSON对象内部框架布局结构图

JSON对象中的每个数据类型 key:value, 若type是字符串类型, 那么在创建该JSON数据类型时候, 除了为key(对应cJSON数据结构中的string成员)分配用户指定的字符串(比如上面示例中的name、company、jobs等)空间大小外, 还会为其对应的valuestring成员申请一片内存空间, 该空间大小为用户实际待存储的value字符串大小.

比如, 下面是向JSON Object中加入第一条数据类型 “name”: “lixiaogang5″时, 其value的内存空间申请过程. 如下:

static cJSON *cJSON_New_Item(void)
{
    #if 0  
    //cJSON_malloc 就是malloc
    static void *(*cJSON_malloc)(size_t sz) = malloc;
    static void (*cJSON_free)(void *ptr) = free;
    #endif 

    cJSON* node = (cJSON*)cJSON_malloc(sizeof(cJSON));
    if (node) memset(node,0,sizeof(cJSON));
    return node;
  }

  //cJSON_strdup就是strdup的一个实现.
  static char* cJSON_strdup(const char* str)
{
        size_t len;
        char* copy;

        len = strlen(str) + 1;
        if (!(copy = (char*)cJSON_malloc(len))) return 0;
        memcpy(copy,str,len);
        return copy;
  }

  cJSON *cJSON_CreateString(const char *string)  
{
    cJSON *item=cJSON_New_Item();
    if(item)
    {
      item->type=cJSON_String;
      //申请用户待存储的字符串的长度内存空间大小, 并将其拷贝到申请的内存空间中.
      item->valuestring=cJSON_strdup(string);
    }

    return item;
  }
复制代码

在示例中, JSON对象内部共有4个字符串的数据类型, 外加一个数值类型. 因此该对象的内存存储释放详细过程如下图2所示.

图片

▲ cJSON释放示例1中JSON对象的流程

图2中的符号 × , 表示在该位置将调用free函数释放对应内存空间. 对于属于字符串类型的数据类型, 将多调用一次free函数, 即释放valuestring成员申请的内存空间.

if (!(c->type&cJSON_IsReference) && c->valuestring) cJSON_free(c->valuestring);
复制代码

2. 调用cJSON_Delete, 程序宕掉, 问题会在哪?

最近的一个项目中, 我遇到了这个问题. 训练算法分析出来的JSON报文中, 内部嵌套的某个JSON对象里, 其中某个为字符串类型的value, 我这边需要用自己模块内部的数据去替换其原有的value. 然后将这个JSON报文存储到文件(不存储数据库表是因为该JSON大小在20KB左右)之后便释放掉该JSON. 然后再cJSON_Delete位置处发生了段错误, 非法操作内存空间导致.

首先贴上算法吐出的JSON报文部分数据, 如下(备注: 以下的这个JSON报文是以透传的方式过来的, 即该JSON是作为网络交互JSON报文中的某个关键字key的value值, 即一个压缩且经转义后的字符串.):

{
    "calibInfo": {      
      "VideoChannels": [{
        ....... //省略若干
        "MediaInfo": {
          "MediaType": 1,          
           /*
                  * FilePath的value是需要我这边解析替换掉, 然后再将
                  * 这条JSON报文存储. 
              */      
          "FilePath": "",   
          "LocalPath": " ",  
            .......  //省略若干    
        },
      ...... //省略若干
      }],            
    }
  }
复制代码

我这边底层模块收到的JSON报文中, 上面这个算法是字符串, 因此需要调用cJSON库中的cJSON_Parse函数解析, 如下:

cJSON *pRootObj = cJSON_Parse(/*收到的训练算法压缩转业后的JSON字符串*/);
assert(pRootObj);
//继续其他义务处理.....
复制代码

然后替换JSON中的对应key的value之后, 需要再次将其格式化为字符串. 存储好转以后的字符串之后, 需要释放解析JSON字符串cJSON_Parse函数的返回值. 即上面提到的 pRootObj指针对象; 然后就在这里使程序宕掉. gdb报错提示:Program received signal SIGABRT, Aborted. 非法操作内存, 导致底层内核触发abort函数, 从而发出SIGABRT信号, 该信号的默认操作是终止程序运行.

2.1 问题定位

经过分析cJSON源码库及自己代码书写review, 导致程序宕掉的根因有两个:

  1. 待替换的关键字key, 在训练算法透传过来的字符串中,其value为空, 因此不会为此(即valuestring成员)分配空间. 这部分可参考cJSON源码cJSON.c文件中的parse_string函数.
/*解析JSON是字符串的序列化数据.*/
  static const char *parse_string(cJSON *item,const char *str)
  {
    const char *ptr=str+1;char *ptr2;char *out;int len=0;unsigned uc,uc2;
    //不是字符串,不满足"key":value
    if (*str!='\"') {ep=str;return 0;}  

    //计算key or value的长度.跳过第一个字符\", 忽略key的紧随后面的一个\"
    while (*ptr!='\"' && *ptr && ++len) if (*ptr++ == '\\') ptr++;  //跳过转义符(转义字符不计算在内) eg:"Key_\"00\":"

    //申请key字符串的大概长度空间(+1 字符串结尾符, \0)
    out=(char*)cJSON_malloc(len+1);
    if (!out) return 0;

    ptr=str+1;
    ptr2=out;

    while (*ptr!='\"' && *ptr)
    {
      if (*ptr!='\\') *ptr2++=*ptr++;
      else
      {
        /*处理转义字符. by lixiaogang5.
         * ---------------------------------------------------------------------------------
         *| 转义字符                 | 意义                                | ASCII码值(十进制) |
         *| \a                      |响铃(BEL)                    | 007            |
         *| \b              |退格(BS),将当前位置移到前一列          | 008               |
         *| \f              |换页(FF),将当期位置移到下页开头        | 012               |
         *| \n                      |换行(LF),将当期位置移到下一行开头      | 010               |
         *| \r                      |回车(CR),将当前位置移到本行开头        | 013               |
         *| \t                      |水平制表(HT),跳到下一个TAB位置         | 009               |
         *| \v                      |垂直制表(VT)                          | 011               |
         *| \\                      |代表一个反斜杠字符'\'                  | 092               |
         *| \'                      |代表一个单引号(撇号)字符               | 039               |
         *| \"                      |代表一个双引号字符                    | 034               |
         *| \?                      |代表一个问号                         | 063               |
         *| \0                      |空字符(NUL)                          | 000               |
         *| \ddd                    |1到3位八进制所代表的任意字符           | 三位八进制         |
         *| \xhh                    |十六进制所代表的任意字符               | 十六进制           |
         * ---------------------------------------------------------------------------------
        */

        ptr++;
        //具体转义字符
        switch (*ptr)
        {
          case 'b': *ptr2++='\b';  break;
          case 'f': *ptr2++='\f';  break;
          case 'n': *ptr2++='\n';  break;
          case 'r': *ptr2++='\r';  break;
          case 't': *ptr2++='\t';  break;
          case 'u':   /* transcode utf16 to utf8.将utf16转换为utf8编码 */
            // ptr+1, 去掉\转义符后面的第一个字符
            uc=parse_hex4(ptr+1);ptr+=4;  /* get the unicode char. */

            if ((uc>=0xDC00 && uc<=0xDFFF) || uc==0)  break;  /* check for invalid.  */

            if (uc>=0xD800 && uc<=0xDBFF)  /* UTF16 surrogate pairs.  */
            {
              if (ptr[1]!='\\' || ptr[2]!='u')  break;  /* missing second-half of surrogate.  */
              uc2=parse_hex4(ptr+3);ptr+=6;
              if (uc2<0xDC00 || uc2>0xDFFF)    break;  /* invalid second-half of surrogate.  */
              uc=0x10000 + (((uc&0x3FF)<<10) | (uc2&0x3FF));
            }

            len=4;if (uc<0x80) len=1;else if (uc<0x800) len=2;else if (uc<0x10000) len=3; ptr2+=len;

            switch (len) {
              case 4: *--ptr2 =((uc | 0x80) & 0xBF); uc >>= 6;
              case 3: *--ptr2 =((uc | 0x80) & 0xBF); uc >>= 6;
              case 2: *--ptr2 =((uc | 0x80) & 0xBF); uc >>= 6;
              case 1: *--ptr2 =(uc | firstByteMark[len]);
            }
            ptr2+=len;
            break;
          default:  *ptr2++=*ptr; break;
        }
        ptr++;
      }
    }

    //填充字符串结尾符 '\0'
    *ptr2=0;
    if (*ptr=='\"') ptr++;

    item->valuestring=out;
    item->type=cJSON_String;
    return ptr;
  }
复制代码
  1. 受C++的根深影响, 一瞬间习惯了对字符串的初始化赋值使用=号, 在C中, 字符串其实是char []的另外一种书写形式, 即char *str[] = “hello”, 等同于 char str[] = [‘h’,‘e’,‘l’,‘l’,‘o’,’\0’]. 因此, 对待替换的key关键字filepath对齐的value替换字符串时候, 需要使用strncpy函数.

2.2 问题解决

同时自己来管理valuestring指针的内存申请. 在重新申请valuestring指针时候, 需要先对其判空处理.若不为空, 则释放掉指定的内存空间, 并置空. 然后realloc/calloc/malloc, 并strncpy. 如下:

//假如待替换的字符串是: char *str = "hello world.";
  if(cJSON->valuestring) free(cJSON->valuestring); cJSON->valuestring = NULL;
    cJSON->valuestring = (char*)calloc(1, strlen(str) + 1);
    assert(cJSON->valuestring);
    strncpy(cJSON->valuestring, str, strlen(str));
    ...... //其他义务处理.
    cJSON_Delete(cJSON对象);
复制代码

自己管理valuestring指针的内存空间, 在处理完其他义务之后, 使用cJSON_Delete函数时候, 在能够成功释放所有申请的内存空间的同时, 程序也完好的运行.

附: 若在使用源码库时候, 程序异常退出, 务必多review下自己的代码书写, 肯定是有某些地方的操作不正确, 或是方式不匹配导致, 包括但不限于cJSON开源库.

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享