-3

I wrote a C program as follows:

void foo(int *a) {
  if (a[1000] == a[1000]) {
    printf("Hello");
  } 
}

int main() {
  int *a;
  foo(a);
  return 0;
}

I was expecting this program to crash because I did not allocate the memory at &a[1000], but the program actually did not crash and printed "Hello". I compiled the program with command

gcc -O0 foo.c

What might be the reason for this?

alk
  • 69,737
  • 10
  • 105
  • 255
user3724417
  • 373
  • 1
  • 3
  • 6
  • 18
    Undefined behaviour is undefined. – Jonathan Potter Oct 30 '15 at 09:11
  • 2
    Just because it *seems* to work doesn't mean it actually works as expected. – Some programmer dude Oct 30 '15 at 09:15
  • 3
    Does it even do the memory accesses? 'x==x' is true for all x, so why bother? – Martin James Oct 30 '15 at 09:40
  • 4
    *I crossed the street without looking both ways, and was not run over by a bus. Why not? Is it always safe to cross a street?* – Bo Persson Oct 30 '15 at 09:40
  • 1
    @MartinJames Precisely. Memory access never happens in this code, GCC optimizes this condition out even in `-O0`. I posted an answer showing that. – Enzo Ferber Oct 30 '15 at 12:27
  • Why do you think that your program should crash ? At this point that comparison is the same as **if (1 == 1)** which evaluate as TRUE, and there nothing else. a[1000] contains the same garbage as the second a[1000] so the comparison becomes TRUE. That's all and of story. – Michi Oct 30 '15 at 18:10
  • @Michi Since I compiled the program with flag -O0, the memory access instructions presumably should not be eliminated. So if I run the program many times there should be a time where it crashes because of invalid memory access. However, this did not happen, and the reason is explained in Ferber's answer. – user3724417 Oct 30 '15 at 18:19
  • @user3724417 It will never crash :)) because the comparison evaluate TRUE, even after you run your program 1 million times. Check my comment from Ferver's Answer – Michi Oct 30 '15 at 18:21

7 Answers7

8

Accessing memory places that has not been allocated is undefined behaviour.

Now, this can lead to either seg fault, if the memory you are accessing is restricted for your program.

Or, as in your case, it wont have any defined effects. It will probably be reading garbage values left by the previous programs. This kind of behavior is called undefined.

It may be working in your case for a particular time, but it will definitely won't work all the time.

Haris
  • 12,120
  • 6
  • 43
  • 70
6

TL;DR

As everyone already noted, accessing out of bounds memory is Undefined Behavior. However, something very interesting is happening in this particular case, making your program not access memory at all. Dead code got removed!

It's not guaranteed, but most compilers of good quality will optimize if(1) { ... } or if(0){ ... } (which is precisely the case of gcc) even in -O0. Check this answer and this answer.


Logic Reasoning

Your compiler is "optimizing" that if condition based on simple logic, that's why it's always working even with the -O0 flag. This memory access will never happen. When your compiler finds a[1000] == a[1000], or really a[n] == a[n] it knows that it's essentially the same thing as saying VAR == VAR which is the same for any variable and is always true for any variable. This comes from Formal Logic and is called Principle of Identity, which states that any element A is equal to itself. I don't know if there's an specific optimization flag for that, but I don't think there is (specially because it happens in -O0). If anyone knows about one, please let me know in the comments.

In other words, your compiler swaps your if(a[1000] == a[1000]) for if(1), which is always true, so it removes the if altogether.

It is very important to note that accessing out of bounds memory is always undefined behavior, HOWEVER, in this case, the translated code never access any memory. To prove it, some disassembled code:

The code you provided, compiled with gcc -O0 -o foo foo.c outputs the following foo function:

(gdb) disass foo
Dump of assembler code for function foo:
   0x000000000040052d <+0>: push   %rbp
   0x000000000040052e <+1>: mov    %rsp,%rbp
   0x0000000000400531 <+4>: sub    $0x10,%rsp
   0x0000000000400535 <+8>: mov    %rdi,-0x8(%rbp)
   0x0000000000400539 <+12>:mov    $0x4005f4,%edi
   0x000000000040053e <+17>:mov    $0x0,%eax
   0x0000000000400543 <+22>:callq  0x400410 <printf@plt>
   0x0000000000400548 <+27>:leaveq 
   0x0000000000400549 <+28>:retq   
End of assembler dump.

Notice the instruction mov %rdi,-0x8(%rbp). This is saving the function argument into the stack. That's your pointer. Right after it, it stores $0x4005f4 into edi (which probably is the address of your "Hello" string in the data segment) and sets eax to zero, then calls printf. Lets check:

(gdb) print (char*)0x4005f4
$3 = 0x400614 "Hello"

Bullseye! Well, wait! Where's that if? I don't see any cmp instructions here, or any other kinds of branches.... That if got "optimized" away. It's not really an optimization option from GCC, rather is a logic optimization. 1 is always equal to 1. The compiler knows that before outputting machine code, so your if never got to the binary and no memory access got done.

However, if you were to do if(a[1000] == a[1001]) and compile with the same gcc -O0 -o foo foo.c you'll get this foo:

(gdb) disass foo
Dump of assembler code for function foo:
   0x000000000040052d <+0>: push   %rbp
   0x000000000040052e <+1>: mov    %rsp,%rbp
   0x0000000000400531 <+4>: sub    $0x10,%rsp
   0x0000000000400535 <+8>: mov    %rdi,-0x8(%rbp)
   0x0000000000400539 <+12>:mov    -0x8(%rbp),%rax
   0x000000000040053d <+16>:add    $0xfa0,%rax
   0x0000000000400543 <+22>:mov    (%rax),%edx
   0x0000000000400545 <+24>:mov    -0x8(%rbp),%rax
   0x0000000000400549 <+28>:add    $0xfa4,%rax
   0x000000000040054f <+34>:mov    (%rax),%eax
   0x0000000000400551 <+36>:cmp    %eax,%edx
   0x0000000000400553 <+38>:jne    0x400564 <foo+55>
   0x0000000000400555 <+40>:mov    $0x400614,%edi
   0x000000000040055a <+45>:mov    $0x0,%eax
   0x000000000040055f <+50>:callq  0x400410 <printf@plt>
   0x0000000000400564 <+55>:leaveq 
   0x0000000000400565 <+56>:retq   
End of assembler dump.

Wow, that's longer!

Now, the usual mov %rdi,-0x8(%rbp) is there. This is saving our parameter into the stack. The next line, mov -0x8(%rbp),%rax loads our pointer into rax. Then, add $0xfa0,%rax add our 1000 * sizeof(int) offset into rax. Until now, all fine. And now, mov (%rax),%edx tries to access the contents of what's being pointed by rax and store it in edx. In other words, this is the actual pointer dereference. If you were steping instructions on GDB, you would get the SIGSEGV on this instruction:

Breakpoint 1, 0x0000000000400531 in foo ()
(gdb) stepi
0x0000000000400535 in foo ()
(gdb) stepi
0x0000000000400539 in foo ()
(gdb) stepi
0x000000000040053d in foo ()
(gdb) stepi
0x0000000000400543 in foo ()
(gdb) stepi

Program received signal SIGSEGV, Segmentation fault.
0x0000000000400543 in foo ()

Note that after it tries to execute the instruction at 400543, it crashes. And what's in 400543? 0x0000000000400543 <+22>:mov (%rax),%edx. Precisely where it tries to access an out of bound memory. BOOM! There's your undefined behavior.

Community
  • 1
  • 1
Enzo Ferber
  • 3,029
  • 1
  • 14
  • 24
  • 2
    Awesome answer :). One note though - `a[1000] == a[1000]` may actually not be true, since the loads are not atomic. Some other thread may change it. The compiler however thinks something like - "an equality every time I run this is a possible and valid outcome, and therefore i'm allowed to make it always happen", and goes ahead eliminating the code. – Leeor Oct 30 '15 at 11:27
  • 1
    Thanks for the awesome answer! Apparently there is something more than just undefined behavior for this question, and this is the only answer that identified and explained it in detail. – user3724417 Oct 30 '15 at 17:47
  • @Leeor The compiler think that it's not `volatile`, and therefore allowed to cache it. – user202729 Jun 24 '18 at 04:44
3

One of the side effects of undefined behavior is expected output.

But this doesn't prove that UB is defined

Gopi
  • 19,784
  • 4
  • 24
  • 36
2

One explanation why it doesn't crash is that the compiler might have optimized away a[1000] == a[1000] as this expression it is always true.

Try with a[1000] != a[1001] maybe then you'll get a crash each time.

But anyway it is undefined behaviour.

Jabberwocky
  • 48,281
  • 17
  • 65
  • 115
1

Your program may as well crash (segmentation fault) or not crash.

The fact that it does not crash does not mean that it works. Actually this is undefined behaviour meaning that anything can happen. It can either read some random values or it can crash because of a segmentation fault. So the fact that it works now when you test it does not mean that it will always work.

You could for instance try to run your program a few times and you might encounter a segfault.

This is due to the fact that a lot of things are not specified by the language standards.

LBes
  • 3,366
  • 1
  • 32
  • 66
0

The behaviour is undefined here, here you are trying to access a memory, you don't know about. This random memory location may be holding some critical data or may be it just a fine location that can be used.
In case1: Your program will crash with "segmentation fault".
In case2: Your program works fine printing "Hello World".
Since an ambigous programme is not a very good one, we refrain from these kinds of practices.
Now that we have better OS, you only get segmentation fault, otherwise in days before this program could crash your system.

ozil
  • 6,930
  • 9
  • 33
  • 56
Muku
  • 538
  • 4
  • 18
  • **in days before this program could crash your system.** Really? Just curious to know If really this small mistake can crash the system. – SKD Oct 30 '15 at 10:29
0

Here the int *a is a stack variable, and since stack variable is not pre initialised it contains some garbage value.

Just by luck this garbage value is within the allowed address or that program and so it is program is not panicking.

IKavanagh
  • 6,089
  • 11
  • 42
  • 47