PVS-Studio: 25 підозрілих фрагментів коду з CoreCLR


Корпорація Microsoft виклала у відкритий доступ вихідний код движка CoreCLR, який є ключовим елементом .NET Core. Ця новина, звичайно ж, не могла не привернути нашу увагу. Адже чим більше аудиторія проекту, тим тривожніше будуть виглядати знайдені підозрілі місця. Незважаючи на авторство Microsoft, як в будь-якому великому проекті, тут є на що подивитися і над чим замислитися.

Введення
CoreCLR є середовищем виконання .NET Core, виконуючи такі функції як збірку сміття або компіляції в кінцевий машинний код. .Net Core — це модульна реалізація .Net, яка може бути використана як база для величезної кількості сценаріїв.

Вихідний код з недавнього часу доступний на GitHub і перевірявся за допомогою PVS-Studio 5.23. Як і я, бажаючі можуть отримати повний лог перевірки за допомогою Microsoft Visual Studio Community Edition, вихід якої теж був недавній новини від Microsoft.


Помилки
Традиційно я починаю з місць, схожих на помилки. В умовних виразах повторюються змінні, константи, макроси або поля структур/класів. Наявність помилки — предмет дискусій, тим не менш такі місця були знайдені і виглядають підозріло.

V501 There are identical sub-expressions 'tree->gtOper == GT_CLS_VAR' to the left and to the right of the '||' operator. ClrJit lsra.cpp 3140
// register variable 
GTNODE(GT_REG_VAR , "regVar" ,0,GTK_LEAF|GTK_LOCAL)
// static data member
GTNODE(GT_CLS_VAR , "clsVar" ,0,GTK_LEAF)
// static data member address
GTNODE(GT_CLS_VAR_ADDR , "&clsVar" ,0,GTK_LEAF) 
....

void LinearScan::buildRefPositionsForNode(GenTree *tree,....)
{
....
if ((tree->gtOper == GT_CLS_VAR ||
tree->gtOper == GT_CLS_VAR) && i == 1)
{
registerType = TYP_PTR;
currCandidates = allRegs(TYP_PTR);
}
....
}

Хоча структура 'GenTree' має схоже по імені полі «tree->gtType», але воно має інший тип з «tree->gtOper». Думаю, тут справа у скопійованій константі. Тобто у вираженні повинна використовуватися ще одна константа, крім GT_CLS_VAR.

V501 There are identical sub-expressions 'DECODE_PSP_SYM' to the left and to the right of the '|' operator. daccess 264
enum GcInfoDecoderFlags
{
DECODE_SECURITY_OBJECT = 0x01,
DECODE_CODE_LENGTH = 0x02,
DECODE_VARARG = 0x04,
DECODE_INTERRUPTIBILITY = 0x08,
DECODE_GC_LIFETIMES = 0x10,
DECODE_NO_VALIDATION = 0x20,
DECODE_PSP_SYM = 0x40,
DECODE_GENERICS_INST_CONTEXT = 0x80,
DECODE_GS_COOKIE = 0x100, 
DECODE_FOR_RANGES_CALLBACK = 0x200,
DECODE_PROLOG_LENGTH = 0x400,
DECODE_EDIT_AND_CONTINUE = 0x800,
};

size_t GCDump::DumpGCTable(PTR_CBYTE table,....)
{
GcInfoDecoder hdrdecoder(table,
(GcInfoDecoderFlags)( DECODE_SECURITY_OBJECT
| DECODE_GS_COOKIE
| DECODE_CODE_LENGTH
| DECODE_PSP_SYM //<==1
| DECODE_VARARG
| DECODE_PSP_SYM //<==1
| DECODE_GENERICS_INST_CONTEXT //<==2
| DECODE_GC_LIFETIMES
| DECODE_GENERICS_INST_CONTEXT //<==2
| DECODE_PROLOG_LENGTH),
0);
....
}

Тут навіть дві повторювані константи, хоча в перерахуванні " GcInfoDecoderFlags" є й інші константи, які умови не використовуються.

Ще схожі місця:
  • V501 There are identical sub-expressions 'varLoc1.vlStk2.vls2BaseReg' to the left and to the right of the '==' operator. cee_wks util.cpp 657
  • V501 There are identical sub-expressions 'varLoc1.vlStk2.vls2Offset' to the left and to the right of the '==' operator. cee_wks util.cpp 658
  • V501 There are identical sub-expressions 'varLoc1.vlFPstk.vlfReg' to the left and to the right of the '==' operator. cee_wks util.cpp 661
V700 Consider inspecting the 'foo = foo = ...' expression. It is odd that variable is initialized through itself. cee_wks zapsig.cpp 172
BOOL ZapSig::GetSignatureForTypeHandle (....)
{
....
CorElementType elemType = elemType =
TryEncodeUsingShortcut(pMT);
....
}

Начебто просто зайве присвоювання, але такі помилки часто роблять при копіюванні коду і забувають що-небудь перейменувати. У будь-якому разі, у такому вигляді код безглуздий.

V523 The 'then' statement is equivalent to the 'else' statement. cee_wks threadsuspend.cpp 2468
enum __MIDL___MIDL_itf_mscoree_0000_0004_0001
{
OPR_ThreadAbort = 0,
OPR_ThreadRudeAbortInNonCriticalregion = .... ,
OPR_ThreadRudeAbortInCriticalRegion = ....) ,
OPR_AppDomainUnload = .... ,
OPR_AppDomainRudeUnload = ( OPR_AppDomainUnload + 1 ) ,
OPR_ProcessExit = ( OPR_AppDomainRudeUnload + 1 ) ,
OPR_FinalizerRun = ( OPR_ProcessExit + 1 ) ,
MaxClrOperation = ( OPR_FinalizerRun + 1 ) 
} EClrOperation;

void Thread::SetRudeAbortEndTimeFromEEPolicy()
{
LIMITED_METHOD_CONTRACT;
DWORD timeout;
if (HasLockInCurrentDomain())
{
timeout = GetEEPolicy()->
GetTimeout(OPR_ThreadRudeAbortInCriticalRegion); //<==
}
else
{
timeout = GetEEPolicy()->
GetTimeout(OPR_ThreadRudeAbortInCriticalRegion); //<==
}
....
}

Дана діагностика знаходить однакові блоки в конструкціях if/else. І тут теж є підозра на друкарську помилку у константі. У першому випадку, за змістом як раз підходить «OPR_ThreadRudeAbortInNonCriticalregion».

Аналогічні місця:
  • V523 The 'then' statement is equivalent to the 'else' statement. ClrJit instr.cpp 3427
  • V523 The 'then' statement is equivalent to the 'else' statement. ClrJit flowgraph.cpp 18815
  • V523 The 'then' statement is equivalent to the 'else' statement. daccess dacdbiimpl.cpp 6374

Список ініціалізації конструктора
V670 The uninitialized class member 'gcInfo' is used to initialize the 'regSet' member. Remember that members are initialized in the order of their declarations inside a class. ClrJit codegencommon.cpp 92
CodeGenInterface *getCodeGenerator(Compiler *comp);

class CodeGenInterface
{
friend class emitter;

public:
....
RegSet regSet; //<=== line 91
....
public:
GCInfo gcInfo; //<=== line 322
....
};

// CodeGen constructor
CodeGenInterface::CodeGenInterface(Compiler* theCompiler) :
compiler(theCompiler),
gcInfo(theCompiler),
regSet(theCompiler, gcInfo)
{
}

Згідно стандарту, порядок ініціалізація членів класу в конструкторі відбувається в порядку їх оголошення в класі. Для виправлення помилки слід перенести оголошення члена класу 'gcInfo' вищий оголошення 'regSet'.

Помилкове, але корисне попередження
V705 It is possible that 'else' block was forgotten or commented out, thus altering the program's operation logics. daccess daccess.cpp 2979
HRESULT Initialize()
{
if (hdr.dwSig == sig)
{
m_rw = eRO;
m_MiniMetaDataBuffSizeMax = hdr.dwTotalSize;
hr = S_OK;
}
else
// when the DAC initializes this for the case where the target is 
// (a) a live process, or (b) a full dump, buff will point to a
// zero initialized memory region (allocated w/ VirtualAlloc)
if (hdr.dwSig == 0 && hdr.dwTotalSize == 0 && hdr.dwCntStreams == 0)
{
hr = S_OK;
}
// otherwise we may have some memory corruption. treat as this
// a liveprocess/full dump
else
{
hr = S_FALSE;
}
....
}

Аналізатор виявив підозріле місце в коді. Тут видно, що код Прокоментований і все нормально. Але такого типу дуже поширені помилки, коли після код 'else' ЗАкомментирован і наступний за ним оператор стає частиною умови. У даному прикладі немає помилки, але її цілком можна допустити при редагуванні цього місця в майбутньому.

64-бітна помилка
V673 The '0xefefefef << 28' expression evaluates to 1080581331517177856. 60 bits are required to store the value, but the expression evaluates to the 'unsigned' type which can only hold '32' bits. cee_dac _dac object.inl 95
inline void Object::EnumMemoryRegions(void)
{
....
SIZE_T size = sizeof(ObjHeader) + sizeof(Object);
....
size |= 0xefefefef << 28;
....
}

Про термін «64-бітна помилка» можна прочитати тут. В даному прикладі, після зсуву буде виконуватися операція «size |= 0xf0000000» у 32-х бітної програмі і «size |= 0x00000000f0000000» в 64-х бітної. Швидше за все, в 64-х бітної програмі планували обчислювати: «size |= 0x0efefefef0000000». Але де ж втрачається старша частина числа?

Число «0xefefefef» має тип 'unsigned', так як не поміщається в тип 'int'. Виконується зсув 32-х бітного числа і в результаті ми отримаємо 0xf0000000 беззнакового типу. Далі це беззнакове число розшириться до SIZE_T і ми отримаємо 0x00000000f0000000.

Для коректної роботи необхідно спочатку виконати явне приведення типу. Приклад коректного коду:
inline void Object::EnumMemoryRegions(void)
{
....
SIZE_T size = sizeof(ObjHeader) + sizeof(Object);
....
size |= SIZE_T(0xefefefef) << 28;
....
}

Ще таке місце:
  • V673 The '0xefefefef << 28' expression evaluates to 1080581331517177856. 60 bits are required to store the value, but the expression evaluates to the 'unsigned' type which can only hold '32' bits. cee_dac dynamicmethod.cpp 807

Код «у відставці»
Часом умови пишуться так, що буквально суперечать один одному.

V637 Two opposite conditions were encountered. The second condition is always false. Check lines: 31825, 31827. cee_wks gc.cpp 31825
void gc_heap::verify_heap (BOOL begin_gc_p)
{
....
if (brick_table [curr_brick] < 0)
{
if (brick_table [curr_brick] == 0)
{
dprintf(3, ("curr_brick %Ix for object %Ix set to 0",
curr_brick, (size_t)curr_object));
FATAL_GC_ERROR();
}
....
}
....
}

Код, який ніколи не отримує управління, але він виглядає не таким значущим, як у наступному прикладі:

V517 The use of 'if (A) {...} else if (A) {...}' pattern was detected. There is a probability of logical error presence. Check lines: 2353, 2391. utilcode util.cpp 2353
void PutIA64Imm22(UINT64 * pBundle, UINT32 slot, INT32 imm22)
{
if (slot == 0)
{
const UINT64 mask0 = UI64(0xFFFFFC000603FFFF);
/* Clear all bits used as part of the imm22 */
pBundle[0] &= mask0;

UINT64 temp0;

temp0 = (UINT64) (imm22 & 0x200000) << 20; // 1 s
temp0 |= (UINT64) (imm22 & 0x1F0000) << 11; // 5 imm5c
temp0 |= (UINT64) (imm22 & 0x00FF80) << 25; // 9 imm9d
temp0 |= (UINT64) (imm22 & 0x00007F) << 18; // 7 imm7b

/* Or in the new bits used in the imm22 */
pBundle[0] |= temp0;
}
else if (slot == 1)
{
....
}
else if (slot == 0) //<==
{
const UINT64 mask1 = UI64(0xF000180FFFFFFFFF);
/* Clear all bits used as part of the imm22 */
pBundle[1] &= mask1;

UINT64 temp1;

temp1 = (UINT64) (imm22 & 0x200000) << 37; // 1 s
temp1 |= (UINT64) (imm22 & 0x1F0000) << 32; // 5 imm5c
temp1 |= (UINT64) (imm22 & 0x00FF80) << 43; // 9 imm9d
temp1 |= (UINT64) (imm22 & 0x00007F) << 36; // 7 imm7b

/* Or in the new bits used in the imm22 */
pBundle[1] |= temp1;
}
FlushInstructionCache(GetCurrentProcess(),pBundle,16);
}

Можливо, дуже важливий код ніколи не отримує управління із-за помилки в каскаді умовних операторів.

Ще підозрілі місця:
  • V637 Two opposite conditions were encountered. The second condition is always false. Check lines: 2898, 2900. daccess nidump.cpp 2898
  • V637 Two opposite conditions were encountered. The second condition is always false. Check lines: 337, 339. utilcode prettyprintsig.cpp 337
  • V637 Two opposite conditions were encountered. The second condition is always false. Check lines: 774, 776. utilcode prettyprintsig.cpp 774

Невизначений поведінка
V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. bcltype metamodel.h 532
static inline mdToken decodeToken (....)
{
//<TODO>@FUTURE: make compile-time calculation</TODO>
ULONG32 ix = (ULONG32)(val & ~(-1 << m_cb[cTokens]));

if (ix >= cTokens)
return rTokens[0];
return TokenFromRid(val >> m_cb[cTokens], rTokens[ix]);
}

Аналізатор виявив операцію зсуву від'ємного числа, що призводить до невизначеної поведінки.

V610 Undefined behavior. Check the shift operator '<<'. The left operand '(~0)' is negative. cee_dac decodemd.cpp 456
#define bits_generation 2
#define generation_mask (~(~0 << bits_generation))

#define MASK(len) (~((~0)<<len))
#define MASK64(len) ((~((~((unsigned __int64)0))<<len)))

void Encoder::Add(unsigned value, unsigned length)
{
....
value = (value & MASK(length));
....
}

Завдяки повідомленням аналізатора V610 я знайшов кілька некоректних макросів. '~0' приводиться до знакової негативного числа типу int, після чого виконується зсув. Як в одному з макросів, необхідно виконати явне перетворення до unsigned:
#define bits_generation 2
#define generation_mask (~(~((unsigned int)0) << bits_generation))

#define MASK(len) (~((~((unsigned int)0))<<len))
#define MASK64(len) ((~((~((unsigned __int64)0))<<len)))

Некоректний sizeof(xx)
V579 The DacReadAll function receives the pointer and its size as arguments. It is possibly a mistake. Inspect the third argument. daccess dacimpl.h 1688
template < class T>
inline bool MisalignedRead(CORDB_ADDRESS addr, T *t)
{
return SUCCEEDED(DacReadAll(TO_TADDR(addr), t, sizeof(t), 'false'));
}

Ось така маленька функція, яка завжди бере розмір покажчика. Швидше за все, тут хотіли написати «sizeof(*t)», ну або «sizeof(T).

Ще один наочний приклад:

V579 Read The function receives the pointer and its size as arguments. It is possibly a mistake. Inspect the third argument. util.cpp 4943
HRESULT GetMTOfObject(TADDR obj, TADDR *mt)
{
if (!mt)
return E_POINTER;

HRESULT hr = rvCache->Read(obj, mt, sizeof(mt), NULL);
if (SUCCEEDED(hr))
*mt &= ~3;

return hr;
}


Сімейство функцій «memFAIL»
З використанням memXXX-функцій можна допустити різні помилки. Для пошуку таких місць в аналізаторі є кілька діагностичних правил.

V512 A call of the 'memset' function will lead to underflow of the buffer 'pAddExpression'. sos strike.cpp 11973
DECLARE_API(Watch)
{
....
if(addExpression.data != NULL || aExpression.data != NULL)
{
WCHAR pAddExpression[MAX_EXPRESSION];
memset(pAddExpression, 0, MAX_EXPRESSION);
swprintf_s(pAddExpression, MAX_EXPRESSION, L"%S",....);
Status = g_watchCmd.Add(pAddExpression);
}
....
}

Поширена помилка, коли забувають робити поправку на розмір типу:
WCHAR pAddExpression[MAX_EXPRESSION];
memset(pAddExpression, 0, sizeof(WCHAR)*MAX_EXPRESSION);

Ще кілька таких місць:
  • V512 A call of the 'memset' function will lead to underflow of the buffer 'pSaveName'. sos strike.cpp 11997
  • V512 A call of the 'memset' function will lead to underflow of the buffer 'pOldName'. sos strike.cpp 12013
  • V512 A call of the 'memset' function will lead to underflow of the buffer 'pNewName'. sos strike.cpp 12016
  • V512 A call of the 'memset' function will lead to underflow of the buffer 'pExpression'. sos strike.cpp 12024
  • V512 A call of the 'memset' function will lead to underflow of the buffer 'pFilterName'. sos strike.cpp 12039
V598 The 'memcpy' function is used to copy the fields of 'GenTree' class. Virtual table pointer will be damaged by this. ClrJit compiler.hpp 1344
struct GenTree
{
....
#if DEBUGGABLE_GENTREE
virtual void DummyVirt() {}
#endif // DEBUGGABLE_GENTREE
....
};

void GenTree::CopyFrom(const GenTree* src, Compiler* comp)
{
....
memcpy(this, src, src->GetNodeSize());
....
}

Якщо оголошена змінна препроцесора 'DEBUGGABLE_GENTREE', то визначається віртуальна функція. Тоді клас містить вказівник на таблицю віртуальних методів і його вже не можна ось так просто копіювати.

V598 The 'memcpy' function is used to copy the fields of 'GCStatistics' class. Virtual table pointer will be damaged by this. cee_wks gc.cpp 287
struct GCStatistics
: public StatisticsBase
{
....
virtual void Initialize();
virtual void DisplayAndUpdate();
....
};

GCStatistics g_LastGCStatistics;

void GCStatistics::DisplayAndUpdate()
{
....
memcpy(&g_LastGCStatistics, this, sizeof(g_LastGCStatistics));
....
}

У цьому місці некоректне копіювання виконується не тільки в режимі налагодження.

V698 Expression 'memcmp(....) == -1' is incorrect. This function can return not only the value '-1', but any negative value. Consider using 'memcmp (....) < 0' instead. sos util.cpp 142
bool operator( )(const GUID& _Key1, const GUID& _Key2) const
{ return memcmp(&_Key1, &_Key2, sizeof(GUID)) == -1; }

Порівнювати результат функції 'memcmp' зі значенням 1 або -1 не коректно. Працездатність таких конструкцій залежить від бібліотек, компілятора, настройок операційної системи, її розрядності і так далі; в такому випадку необхідно перевіряти одне з трьох станів: '< 0', '0' або '> 0'.

Аналогічне місце:
  • V698 Expression 'wcscmp(....) == -1' is incorrect. This function can return not only the value '-1', but any negative value. Consider using 'wcscmp (....) < 0' instead. sos strike.cpp 3855

Про покажчики
V522 Dereferencing of the null pointer 'hp' might take place. cee_wks gc.cpp 4488
heap_segment* gc_heap::get_segment_for_loh (size_t size
#ifdef MULTIPLE_HEAPS
, gc_heap* hp
#endif //MULTIPLE_HEAPS
)
{
#ifndef MULTIPLE_HEAPS
gc_heap* hp = 0;
#endif //MULTIPLE_HEAPS
heap_segment* res = hp>get_segment (size, TRUE);
....
}

Якщо 'MULTIPLE_HEAPS' не визначено, то біда. Покажчик виявиться дорівнює нулю.

V595 The 'tree' pointer was utilized before it was verified against nullptr. Check lines: 6970, 6976. ClrJit gentree.cpp 6970
void Compiler::gtDispNode(GenTreePtr tree,....)
{
....
if (tree->gtOper >= GT_COUNT)
{
printf("**** ILLEGAL NODE****");
return;
}

if (tree && printFlags)
{
/* First print the flags associated with the node */
switch (tree->gtOper)
{
....
}
....
}
....
}

У вихідному коді поширені місця, коли валідність покажчика таки перевіряється, але вже після розіменування.

Весь список: CoreCLR_V595.txt.

Зайві перевірки
Навіть якщо зайвий код не завдає шкоди, то його наявність може просто відволікати увагу розробки від більш важливих місць.

V503 This is a nonsensical comparison: pointer >= 0. cee_wks gc.cpp 21707
void gc_heap::make_free_list_in_brick (BYTE* tree,
make_free_args* args)
{
assert ((tree >= 0));
....
}

Ось така перевірка покажчика. Ще приклади:
  • V503 This is a nonsensical comparison: pointer >= 0. cee_wks gc.cpp 23204
  • V503 This is a nonsensical comparison: pointer >= 0. cee_wks gc.cpp 27683
V547 Expression 'maxCpuId >= 0' is always true. Unsigned type value is always >= 0. cee_wks codeman.cpp 1219
void EEJitManager::SetCpuInfo()
{
....
unsigned char buffer[16];
DWORD maxCpuId = getcpuid(0, buffer);
if (maxCpuId >= 0)
{
....
}

Схожий приклад, тільки типу DWORD.

V590 Consider inspecting the 'wzPath[0] != L'\0' && wzPath[0] == L'\\" expression. The expression is excessive or contains a misprint. cee_wks path.h 62
static inline bool
HasUncPrefix(LPCWSTR wzPath)
{
_ASSERTE(!clr::str::IsNullOrEmpty(wzPath));
return wzPath[0] != W('\0') && wzPath[0] == W('\\')
&& wzPath[1] != W('\0') && wzPath[1] == W('\\')
&& wzPath[2] != W('\0') && wzPath[2] != W('?');
}

Цю функцію можна спростити до такого варіанту:
static inline bool
HasUncPrefix(LPCWSTR wzPath)
{
_ASSERTE(!clr::str::IsNullOrEmpty(wzPath));
return wzPath[0] == W('\\')
&& wzPath[1] == W('\\')
&& wzPath[2] != W('\0')
&& wzPath[2] != W('?');
}

Ще таке місце:
  • V590 Consider inspecting this expression. The expression is excessive or contains a misprint. cee_wks path.h 72
V571 Recurring check. The 'if (moduleInfo[MSCORWKS].baseAddr == 0)' condition was already verified in line 749. sos util.cpp 751
struct ModuleInfo
{
ULONG64 baseAddr;
ULONG64 size;
BOOL hasPdb;
};

HRESULT CheckEEDll()
{
....
// Do we have clr.dll
if (moduleInfo[MSCORWKS].baseAddr == 0) //<==
{
if (moduleInfo[MSCORWKS].baseAddr == 0) //<==
g_ExtSymbols->GetModuleByModuleName (
MAIN_CLR_MODULE_NAME_A,0,NULL,
&moduleInfo[MSCORWKS].baseAddr);
if (moduleInfo[MSCORWKS].baseAddr != 0 && //<==
moduleInfo[MSCORWKS].hasPdb == FALSE)
{
....
}
....
}
....
}

У другому випадку 'baseAddr' вже можна не перевіряти.

V704 'this == nullptr' expression should be avoided — this expression is always false on newer компілятори, because 'this' pointer can never be NULL. ClrJit gentree.cpp 12731
bool FieldSeqNode::IsFirstElemFieldSeq()
{
if (this == nullptr)
return false;
return m_fieldHnd == FieldSeqStore::FirstElemPseudoField;
}

Згідно стандарту З++, вказівник this ніколи не може бути нульовим. Про можливі наслідки такого коду можна докладно прочитати в описі діагностики V704. Те, що такий код може працювати коректно після компіляції компілятором Visual C++, це просто везіння і покладатися на нього по-чесному не можна.

Весь список: CoreCLR_V704.txt.

V668 There is no sense testing in the 'newChunk' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. ClrJit stresslog.h 552
FORCEINLINE BOOL GrowChunkList ()
{
....
StressLogChunk * newChunk = new StressLogChunk (....);
if (newChunk == NULL)
{
return FALSE;
}
....
}

Якщо оператор 'new' не зміг виділити пам'ять, то згідно стандарту мови Сі++, генерується виключення std::bad_alloc(). Таким чином перевіряти вказівник на рівність нулю не має сенсу.

Краще перевірити такі місця, ось повний список: CoreCLR_V668.txt.

Висновок
Нещодавно відкритий проект CoreCLR хороший приклад того, як може виглядати софт з закритим вихідним кодом. На цю тему постійно ведуться дискусії і ось вам ще один привід для роздумів та обговорень.

Для нас же важливо, що в будь-якому великому проекті можна знайти якісь помилки і найкраще застосування статичного аналізатора — це регулярні перевірки. Не лінуйтеся, завантажити PVS-Studio та перевірте свій проект.

Ця стаття англійською
Якщо хочете поділитися цією статтею з англомовної аудиторією, то прошу використовувати посилання на переклад: Svyatoslav Razmyslov. PVS-Studio: 25 Suspicious Code Fragments in CoreCLR.

Прочитали статтю і є питання?Часто до наших статей задають одні і ті ж питання. Відповіді на них ми зібрали тут: Відповіді на питання читачів статей про PVS-Studio і CppCat, версія 2014. Будь ласка, ознайомтеся зі списком.


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.