LLVM 5.0.0 was already released back in September, but I still would like to mention a couple interesting things I encountered while using clang 5. This will not cover all the new things there are, please check the release notes of the respective LLVM components for that.
More aggressive optimizations
I could not find a mention of this in the release notes, but clang will now eliminate checks for null pointers in more cases. In the example below, the program outputs i is not nullptr
, although that is clearly not the case.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <iostream> int f_unused_parameter(int& i) { return 42; } void f(int* i) { f_unused_parameter(*i); if (i) std::cout << "i is not nullptr" << std::endl; else std::cout << "i is nullptr" << std::endl; } int main(int argc, char** argv) { f(nullptr); return 0; } |
1 2 3 |
$ clang++ -std=c++11 opt-null.cpp -o opt-null -O1 $ ./opt-null i is not nullptr |
Here is what happens when clang generates the code for f()
: int* i
is dereferenced in line 10. By definition, a nullptr
may not be dereferenced (undefined behavior!), so clang infers that i != nullptr
. Consequently, the check in the next line can be removed, and only the code for the “true” branch needs to be generated. Irrespective of the actual argument passed, the same message will be printed.
You may wonder why the dereferencing of i
for the function call does not lead to a crash at runtime. The reason is that this does not generate any code that could crash; only accessing int& i
in f_unused_parameter()
would do that. On the other hand, UndefinedBehaviorSanitizer does complain:
1 2 3 4 |
$ clang++ -std=c++11 opt-null.cpp -fsanitize=undefined -o opt-null-ubsan -O1 $ ./opt-null-ubsan opt-null.cpp:11:20: runtime error: reference binding to null pointer of type 'int' i is not nullptr |
The optimization that removes the check happens at -O1
and above. It did not happen with clang 4 even at -O3
.
AddressSanitizer: stack-use-after-scope
Variables located on the stack have a defined lifetime, or scope. When declared in a function body, the scope ends at the end of the function. The same is true in a scope manually defined by braces. Using a variable past the end of the lifetime, for example by handing out a pointer to it, is undefined behavior.
AddressSanitizer now checks for this coding error by default. The feature has been there for quite some time, but now it seems ready for prime time. Memory usage seems to be lower than when I last tested it about half a year ago. The check finds a few interesting things, here are two examples:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <iostream> struct Data { Data(int i): value(i) { } ~Data() { } int value; }; class TraceGuard { public: TraceGuard(const Data& data): m_data(data) { } ~TraceGuard() { std::cout << m_data.value << std::endl; } private: const Data& m_data; }; int main() { TraceGuard tg(Data(1)); return 0; } |
Can you spot the error? AddressSanitizer surely can:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ clang++ -fsanitize=address temporary-object.cpp -o temporary-object $ ./temporary-object ================================================================= ==14559==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7ffc86a336e0 at pc 0x0000004e9e3a bp 0x7ffc86a33650 sp 0x7ffc86a33648 READ of size 4 at 0x7ffc86a336e0 thread T0 #0 0x4e9e39 in TraceGuard::~TraceGuard() (temporary-object+0x4e9e39) #1 0x4e9b87 in main (temporary-object+0x4e9b87) #2 0x2acd40d93b04 in __libc_start_main (/lib64/libc.so.6+0x21b04) #3 0x41ca17 in _start start.S:122 Address 0x7ffc86a336e0 is located in stack of thread T0 at offset 64 in frame #0 0x4e9a6f in main (temporary-object+0x4e9a6f) This frame has 2 object(s): [32, 40) 'tg' [64, 68) 'ref.tmp' <== Memory access at offset 64 is inside this variable ... |
Apparently a stack variable is accessed after its scope has already ended. The variable is called ref.tmp
. We do not have a variable by that name in the program, so it must have been generated by the compiler. AddressSanitizer can narrow this down further when we are building with -g1
or above. This will generate debug info, and the second part of the report will then look like this:
1 2 3 4 5 6 |
Address 0x7ffebc4bc660 is located in stack of thread T0 at offset 64 in frame #0 0x4e9a6f in main temporary-object.cpp:29 This frame has 2 object(s): [32, 40) 'tg' (line 30) [64, 68) 'ref.tmp' (line 30) <== Memory access at offset 64 is inside this variable |
Ok, so we know we only have to check line 30:
30 |
TraceGuard tg(Data(1)); |
There are only two objects here, the TraceGuard
object with a scope until the end of main()
, and the temporary Data
object which goes out of scope at the end of the statement, but is still referenced by tg
! So we have found the problem, and here is a possible fix:
27 28 29 30 31 32 33 34 |
... int main() { Data data(1); TraceGuard tg(data); return 0; } |
data
is now guaranteed to outlive tg
.
Here is a less obvious version of the same problem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <iostream> class TraceGuard { public: TraceGuard(const int& data): m_data(data) { } ~TraceGuard() { std::cout << m_data << std::endl; } private: const int& m_data; }; int main() { int value = 42; TraceGuard tg(static_cast<const int>(value)); return 0; } |
This will trigger the same report as above. Even the static_cast
produces a temporary here, and AddressSanitizer keenly tracks its lifetime and reports our error.
These coding errors are relatively benign, and may not even lead to problems. But they are undefined behavior, and the compiler is free to re-use the stack space where the temporary resides, which would lead to subtle and hard-to-find bugs. There is a gcc option to control reuse of stack variables:
-fstack-reuse=reuse-level
This option controls stack space reuse for user declared local/auto variables and compiler generated temporaries. reuse_level can be ‘all’, ‘named_vars’, or ‘none’.
When you suspect such problems in a large codebase that you cannot immediately fix, this may be a helpful short-term workaround.