Most components of PC-Lint Plus analyze a static compile-time view of your code. This is sufficient for most static analysis tasks, but detection of certain runtime problems requires a deeper approach. Value Tracking combines static and dynamic analysis with an in-depth knowledge of C and C++ programming patterns to find potential runtime errors by performing an approximate symbolic interpretation of your code without the exponential slowdown incurred from a blind purely symbolic analysis. For example:
1int g(int x) { 2 x = x - 5; 3 return 100 / x; 4} 5 6void f() { 7 int a = 3; 8 a += 2; 9 a = g(a); 10}
Value Tracking will find the division by zero error and explain how it occurs:
3 warning 414: possible division by zero return 100 / x; ^ ~ 9 supplemental 894: during specific walk g(5) a = g(a); ^ 2 supplemental 831: assignment yields 0 x = x - 5; ~~^~~~~~~ 2 supplemental 831: operator - yields 0 x = x - 5; ~~^~~ 1 supplemental 831: argument initialization yields 5 int g(int x) { ~~~~^ 9 supplemental 831: argument passing yields 5 a = g(a); ^ 8 supplemental 831: operator + yields 5 a += 2; ~~^~~~
The division by zero warning shown in the previous example consists of three parts. The primary message, warning 414 , describes the problem that Value Tracking has detected. The next message in the message group is supplementary message 894 , which is used to indicate that the primary message was issued during a specific walk of a function using arguments passed to it in a function call within an earlier walk. Message 894 may occur multiple times in a message group and indicates the values of function arguments on the specific call stack. The repeated instances of message 831 are used to show how the history of a relevant variable led to the occurrence of the situation that was reported as suspicious in the primary message, in reverse chronological order. The first instance of message 831 shows how x came to have the value 0 and the following messages proceed backwards to the value’s origin.
Inside of a conditionally executed region, such as the body of an if statement, PC-lint Plus will infer that the condition required for the region to execute must be true when that region begins executing. In the case of an if statement, it will further infer that the opposite of such a condition must be true when a matching else statement begins executing. Inferred values and modifications made to variables within conditional regions generally do not survive once the conditional scope exits. If the condition can be determined to always be true or false under the circumstances, or one branch of an if statement always returns, then changes, inferences, or even a reversed inference may persist beyond the conditionally executed region. Inferencing can be disabled using the ii flag, although this is not recommended.
Values can be inferred from expressions passed as arguments to functions with the assert semantic. Inferences derived from assertions work in the same manner as inferences derived from conditionals. The standard assert macro is typically redefined as an invocation of __lint_assert with the option ++dassert(x)=__lint_assert(!!(x)). __lint_assert is a builtin function in PC-lint Plus that takes a single Boolean argument and has the assert semantic which will cause PC-lint Plus to assume the condition is true following the invocation. Other user-defined assertion macros can be redefined in the same way. Other functions may be endowed with assertion semantics by copying the semantics of __lint_assert using the -function option. See Chapter 9 Semantics for more information. For example:
1#define my_assert(x) __lint_assert(!!(x)) 2 3int x; 4 5int* g() { 6 if (x) return &x; 7 else return nullptr; 8} 9 10void f() { 11 int* a = g(); 12 my_assert(a); 13 *a = 10; // no warning about potential use of null pointer due to assert 14}
Rather than tracking only a single value, an integer can be represented by a range of possible values. For
example:
1void f(int a) { 2 if (a > 5 && a < 10) { 3 /* a is known to be between six and nine inclusive here */ 4 } 5} 6 7void g(unsigned a) { 8 if (a > 10) return; 9 /* a is known to be between zero and ten inclusive here */ 10}
You can test these examples using the integrated debugger as explained below in Section 8.7 Debugger .
General walk - The initial analysis of a function, independent of the rest of the program. The values of function arguments are completely unknown during a general walk. PC-Lint Plus generally will not report violations relating to these unknown arguments unless it has learned something to constrain their possible values. This helps to avoid false positives. Function calls encountered in the general walk launch specific walks.
Specific walk - A walk of a function launched by a function call during a general walk or an earlier specific walk (up to the depth limit). The arguments passed at the call site are known during this walk.
Depth - The number of nested specific walks at the current point in execution.
Pass - A repeatable event consisting of the processing or reprocessing of each source file. Each file is read from the disk and analyzed again in each pass. Information stored between passes allows some messages to provide more information as the number of passes increases. Default operation involves two passes, one in which analysis local to each module is performed while globally relevant information is collected, and another in which global information collected in the previous pass is acted on for each module (Global Wrap-up). Global Wrap-up only occurs once, in the second pass, if Global Wrap-up is enabled at all. Value Tracking is performed again in each pass.
The value of an integer will be printed in base ten. If there are a range of possible values, the minimum and maximum values (both inclusive) will be printed, separated by a colon. If the value of an integer is not known but there is reason to believe it may be zero, a zero followed by a question mark will be printed.
If a pointer is null, the string nullptr will be printed. If a pointer may possibly be null, a question mark will be printed after the pointer’s value. If a pointer is custodial, the value will be followed by a suffix of C or c, with the lowercase form indicating some degree of uncertainty. A pointer derived from the conversion of an integer literal specifying a fixed constant address will be indicated with a suffix of F.
A pointer to a unique object not considered to be part of an array or multi-element allocation will be printed as &(V) where V represents the value of the target object.
A pointer into a multi-element allocation will be printed as [E]@I/S. E represents the size of the allocation in bytes. I represents the index into the allocation in bytes. E and I may be displayed as ranges under the rules specified above for integers. S is the size of the element type. The suffix NUL@Z represents the belief that a null terminator is present at index Z, which may be a range.
Members and base class sub-objects are printed in a comma separated list delimited by curly braces and prefixed with . or : respectively. For example, given these structures:
1struct X { 2 int a; 3} 4 5struct Y : X { 6 int b; 7};
an object of type Y may be displayed as { :X = { .a = 42 }, .b = 11 }.
A file stream is the value of a file object represented by a FILE pointer such as is returned by fopen or freopen.
PC-lint Plus tracks various attributes of file streams including the file name, the mode in which the file was opened,
the orientation of the file, and its current state.
The representation of a FILE object looks like:
FILE {.filename = "test.txt", .mode = r, .state = opened, .orientation = none}
filename is the name of the file or unknown if the name isn’t known. The mode represents the mode in which the file
was opened, e.g. rw, r+, etc. The state is opened if no operations have been performed since the file was opened or
closed if the file is closed. Otherwise the state represents the last operation performed on the stream with the
possible values being:
| r | read (e.g. fread) |
| w | write (e.g. fwrite) |
| u | pushback (e.g. ungetc) |
| p | file positioning operation (e.g. fseek) |
| t | a tell operation (e.g. ftell) |
| f | flush (e.g. fflush) |
The orientation represents the file’s orientation and is one of byte, wide, unknown, or none. The value of none
means that the file is not oriented because an orientation-inducing operation has not been performed on the file
stream since it was opened.
Since file streams represent a FILE pointer, the actual file stream will be represented as a pointer to a FILE object:
&(FILE {.filename = unknown, .mode = r, .state = opened, .orientation = none})? C
and may include a ? to indicate the possibility of a null pointer or C to indicate the pointer is custodial.
Uninitialized values are represented using the string uninitialized. A question mark suffix may appear if a value is only possibly uninitialized.
Value Tracking is enabled by default. More information can be revealed by increasing the specific walk depth using the -vt_depth option. Larger values will increase runtime and (peak) memory usage. A variety of more specific Value Tracking features can be controlled using 9 Semantics .
A debugger in the spirit of gdb is provided to probe the state of the Value Tracking interpreter during execution. This functionality can be accessed using the +fvd. The flag can be enabled in a lint comment to avoid triggering the debugger earlier in the program. For example, given a.cpp:
1void f(int a) { 2 int b = 5; 3 a = b + 2; 4 int c = a; 5}
running with the debugger enabled will present the following modal interface:
@ a.cpp:2 ^ # void f(int a) { --> # int b = 5; # a = b + 2; # int c = a; (vt)
which indicates that the debugger has stopped prior to the execution of the line indicated by the arrow in the left margin and listed after the filename in the header. Pressing enter without inputting any text at the (vt) prompt will proceed to the next statement.
@ a.cpp:3 # void f(int a) { # int b = 5; --> # a = b + 2; # int c = a; # }
Entering ? will print the current values of all variables in scope:
### unknown storage ### ### static storage ### ### dynamic storage ### ### function f parameters ### a = unknown $1 ### compound statement ### b = 5 $3
Each variable belongs to the scope (denoted with triple hash headings) that it appears immediately under. The
variable b is local to the compound statement of the function body, and has the value 5. The $3 following the value
represents the simulated memory slot in which the value of b resides. If the program is stepped to the end of the
function, it will stop on the closing brace to provide a final opportunity to issue commands before leaving the scope.
More advanced features are available and listed in the output of the help command at the prompt. The debugger is
considered experimental and subject to change. Value Tracking debugging is not available when using the Parallel
Analysis feature.
Interfunction Value Tracking operates differently depending on whether a given function call is connecting two
functions within the same module or two functions within different modules. PC-lint Plus can track values across an
arbitrary number of intramodule function calls limited only by the value of the -vt_depth option and the amount of
time available. The order in which functions are defined within a single module does not influence Value Tracking.
While intramodule calls are processed depth-first, intermodule calls are processed breadth-first and the boundary
from a given module to any other module can only be crossed in the next pass. The initial processing of each source
module constitutes a single pass and an additional pass to utilize the information stored during this
pass before the first intermodule results are produced. Another pass will occur by default as part
of Global Wrap-up processing. Additional passes can be requested using the -vt_passes option.
Calls to library functions will not be walked unless the flf flag has been set to 2.
The initial values of non-const static duration variables are not currently considered during Value Tracking. Changes to static variables within a function or call chain are tracked. For example:
1int a; 2int b; 3 4int f() { 5 return 5 / a; 6} 7 8int g() { 9 b = 0; 10 return 5 / b; 11}
In the general walk, PC-lint Plus will report on the division by zero in g but will not report on the division by a in f because while a is initialized to 0, there is no information in the function to suggest that a, which could have been modified by another function in the program, has any particular value.
Correlations between independent variables are not currently tracked. In this example:
1void f(int* p) { 2 int* a = 0; 3 if (p) { a = p; } 4 bool is_null = !p; 5 if (!is_null) { 6 *a = 10; 7 } 8}
PC-lint Plus will report the potential for a null pointer dereference on line 6 while this is technically not possible because the value of is_null is correlated with the value of a. Nonetheless, reports under these circumstances can be useful because use of this pattern can lead to brittle code. This can be remedied by placing an assertion before line 6, such as assert(a);, which will prevent the warning.
PC-lint Plus will make an exception to the depth limit for a call to a:
constexpr function
literal operator
const member function returning bool
This helps avoid unexpected results in certain cases where the depth limit would otherwise need to be increased. This is not a substitute for specifying the optimal depth for your desired analysis using the -vt_depth option.
There is no longer a distinction between static and dynamic messages for suppression purposes because the new architecture of PC-lint Plus does not run non-dynamic checks multiple times.
Value Tracking is now performed depth-first, the same way your program executes on a real machine. Previous products performed a breadth-first search due to the multiple pass architecture used. This implies the removal of -static_depth, which is now effectively infinite.
Integer tracking can now track a range of possible values.
Floating point values, pointer targets, function pointers, and structure members can now be tracked.
The reference information format has changed. An example of the new, more detailed reference information is provided in the introduction.
The "conceivable" severity for conditions dependent on a loop never being entered has been retired.