数孊゜フトりェアの䜿甚による頭痛





写真2










たたたた、むンタヌネット䞊で、䞀芋異なるトピック、぀たり倧孊ず孊生向けの無料のMatlabの代替案、および静的コヌド分析を䜿甚したアルゎリズムの゚ラヌの発芋に぀いお話しおいたした。 これらの議論はすべお、珟代のプログラムのコヌドのひどい品質を組み合わせおいたす。 特に、数孊者や科孊者向けの゜フトりェアの品質。 問題は、そのようなプログラムの助けを借りお実行された蚈算ず研究ぞの信頌から即座に生じたす。 このトピックを振り返り、゚ラヌを探しおみたしょう。



はじめに



「アルゎリズム」ずいう甚語を定矩するこずから始めたいず思いたす。 アルゎリズムは、特定の結果を達成するための゚グれキュヌタヌのアクションの順序を説明する䞀連の呜什です りィキペディア 。 したがっお、゜ヌスコヌドをアルゎリズムず残りのコヌドに分割しないでください。 たずえば、゜ヌトアルゎリズムは、ファむルを開く、文字列内の文字を怜玢するなどのコヌドずたったく同じです。 コヌドにぱラヌが含たれおいる可胜性があり、幞いなこずに、静的コヌド分析のツヌルを䜿甚しお、初期段階で倚くの゚ラヌを怜出できたす。



ただし、いわゆる「アルゎリズム」゚ラヌを怜玢するために、いく぀かの数孊的パッケヌゞのコヌドを分析するこずにしたした。 このようなコヌドには、いく぀かの数匏が単玔にプログラムされる倚くの機胜がありたす。 コヌドに぀いおそう考えおいない人もいるこずがわかりたす。 そしお、それに応じお、どのような゚ラヌが発生する可胜性がありたす。



この蚘事に蚘茉されおいるすべおのコヌドの欠陥を特定するために、C / C ++ / Cプログラミング蚀語のWindows / Linuxで実行されるPVS-Studio静的アナラむザヌバヌゞョン6.15を䜿甚したした。



サヌドパヌティからの゚ラヌ



この話は、 Point Cloud Library PCL、 GitHub プロゞェクトでバグを芋぀けるこずから始たりたした。 倚くの間違いを芋぀けお蚘事を曞くずいう目暙を自分自身に蚭定せずに、レポヌトを調べたずころ、非垞に興味深い゚ラヌが芋぀かりたした。



V533 「for」挔算子内で誀った倉数がむンクリメントされおいる可胜性がありたす。 「i」の怜蚎を怜蚎しおください。 sparsematrix.inl 212

template<class T> SparseMatrix<T>& SparseMatrix<T>::operator *= (const T& V) { for( int i=0 ; i<rows ; i++ ) for( int ii=0 ; ii<rowSizes[i] ; i++ ) m_ppElements[i][ii].Value *= V; return *this; }
      
      





オヌバヌロヌドされた挔算子 "* ="では、すべおの行列芁玠に特定のV倀を乗算したす。 コヌドの䜜成者は、このアルゎリズムに察しお非垞に重倧な誀りを犯したした。その理由は、行列の最初の列のみが倉化し、配列の境界を超える無限ルヌプも発生する可胜性があるためです。



このコヌドは、 Poisson Surface Reconstruction数孊ラむブラリからのものです。 ゚ラヌがコヌドの最新バヌゞョンに存圚するこずを確認したした。 このラむブラリがさらに倚くのプロゞェクトに含たれるこずを考えるのは怖いです。



次に、もう1぀の奇劙なコヌドを瀺したす。



V607所有者のない衚珟「j <残り」。 allocator.h 120

 void rollBack(const AllocatorState& state){ .... if(state.index<index){ .... for(int j=0;j<remains;j++){ memory[index][j].~T(); new(&memory[index][j]) T(); } index=state.index; remains=state.remains; } else{ for(int j=0;j<state.remains;j<remains){ // <= memory[index][j].~T(); new(&memory[index][j]) T(); } remains=state.remains; } .... }
      
      





この奇劙なルヌプはただコヌドに残っおいるため、頻繁に実行されるこずはないず思いたす。 しかし、おそらくプログラムの異垞終了により、誰かが奇劙なハングアップを起こしたした。 コヌドの品質に関するいく぀かのアむデアが圢成されたした。 それでは、より倧きなプロゞェクトであるScilabに移りたしょう。Scilabでは、頭痛の皮が埅っおいたす。



サむラブ



プロゞェクトに぀いお



Scilabは、工孊技術および科孊蚈算のためのオヌプンな環境を提䟛する、応甚数孊プログラムのパッケヌゞです。 この開発環境は、さたざたな機関や研究で広く䜿甚されおいるMatlabの公に利甚可胜な代替手段の1぀です。 Matlabのもう1぀の人気のある代替手段はGNU Octaveであり、以前はこれらのプロゞェクトに既に泚意を払っおいたした。

Scilabに関する新しい蚘事を曞く前に、叀い蚘事を読んで、2぀の結論だけを出したした。
  1. 3幎間、圌らはほんの2、3の堎所を修正したせんでした「すべおがそのように動䜜するのに、なぜ䞍明確な動䜜を修正するのですか」-開発者は考えたようです。
  2. プロゞェクトには倚くの新しいバグがありたす。 読者を飜きさせないように、蚘事に数ダヌスだけ入れるこずにしたした。


Visual StudioのプロゞェクトファむルはScilabの゜ヌスにすぐに存圚するので、ワンクリックでプロゞェクトを開いお確認できたす。



矎しいタむプミス



V530関数 'back'の戻り倀を䜿甚する必芁がありたす。 sci_mscanf.cpp 274

 types::Function::ReturnValue sci_mscanf(....) { .... std::vector<types::InternalType*> pITTemp = std::vector<...>(); .... case types::InternalType::ScilabString : { .... pITTemp.pop_back(); // <= pITTemp.push_back(pType); } break; case types::InternalType::ScilabDouble : { .... pITTemp.back(); // <= ??? pITTemp.push_back(pType); } break; .... }
      
      





コヌド補完がプログラマヌのトリックを挔じたようです。 コヌドでは、 sci_mscanf関数は垞にベクトルの最埌の芁玠を削陀しおから新しい芁玠を远加したすが、プログラマヌはpop_backの代わりにback関数を呌び出すこずでミスを犯したした。 このようにback関数を呌び出しおも意味がありたせん。



V595 nullptrに察しお怜蚌される前に、「Block.inptr」ポむンタヌが䜿甚されたした。 行を確認しおください478、479。sci_model2blk.cpp 478

 types::Function::ReturnValue sci_model2blk(....) { .... Block.inptr[i] = MALLOC(size); if (Block.inptr == nullptr) { freeBlock(&Block); Scierror(888, _("%s : Allocation error.\n"), name.data()); return types::Function::Error; } memset(Block.inptr[i], 0x00, size); .... }
      
      





タむプミスの非垞に興味深いケヌスで、メモリの割り圓おの制埡が機胜しなくなったためです。 ほずんどの堎合、正しいコヌドは次のようになりたす。

 Block.inptr[i] = MALLOC(size); if (Block.inptr[i] == nullptr) { .... }
      
      





V595 nullptrに察しお怜蚌される前に、「pwstLines」ポむンタヌが䜿甚されたした。 行をチェック78、79。mgetl.cpp 78

 int mgetl(int iFileID, int iLineCount, wchar_t ***pwstLines) { *pwstLines = NULL; .... *pwstLines = (wchar_t**)MALLOC(iLineCount * sizeof(wchar_t*)); if (pwstLines == NULL) { return -1; } .... }
      
      





驚くほど䌌た゚ラヌ。 コヌドの䜜成者は星をカりントしなかったため、条件で間違ったポむンタヌがチェックされたす。



V595 nullptrに察しお怜蚌される前に、「array_size」ポむンタヌが䜿甚されたした。 行をチェック67、68。diary_manager.cpp 67

 wchar_t **getDiaryFilenames(int *array_size) { *array_size = 0; if (SCIDIARY) { std::list<std::wstring> wstringFilenames = SCIDIARY->get.... *array_size = (int)wstringFilenames.size(); if (array_size > 0) { .... } .... }
      
      





安定性は習熟の兆候です。 プログラマヌは再びポむンタヌを逆参照するのを忘れたした。これは、ある配列のサむズがれロず比范されるのではなく、この倉数ぞのポむンタヌが比范される理由です。



V501 「||」の巊偎ず右偎に同䞀の副次匏「strncmptx、 "pi"、3== 0」がありたす。 挔算子。 stringtocomplex.c 276

 static int ParseNumber(const char* tx) { .... else if (strlen(tx) >= 4 && (strncmp(tx, "%eps", 4) == 0 || strncmp(tx, "+%pi", 4) == 0 || strncmp(tx, "-%pi", 4) == 0 || strncmp(tx, "+Inf", 4) == 0 || strncmp(tx, "-Inf", 4) == 0 || strncmp(tx, "+Nan", 4) == 0 || strncmp(tx, "-Nan", 4) == 0 || strncmp(tx, "%nan", 4) == 0 || strncmp(tx, "%inf", 4) == 0 )) { return 4; } else if (strlen(tx) >= 3 && (strncmp(tx, "+%e", 3) == 0 || strncmp(tx, "-%e", 3) == 0 || strncmp(tx, "%pi", 3) == 0 // <= || strncmp(tx, "Nan", 3) == 0 || strncmp(tx, "Inf", 3) == 0 || strncmp(tx, "%pi", 3) == 0)) // <= { return 3; } .... }
      
      





この関数には、数倀を解析するためのコヌドが含たれおいたす。 アナラむザヌは、「pi」の2぀の同䞀行ず疑わしい比范を芋぀けたした。 隣接するコヌドを芋るず、重耇した行の代わりに、「-pi」たたは「-Inf」ずいう行があるず想定できたす。 これは䜙分なコピヌされたコヌド行である可胜性もありたすが、削陀する方が適切です。



操䜜の優先順䜍



V502おそらく、「?:」挔算子は予想ずは異なる方法で動䜜したす。 「」挔算子の優先順䜍は、「==」挔算子よりも䜎くなっおいたす。 sci_sparse.cpp 49

 types::Function::ReturnValue sci_sparse(....) { bool isValid = true; .... for (int i = 0 ; isValid && i < in.size() ; i++) { switch (in[i]->getType()) { case types::InternalType::ScilabBool : case types::InternalType::ScilabSparseBool : { isValid = (i == (in.size() > 1) ? 1 : 0); } .... }
      
      





操䜜の優先順䜍に関する゚ラヌは、最新のコヌドでは非垞に䞀般的です。 蚘事「 C / C ++の論理匏。専門家が間違える方法 」を参照。



䞊蚘のコヌドスニペットにも゚ラヌがありたすが、非垞に幞運なため、゚ラヌコヌドはプログラマヌが期埅したずおりに機胜したす。 むンデックス0ず1を持぀配列芁玠が比范に関䞎し、真理ず停りの敎数衚珟も倀0ず1であるずいう事実だけが原因で、このコヌドは奇跡的にただ正しく機胜しおいたす。



コヌドを操䜜の正しい優先床に曞き換える必芁がありたす。

 isValid = (i == (in.size() > 1 ? 1 : 0));
      
      





V590 「iType=-1 && iType == 8」匏の怜査を怜蚎しおください。 衚珟が過剰であるか、誀怍が含たれおいたす。 scilabview.cpp 175

 void ScilabView::createObject(int iUID) { int iType = -1; int *piType = &iType; getGraphicObjectProperty(....); if (iType != -1 && iType == __GO_FIGURE__) { m_figureList[iUID] = -1; setCurrentFigure(iUID); } .... }
      
      





このフラグメントには、操䜜の優先順䜍に関する゚ラヌが含たれおいたす。これは、以前に提案された蚘事でも怜蚎されおいたす。



条件付き郚分匏iType= -1は、条件匏党䜓の結果には圱響したせん。 この䟋の真理倀衚を䜜成するこずにより、゚ラヌを怜蚌できたす。



別のそのような䟋

誀った゚ラヌメッセヌゞ



Scilabの゚ラヌに関する以前の蚘事では、メッセヌゞを印刷する際の゚ラヌに関する小さなセクションもありたした。 たた、新しいタむプのコヌドには、このタむプの゚ラヌが非垞に倚くありたした。



V517 「ifA{...} else ifA{...}」パタヌンの䜿甚が怜出されたした。 論理゚ラヌが存圚する可胜性がありたす。 行を確認しおください159、163。cdfbase.c 159

 void cdf_error(char const* const fname, int status, double bound) { switch (status) { .... case 10: if (strcmp(fname, "cdfchi") == 0) // <= { Scierror(999 _("%s: cumgam returned an error\n"), fname); } else if (strcmp(fname, "cdfchi") == 0) // <= { Scierror(999, _("%s: gamma or inverse gamma routine failed\n"), fname); } break; .... }
      
      





Scilabには倚数のcdf関数がありたす。 提瀺されたコヌドフラグメントでは、これらの関数のリタヌンコヌドの解釈が実行されたす。 そしお、ここに問題がありたす-関数名のタむプミスにより、䜕らかの゚ラヌ譊告が衚瀺されるこずはありたせん。 この投皿を怜玢するず、 cdfgam関数に぀ながりたす。 この機胜を䜿甚しおいお、数孊パッケヌゞの䜜者のタむプミスのためにいく぀かの問題を芋぀けるこずができなかったナヌザヌに同情したいず思いたす。



V510 'Scierror'関数は、3番目の実匕数ずしおクラス型倉数を受け取るこずを期埅されおいたせん。 sci_winqueryreg.cpp 149

 const std::string fname = "winqueryreg"; types::Function::ReturnValue sci_winqueryreg(....) { .... if (rhs != 2 && rhs != 3) { Scierror(77, _("%s: Wrong number...\n"), fname.data(), 2, 3); return types::Function::Error; } .... else { Scierror(999, _("%s: Cannot open Windows regist..."), fname); return types::Function::Error; } .... }
      
      





1か所で行を印刷するずきに、 dataメ゜ッドを呌び出すのを忘れおいたした。



V746タむプのスラむス。 䟋倖は、倀ではなく参照によっおキャッチする必芁がありたす。 sci_scinotes.cpp 48

 int sci_scinotes(char * fname, void* pvApiCtx) { .... try { callSciNotesW(NULL, 0); } catch (GiwsException::JniCallMethodException exception) { Scierror(999, "%s: %s\n", fname, exception.getJavaDescription().c_str()); } catch (GiwsException::JniException exception) { Scierror(999, "%s: %s\n", fname, exception.whatStr().c_str()); } .... }
      
      





倀によっお䟋倖がキャッチされたした。 これは、コピヌコンストラクタの助けを借りお新しいオブゞェクトが構築され、䟋倖に関する情報の䞀郚が倱われるこずを意味したす。 正しいオプションは、参照によっお䟋倖をキャッチするこずです。



そのような堎所がいく぀かありたす。

奇劙なコヌド



奇劙なコヌド。なぜこのように曞くべきか、どうすればそれをより良く修正するかが明確ではないからです。



V523 「then」ステヌトメントは「else」ステヌトメントず同等です。 data3d.cpp 51

 void Data3D::getDataProperty(int property, void **_pvData) { if (property == UNKNOWN_DATA_PROPERTY) { *_pvData = NULL; } else { *_pvData = NULL; } }
      
      





垞にポむンタヌをれロにする単玔な関数を次に瀺したす。



V575 「memset」機胜は「0」芁玠を凊理したす。 3番目の匕数を調べたす。 win_mem_alloc.c 91

 void *MyHeapAlloc(size_t dwSize, char *file, int line) { LPVOID NewPointer = NULL; if (dwSize > 0) { _try { NewPointer = malloc(dwSize); NewPointer = memset (NewPointer, 0, dwSize); } _except (EXCEPTION_EXECUTE_HANDLER) { } .... } else { _try { NewPointer = malloc(dwSize); NewPointer = memset (NewPointer, 0, dwSize); } _except (EXCEPTION_EXECUTE_HANDLER) { } } return NewPointer; }
      
      





dwSize倉数の倀に関係なく、同じコヌドが垞に実行されたす。 なぜそれを耇補するのですか



V695範囲の亀差は条件匏内で可胜です。 䟋ifA <5{...} else ifA <2{...}。 行を確認しおください438、442。sci_sorder.c 442

 int sci_sorder(char *fname, void* pvApiCtx) { .... if (iRows * iCols > 0) { dblTol1 = pdblTol[0]; } else if (iRows * iCols > 1) { dblTol2 = pdblTol[1]; } .... }
      
      





EXPR> 0の堎合、 EXPR> 1のチェックは意味をなさないため、2番目の条件は垞にfalseです。 このコヌドには明らかに䜕らかの゚ラヌがありたす。



ヌルポむンタヌの逆参照ず未定矩の動䜜



V522 NULLポむンタヌ「dataz」の逆参照が行われる堎合がありたす。 polylinedata_wrap.c 373

 BOOL translatePolyline(int uid, double x, double y, double z, int flagX, int flagY, int flagZ) { double *datax = NULL; double *datay = NULL; double *dataz = NULL; // <= int i = 0; if (x != 0.0) { datax = getDataX(uid); if (datax == NULL) return FALSE; .... if (z != 0 && isZCoordSet(uid)) { if (flagZ) { for (i = 0; i < getDataSize_(uid); ++i) { dataz[i] = pow(10.,log10(dataz[i]) + z); // <= } } else { for (i = 0; i < getDataSize_(uid); ++i) { dataz[i] += z; // <= } } } return TRUE; }
      
      





datax 、 datay 、およびdatazの配列がありたす。 埌者はどこでも初期化されたせんが、特定の条件䞋で䜿甚されたす。



V595 nullptrに察しお怜蚌される前に、「番号」ポむンタヌが䜿甚されたした。 行をチェックしおください410、425。scilab_sscanf.cpp 410

 int scilab_sscanf(....) { .... wchar_t* number = NULL; .... number = (wchar_t*)MALLOC((nbrOfDigit + 1) * sizeof(wchar_t)); memcpy(number, wcsData, nbrOfDigit * sizeof(wchar_t)); number[nbrOfDigit] = L'\0'; iSingleData = wcstoul(number, &number, base); if ((iSingleData == 0) && (number[0] == wcsData[0])) { .... } if (number == NULL) { wcsData += nbrOfDigit; } else { wcsData += (nbrOfDigit - wcslen(number)); } .... }
      
      





メモリヌはmalloc関数を䜿甚しお番号行の䞋に割り圓おられたしたが、ポむンタヌをチェックする前に数回参照解陀され、 memcpy関数に匕数ずしお枡されたすが、これは受け入れられたせん。



V595 nullptrに察しお怜蚌される前に、「OuputStrings」ポむンタヌが䜿甚されたした。 行を確認271、272。spawncommand.c 271

 char **CreateOuput(pipeinfo *pipe, BOOL DetachProcess) { char **OuputStrings = NULL; .... OuputStrings = (char**)MALLOC((pipe->NumberOfLines) * ....); memset(OuputStrings, 0x00,sizeof(char*) * pipe->NumberOfLines); if (OuputStrings) { char *line = strtok(buffer, LF_STR); int i = 0; while (line) { OuputStrings[i] = convertLine(line, DetachProcess); .... }
      
      





ここでは、動的メモリがOuputStrings倉数に割り圓おられおいたすが、このポむンタヌをチェックする前に、割り圓おられたメモリはmemset関数を䜿甚しおリセットされるため、これを行うこずはできたせん。 関数のドキュメントからの匕甚「 'dest'がNULLポむンタヌの堎合の動䜜は未定矩です 」。



メモリリヌクず閉じられおいないリ゜ヌス



V611メモリヌは「new T []」挔算子を䜿甚しお割り圓おられたしたが、「delete」挔算子を䜿甚しお解攟されたした。 このコヌドを調べるこずを怜蚎しおください。 「delete [] piP;」を䜿甚する方がおそらく良いでしょう。 sci_grand.cpp 990



V611メモリヌは「new T []」挔算子を䜿甚しお割り圓おられたしたが、「delete」挔算子を䜿甚しお解攟されたした。 このコヌドを調べるこずを怜蚎しおください。 「delete [] piOut;」を䜿甚するこずをお勧めしたす。 sci_grand.cpp 991

 types::Function::ReturnValue sci_grand(....) { .... int* piP = new int[vectpDblInput[0]->getSize()]; int* piOut = new int[pDblOut->getSize()]; .... delete piP; delete piOut; .... }
      
      





ここでは、2぀の重倧な間違いがありたした。 配列に動的メモリを割り圓おた埌、 delete []挔算子、぀たり 角かっこ付き。



V773 'doc'ポむンタヌを解攟せずに関数が終了したした。 メモリリヌクが発生する可胜性がありたす。 sci_builddoc.cpp 263

 int sci_buildDoc(char *fname, void* pvApiCtx) { .... try { org_scilab_modules_helptools::SciDocMain * doc = new .... if (doc->setOutputDirectory((char *)outputDirectory.c_str())) { .... } else { Scierror(999, _("...."), fname, outputDirectory.c_str()); return FALSE; // <= } if (doc != NULL) { delete doc; } } catch (GiwsException::JniException ex) { Scierror(....); Scierror(....); Scierror(....); return FALSE; } .... }
      
      





状況によっおは、 ドキュメントポむンタヌをクリアせずに関数が終了したす。 たた、 docポむンタヌをNULLず比范するこずは正しくありたせん。 new挔算子がメモリの割り圓おに倱敗した堎合、 NULLを返すのではなく、䟋倖をスロヌしたす 。



これは、Scilabプロゞェクトで芋぀かったメモリリヌクの最も明らかな䟋です。 圌らはメモリを解攟するこずを蚈画しおいるこずがわかりたすが、ある堎所ではそれをするのを忘れおいたした。



䞀般に、プロゞェクトで倚くのメモリリヌクが芋぀かりたした。ポむンタは単玔にクリアされず、どこにも保存されたせん。 なぜなら 私はScilab開発者ではありたせん。そのような堎合に゚ラヌが発生する堎所ず発生しない堎所を刀断するのは困難です。 しかし、私は倚くのメモリリヌクがあるず考えおいたす。 この数孊パッケヌゞのナヌザヌは、私の蚀葉を確認できたす。



V773 'hProcess'ハンドルの可芖性スコヌプは、リ゜ヌスを解攟せずに終了したした。 リ゜ヌスリヌクが発生する可胜性がありたす。 killscilabprocess.c 35

 void killScilabProcess(int exitCode) { HANDLE hProcess; /* Ouverture de ce Process avec droit pour le tuer */ hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, ....); if (hProcess) { /* Tue ce Process */ TerminateProcess(hProcess, exitCode); } else { MessageBox(NULL, "....", "Warning", MB_ICONWARNING); } }
      
      





リ゜ヌスリヌク。 ドキュメントによるず、 OpenProcess関数を呌び出した埌、 CloseHandle関数を呌び出す必芁がありたす。



おわりに



珟時点では、Scilab 6.0.0はScilabの公匏Webサむトで安定版ず芋なされおいたすが、気づいたように、安定性はここから遠いです。 アナラむザヌはリポゞトリから最新バヌゞョンをチェックしたしたが、原則ずしお、゚ラヌは非垞に長い時間コヌド内に存圚し、おそらく「安定した」バヌゞョンに分類されたす。 私自身もScilabナヌザヌでしたが、ずっず前に゚ラヌの数を確認できたした。 このような゜フトりェアが、数孊的な蚈算にこのようなツヌルを䜿甚しおいる人々の研究を遅らせないこずを願っおいたす。



倚くの数孊があり、さたざたな研究で需芁がある次の実瞟のあるプロゞェクトは、 OpenCVラむブラリです。



同僚のアンドレむ・カルポフに泚意しおください。 この蚘事のトピックは、私が蚘事で述べた考えず匷く重耇しおいたす。

おそらく読者は圌らに粟通するこずに興味があるでしょう。







英語を話す聎衆ずこの蚘事を共有したい堎合は、翻蚳ぞのリンクを䜿甚しおくださいSvyatoslav Razmyslov。 数孊゜フトりェアの䜿甚による頭痛



蚘事を読んで質問がありたすか
倚くの堎合、蚘事には同じ質問が寄せられたす。 ここで回答を収集したした PVS-Studioバヌゞョン2015に関する蚘事の読者からの質問ぞの回答 。 リストをご芧ください。



All Articles