C++
सी ++ डिबगिंग और डिबग-रोकथाम उपकरण और तकनीक
खोज…
परिचय
C ++ डेवलपर्स से बहुत समय डिबगिंग में बिताया जाता है। यह विषय इस कार्य में सहायता करने और तकनीकों के लिए प्रेरणा देने के लिए है। उपकरण या उल्लेखित उपकरणों पर एक मैनुअल द्वारा तय मुद्दों और समाधानों की एक विस्तृत सूची की उम्मीद न करें।
टिप्पणियों
यह विषय अभी तक पूरा नहीं हुआ है, निम्नलिखित तकनीकों / उपकरणों पर उदाहरण उपयोगी होंगे:
- अधिक स्थैतिक विश्लेषण उपकरण का उल्लेख करें
- बाइनरी इंस्ट्रूमेंटेशन टूल्स (जैसे UBSan, TSan, MSan, ESan ...)
- हार्डनिंग (CFI ...)
- fuzzing
मेरा सी ++ प्रोग्राम सीगफॉल्ट के साथ समाप्त होता है - वेलग्रिंड
चलो एक बुनियादी विफलता कार्यक्रम है:
#include <iostream>
void fail() {
int *p1;
int *p2(NULL);
int *p3 = p1;
if (p3) {
std::cout << *p3 << std::endl;
}
}
int main() {
fail();
}
इसे बनाएँ (डीबग जानकारी शामिल करने के लिए जोड़ें):
g++ -g -o main main.cpp
Daud:
$ ./main
Segmentation fault (core dumped)
$
आइए इसे वेलग्रिंड से डीबग करें:
$ valgrind ./main
==8515== Memcheck, a memory error detector
==8515== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==8515== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==8515== Command: ./main
==8515==
==8515== Conditional jump or move depends on uninitialised value(s)
==8515== at 0x400813: fail() (main.cpp:7)
==8515== by 0x40083F: main (main.cpp:13)
==8515==
==8515== Invalid read of size 4
==8515== at 0x400819: fail() (main.cpp:8)
==8515== by 0x40083F: main (main.cpp:13)
==8515== Address 0x0 is not stack'd, malloc'd or (recently) free'd
==8515==
==8515==
==8515== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==8515== Access not within mapped region at address 0x0
==8515== at 0x400819: fail() (main.cpp:8)
==8515== by 0x40083F: main (main.cpp:13)
==8515== If you believe this happened as a result of a stack
==8515== overflow in your program's main thread (unlikely but
==8515== possible), you can try to increase the size of the
==8515== main thread stack using the --main-stacksize= flag.
==8515== The main thread stack size used in this run was 8388608.
==8515==
==8515== HEAP SUMMARY:
==8515== in use at exit: 72,704 bytes in 1 blocks
==8515== total heap usage: 1 allocs, 0 frees, 72,704 bytes allocated
==8515==
==8515== LEAK SUMMARY:
==8515== definitely lost: 0 bytes in 0 blocks
==8515== indirectly lost: 0 bytes in 0 blocks
==8515== possibly lost: 0 bytes in 0 blocks
==8515== still reachable: 72,704 bytes in 1 blocks
==8515== suppressed: 0 bytes in 0 blocks
==8515== Rerun with --leak-check=full to see details of leaked memory
==8515==
==8515== For counts of detected and suppressed errors, rerun with: -v
==8515== Use --track-origins=yes to see where uninitialised values come from
==8515== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
$
पहले हम इस ब्लॉक पर ध्यान केंद्रित करते हैं:
==8515== Invalid read of size 4
==8515== at 0x400819: fail() (main.cpp:8)
==8515== by 0x40083F: main (main.cpp:13)
==8515== Address 0x0 is not stack'd, malloc'd or (recently) free'd
पहली पंक्ति हमें बताती है कि सेगफॉल्ट 4 बाइट्स पढ़ने के कारण होता है। दूसरी और तीसरी पंक्ति कॉल स्टैक हैं। इसका मतलब यह है कि अमान्य रीड fail()
फंक्शन, मेन्यू। 8 लाइन की लाइन पर किया जाता है, जिसे मेन द्वारा कहा जाता है।
हम देख रहे हैं। main.cpp की लाइन 8 को देख रहे हैं
std::cout << *p3 << std::endl;
लेकिन हम पहले पॉइंटर की जांच करते हैं, तो क्या गलत है? दूसरे ब्लॉक की जाँच करें:
==8515== Conditional jump or move depends on uninitialised value(s)
==8515== at 0x400813: fail() (main.cpp:7)
==8515== by 0x40083F: main (main.cpp:13)
यह हमें बताता है कि पंक्ति 7 में एक इकाई परिवर्तनशील चर है और हम इसे पढ़ते हैं:
if (p3) {
जो हमें उस रेखा की ओर इंगित करता है जहां हम पी 2 के बजाय पी 3 की जांच करते हैं। लेकिन यह कैसे संभव है कि पी 3 असिंचित है? हम इसे इनिशियलाइज़ करते हैं:
int *p3 = p1;
वेलग्रिंड साथ फिर से चलाएं करने के लिए हमें सलाह --track-origins=yes
, चलिए इसे करते हैं:
valgrind --track-origins=yes ./main
वैलग्राइंड के लिए तर्क वेलग्रिंड के बाद है। यदि हम इसे अपने कार्यक्रम के बाद रखते हैं, तो यह हमारे कार्यक्रम को पारित कर दिया जाएगा।
आउटपुट लगभग समान है, केवल एक ही अंतर है:
==8517== Conditional jump or move depends on uninitialised value(s)
==8517== at 0x400813: fail() (main.cpp:7)
==8517== by 0x40083F: main (main.cpp:13)
==8517== Uninitialised value was created by a stack allocation
==8517== at 0x4007F6: fail() (main.cpp:3)
जो हमें बताता है कि लाइन 7 पर हमने जो अनइंस्टाल्यूटेड वैल्यू इस्तेमाल की है, उसे लाइन 3 में बनाया गया था:
int *p1;
जो हमें हमारे निर्विवाद पॉइंटर का मार्गदर्शन करता है।
GDB के साथ सेगफॉल्ट विश्लेषण
इस उदाहरण के लिए ऊपर दिए गए समान कोड का उपयोग करें।
#include <iostream>
void fail() {
int *p1;
int *p2(NULL);
int *p3 = p1;
if (p3) {
std::cout << *p2 << std::endl;
}
}
int main() {
fail();
}
पहले इसे संकलित करने दें
g++ -g -o main main.cpp
इसे gdb के साथ चलाते हैं
gdb ./main
अब हम gdb शेल में होंगे। टाइप रन।
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/opencog/code-snippets/stackoverflow/a.out
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400850 in fail () at debugging_with_gdb.cc:11
11 std::cout << *p2 << std::endl;
हम देखते हैं कि विभाजन की गलती लाइन 11 पर हो रही है। इसलिए इस लाइन में उपयोग किया जा रहा एकमात्र वेरिएबल पॉइंटर पी 2 है। इसकी सामग्री टाइपिंग प्रिंट की जांच करें।
(gdb) print p2
$1 = (int *) 0x0
अब हम देखते हैं कि P2 को 0x0 पर आरंभीकृत किया गया था जो NULL के लिए है। इस रेखा पर, हम जानते हैं कि हम एक NULL पॉइंटर को रोकने की कोशिश कर रहे हैं। इसलिए हम इसे ठीक करते हैं।
साफ कोड
डिबगिंग उस कोड को समझने के साथ शुरू होती है जिसे आप डिबग करने की कोशिश कर रहे हैं।
खराब कोड:
int main() {
int value;
std::vector<int> vectorToSort;
vectorToSort.push_back(42); vectorToSort.push_back(13);
for (int i = 52; i; i = i - 1)
{
vectorToSort.push_back(i *2);
}
/// Optimized for sorting small vectors
if (vectorToSort.size() == 1);
else
{
if (vectorToSort.size() <= 2)
std::sort(vectorToSort.begin(), std::end(vectorToSort));
}
for (value : vectorToSort) std::cout << value << ' ';
return 0; }
बेहतर कोड:
std::vector<int> createSemiRandomData() {
std::vector<int> data;
data.push_back(42);
data.push_back(13);
for (int i = 52; i; --i)
vectorToSort.push_back(i *2);
return data;
}
/// Optimized for sorting small vectors
void sortVector(std::vector &v) {
if (vectorToSort.size() == 1)
return;
if (vectorToSort.size() > 2)
return;
std::sort(vectorToSort.begin(), vectorToSort.end());
}
void printVector(const std::vector<int> &v) {
for (auto i : v)
std::cout << i << ' ';
}
int main() {
auto vectorToSort = createSemiRandomData();
sortVector(std::ref(vectorToSort));
printVector(vectorToSort);
return 0;
}
आपके द्वारा पसंद और कोडिंग शैलियों के बावजूद, एक सुसंगत कोडिंग (और स्वरूपण) शैली होने से आपको कोड को समझने में मदद मिलेगी।
ऊपर दिए गए कोड को देखकर, कोई भी सुधार और पठनीयता में सुधार करने के लिए कुछ सुधारों की पहचान कर सकता है:
अलग-अलग कार्यों के लिए अलग-अलग कार्यों का उपयोग
यदि आप विवरण में रुचि नहीं रखते हैं, तो अलग-अलग फ़ंक्शन का उपयोग आपको डीबगर में कुछ कार्यों को छोड़ने की अनुमति देता है। इस विशिष्ट मामले में, आप डेटा के निर्माण या मुद्रण में दिलचस्पी नहीं ले सकते हैं और केवल छंटनी में कदम रखना चाहते हैं।
एक और लाभ यह है कि आपको कोड के माध्यम से कदम बढ़ाते समय कम कोड (और इसे याद रखना) की आवश्यकता होती है। अब आपको पूरे फ़ंक्शन के बजाय इसे समझने के लिए main()
कोड की 3 पंक्तियों को पढ़ने की आवश्यकता है।
तीसरा लाभ यह है कि आपके पास देखने के लिए बस कम कोड है, जो सेकंड के भीतर इस बग को स्पॉट करने में प्रशिक्षित आंख की मदद करता है।
लगातार प्रारूपण / निर्माण का उपयोग करना
सुसंगत स्वरूपण और निर्माण का उपयोग कोड से अव्यवस्था को हटा देगा, जिससे पाठ के बजाय कोड पर ध्यान केंद्रित करना आसान हो जाएगा। 'सही' प्रारूपण शैली पर बहुत सारी चर्चाएँ की गई हैं। उस शैली के बावजूद, कोड में एक एकल सुसंगत शैली होने से परिचितता में सुधार होगा और कोड पर ध्यान केंद्रित करना आसान होगा।
चूंकि कोडिंग फॉर्मेट समय लेने वाला कार्य है, इसलिए इसके लिए एक समर्पित उपकरण का उपयोग करने की सिफारिश की जाती है। अधिकांश IDE के पास इसके लिए कम से कम किसी प्रकार का समर्थन है और यह मनुष्यों की तुलना में अधिक संगत स्वरूपण कर सकता है।
आप ध्यान दे सकते हैं कि यह शैली रिक्त स्थान और न्यूलाइन्स तक सीमित नहीं है क्योंकि हम कंटेनर के आरंभ / समाप्ति के लिए फ़्री-स्टाइल और सदस्य कार्यों को नहीं मिलाते हैं। ( v.begin()
बनाम std::end(v)
)।
अपने कोड के महत्वपूर्ण भागों पर ध्यान दें।
आपके द्वारा चुने जाने की शैली के बावजूद, उपरोक्त कोड में कुछ मार्कर होते हैं जो आपको संकेत दे सकते हैं कि क्या हो सकता है:
-
optimized
एक टिप्पणी, यह कुछ फैंसी तकनीकों को इंगित करता है -
sortVector()
में कुछ शुरुआती रिटर्न बताते हैं कि हम कुछ खास कर रहे हैं -
std::ref()
इंगित करता है कि कुछsortVector()
साथ चल रहा हैsortVector()
निष्कर्ष
स्वच्छ कोड होने से आपको कोड को समझने में मदद मिलेगी और यह उस समय को कम कर देगा जब आपको इसे डीबग करना होगा। दूसरे उदाहरण में, एक कोड समीक्षक बग को पहली नज़र में भी देख सकता है, जबकि बग को पहले एक विवरण में छिपाया जा सकता है। (पुनश्च: बग 2
साथ तुलना में है।)
स्थैतिक विश्लेषण
स्टेटिक एनालिसिस वह तकनीक है जिसमें ज्ञात बग से जुड़े पैटर्न के लिए कोड की जांच करता है। इस तकनीक का उपयोग करना एक कोड समीक्षा की तुलना में कम समय लगता है, हालांकि, इसकी जांच केवल उपकरण में प्रोग्राम किए गए तक ही सीमित है।
चेक में एडवांस्ड ग्राफ एल्गोरिदम तक if (var);
-स्टेटमेंट ( if (var);
) के पीछे गलत सेमी-कॉलोन शामिल हो सकते हैं जो यह निर्धारित करते हैं कि कोई वैरिएबल इनिशियलाइज़ नहीं है।
संकलक चेतावनी
स्थैतिक विश्लेषण को सक्षम करना सबसे सरल संस्करण है, जो आपके कंपाइलर में पहले से ही अंतर्निहित है:
यदि आप इन विकल्पों को सक्षम करते हैं, तो आप नोटिस करेंगे कि प्रत्येक संकलक को बग्स दूसरों को नहीं मिलेंगे और आपको उन तकनीकों पर त्रुटियां मिलेंगी जो किसी विशिष्ट संदर्भ में मान्य या मान्य हो सकती हैं। while (staticAtomicBool);
while (localBool);
भी स्वीकार्य हो सकता है, while (localBool);
नहीं है।
इसलिए कोड की समीक्षा के विपरीत, आप एक उपकरण से लड़ रहे हैं जो आपके कोड को समझता है, आपको बहुत सारे उपयोगी कीड़े बताता है और कभी-कभी आपसे असहमत होता है। इस अंतिम मामले में, आपको स्थानीय रूप से चेतावनी को दबाना पड़ सकता है।
जैसा कि ऊपर दिए गए विकल्प सभी चेतावनियों को सक्षम करते हैं, वे उन चेतावनियों को सक्षम कर सकते हैं जो आप नहीं चाहते हैं। (आपका कोड C ++ 98 संगत क्यों होना चाहिए?) यदि हां, तो आप बस उस विशिष्ट चेतावनी को अक्षम कर सकते हैं:
-
clang++ -Wall -Weverything -Werror -Wno-errortoaccept ...
-
g++ -Wall -Weverything -Werror -Wno-errortoaccept ...
-
cl.exe /W4 /WX /wd<no of warning>...
जहां कंपाइलर चेतावनी विकास के दौरान आपकी सहायता करते हैं, वे संकलन को थोड़ा धीमा कर देते हैं। यही कारण है कि आप हमेशा उन्हें डिफ़ॉल्ट रूप से सक्षम नहीं करना चाहते हैं। या तो आप उन्हें डिफ़ॉल्ट रूप से चलाते हैं या आप अधिक महंगे चेक (या उन सभी) के साथ कुछ निरंतर एकीकरण को सक्षम करते हैं।
बाहरी उपकरण
यदि आप कुछ निरंतर एकीकरण का निर्णय लेते हैं, तो अन्य उपकरणों का उपयोग इस तरह का खिंचाव नहीं है। क्लैंग-टिड्डी जैसे उपकरण में चेक की एक सूची होती है जो कई प्रकार के मुद्दों को कवर करती है, कुछ उदाहरण:
- वास्तविक कीड़े
- टुकड़ा करने की रोकथाम
- साइड इफेक्ट के साथ जोर देता है
- पठनीयता की जाँच
- भ्रामक आक्रोश
- पहचानकर्ता नामकरण की जाँच करें
- आधुनिकीकरण की जाँच
- Make_unique () का उपयोग करें
- Nullptr का प्रयोग करें
- प्रदर्शन की जाँच
- अनावश्यक प्रतियां खोजें
- अक्षम एल्गोरिथ्म कॉल का पता लगाएं
यह सूची उतनी बड़ी नहीं हो सकती है, क्योंकि क्लैंग में पहले से ही बहुत सारे कंपाइलर चेतावनी हैं, हालांकि यह आपको एक उच्च गुणवत्ता वाले कोड बेस के करीब एक कदम लाएगा।
अन्य उपकरण
समान उद्देश्य वाले अन्य उपकरण मौजूद हैं, जैसे:
- बाहरी उपकरण के रूप में दृश्य स्टूडियो स्थिर विश्लेषक
- क्लैट , क्यूटी कोड की जाँच के लिए एक क्लैंग संकलक प्लगइन
निष्कर्ष
C ++ के लिए बहुत सारे स्थैतिक विश्लेषण उपकरण मौजूद हैं, दोनों बाहरी उपकरणों के रूप में कंपाइलर में निर्मित होते हैं। उन्हें आज़माने में आसान सेटअप के लिए उतना समय नहीं लगता है और वे ऐसे कोड पाएंगे जो आपको कोड समीक्षा में याद आ सकते हैं।
सुरक्षित-ढेर (ढेर भ्रष्टाचार)
स्टैक के भ्रष्टाचार को देखने के लिए परेशान करने वाले कीड़े हैं। जैसा कि स्टैक दूषित है, डिबगर अक्सर आपको एक अच्छा स्टैक ट्रेस नहीं दे सकता है कि आप कहां हैं और आप वहां कैसे पहुंचे।
यह वह जगह है जहाँ सुरक्षित-ढेर खेलने में आता है। अपने धागे के लिए एक एकल स्टैक का उपयोग करने के बजाय, यह दो का उपयोग करेगा: एक सुरक्षित स्टैक और एक खतरनाक स्टैक। सुरक्षित स्टैक ठीक उसी तरह काम करता है जैसा पहले किया था, सिवाय इसके कि कुछ हिस्सों को खतरनाक स्टैक में ले जाया जाता है।
स्टैक के किन हिस्सों को स्थानांतरित किया जाता है?
हर भाग जिसमें स्टैक को दूषित करने की क्षमता है, सुरक्षित स्टैक से बाहर निकल जाएगा। जैसे ही स्टैक पर एक वैरिएबल संदर्भ द्वारा पारित हो जाता है या कोई इस वैरिएबल का पता लेता है, कंपाइलर इसे सुरक्षित के बजाय दूसरे स्टैक पर आवंटित करने का निर्णय लेगा।
नतीजतन, आप उन बिंदुओं के साथ कोई भी ऑपरेशन करते हैं, आपके द्वारा मेमोरी पर किए गए किसी भी संशोधन (उन बिंदुओं / संदर्भों के आधार पर) केवल मेमोरी को दूसरे स्टैक में प्रभावित कर सकते हैं। जैसा कि कभी भी एक पॉइंटर नहीं मिलता है जो सुरक्षित स्टैक के करीब है, स्टैक स्टैक को भ्रष्ट नहीं कर सकता है और डीबगर अभी भी स्टैक पर सभी कार्यों को एक अच्छा ट्रेस देने के लिए पढ़ सकता है।
यह वास्तव में किसके लिए प्रयोग किया जाता है?
सुरक्षित स्टैक का आविष्कार आपको बेहतर डिबगिंग अनुभव देने के लिए नहीं किया गया था, हालांकि, यह गंदा कीड़े के लिए एक अच्छा दुष्प्रभाव है। यह मूल उद्देश्य कोड-पॉइंटर इंटीग्रिटी (CPI) प्रोजेक्ट का हिस्सा है , जिसमें वे कोड इंजेक्शन को रोकने के लिए रिटर्न एड्रेस को ओवरराइड करने से रोकने की कोशिश करते हैं। दूसरे शब्दों में, वे एक हैकर्स कोड को निष्पादित करने से रोकने की कोशिश करते हैं।
इस कारण से, गुण क्रोमियम पर सक्रिय हो गया है और एक <1% सीपीयू ओवरहेड होने की सूचना दी गई है।
इसे कैसे सक्षम करें?
अभी, विकल्प केवल -fsanitize=safe-stack
कंपाइलर में उपलब्ध है, जहां कोई कंपाइलर के पास -fsanitize=safe-stack
पास कर सकता है। जीसीसी में समान सुविधा को लागू करने के लिए एक प्रस्ताव बनाया गया था।
निष्कर्ष
सुरक्षित स्टैक सक्षम होने पर स्टैक दूषित डिबग करना आसान हो सकता है। ओवरहेड कम प्रदर्शन के कारण, आप अपने बिल्ड कॉन्फ़िगरेशन में डिफ़ॉल्ट रूप से भी सक्रिय हो सकते हैं।