-1

During a discussion I had with a couple of colleagues the other day I threw together a piece of code in C++ to illustrate a memory access violation.

I am currently in the process of slowly returning to C++ after a long spell of almost exclusively using languages with garbage collection and, I guess, my loss of touch shows, since I've been quite puzzled by the behaviour my short program exhibited.

The code in question is as such:

#include <iostream>

using std::cout;
using std::endl;

struct A
{
    int value;
};

void f()
{
    A* pa;    // Uninitialized pointer
    cout<< pa << endl;
    pa->value = 42;    // Writing via an uninitialized pointer
}

int main(int argc, char** argv)
{   
    f();

    cout<< "Returned to main()" << endl;
    return 0;
}

I compiled it with GCC 4.9.2 on Ubuntu 15.04 with -O2 compiler flag set. My expectations when running it were that it would crash when the line, denoted by my comment as "writing via an uninitialized pointer", got executed.

Contrary to my expectations, however, the program ran successfully to the end, producing the following output:

0
Returned to main()

I recompiled the code with a -O0 flag (to disable all optimizations) and ran the program again. This time, the behaviour was as I expected:

0
Segmentation fault

(Well, almost: I didn't expect a pointer to be initialized to 0.) Based on this observation, I presume that when compiling with -O2 set, the fatal instruction got optimized away. This makes sense, since no further code accesses the pa->value after it's set by the offending line, so, presumably, the compiler determined that its removal would not modify the observable behaviour of the program.

I reproduced this several times and every time the program would crash when compiled without optimization and miraculously work, when compiled with -O2.

My hypothesis was further confirmed when I added a line, which outputs the pa->value, to the end of f()'s body:

cout<< pa->value << endl;

Just as expected, with this line in place, the program consistently crashes, regardless of the optimization level, with which it was compiled.

This all makes sense, if my assumptions so far are correct. However, where my understanding breaks somewhat is in case where I move the code from the body of f() directly to main(), like so:

int main(int argc, char** argv)
{   
    A* pa;
    cout<< pa << endl;
    pa->value = 42;
    cout<< pa->value << endl;

    return 0;
}

With optimizations disabled, this program crashes, just as expected. With -O2, however, the program successfully runs to the end and produces the following output:

0
42

And this makes no sense to me.

This answer mentions "dereferencing a pointer that has not yet been definitely initialized", which is exactly what I'm doing, as one of the sources of undefined behaviour in C++.

So, is this difference in the way optimization affects the code in main(), compared to the code in f(), entirely explained by the fact that my program contains UB, and thus compiler is technically free to "go nuts", or is there some fundamental difference, which I don't know of, between the way code in main() is optimized, compared to code in other routines?

Community
  • 1
  • 1
TerraPass
  • 1,562
  • 11
  • 21
  • 1
    The two alternatives in your final paragraph are not mutually exclusive. – Benjamin Lindley Apr 13 '16 at 23:46
  • 1
    If UB is involved, then you cannot expect C++ standard to describe the resulting program and its result. ("...imposes no requirements") – milleniumbug Apr 13 '16 at 23:46
  • 1
    @BenjaminLindley Well, I said "or", not "xor". :) – TerraPass Apr 13 '16 at 23:46
  • I'm suprised that you got the crash with `-O2` -- I would expect the function to be inlined and then the uses of `value` to be copy-propagated away (as happens when you write it all in main). The fact that it was not indicates you hit an odd corner case where a possible optimization is not done for some reason. – Chris Dodd Apr 14 '16 at 00:20
  • -O2 is a set of optimization options. If you want to investigate this further try manually activating/deactivating the set piece by piece. – Andreas May 01 '16 at 10:34

2 Answers2

2

Your program has undefined behaviour. This means that anything may happen. The program is not covered at all by the C++ Standard. You should not go in with any expectations.

It's often said that undefined behaviour may "launch missiles" or "cause demons to fly out of your nose", to reinforce that point. The latter is more far-fetched but the former is feasible, imagine your code is on a nuclear launch site and the wild pointer happens to write a piece of memory that starts global thermouclear war..

M.M
  • 138,810
  • 21
  • 208
  • 365
  • Ah, is this where the phrase "nasal demons", routinely used by commenters on SO actually comes from? I used to think people employed it sarcastically, meaning "you haven't given enough code for us to answer your question". But now I see that it may be actually referring to UB. – TerraPass Apr 14 '16 at 12:46
  • It's a decades-old term, and it means the effects of UB – M.M Apr 14 '16 at 12:52
  • Could you please take a look at [my comment](http://stackoverflow.com/questions/36611401/is-this-compiler-optimization-inconsistency-entirely-explained-by-undefined-beha#comment60841836_36614702) under @supercat's answer? I understand that the standard places no requirements on programs, which invoke UB. However, I'm curious whether *in practice* there is a link between optimization and the nature of UB, exhibited by such programs. – TerraPass Apr 14 '16 at 13:05
1

Writing unknown pointers has always been something which could have unknown consequences. What's nastier is a currently-fashionable philosophy which suggests that compilers should assume that programs will never receive inputs that cause UB, and should thus optimize out any code which would test for such inputs if such tests would not prevent UB from occurring.

Thus, for example, given:

uint32_t hey(uint16_t x, uint16_t y)
{
  if (x < 60000)
    launch_missiles();
  else
    return x*y;
}
void wow(uint16_t x)
{
  return hey(x,40000);
}

a 32-bit compiler could legitimately replace wow with an unconditional call to launch_missiles without regard for the value of x, since x "can't possibly" be greater than 53687 (any value beyond that would cause the calculation of x*y to overflow. Even though the authors of C89 noted that the majority of compilers of that era would calculate the correct result in a situation like the above, since the Standard doesn't impose any requirements on compilers, hyper-modern philosophy regards it as "more efficient" for compilers to assume programs will never receive inputs that would necessitate reliance upon such things.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • That's interesting. So, does it mean that __in practice__, the most egregious instances of undefined behaviour actually occur due to aggressive compiler optimizations? (By "egregious" I mean not simply a crash, but the program executing and doing things the programmer never intended.) – TerraPass Apr 14 '16 at 12:24
  • (I understand that *theoretically*, even if optimization is disabled entirely, no assumptions can be made as to the behaviour of the program, which contains UB. But the code I posted actually behaved in a predictable way (caused segmentation fault) when optimizations were disabled, that's why I'm curious.) – TerraPass Apr 14 '16 at 12:36
  • @TerraPass: A straightforward interpretation of your program's meaning would be "write a value somewhere in memory--I don't care where". As I said in my first sentence, because there's almost no limit to what actions could be triggered by writing to areas of memory, writing to an unknown area of memory is a universally-recognized recipe for disaster. On many machines, writing to certain areas of memory will trigger a segmentation fault; since you didn't say where you wanted the value written, you should not be surprised if the compiler arbitrarily chose an area that triggered such a fault. – supercat Apr 14 '16 at 15:24
  • Yes. Like I said, I'm *not* surprised by my code causing a segmentation fault: that's exactly what I intended when writing it. On the contrary, what surprises me is that, *when optimization is enabled*, no segmentation fault occurs. As far as I understand, this is due to the nature of UB, since *"anything may happen"*, as M.M puts it. – TerraPass Apr 14 '16 at 15:49
  • @TerraPass: I don't know that I'd consider that any more surprising than having something like `int main(void) { int x=0; int y=1234/x; return y;}` not trigger a divide-by-zero fault (or SIGFPE or whatever). The compiler knows when the `int y=1234/x;` is reached that `x` cannot hold a value that would make the computation meaningful, so there's no reason for it to generate code that would attempt the operation. It was common for compilers to add code to force a divide exception, to make behavior when using constants match the behavior when using non-constants, but it wasn't universal. – supercat Apr 14 '16 at 16:10
  • Yes, I suppose this makes sense. One last question: what if we expressly forbid the compiler to optimize (by setting `-O0`, for example). Would it be reasonable for us in this case to assume that a divide-by-zero fault will always be triggered? – TerraPass Apr 14 '16 at 16:17
  • @TerraPass: I would regard an assumption that reading a variable will yield the last value written [and that someone won't e.g. pause the code in a debugger and edit the value of the variable] as an "optimization" that it should be possible to disable, so the behavior of the code with an `x` that's always zero should be the same as with an `x` that happens to be zero [though I wouldn't fault a compiler writer that thought testing the divisor and skipping the division in the zero case was more useful than letting divide-by-zero trap]. For `int y=1234/0;`, omission of the trap would not be... – supercat Apr 14 '16 at 16:51
  • ...an "optimization" as such, since `int y=const1/const2;` would naturally generate code equivalent to `int y=someConstant;` for all cases where the divide wouldn't trap, and a compiler would have to go out of its way to cause a trap in those cases where the constants would cause it to do so. Likewise, even if `x/y` would trap when `x` happens to be INT_MIN and `y` happens to be -1, I would not be surprised if `x/-1` fails to trap when `x` is INT_MIN, since I wouldn't consider replacement with `x=-x;` to be an "optimization" as such. – supercat Apr 14 '16 at 16:53