This lecture provides an introduction to debugging, a crucial activity in every developer's life. After an elementary discussion of some useful debugging concepts, the lecture goes on with a detailed review of general debugging techniques, independent of any specific software. The final part of the lecture is dedicated to analysing problems related to the use of C++ , the main programming language commonly employed in particle physics nowadays.
1. General concepts
about debugging. After many days of brainstorming, designing and coding, the programmer finally have a wonderful piece of code. He compiles it and runs it. Everything seems pretty straightforward but unfortunately it doesn't work! And now? Now the great fun starts! Time to dig into the wonderful world of debugging. Despite being the realm of ingenuity and uncertainty, a debugging process can be divided into four main steps:
1. localising a bug,
2. classifying a bug,
3. understanding a bug,
4. repairing a bug.
1.1 Localizing a bug
A typical attitude of inexperienced programmers towards bugs is to consider their localization an easy task: they notice their code does not do what they expected, and they are led astray by their confidence in knowing what their code should do. This confidence is completely deceptive because spotting a bug can be very difficult. As it was explained in the introduction, all bugs stem from the premise that something thought to be right, was in fact wrong.
Noticing a bug implies testing. Testing should be performed with discipline and, when possible, automatically, for example after each build of the code. In case of a test failure, the programmer must be able to see what went wrong easily, so tests must be prepared carefully. This lecture will not cover the basic of testing.
1.2 Classifying a bug
Despite the appearance, bugs have often a common background. This allows to attempt a quite coarse, but sometimes useful, classification. The list is arranged in order of increasing difficulty (which fortunately means in order of decreasing frequency).
Syntactical Errors should be easily caught by your compiler. I say "should" because compilers, beside being very complicated, can be buggy themselves. In any case, it is vital to remember that quite often the problem might not be at the exact position indicated by the compiler error message.
Build Errors derive from linking object files which were not rebuilt after a change in some source files. These problems can easily be avoided by using tools to drive software building.
Basic Semantic Errors comprise using un initialized variables, dead code (code that will never be executed) and problems with variable types. A compiler can highlight them to your attention, although it usually has to be explicitly asked through flags (cp. 2.1).
Semantic Errors include using wrong variables or operators (e.g., & instead of && in C++). No tool can catch these problems, because they are syntactically correct statements, although logically wrong. A test case or a debugger is necessary to spot them.
A funny physical classification distinguishes between Bohrbugs and Heisenbugs. Bohrbugs are deterministic: a particular input will always manifest them with the same result. Heisenbugs are random : difficult to reproduce reliably, since they seem to depend on environmental factors (e.g. a particular memory allocation, the way the operating system schedules processes, the phase of the moon and so on). In C++ a Heisenbug is very often the result of an error with pointers.
1.3 Understanding a bug
A bug should be fully understood before attempting to fix it. Trying to fix a bug before understanding it completely could end in provoking even more damage to the code, since the problem could change form and manifest itself somewhere else, maybe randomly. Again, a typical example is memory corruption: if there is any suspect memory was corrupted during the execution of some algorithm, all the data involved in the algorithm must be checked before trying to change them.
The following check-list is useful to assure a correct approach to the investigation:
- do not confuse observing symptoms with finding the real source of the problem;
- check if similar mistakes (especially wrong assumptions) were made elsewhere in the code;
- verify that just a programming error, and not a more fundamental problem (e.g. an incorrect algorithm), was found.
1.4 Repairing a bug
The final step in the debugging process is bug fixing. Repairing a bug is more than modifying code. Any fixes must be documented in the code and tested properly. More important, learning from mistakes is an effective attitude: it is good practice filling a small file with detailed explanations about the way the bug was discovered and corrected. A check-list can be a useful aid.
Several points are worth recording:
- How the bug was noticed, to help in writing a test case;
- How it was tracked down, to give you a better insight on the approach to choose in similar circumstances;
- What type of bug was encountered?
- If this bug was encountered often, in order to set up a strategy to prevent it from recurring;
- If the initial assumptions were unjustified; this is often the main reason why tracking a bug is so time consuming.
2 General debugging techniques
As said before, debugging is often the realm of ingenuity and uncertainty. Yet a number of tricks can be adopted in the daily programming activity to ease the hunt for problems.
2.1 Exploiting compiler features
A good compiler can do some static analysis on the code. Static code analysis is the analysis of software that is performed without actually executing programs built from that software. Static analysis can help in detecting a number of basic semantic problems, e.g. type mismatch or dead code.
Having a look at the user manual of the compiler employed, where all the features should be documented, is highly recommended. For gcc, the standard compiler on GNU/Linux systems, there are a number of options that affect what static analysis can be performed. They are usually divided into two classes: warning options and optimization flags.
2.2 Reading the right documentation
This seems quite an obvious tip, but too often inexperienced programmers read the wrong papers looking for hints about the task they have to accomplish. The relevant documentation for the task, the tools, the libraries and the algorithms employed must be at fingertips to find the relevant information easily.
As far as documentation is concerned, the most important distinction is between tutorials and references. A tutorial is a pedagogical paper, usually with plenty of examples. It doesn't assume any previous knowledge of the topic and its first aim is to convey ideas about the subject. Reference manuals, on the contrary, are comprehensive and exhaustive descriptions, which allow finding the answers to questions through indexes and cross-references.
In the world of programming, all these types of documents are usually in electronic format. The reference documentation must be up to date, accurate and corresponding to the problems and tools used: looking up in a wrong reference manual could end up in trying to use a feature that is not supported by the current version of the tool, for example.
2.3 The abused cout debugging technique
The cout technique takes its names from the C++ statement for printing on the standard output stream (usually the terminal screen). It consists of adding print statements in the code to track the control flow and data values during code execution. Although it is the favourite technique of all the novices, it is unbelievable how many experienced programmers still refuse to evolve and abandon this absolutely time-wasting and very ad-hoc method.
Despite its popularity, this technique has strong disadvantages. First of all, it is very ad-hoc, because code insertion is temporary, to be removed as soon as the bug is fixed. A new bug means a new insertion, making it a waste of time. In debugging as well as in coding, the professional should aim to find reusable solutions whenever possible. Printing statements are not reusable, and so are deprecated. As we will see shortly, there are more effective ways to track the control flow through messages. In addition, printing statements clobber the normal output of the program, making it extremely confused. They also slow the program down considerably: accessing to the outputting peripherals becomes a bottleneck. Finally, often they do not help at all, because for performance reasons, output is usually buffered and, in case of crash, the buffer is destroyed and the important information is lost, possibly resulting in starting the debugging process in the wrong place.
In some (very few) circumstances cout debugging can be appropriate, although it can always be replaced by other techniques. For these cases, here are some tips. To begin with, output must be produced on the standard error, because this channel is unbuffered and it is less likely to miss the last information before a crash. Then, printing statements should not be used directly: a macro should be defined around them (as illustrated in listing 2) so to switch debugging code on and off easily. Finally, debugging levels should be used to manage the amount of debugging information.
Listing 2: An example of cout technique - Declaration
#ifndef DEBUG_H
#define DEBUG_H
#inc lude < stdarg.h>
#if defined (NDEBUG) && defined (__GNUC__)
/* gcc's cpp has extensions; it allows for macros with a variable number of arguments. We use this extension here to preprocess pmesg away. */
#define pmesg ( level, forma t, args. . . )(( void ) 0 )
#else
void pmesg ( int level , char * forma t , . . . ) ;
/ * print a message , if it is con sidered significant enough */
#endif
#endif /* DEBUG_H */
Listing3:An example of cout technique-Implementation
#include "debug.h"
#include
extern int msglevel; /* the higher,t
บรรยายนี้แนะนำการตรวจแก้จุดบกพร่อง กิจกรรมที่สำคัญในชีวิตของนักพัฒนาทุกทาง หลังจากการสนทนาระดับประถมของ ประโยชน์แนวคิด การดีบักการบรรยายไป ด้วยการทบทวนรายละเอียดทั่วไปตรวจแก้จุดบกพร่องเทคนิค ขึ้นอยู่กับซอฟต์แวร์เฉพาะ ส่วนสุดท้ายของการบรรยายจะทุ่มเทเพื่อวิเคราะห์ปัญหาเกี่ยวกับการใช้ c ++, ภาษาการเขียนโปรแกรมหลักโดยทั่วไปพนักงานในฟิสิกส์อนุภาคในปัจจุบัน1. แนวคิดทั่วไป เกี่ยวกับดีบัก หลังจากจำนวนวันของการระดมความคิด การออกแบบ และเขียนโค้ด โปรแกรมเมอร์ที่มีชิ้นยอดเยี่ยมของรหัสในที่สุด เขาคอมไพล์มัน และทำมัน ตกแต่งสวยตรงไปตรงมา แต่น่าเสียดายที่มันไม่ทำงาน และตอนนี้ ตอนนี้เริ่มสนุกดี เวลาขุดเข้าไปในโลกมหัศจรรย์ของดีบัก แม้จะเป็นขอบเขตของการประดิษฐ์คิดค้นและความไม่แน่นอน กระบวนการตรวจแก้จุดบกพร่องสามารถแบ่งออกเป็นสี่ขั้นตอนหลัก:1. บกพร่อง localising2. ประเภทบกพร่อง3. บกพร่อง ความเข้าใจ4. ซ่อมบกพร่อง1.1 ทั้งบกพร่องทัศนคติทั่วไปของโปรแกรมเมอร์มือใหม่ไปทางโรคจิตจะพิจารณาแปลความอย่างละเอียด: แจ้งรหัสของพวกเขาทำสิ่งที่พวกเขาคาดว่าไม่ และพวกเขาจะนำผู้เสียคน โดยความความเชื่อมั่นในการรู้รหัสของพวกเขาควรทำอะไร ความเชื่อมั่นนี้จะหลอกลวงทั้งหมดเนื่องจากจำบกพร่องอาจเป็นเรื่องยากมาก ตามที่มันถูกอธิบายไว้ในการแนะนำ ข้อบกพร่องทั้งหมดเกิดจากหลักฐานที่ว่า สิ่งที่คิดว่า ถูก ผิดในความเป็นจริง สังเกตเห็นบกพร่องหมายถึงการทดสอบ ทดสอบควรดำเนินการวินัย และ เมื่อเป็นไป ได้ โดยอัตโนมัติ ตัวอย่างหลังจากที่สร้างแต่ละรหัส ในกรณีของความล้มเหลวในการทดสอบ ผู้เขียนโปรแกรมต้องสามารถเห็นสิ่งที่ผิดได้ง่าย จึงต้องจัดเตรียมการทดสอบอย่างระมัดระวัง บรรยายนี้จะครอบคลุมพื้นฐานของการทดสอบ 1.2 ประเภทบกพร่องแม้ มีลักษณะที่ปรากฏ ข้อบกพร่องได้มักพื้นทั่วไป นี้ได้พยายามจัดประเภทค่อนข้างหยาบ แต่บางครั้งมี ประโยชน์ รายการถูกจัดอยู่ในลำดับของการเพิ่มความยากลำบาก (ซึ่งโชคดีหมายถึงลำดับของการลดความถี่) ข้อผิดพลาด syntactical ควรถูกจับ โดยคอมไพเลอร์ของคุณได้อย่างง่ายดาย ผมบอกว่า "ควร" เนื่องจากคอมไพเลอร์ ข้างมีความซับซ้อนมาก ได้รถตัวเอง มันมีความสำคัญที่ต้องจำไว้ว่า ค่อนข้างบ่อยปัญหาอาจไม่อยู่ในตำแหน่งแน่นอนที่ระบุ ด้วยข้อความข้อผิดพลาดของคอมไพเลอร์สร้างข้อผิดพลาดที่ได้รับจากการเชื่อมโยงแฟ้มวัตถุที่ถูกสร้างใหม่หลังจากการเปลี่ยนแปลงในแฟ้มต้นฉบับบางส่วนไม่ ปัญหาเหล่านี้สามารถสามารถหลีก โดยใช้เครื่องมือในการสร้างซอฟแวร์ไดรฟ์ข้อผิดพลาดทางตรรกพื้นฐานประกอบด้วยสหประชาชาติในการเตรียมใช้งานตัวแปร ตายรหัส (รหัสที่จะไม่ดำเนินการ) และปัญหาชนิดตัวแปร คอมไพเลอร์ที่สามารถเน้นให้ความสนใจของคุณ แม้ว่าจะมีการถามได้อย่างชัดเจนผ่านค่าสถานะ (cp. 2.1) การมีข้อผิดพลาดทางตรรกโดยใช้ตัวแปรไม่ถูกต้องหรือตัวดำเนินการ (เช่น และแทนและและ ใน c ++) เครื่องมือไม่สามารถจับปัญหาเหล่านี้ เพราะเป็นงบที่ถูกต้องทางไวยากรณ์ แม้ว่าตรรกะไม่ถูกต้อง กรณีทดสอบหรือดีบักเกอร์จำเป็นต้องให้ประเภทตลกทางกายภาพที่แตกต่างระหว่าง Bohrbugs และ Heisenbugs Bohrbugs เป็น deterministic: ป้อนข้อมูลใดจะเสมอรายการนั้น มีผลลัพธ์เดียวกัน Heisenbugs เป็นแบบสุ่ม: ยากที่จะทำได้ เนื่องจากพวกเขาดูเหมือนจะขึ้นอยู่กับปัจจัยสิ่งแวดล้อม (เช่นการเฉพาะหน่วยความจำปันส่วน วิธีระบบปฏิบัติการจัดกำหนดการกระบวนการ ระยะของดวงจันทร์และ) ใน c ++ Heisenbug มีผลลัพธ์ของข้อผิดพลาดตัวชี้1.3 ทำความเข้าใจเกี่ยวกับปัญหาการบกพร่องควรจะเข้าใจก่อนที่จะพยายามแก้ไขอย่างเต็ม พยายามแก้ไขบกพร่องก่อนที่จะทำความเข้าใจอย่างสมบูรณ์สามารถสิ้นสุดใน provoking รหัส ความเสียหายมากขึ้นเนื่องจากปัญหาสามารถเปลี่ยนฟอร์ม และรายการอื่น ตัวเองอาจโดยการสุ่ม อีกครั้ง เป็นตัวอย่างโดยทั่วไปเป็นความเสียหายของหน่วยความจำ: ถ้าไม่เกิดความเสียหายใด ๆ จำสงสัยในระหว่างการดำเนินการบางอัลกอริทึม ข้อมูลทั้งหมดเกี่ยวข้องกับอัลกอริทึมต้องตรวจสอบก่อนที่จะพยายามเปลี่ยนแปลงรายการตรวจสอบที่ต่อไปนี้จะเป็นประโยชน์เพื่อให้มั่นใจว่าวิธีที่ถูกต้องเพื่อการตรวจสอบ:-สับสนอาการสังเกตกับการหาสาเหตุแท้ของปัญหา-ตรวจสอบว่าข้อผิดพลาดที่คล้ายกัน (สมมติฐานที่ผิดโดยเฉพาะ) ที่เกิดขึ้นอื่น ๆ ในรหัส-ตรวจสอบว่า เพียงความผิดพลาดของโปรแกรม และไม่มีพื้นฐานปัญหา (เช่นการถูกอัลกอริทึม), พบ1.4 ซ่อมแซมข้อผิดพลาดขั้นตอนสุดท้ายในกระบวนการตรวจแก้จุดบกพร่องข้อผิดพลาดที่แก้ไขได้ ซ่อมแซมข้อผิดพลาดเป็นมากกว่าการปรับเปลี่ยนรหัส การแก้ไขต้องจัดในรหัส และทดสอบได้อย่างถูกต้อง สำคัญ เรียนรู้จากความผิดพลาดเป็นทัศนคติมีผลบังคับใช้: ก็ดีที่บรรจุแฟ้มขนาดเล็กพร้อมคำอธิบายโดยละเอียดเกี่ยวกับวิธีการค้นพบ และแก้ไขจุดบกพร่อง รายการตรวจสอบสามารถช่วยเป็นประโยชน์หลายจุดจะบันทึก:-มีพบบั๊ก เพื่อช่วยในการเขียนกรณีทดสอบ-มันถูกติดตามลง เพื่อให้คุณเข้าใจดีกว่าวิธีการเลือกในสถานการณ์ที่คล้ายกัน-ชนิดของข้อผิดพลาดเกิดขึ้นหรือไม่-ถ้าปัญหานี้พบบ่อย การตั้งค่ากลยุทธ์เพื่อป้องกันไม่ให้เกิดซ้ำ-ถ้าสมมติฐานเบื้องต้นที่ unjustified นี้มักจะเป็นเหตุผลหลักที่ทำไมการติดตามข้อผิดพลาดเป็นดังนั้นใช้เวลานานเทคนิคการตรวจแก้จุดบกพร่องทั่วไป 2ที่กล่าวก่อน ดีบักเป็นขอบเขตของการประดิษฐ์คิดค้นและความไม่แน่นอน ยัง สามารถนำตัวเลขของเทคนิคในกิจกรรมเขียนโปรแกรมทุกวันเพื่อความสะดวกการล่าสัตว์สำหรับปัญหา2.1 ลักษณะการทำงานของคอมไพเลอร์ exploitingคอมไพเลอร์ที่ดีสามารถทำการวิเคราะห์รหัสบางคง รหัสคงวิเคราะห์เป็นการวิเคราะห์ของซอฟต์แวร์ที่ดำเนินการ โดยไม่มีการดำเนินการโปรแกรมที่สร้างจากซอฟต์แวร์ที่จริง วิเคราะห์คงสามารถช่วยในการตรวจสอบพื้นฐานปัญหาทางตรรก เช่นชนิดไม่ตรงกันหรือรหัสตายขอแนะนำมีดูที่คู่มือผู้ใช้ของคอมไพเลอร์จ้าง ซึ่งคุณลักษณะทั้งหมดควรจะจัดทำเอกสาร ใน gcc คอมไพเลอร์มาตรฐานในระบบ GNU/Linux มีจำนวนตัวเลือกที่มีผลต่อการวิเคราะห์ว่าคงสามารถดำเนินการได้ พวกเขามักจะแบ่งออกเป็น 2 ประเภท: ตัวเลือกการแจ้งเตือนและสถานะประสิทธิภาพสูงสุด2.2 อ่านเอกสารที่เหมาะสม นี้ดูเหมือนว่าเป็นคำแนะนำชัดเจน แต่โปรแกรมเมอร์มือใหม่มักจะไปอ่านหาคำแนะนำเกี่ยวกับงานพวกเขาต้องทำเอกสารไม่ถูกต้อง เอกสารที่เกี่ยวข้องสำหรับงาน เครื่องมือ รี และอัลกอริทึมที่ทำงานต้องเป็นที่ปลายนิ้วในการค้นหาข้อมูลที่เกี่ยวข้องได้อย่างง่ายดาย เป็นที่เกี่ยวข้องเอกสาร ความแตกต่างที่สำคัญที่สุดคือระหว่างสอนและอ้างอิง กวดวิชาเป็นกระดาษสอน มักจะ มีตัวอย่างมากมาย มันไม่คิดว่าความรู้ใด ๆ ก่อนหน้าของหัวข้อ และเป้าหมายแรกคือการ ถ่ายทอดความคิดเกี่ยวกับเรื่อง อ้างอิงคู่มือ การ์ตูน ครอบคลุม และครบถ้วนสมบูรณ์คำอธิบาย ซึ่งช่วยให้การค้นหาคำตอบของคำถามผ่านดัชนีและการอ้างอิงโยงIn the world of programming, all these types of documents are usually in electronic format. The reference documentation must be up to date, accurate and corresponding to the problems and tools used: looking up in a wrong reference manual could end up in trying to use a feature that is not supported by the current version of the tool, for example.2.3 The abused cout debugging technique The cout technique takes its names from the C++ statement for printing on the standard output stream (usually the terminal screen). It consists of adding print statements in the code to track the control flow and data values during code execution. Although it is the favourite technique of all the novices, it is unbelievable how many experienced programmers still refuse to evolve and abandon this absolutely time-wasting and very ad-hoc method. Despite its popularity, this technique has strong disadvantages. First of all, it is very ad-hoc, because code insertion is temporary, to be removed as soon as the bug is fixed. A new bug means a new insertion, making it a waste of time. In debugging as well as in coding, the professional should aim to find reusable solutions whenever possible. Printing statements are not reusable, and so are deprecated. As we will see shortly, there are more effective ways to track the control flow through messages. In addition, printing statements clobber the normal output of the program, making it extremely confused. They also slow the program down considerably: accessing to the outputting peripherals becomes a bottleneck. Finally, often they do not help at all, because for performance reasons, output is usually buffered and, in case of crash, the buffer is destroyed and the important information is lost, possibly resulting in starting the debugging process in the wrong place. In some (very few) circumstances cout debugging can be appropriate, although it can always be replaced by other techniques. For these cases, here are some tips. To begin with, output must be produced on the standard error, because this channel is unbuffered and it is less likely to miss the last information before a crash. Then, printing statements should not be used directly: a macro should be defined around them (as illustrated in listing 2) so to switch debugging code on and off easily. Finally, debugging levels should be used to manage the amount of debugging information.รายการ 2: ตัวอย่างของเทคนิค cout - รายงาน#ifndef DEBUG_H#define DEBUG_H#inc lude < stdarg.h >กำหนด #if (NDEBUG) & & กำหนด (__GNUC__)/ * cpp ของ gcc มีส่วนขยาย จะช่วยให้แมโครที่มีหมายเลขตัวแปรของอาร์กิวเมนต์ เราใช้นามสกุลนี้นี่การประมวลผลเบื้องต้น pmesg ไป */#define pmesg (ระดับ forma t อาร์กิวเมนต์...) ((ยกเลิก) 0)#elseยกเลิก pmesg (int ระดับ อักขระ * forma t,...);/ * พิมพ์ข้อความ ถ้าเป็นคอน sidered สำคัญพอ * /#endif#endif / * DEBUG_H * /Listing3: ตัวอย่างของเทคนิคใช้ cout#include "debug.h"#includeextern int msglevel / * t สูง
การแปล กรุณารอสักครู่..
