C/C++インタプリタ CINTのデストラクタ不正呼び出し問題と格闘した話 その2

## 以前の記事

[その1](https://qiita.com/NCScript/items/b4f0d244c8392762b802)

## 本記事で扱うCINTのバージョンについて

ROOT 5で使用されているCINTは最終バージョンの5.18ですが、私が自作ツールでCINTを扱い始めた当時の最新バージョンである5.15を長年使用しているため、このバージョンのソースコードで解説したいと思います。

## 変数管理テーブル

まずは、CINTの変数管理テーブル`G__var_array`構造体の定義から見ていきます。本記事では、デストラクタ不正呼び出し問題の修正において重要と思われるメンバー変数のみ解説します。

```C:G__ci.h
/**************************************************************************
* structure for variable table
*
**************************************************************************/
struct G__var_array {
  /* union for variable pointer */
  long p[G__MEMDEPTH]; /* used to be int */
  int allvar;
#ifndef G__OLDIMPLEMENTATION1543
  char *varnamebuf[G__MEMDEPTH]; /* variable name */
#else
  char varnamebuf[G__MEMDEPTH][G__MAXNAME]; /* variable name */
#endif
  int hash[G__MEMDEPTH];                    /* hash table of varname */
  int varlabel[G__MEMDEPTH+1][G__MAXVARDIM];  /* points varpointer */
  short paran[G__MEMDEPTH];
  char bitfield[G__MEMDEPTH];
#ifdef G__VARIABLEFPOS
  int filenum[G__MEMDEPTH];
  int linenum[G__MEMDEPTH];
#endif

  /* type information,
     if pointer : Char,Int,Short,Long,Double,U(struct,union)
     if value   : char,int,short,long,double,u(struct,union) */
  G__SIGNEDCHAR_T  type[G__MEMDEPTH];
  G__SIGNEDCHAR_T constvar[G__MEMDEPTH];
  short p_tagtable[G__MEMDEPTH];        /* tagname if struct,union */
  short p_typetable[G__MEMDEPTH];       /* typename if typedef */
  short statictype[G__MEMDEPTH];
  G__SIGNEDCHAR_T reftype[G__MEMDEPTH];

  /* chain for next G__var_array */
  struct G__var_array *next;

  G__SIGNEDCHAR_T access[G__MEMDEPTH];  /* private, protected, public */

#ifdef G__SHOWSTACK /* not activated */
  struct G__ifunc_table *ifunc;
  int ifn;
  struct G__var_array *prev_local;
  int prev_filenum;
  short prev_line_number;
  long struct_offset;
  int tagnum;
  int exec_memberfunc;
#endif
#ifdef G__VAARG
  struct G__param *libp;
#endif

#ifndef G__NEWINHERIT
  char isinherit[G__MEMDEPTH];
#endif
  G__SIGNEDCHAR_T globalcomp[G__MEMDEPTH];

#ifdef G__FONS_COMMENT
  struct G__comment_info comment[G__MEMDEPTH];
#endif

#ifndef G__OLDIMPLEMENTATION2038
  struct G__var_array *enclosing_scope;
  struct G__var_array **inner_scope;
#endif

} ;
```

`allvar`には登録済みの変数の個数が格納されています。変数が`G__MEMDEPTH`個を超えた場合は新たに`sizeof(struct G__var_array)`バイトの領域が確保され、`next`がその領域を指します。

`type`は登録済みの変数の型情報を格納するための配列で、型の種類に対応する値は以下の表のとおりです。例えば`char*`であれば`'C'`が格納されます。

| type | 型の種類 |
|:-:|:--|
| 'c' | char |
| 'b' | unsigned char |
| 's' | short |
| 'r' | unsigned short |
| 'i' | int |
| 'h' | unsigned int |
| 'l' | long |
| 'k' | unsigned long |
| 'g' | bool |
| 'f' | float |
| 'd' | double |
| 'u' | class / struct / union |
| 大文字 | ポインタ型 |
| 'E' | FILE* |
| 'Y' | void* |

`p_tagtable`はユーザー定義型の管理IDを格納するための配列で、`type`の値が`'u'`または`'U'`の場合にのみ意味を持ちます。有効な場合は0以上の値が、無効な場合は-1が格納されます。

## 解放処理

次に、変数の解放処理を行う`G__destroy`関数を見ていきます。この関数はインタプリタ上で実行された関数が正常終了したか、それとも構文エラー等で中断されたかに関わらず呼び出されます。1番目の引数は変数管理テーブルへのポインタで、2番目の引数はグローバル変数の管理テーブルかどうかを表すフラグです。

```C:end.c
/***********************************************************************
* void G__destroy(var,isglobal)
***********************************************************************/
/* destroy local variable and free memory*/
void G__destroy(var,isglobal)
struct G__var_array *var;
int isglobal;
{
```

登録されている変数が`G__MEMDEPTH`個を超える場合は、`G__destroy`関数を再帰的に呼び出します。その後のfor文で、登録時とは逆順に変数を解放していきます。

```C:end.c
  /*******************************************
   * If there are any sub var array list,
   * destroy it too.
   *******************************************/
  if(var->next) {
#ifndef G__OLDIMPLEMENTATION1081
    if(var->allvar==G__MEMDEPTH) {
#endif
      G__destroy(var->next,isglobal);
      free((void*)var->next);
      var->next=NULL;
#ifndef G__OLDIMPLEMENTATION1081
    }
    else {
      fprintf(stderr,"!!!Fatal Error: Interpreter memory overwritten by illegal access.!!!\n");
      fprintf(stderr,"!!!Terminate session!!!\n");
    }
#endif
  }
  
  for(itemp=var->allvar-1;itemp>=remain;itemp--) {
```

以下が、for文の中でデストラクタを呼び出すかどうかを判断している箇所です。未初期化の変数だった場合は、if文の中が実行されないようにする必要があります。

```C:end.c
      /****************************************************
       * If C++ class, destructor has to be called
       ****************************************************/
      if(var->type[itemp]=='u' && 0==G__ansiheader && 0==G__prerun) {
```

私は以下の2つの修正方法を考えました。

* `G__var_array`構造体にコンストラクタが呼び出し済みかどうかを表すメンバー変数を追加する。
* コンストラクタの呼び出し前にエラー終了する場合は`type`の値を`'u'`以外に変更する。

この時は、修正箇所が少なくて済みそうだという理由で2番目の方法を選択しました。この安易な選択のせいで後々苦労することになったのかもしれません。

## 生成処理

最後に、変数を生成するために定義文を処理する`G__define_var`関数を見ていきます。1番目の引数はユーザー定義型の管理IDで、2番目の引数はtypedefされた型名の管理IDです。この関数に、エラー発生時に`type`の値を差し替える処理を追加する必要があります。

```C:decl.c
/******************************************************************
* void G__define_var(tagnum,typenum)
*
* Called by
*   G__DEFVAR       macro
*   G__DEFREFVAR    macro
*   G__define_struct()
*   G__define_type()
*   G__define_type()
*
*  Declaration of variable, function or ANSI function header
*
*  variable:   type  varname1, varname2=initval ;
*                  ^
*  function:   type  funcname(param decl) { body }
*                  ^
*  ANSI function header: funcname(  type para1, type para2,...)
*                                 ^     or     ^
******************************************************************/
void G__define_var(tagnum,typenum)
int tagnum,typenum;      /* overrides global variables */
{
  G__value reg;
```

関数内で使用されている`G__value`構造体の定義は以下の通りです。メンバー変数`type`の意味は`G__var_array`構造体の`type`と同じで、`tagnum`の意味は`p_tagtable`と同じです。

```C:G__ci.h
/**************************************************************************
* struct of internal data
*
**************************************************************************/
typedef struct {
  union {
    double d;
    long    i; /* used to be int */
    struct G__p2p reftype;
#ifndef G__OLDIMPLEMENTATION845
    char ch;
    short sh;
    int in;
    float fl;
    unsigned char uch;
    unsigned short ush;
    unsigned int uin;
    unsigned long ulo;
#endif
#ifndef G__OLDIMPLEMENTATION2189
    G__int64 ll;
    G__uint64 ull;
    long double ld;
#endif
  } obj;
  int type;
  int tagnum;
  int typenum;
#ifdef G__REFERENCETYPE2
  long ref;
#endif
#ifndef G__OLDIMPLEMENTATION1259
  G__SIGNEDCHAR_T isconst;
#endif
} G__value ;
```

以下が、メモリの確保と変数管理テーブルへの登録を行っている箇所です。コンパイル済みライブラリに含まれている型だった場合は、まだ処理が行われません。

この時点では`reg.type`の値が`'\0'`に設定されているため、`G__letvariable`関数はコンストラクタの呼び出しを行いません。

```C:decl.c
        /************************************************************
        * memory allocation and table entry generation
        ************************************************************/
        store_struct_offset = G__store_struct_offset;
        if(G__CPPLINK!=G__struct.iscpplink[tagnum]) {
          /* allocate memory area for constructed object by interpreter */
          G__var_type = var_type;
#ifndef G__OLDIMPLEMENTATION1349
          G__decl_obj=1;
#endif
          G__store_struct_offset=G__int(G__letvariable(new_name,reg,&G__global
                                                       ,G__p_local));
#ifndef G__OLDIMPLEMENTATION1349
          G__decl_obj=0;
#endif
#ifndef G__OLDIMPLEMENTATION1073
          if(0==G__store_struct_offset &&
             G__asm_wholefunction && G__asm_noverflow) {
            G__store_struct_offset = G__PVOID;
          }
#endif
        }
        else {
          /* precompiled class, 
           * memory will be allocated by new in constructor function below */
          G__store_struct_offset = G__PVOID;
        }
```

次に、コンストラクタに引数として渡される式の評価が`G__getexpr`関数によって行われます。

```C:decl.c
              reg=G__getexpr(temp);
```

最後に、コンストラクタの呼び出しが行われます。コンパイル済みライブラリに含まれている型とインタプリタで処理される型で処理が分かれています。本記事では、前者の処理については省略します。

エラーが無ければ、`reg.type`と`reg.tagnum`の値が`G__getexpr`関数で評価された式の結果の型に設定されているため、`G__letvariable`関数はコンストラクタの呼び出しを行います。その後のif文で、コンストラクタの呼び出しがエラーになった場合の後始末を行っています。

```C:decl.c
              if(G__CPPLINK==G__struct.iscpplink[tagnum]) {
                /* 略 */
              }
              else {
#ifndef G__OLDIMPLEMENTATION1073
                if(G__asm_wholefunction) {
                  G__oprovld=1;
                }
#endif
                G__letvariable(new_name ,reg ,&G__global ,G__p_local);
#ifndef G__OLDIMPLEMENTATION1073
                if(G__asm_wholefunction) {
                  G__oprovld=0;
#ifdef G__ASM_DBG
                  if(G__asm_dbg) G__fprinterr(G__serr,"%3x: POPSTROS\n",G__asm_cp);
#endif
                  G__asm_inst[G__asm_cp]=G__POPSTROS;
                  G__inc_cp_asm(1,0);
                }
#endif
              }
              if(G__return>G__RETURN_NORMAL) {
                G__decl=store_decl;
                G__constvar=0;
                G__tagnum = store_tagnum;
                G__typenum = store_typenum;
                G__reftype=G__PARANORMAL;
                G__static_alloc=store_static_alloc2;
#ifndef G__OLDIMPLEMENTATION1119
                G__dynconst=0;
#endif
#ifndef G__OLDIMPLEMENTATION1322
                G__globalvarpointer = G__PVOID;
#endif
#ifndef G__PHILIPPE21
                G__prerun = store_prerun;
#endif
                return;
              }
```

## 修正

エラー発生時に`type`の値を差し替える処理は、`G__getexpr`関数の呼び出しとコンストラクタの呼び出しの間に追加します。以下のように、差し替え後に後始末をして関数を終了しています。

差し替えに使用した値は`'u'`以外であれば何でも良かったのですが、他の型と被らないように記号を使用しました。

```C:decl.c
#ifndef G__NCPRO_OLDIMPLEMENTATION39
              if(G__return>G__RETURN_NORMAL) {
                G__hash(name,hash,i)
                var = G__getvarentry(name,hash,&ig15,&G__global,G__p_local);
                if(var) {
                  var->type[ig15] = '@';
                  var->p_tagtable[ig15] = -1;
                  var->p_typetable[ig15] = -1;
                }
                G__decl=store_decl;
                G__constvar=0;
                G__tagnum = store_tagnum;
                G__typenum = store_typenum;
                G__reftype=G__PARANORMAL;
                G__static_alloc=store_static_alloc2;
#ifndef G__OLDIMPLEMENTATION1119
                G__dynconst=0;
#endif
#ifndef G__OLDIMPLEMENTATION1322
                G__globalvarpointer = G__PVOID;
#endif
#ifndef G__PHILIPPE21
                G__prerun = store_prerun;
#endif
                return;
              }
#endif
```

それと、追加された処理内で使用している変数の宣言文と初期化処理を、ユーザー定義型の処理を行っているスコープの先頭部分に追加する必要があります。

```C:decl.c
      /**************************************************************
      * declaration of struct object, no pointer, no reference type
      **************************************************************/
      if(var_type=='u'&&
#ifndef G__OLDIMPLEMENTATION871
         (G__def_struct_member==0||-1==G__def_tagnum||
          'n'==G__struct.type[G__def_tagnum])
#else
         G__def_struct_member==0
#endif
         &&new_name[0]!='*'&&
         G__reftype==G__PARANORMAL) {

#ifndef G__NCPRO_OLDIMPLEMENTATION39
        struct G__var_array *var;
        char name[G__MAXNAME];
        char *p;
        int hash,ig15;

        strcpy(name,new_name);
        p=strchr(name,'[');
        if(p) *p='\0';
#endif
```

以上で、変数の定義文で式の評価がエラーになった場合に発生していた、未初期化の変数に対してデストラクタが不正に呼び出されてしまう問題の修正は終わりです。

次回の記事(その3)では、別のケースで発生するデストラクタ不正呼び出し問題について解説したいと思います。

[その3](https://qiita.com/NCScript/items/66463bdad967e7d25bff)