Valid-value (V) bits
It is simplest to think of Memcheck implementing a synthetic CPU which is identical to a real CPU, except for one crucial detail. Every bit (literally) of data processed, stored and handled by the real CPU has, in the synthetic CPU, an associated "valid-value" bit, which says whether or not the accompanying bit has a legitimate value. In the discussions which follow, this bit is referred to as the V (valid-value) bit.
Each byte in the system therefore has a 8 V bits which follow it wherever it goes. For example, when the CPU loads a word-size item (4 bytes) from memory, it also loads the corresponding 32 V bits from a bitmap which stores the V bits for the process' entire address space. If the CPU should later write the whole or some part of that value to memory at a different address, the relevant V bits will be stored back in the V-bit bitmap.
In short, each bit in the system has (conceptually) an associated V bit, which follows it around everywhere, even inside the CPU. Yes, all the CPU's registers (integer, floating point, vector and condition registers) have their own V bit vectors. For this to work, Memcheck uses a great deal of compression to represent the V bits compactly.
Copying values around does not cause Memcheck to check for, or report on, errors. However, when a value is used in a way which might conceivably affect your program's externally-visible behaviour, the associated V bits are immediately checked. If any of these indicate that the value is undefined (even partially), an error is reported.
Here's an (admittedly nonsensical) example:
Memcheck emits no complaints about this, since it merely copies uninitialised values from a[]
into b[]
, and doesn't use them in a way which could affect the behaviour of the program. However, if the loop is changed to:
then Memcheck will complain, at the if
, that the condition depends on uninitialised values. Note that it doesn't complain at the j += a[i];
, since at that point the undefinedness is not "observable". It's only when a decision has to be made as to whether or not to do the printf
-- an observable action of your program -- that Memcheck complains.
Most low level operations, such as adds, cause Memcheck to use the V bits for the operands to calculate the V bits for the result. Even if the result is partially or wholly undefined, it does not complain.
Checks on definedness only occur in three places: when a value is used to generate a memory address, when control flow decision needs to be made, and when a system call is detected, Memcheck checks definedness of parameters as required.
If a check should detect undefinedness, an error message is issued. The resulting value is subsequently regarded as well-defined. To do otherwise would give long chains of error messages. In other words, once Memcheck reports an undefined value error, it tries to avoid reporting further errors derived from that same undefined value.
This sounds overcomplicated. Why not just check all reads from memory, and complain if an undefined value is loaded into a CPU register? Well, that doesn't work well, because perfectly legitimate C programs routinely copy uninitialised values around in memory, and we don't want endless complaints about that. Here's the canonical example. Consider a struct like this:
The question to ask is: how large is struct S
, in bytes? An int
is 4 bytes and a char
one byte, so perhaps a struct S
occupies 5 bytes? Wrong. All non-toy compilers we know of will round the size of struct S
up to a whole number of words, in this case 8 bytes. Not doing this forces compilers to generate truly appalling code for accessing arrays of struct S
's on some architectures.
So s1
occupies 8 bytes, yet only 5 of them will be initialised. For the assignment s2 = s1
, GCC generates code to copy all 8 bytes wholesale into s2
without regard for their meaning. If Memcheck simply checked values as they came out of memory, it would yelp every time a structure assignment like this happened. So the more complicated behaviour described above is necessary. This allows GCC to copy s1
into s2
any way it likes, and a warning will only be emitted if the uninitialised values are later used.
As explained above, Memcheck maintains 8 V bits for each byte in your process, including for bytes that are in shared memory. However, the same piece of shared memory can be mapped multiple times, by several processes or even by the same process (for example, if the process wants a read-only and a read-write mapping of the same page). For such multiple mappings, Memcheck tracks the V bits for each mapping independently. This can lead to false positive errors, as the shared memory can be initialised via a first mapping, and accessed via another mapping. The access via this other mapping will have its own V bits, which have not been changed when the memory was initialised via the first mapping. The bypass for these false positives is to use Memcheck's client requests VALGRIND_MAKE_MEM_DEFINED
and VALGRIND_MAKE_MEM_UNDEFINED
to inform Memcheck about what your program does (or what another process does) to these shared memory mappings.
Valid-address (A) bits
Notice that the previous subsection describes how the validity of values is established and maintained without having to say whether the program does or does not have the right to access any particular memory location. We now consider the latter question.
As described above, every bit in memory or in the CPU has an associated valid-value (V) bit. In addition, all bytes in memory, but not in the CPU, have an associated valid-address (A) bit. This indicates whether or not the program can legitimately read or write that location. It does not give any indication of the validity of the data at that location -- that's the job of the V bits -- only whether or not the location may be accessed.
Every time your program reads or writes memory, Memcheck checks the A bits associated with the address. If any of them indicate an invalid address, an error is emitted. Note that the reads and writes themselves do not change the A bits, only consult them.
So how do the A bits get set/cleared? Like this:
- When the program starts, all the global data areas are marked as accessible.
- When the program does
malloc
/new
, the A bits for exactly the area allocated, and not a byte more, are marked as accessible. Upon freeing the area the A bits are changed to indicate inaccessibility. - When the stack pointer register (
SP
) moves up or down, A bits are set. The rule is that the area fromSP
up to the base of the stack is marked as accessible, and belowSP
is inaccessible. (If that sounds illogical, bear in mind that the stack grows down, not up, on almost all Unix systems, including GNU/Linux.) TrackingSP
like this has the useful side-effect that the section of stack used by a function for local variables etc is automatically marked accessible on function entry and inaccessible on exit. - When doing system calls, A bits are changed appropriately. For example,
mmap
magically makes files appear in the process' address space, so the A bits must be updated ifmmap
succeeds. - Optionally, your program can tell Memcheck about such changes explicitly, using the client request mechanism described above.
Putting it all together
Memcheck's checking machinery can be summarised as follows:
- Each byte in memory has 8 associated V (valid-value) bits, saying whether or not the byte has a defined value, and a single A (valid-address) bit, saying whether or not the program currently has the right to read/write that address. As mentioned above, heavy use of compression means the overhead is typically around 25%.
- When memory is read or written, the relevant A bits are consulted. If they indicate an invalid address, Memcheck emits an Invalid read or Invalid write error.
- When memory is read into the CPU's registers, the relevant V bits are fetched from memory and stored in the simulated CPU. They are not consulted.
- When a register is written out to memory, the V bits for that register are written back to memory too.
-
When values in CPU registers are used to generate a memory address, or to determine the outcome of a conditional branch, the V bits for those values are checked, and an error emitted if any of them are undefined.
-
When values in CPU registers are used for any other purpose, Memcheck computes the V bits for the result, but does not check them.
-
Once the V bits for a value in the CPU have been checked, they are then set to indicate validity. This avoids long chains of errors.
-
When values are loaded from memory, Memcheck checks the A bits for that location and issues an illegal-address warning if needed. In that case, the V bits loaded are forced to indicate Valid, despite the location being invalid.
This apparently strange choice reduces the amount of confusing information presented to the user. It avoids the unpleasant phenomenon in which memory is read from a place which is both unaddressable and contains invalid values, and, as a result, you get not only an invalid-address (read/write) error, but also a potentially large set of uninitialised-value errors, one for every time the value is used.
There is a hazy boundary case to do with multi-byte loads from addresses which are partially valid and partially invalid. See details of the option --partial-loads-ok
for details.
Memcheck intercepts calls to malloc
, calloc
, realloc
, valloc
, memalign
, free
, new
, new[]
, delete
and delete[]
. The behaviour you get is:
malloc
/new
/new[]
: the returned memory is marked as addressable but not having valid values. This means you have to write to it before you can read it.calloc
: returned memory is marked both addressable and valid, sincecalloc
clears the area to zero.realloc
: if the new size is larger than the old, the new section is addressable but invalid, as withmalloc
. If the new size is smaller, the dropped-off section is marked as unaddressable. You may only pass torealloc
a pointer previously issued to you bymalloc
/calloc
/realloc
.free
/delete
/delete[]
: you may only pass to these functions a pointer previously issued to you by the corresponding allocation function. Otherwise, Memcheck complains. If the pointer is indeed valid, Memcheck marks the entire area it points at as unaddressable, and places the block in the freed-blocks-queue. The aim is to defer as long as possible reallocation of this block. Until that happens, all attempts to access it will elicit an invalid-address error, as you would hope.