Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why this program will never terminate with flag `-O3`?

The program below has different behaviors with different option levels. When I compile it with -O3, it will never terminate. when I compile it with -O0, it will always terminate very soon.

#include <stdio.h>
#include <pthread.h>

void *f(void *v) {
    int *i = (int *)v;
    *i = 0;
    printf("set to 0!\n");
    return NULL;
}

int main() {
    const int c = 1;
    int i = 0;
    pthread_t thread;
    void *ptr = (void *)&c;
    while (c) {
        i++;
        if (i == 1000) {
            pthread_create(&thread, NULL, &f, ptr);
        }
    }
    printf("done\n");
}

This is the result of running it with different optimization flags.

username@hostname:/src$  gcc -O0 main.c -o main
username@hostname:/src$  ./main 
done
set to 0!
set to 0!
username@hostname:/src$  gcc -O3 main.c -o main
username@hostname:/src$  ./main 
set to 0!
set to 0!
set to 0!
set to 0!
set to 0!
set to 0!
^C
username@hostname:/src$ 

The answer given by the professor's slide is like this:

  • Will it always terminate?

  • Depends of gcc options

  • With –O3 (all optimisations): no

Why?

  • The variable c is likely to stay local in a register, hence it will not be shared.

Solution « volatile »


Thank you for your replies. I now realize that volatile is a keyword in C. The description of the volatile keyword:

A volatile specifier is a hint to a compiler that an object may change its values in ways not specified by the language so that aggressive optimizations must be avoided.

According to my understanding, there is a shared register that stores the c value when we use -O3 flag. So the main thread and sub-thread will share it. In this case, if a sub-thread modifies c to 0, the main thread will get 0 when it wants to read c to compare in the while(c) statement. Then, the loop stops.

There is no register storing c that can be shared by the main thread and sub-threads when we use -O0 flag. Though the c is modified by a sub-thread, this change may not be written to memory and just be stored in a register, or it is written to memory while the main thread just uses the old value which is read and saved in a register. As a result, the loop is infinite.

If I declared the c value with const: const volatile int c = 1;, the program will terminate finally even if we compiled it with -O3. I guess all threads will read c from the main memory and write back to the main memory if they change the c value.


I know, according to the specifications or rules about C language, we are not allowed to modify a value that is declared by the const keyword. But I don't understand what is un behavior.

I wrote a test program:

#include "stdio.h"

int main() {
    const int c = 1;
    int *i = &c;
    *i = 2;
    printf("c is : %d\n", c);
}

output

username@hostname:/src$ gcc test.c -o test
test.c: In function ‘main’:
test.c:9:14: warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
    9 |     int *i = &c;
      |              ^
username@hostname:/src$ ./test
c is : 2
username@hostname:/src$ 

The result is 2 which means a variable declared with the const can be modified but this behavior is not suggested, right?


I also tried changing the judgment condition. If it is changed to while (1){ from while(c){, the loop will be an infinite one no matter using -O0 or -O3


This program is not a good one as it violates the specifications or rules of C language. Actually it comes from the lecture about software security.

Can I just understand like this? All threads share the same register storing c when we compile the program with -O0.

While the value c is in un-shared registers, so main thread is not informed when sub-threads modify value c when we use -O3. Or, while(c){ is replaced by while(1){ when we use -O3 so the loop is infinite.

I know this question can be solved easily if I check the generated assembly code. But I am not good at it.

like image 683
Ruibin Zhang Avatar asked Feb 22 '26 12:02

Ruibin Zhang


2 Answers

This is undefined behavior. Per 6.7.3 Type qualifiers, paragraph 6 of the (draft) C11 standard:

If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined.

There's no requirement for any particular behavior on the program. How it behaves is literally outside the specifications of the C language.

Your professor's observation of how it behaves may be correct. But he goes off the rails. There is no "why" for undefined behavior. What happens can change with changes to compiler options, particulars of the source code, time of day, or phase of the moon. Anything. Any expectation for any particular behavior is unfounded.

And

Solution « volatile »

is flat-out WRONG.

volatile does not provide sufficient guarantees for multithreaded access. See Why is volatile not considered useful in multithreaded C or C++ programming?.

volatile can appear to "work" because of particulars of the system, or just because any race conditions just don't happen to be triggered in an observable manner, but that doesn't make it correct. It doesn't "work" - you just didn't observe any failure. "I didn't see it break" does not mean "it works".

Note that some C implementations do define volatile much more extensively than the C standard requires. Microsoft in particular defines volatile much more expansively, making volatile much more effective and even useful and correct in multithreaded programs.

But that does not apply to all C implementations. And if you read that link, you'll find it doesn't even apply to Microsoft-compiled code running on ARM hardware...

like image 184
Andrew Henle Avatar answered Feb 24 '26 14:02

Andrew Henle


The professor's explanation is not quite right.

The initial value of c is 1, which is truthy. It's declared as a constant, so its value can't change. Thus, the condition in while (c) is guaranteed to always be true, so there's no need to test the variable at all when the program is running. Just generate code for an infinite loop.

This optimization of not reading the variable is not done when optimization is disabled. In practice, declaring the variable volatile also forces it to be read whenever the variable is referenced in code.

Note that optimizations are implementation-dependent. Assigning to a const variable by accessing it through a non-const pointer results in undefined behavior, so any result is possible.

The typical use of a const volatile variable is for variables that reference read-only hardware registers that can be changed asynchronously (e.g. I/O ports on microcontrollers). This allows the application to read the register but code that tries to assign to the variable will not compile.

like image 20
Barmar Avatar answered Feb 24 '26 16:02

Barmar