
FreeRDP เป็นซอฟต์แวร์โอเพนซอร์สที่นำโปรโตคอล Remote Desktop Protocol (RDP) มาใช้ ซึ่งเป็นโปรโตคอลที่พัฒนาโดย Microsoft สำหรับการควบคุมคอมพิวเตอร์จากระยะไกล โครงการนี้รองรับหลายแพลตฟอร์ม รวมถึง Windows, Linux, macOS และแม้แต่ iOS ด้วย Androidโครงการนี้ได้รับการคัดเลือกให้เป็นโครงการแรกในชุดบทความที่เกี่ยวกับการทดสอบไคลเอ็นต์ RDP โดยใช้เครื่องมือวิเคราะห์แบบคงที่ PVS-Studio
บิตของประวัติศาสตร์
โครงการ เกิดขึ้นหลังจากที่ Microsoft เปิดข้อกำหนดสำหรับโปรโตคอล RDP ที่เป็นกรรมสิทธิ์ของตน ในเวลานั้น มีไคลเอ็นต์ rdesktop ที่ใช้งานซึ่งอิงตามผลลัพธ์ของ Reverse Engineering
เมื่อมีการนำโปรโตคอลไปใช้ การเพิ่มฟังก์ชันการทำงานใหม่ๆ กลายเป็นเรื่องยากมากขึ้นเนื่องจากสถาปัตยกรรมโปรเจ็กต์ที่มีอยู่ในขณะนั้น การเปลี่ยนแปลงทำให้เกิดความขัดแย้งระหว่างนักพัฒนาซึ่งนำไปสู่การสร้างทางแยกของ rdesktop - FreeRDP การจำหน่ายผลิตภัณฑ์เพิ่มเติมถูกจำกัดโดยใบอนุญาต GPLv2 ซึ่งเป็นผลมาจากการตัดสินใจที่จะคืนใบอนุญาตให้กับ Apache License v2 อย่างไรก็ตาม ไม่ใช่ทุกคนที่ตกลงที่จะเปลี่ยนใบอนุญาตของรหัสของตน ดังนั้นนักพัฒนาจึงตัดสินใจเขียนโครงการใหม่ ส่งผลให้มีฐานรหัสที่ทันสมัย
คุณสามารถอ่านเพิ่มเติมเกี่ยวกับประวัติของโครงการได้ในบล็อกโพสต์อย่างเป็นทางการ: “ประวัติของโครงการ FreeRDP”
ใช้เป็นเครื่องมือในการระบุข้อผิดพลาดและช่องโหว่ที่อาจเกิดขึ้นในโค้ด เป็นเครื่องมือวิเคราะห์โค้ดแบบคงที่สำหรับภาษา C, C++, C# และ Java ซึ่งใช้งานได้บนแพลตฟอร์มต่างๆ Windows, Linux и macOS.
บทความนี้นำเสนอเฉพาะข้อผิดพลาดที่ดูน่าสนใจที่สุดสำหรับฉันเท่านั้น
หน่วยความจำรั่ว
ออกจากฟังก์ชันโดยไม่ปล่อยตัวชี้ 'cwd' หน่วยความจำรั่วเป็นไปได้ สิ่งแวดล้อม.c 84
DWORD GetCurrentDirectoryA(DWORD nBufferLength, LPSTR lpBuffer)
{
char* cwd;
....
cwd = getcwd(NULL, 0);
....
if (lpBuffer == NULL)
{
free(cwd);
return 0;
}
if ((length + 1) > nBufferLength)
{
free(cwd);
return (DWORD) (length + 1);
}
memcpy(lpBuffer, cwd, length + 1);
return length;
....
}ส่วนนี้คัดลอกมาจากระบบย่อย winpr ซึ่งใช้ตัวห่อ WINAPI สำหรับสิ่งที่ไม่ใช่...Windows ระบบต่างๆ กล่าวคือ มันเป็นเวอร์ชันน้ำหนักเบาของ Wine ตรงนี้คุณจะเห็นการรั่วไหลของหน่วยความจำ: หน่วยความจำที่จัดสรรโดยฟังก์ชัน รับcwdจะถูกปล่อยออกมาเมื่อจัดการกรณีพิเศษเท่านั้น หากต้องการแก้ไขข้อผิดพลาด คุณต้องเพิ่มการโทร ฟรี หลังจาก เมมปี้.
อาร์เรย์อยู่นอกขอบเขต
การโอเวอร์รันอาร์เรย์เป็นไปได้ ค่าของดัชนี 'event->EventHandlerCount' อาจถึง 32 PubSub.c 117
#define MAX_EVENT_HANDLERS 32
struct _wEventType
{
....
int EventHandlerCount;
pEventHandler EventHandlers[MAX_EVENT_HANDLERS];
};
int PubSub_Subscribe(wPubSub* pubSub, const char* EventName,
pEventHandler EventHandler)
{
....
if (event->EventHandlerCount <= MAX_EVENT_HANDLERS)
{
event->EventHandlers[event->EventHandlerCount] = EventHandler;
event->EventHandlerCount++;
}
....
}ตัวอย่างนี้จะเพิ่มองค์ประกอบใหม่ลงในรายการ แม้ว่าจำนวนองค์ประกอบจะถึงจำนวนสูงสุดแล้วก็ตาม นี่ก็เพียงพอแล้วที่จะเปลี่ยนผู้ปฏิบัติงาน <= บน <เพื่อไม่ให้เกินขอบเขตของอาเรย์
พบข้อผิดพลาดประเภทนี้อีก:
- สามารถโอเวอร์รันอาร์เรย์ V557 ได้ ค่าของดัชนี 'iBitmapFormat' อาจสูงถึง 8. orders.c 2623
ความผิดพลาด
ส่วนที่ 1
นิพจน์ '!pipe->In' จะเป็นเท็จเสมอ MessagePipe.c 63
wMessagePipe* MessagePipe_New()
{
....
pipe->In = MessageQueue_New(NULL);
if (!pipe->In)
goto error_in;
pipe->Out = MessageQueue_New(NULL);
if (!pipe->In) // <=
goto error_out;
....
}ที่นี่เราเห็นการพิมพ์ผิดทั่วไป: เงื่อนไขที่สองจะตรวจสอบตัวแปรเดียวกันกับเงื่อนไขแรก เป็นไปได้มากว่าข้อผิดพลาดเกิดขึ้นอันเป็นผลมาจากการคัดลอกโค้ดไม่สำเร็จ
ส่วนที่ 2
พบข้อความที่เหมือนกันสองช่วงตึก บล็อกที่สองเริ่มจากบรรทัด 771 tsg.c 770
typedef struct _TSG_PACKET_VERSIONCAPS
{
....
UINT16 majorVersion;
UINT16 minorVersion;
....
} TSG_PACKET_VERSIONCAPS, *PTSG_PACKET_VERSIONCAPS;
static BOOL TsProxyCreateTunnelReadResponse(....)
{
....
PTSG_PACKET_VERSIONCAPS versionCaps = NULL;
....
/* MajorVersion (2 bytes) */
Stream_Read_UINT16(pdu->s, versionCaps->majorVersion);
/* MinorVersion (2 bytes) */
Stream_Read_UINT16(pdu->s, versionCaps->majorVersion);
....
}การพิมพ์ผิดอีกประการหนึ่ง: ความคิดเห็นของโค้ดบอกเป็นนัยว่าเธรดควรจะมา รุ่นรองอย่างไรก็ตาม การอ่านเกิดขึ้นในตัวแปรที่ชื่อ เวอร์ชันหลัก- อย่างไรก็ตาม ฉันไม่คุ้นเคยกับโปรโตคอล ดังนั้นนี่เป็นเพียงการคาดเดาเท่านั้น
ส่วนที่ 3
เป็นเรื่องแปลกที่เนื้อความของฟังก์ชัน 'trio_index_last' เทียบเท่ากับเนื้อความของฟังก์ชัน 'trio_index' โดยสมบูรณ์ ไตรโอสเตร.ซี 933
/**
Find first occurrence of a character in a string.
....
*/
TRIO_PUBLIC_STRING char *
trio_index
TRIO_ARGS2((string, character),
TRIO_CONST char *string,
int character)
{
assert(string);
return strchr(string, character);
}
/**
Find last occurrence of a character in a string.
....
*/
TRIO_PUBLIC_STRING char *
trio_index_last
TRIO_ARGS2((string, character),
TRIO_CONST char *string,
int character)
{
assert(string);
return strchr(string, character);
}ตัดสินโดยความคิดเห็นฟังก์ชั่น trio_index ค้นหาอักขระตัวแรกที่ตรงกันในสตริงเมื่อ trio_index_last - สิ่งสุดท้าย. แต่เนื้อความของฟังก์ชันเหล่านี้เหมือนกัน! เป็นไปได้มากว่านี่คือการพิมพ์ผิดและอยู่ในฟังก์ชัน trio_index_last จำเป็นต้องใช้ strrhr แทน ยืด- จากนั้นจะเกิดพฤติกรรมที่คาดหวัง
ส่วนที่ 4
ตัวชี้ 'ข้อมูล' ในนิพจน์เท่ากับ nullptr ค่าผลลัพธ์ของการดำเนินการทางคณิตศาสตร์บนตัวชี้นี้ไม่สมเหตุสมผลและไม่ควรใช้ nsc_encode.c 124
static BOOL nsc_encode_argb_to_aycocg(NSC_CONTEXT* context,
const BYTE* data,
UINT32 scanline)
{
....
if (!context || data || (scanline == 0))
return FALSE;
....
src = data + (context->height - 1 - y) * scanline;
....
}ดูเหมือนว่าผู้ดำเนินการปฏิเสธพลาดไปที่นี่โดยไม่ได้ตั้งใจ ! ใกล้ ข้อมูล- เป็นเรื่องแปลกที่เรื่องนี้ไม่มีใครสังเกตเห็น
ส่วนที่ 5
ตรวจพบรูปแบบ 'if (A) {…} else if (A) {…}' มีความเป็นไปได้ที่จะเกิดข้อผิดพลาดเชิงตรรกะ ตรวจสอบบรรทัด: 213, 222. rdpei_common.c 213
BOOL rdpei_write_4byte_unsigned(wStream* s, UINT32 value)
{
BYTE byte;
if (value <= 0x3F)
{
....
}
else if (value <= 0x3FFF)
{
....
}
else if (value <= 0x3FFFFF)
{
byte = (value >> 16) & 0x3F;
Stream_Write_UINT8(s, byte | 0x80);
byte = (value >> 8) & 0xFF;
Stream_Write_UINT8(s, byte);
byte = (value & 0xFF);
Stream_Write_UINT8(s, byte);
}
else if (value <= 0x3FFFFF)
{
byte = (value >> 24) & 0x3F;
Stream_Write_UINT8(s, byte | 0xC0);
byte = (value >> 16) & 0xFF;
Stream_Write_UINT8(s, byte);
byte = (value >> 8) & 0xFF;
Stream_Write_UINT8(s, byte);
byte = (value & 0xFF);
Stream_Write_UINT8(s, byte);
}
....
}สองเงื่อนไขสุดท้ายเหมือนกัน: เห็นได้ชัดว่ามีคนลืมตรวจสอบหลังจากคัดลอกแล้ว จากโค้ดจะสังเกตได้ว่าส่วนสุดท้ายทำงานกับค่าสี่ไบต์ ดังนั้นเราจึงสรุปได้ว่าเงื่อนไขสุดท้ายควรเป็น ค่า <= 0x3FFFFFFFF.
พบข้อผิดพลาดประเภทนี้อีก:
- V517 ตรวจพบรูปแบบ 'if (A) {…} else if (A) {…}' มีความเป็นไปได้ที่จะเกิดข้อผิดพลาดเชิงตรรกะ ตรวจสอบบรรทัด: 169, 173. file.c 169
การตรวจสอบความถูกต้องของข้อมูลอินพุต
ส่วนที่ 1
นิพจน์ 'strcat(target, source) != NULL' เป็นจริงเสมอ ไตรโอสเตร.ซี 425
TRIO_PUBLIC_STRING int
trio_append
TRIO_ARGS2((target, source),
char *target,
TRIO_CONST char *source)
{
assert(target);
assert(source);
return (strcat(target, source) != NULL);
}การตรวจสอบผลลัพธ์ของฟังก์ชันในตัวอย่างนี้ไม่ถูกต้อง การทำงาน สเตรทแคท ส่งคืนตัวชี้ไปยังเวอร์ชันสุดท้ายของสตริงเช่น พารามิเตอร์แรกผ่านไปแล้ว ในกรณีนี้ก็คือ เป้า- แต่ถ้าจะเท่ากัน NULLแสดงว่าสายเกินไปที่จะตรวจสอบเนื่องจากอยู่ในฟังก์ชัน สเตรทแคท มันจะถูกเลื่อนออกไป
ส่วนที่ 2
นิพจน์ 'แคช' เป็นจริงเสมอ glyph.c 730
typedef struct rdp_glyph_cache rdpGlyphCache;
struct rdp_glyph_cache
{
....
GLYPH_CACHE glyphCache[10];
....
};
void glyph_cache_free(rdpGlyphCache* glyphCache)
{
....
GLYPH_CACHE* cache = glyphCache->glyphCache;
if (cache)
{
....
}
....
}ในกรณีนี้คือตัวแปร แคช มีการกำหนดที่อยู่ของอาร์เรย์แบบคงที่ glyphCache->glyphCache- ดังนั้นให้ตรวจสอบ ถ้า (แคช) สามารถละเว้นได้
ข้อผิดพลาดในการจัดการทรัพยากร
ได้รับทรัพยากรโดยใช้ฟังก์ชัน 'CreateFileA' แต่ถูกปล่อยออกมาโดยใช้ฟังก์ชัน 'fclose' ที่เข้ากันไม่ได้ ใบรับรอง c 447
BOOL certificate_data_replace(rdpCertificateStore* certificate_store,
rdpCertificateData* certificate_data)
{
HANDLE fp;
....
fp = CreateFileA(certificate_store->file, GENERIC_READ | GENERIC_WRITE, 0,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
....
if (size < 1)
{
CloseHandle(fp);
return FALSE;
}
....
if (!data)
{
fclose(fp);
return FALSE;
}
....
}ตัวอธิบายไฟล์ fpสร้างขึ้นโดยการเรียกใช้ฟังก์ชัน สร้างไฟล์ ปิดโดยไม่ได้ตั้งใจตามฟังก์ชัน ปิด จากไลบรารีมาตรฐานไม่ใช่ ปิดแฮนเดิล.
เงื่อนไขเดียวกัน
นิพจน์แบบมีเงื่อนไขของคำสั่ง 'if' ที่อยู่ข้างๆ กันจะเหมือนกัน ตรวจสอบบรรทัด: 269, 283. ndr_structure.c 283
void NdrComplexStructBufferSize(PMIDL_STUB_MESSAGE pStubMsg,
unsigned char* pMemory, PFORMAT_STRING pFormat)
{
....
if (conformant_array_description)
{
ULONG size;
unsigned char array_type;
array_type = conformant_array_description[0];
size = NdrComplexStructMemberSize(pStubMsg, pFormat);
WLog_ERR(TAG, "warning: NdrComplexStructBufferSize array_type: "
"0x%02X unimplemented", array_type);
NdrpComputeConformance(pStubMsg, pMemory + size,
conformant_array_description);
NdrpComputeVariance(pStubMsg, pMemory + size,
conformant_array_description);
MaxCount = pStubMsg->MaxCount;
ActualCount = pStubMsg->ActualCount;
Offset = pStubMsg->Offset;
}
if (conformant_array_description)
{
unsigned char array_type;
array_type = conformant_array_description[0];
pStubMsg->MaxCount = MaxCount;
pStubMsg->ActualCount = ActualCount;
pStubMsg->Offset = Offset;
WLog_ERR(TAG, "warning: NdrComplexStructBufferSize array_type: "
"0x%02X unimplemented", array_type);
}
....
}ตัวอย่างนี้อาจไม่ใช่จุดบกพร่อง อย่างไรก็ตาม ทั้งสองเงื่อนไขมีข้อความเดียวกัน ซึ่งข้อความหนึ่งน่าจะถูกนำออกได้มากที่สุด
ทำความสะอาดพอยน์เตอร์โมฆะ
ตัวชี้ค่าว่างจะถูกส่งผ่านไปยังฟังก์ชัน 'ฟรี' ตรวจสอบอาร์กิวเมนต์แรก smartcard_pcsc.c 875
WINSCARDAPI LONG WINAPI PCSC_SCardListReadersW(
SCARDCONTEXT hContext,
LPCWSTR mszGroups,
LPWSTR mszReaders,
LPDWORD pcchReaders)
{
LPSTR mszGroupsA = NULL;
....
mszGroups = NULL; /* mszGroups is not supported by pcsc-lite */
if (mszGroups)
ConvertFromUnicode(CP_UTF8,0, mszGroups, -1,
(char**) &mszGroupsA, 0,
NULL, NULL);
status = PCSC_SCardListReaders_Internal(hContext, mszGroupsA,
(LPSTR) &mszReadersA,
pcchReaders);
if (status == SCARD_S_SUCCESS)
{
....
}
free(mszGroupsA);
....
}ในฟังก์ชั่น ฟรี คุณสามารถส่งผ่านตัวชี้ว่างได้และผู้วิเคราะห์จะรู้เรื่องนี้ แต่หากตรวจพบสถานการณ์ที่ตัวชี้ถูกส่งผ่านค่าว่างเสมอ ดังตัวอย่างนี้ จะมีการออกคำเตือน
ตัวชี้ mszGroupsA ในตอนแรกเท่ากัน NULL และไม่ได้เริ่มต้นที่อื่น รหัสสาขาเดียวที่สามารถเตรียมใช้งานตัวชี้ได้ไม่สามารถเข้าถึงได้
มีข้อความอื่นๆ เช่น:
- V575 ตัวชี้ค่าว่างถูกส่งผ่านไปยังฟังก์ชัน 'ฟรี' ตรวจสอบอาร์กิวเมนต์แรก ใบอนุญาต c 790
- V575 ตัวชี้ค่าว่างถูกส่งผ่านไปยังฟังก์ชัน 'ฟรี' ตรวจสอบอาร์กิวเมนต์แรก rdpsnd_alsa.c 575
เป็นไปได้มากว่าตัวแปรที่ถูกลืมนั้นเกิดขึ้นในระหว่างกระบวนการรีแฟคเตอร์และสามารถลบออกได้อย่างง่ายดาย
ล้นที่เป็นไปได้
ล้นที่เป็นไปได้ พิจารณาแคสต์ตัวถูกดำเนินการ ไม่ใช่ผลลัพธ์ makecert.c 1087
// openssl/x509.h
ASN1_TIME *X509_gmtime_adj(ASN1_TIME *s, long adj);
struct _MAKECERT_CONTEXT
{
....
int duration_years;
int duration_months;
};
typedef struct _MAKECERT_CONTEXT MAKECERT_CONTEXT;
int makecert_context_process(MAKECERT_CONTEXT* context, ....)
{
....
if (context->duration_months)
X509_gmtime_adj(after, (long)(60 * 60 * 24 * 31 *
context->duration_months));
else if (context->duration_years)
X509_gmtime_adj(after, (long)(60 * 60 * 24 * 365 *
context->duration_years));
....
}นำผลมาสู่. ยาว ไม่ใช่การป้องกันโอเวอร์โฟลว์ เนื่องจากการคำนวณนั้นเกิดขึ้นโดยใช้ประเภทนั้น int.
การยกเลิกการอ้างอิงตัวชี้ในการเริ่มต้น
ตัวชี้ 'บริบท' ถูกใช้ก่อนที่จะได้รับการตรวจสอบกับ nullptr ตรวจสอบบรรทัด: 746, 748. gfx.c 746
static UINT gdi_SurfaceCommand(RdpgfxClientContext* context,
const RDPGFX_SURFACE_COMMAND* cmd)
{
....
rdpGdi* gdi = (rdpGdi*) context->custom;
if (!context || !cmd)
return ERROR_INVALID_PARAMETER;
....
}นี่คือตัวชี้ สิ่งแวดล้อม ถูกยกเลิกการอ้างอิงระหว่างการเริ่มต้น - ก่อนที่จะตรวจสอบ
พบข้อผิดพลาดอื่นๆ ประเภทนี้:
- V595 ตัวชี้ 'ntlm' ถูกใช้ก่อนที่จะได้รับการตรวจสอบกับ nullptr ตรวจสอบบรรทัด: 236, 255. ntlm.c 236
- V595 ตัวชี้ 'บริบท' ถูกใช้ก่อนที่จะได้รับการตรวจสอบกับ nullptr ตรวจสอบบรรทัด: 1003, 1007. rfx.c 1003
- V595 ตัวชี้ 'rdpei' ถูกใช้ก่อนที่จะได้รับการตรวจสอบกับ nullptr ตรวจสอบบรรทัด: 176, 180. rdpei_main.c 176
- V595 ตัวชี้ 'gdi' ถูกใช้ก่อนที่จะได้รับการตรวจสอบกับ nullptr ตรวจสอบบรรทัด: 121, 123. xf_gfx.c 121
สภาพที่ไร้ความหมาย
นิพจน์ 'rdp->state >= CONNECTION_STATE_ACTIVE' เป็นจริงเสมอ การเชื่อมต่อ c 1489
int rdp_server_transition_to_state(rdpRdp* rdp, int state)
{
....
switch (state)
{
....
case CONNECTION_STATE_ACTIVE:
rdp->state = CONNECTION_STATE_ACTIVE; // <=
....
if (rdp->state >= CONNECTION_STATE_ACTIVE) // <=
{
IFCALLRET(client->Activate, client->activated, client);
if (!client->activated)
return -1;
}
....
}
....
}สังเกตได้ง่ายว่าเงื่อนไขแรกไม่มีความหมายเนื่องจากมีการกำหนดค่าที่เกี่ยวข้องไว้ก่อนหน้านี้
การแยกวิเคราะห์สตริงไม่ถูกต้อง
รูปแบบไม่ถูกต้อง ลองตรวจสอบอาร์กิวเมนต์จริงตัวที่สามของฟังก์ชัน 'sscanf' คาดว่าจะมีตัวชี้ไปยังประเภท int ที่ไม่ได้ลงนาม พร็อกซี.c 220
ส่วนหนึ่งของนิพจน์เงื่อนไขเป็นจริงเสมอ: (rc >= 0) พร็อกซี.c 222
static BOOL check_no_proxy(....)
{
....
int sub;
int rc = sscanf(range, "%u", &sub);
if ((rc == 1) && (rc >= 0))
{
....
}
....
}เครื่องวิเคราะห์จะออกคำเตือน 2 รายการทันทีสำหรับส่วนนี้ ตัวระบุ %u คาดหวังตัวแปรประเภท int ที่ไม่ได้ลงนามแต่แปรผัน ด้านล่าง มีประเภท int- ต่อไปเราจะเห็นการตรวจสอบที่น่าสงสัย: เงื่อนไขทางด้านขวาไม่สมเหตุสมผลเนื่องจากในตอนแรกมีการเปรียบเทียบกับสิ่งหนึ่ง ฉันไม่รู้ว่าผู้เขียนโค้ดนี้หมายถึงอะไร แต่มีบางอย่างผิดปกติอย่างชัดเจน
การตรวจสอบที่ไม่เป็นระเบียบ
นิพจน์ 'สถานะ == 0x00090314' จะเป็นเท็จเสมอ ntlm.c 299
BOOL ntlm_authenticate(rdpNtlm* ntlm, BOOL* pbContinueNeeded)
{
....
if (status != SEC_E_OK)
{
....
return FALSE;
}
if (status == SEC_I_COMPLETE_NEEDED) // <=
status = SEC_E_OK;
else if (status == SEC_I_COMPLETE_AND_CONTINUE) // <=
status = SEC_I_CONTINUE_NEEDED;
....
}เงื่อนไขที่ตรวจสอบจะเป็นเท็จเสมอ เนื่องจากการดำเนินการจะไปถึงเงื่อนไขที่สองเท่านั้น สถานะ == SEC_E_OK- รหัสที่ถูกต้องอาจมีลักษณะดังนี้:
if (status == SEC_I_COMPLETE_NEEDED)
status = SEC_E_OK;
else if (status == SEC_I_COMPLETE_AND_CONTINUE)
status = SEC_I_CONTINUE_NEEDED;
else if (status != SEC_E_OK)
{
....
return FALSE;
}ข้อสรุป
ดังนั้นการตรวจสอบโครงการจึงเผยให้เห็นปัญหามากมาย แต่มีเพียงส่วนที่น่าสนใจที่สุดเท่านั้นที่อธิบายไว้ในบทความ ผู้พัฒนาโครงการสามารถตรวจสอบโครงการได้ด้วยตนเองโดยขอรหัสลิขสิทธิ์ชั่วคราวบนเว็บไซต์ - นอกจากนี้ยังมีผลบวกลวงซึ่งจะช่วยปรับปรุงเครื่องวิเคราะห์ได้ อย่างไรก็ตาม การวิเคราะห์แบบคงที่ถือเป็นสิ่งสำคัญหากคุณต้องการไม่เพียงแต่ปรับปรุงคุณภาพของโค้ดของคุณ แต่ยังช่วยลดเวลาที่ใช้ในการค้นหาข้อผิดพลาด และ PVS-Studio สามารถช่วยคุณได้
หากคุณต้องการแบ่งปันบทความนี้กับผู้ชมที่พูดภาษาอังกฤษ โปรดใช้ลิงก์การแปล: Sergey Larin
ที่มา: will.com
