Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In C, what happens if we left shift the bits out of range and again right shift the values in the same operation

Tags:

c

In case of unsigned short I shifted 383 by 11 positions towards left and again in the same instruction shifted it by 15 positions right, I expected the value to be 1 but it was 27. But when I used both the shift operations in different instructions one after another(first left shift and then right), output was 1. here is a sample code :-

unsigned short seed = 383;
printf("size of short: %d\n",sizeof(short));
unsigned short seedout,seed1,seed2,seed3,seedout1;
seed1 = (seed<<11);
seed2 = (seed1>>15);
seed3 = ((seed<<11)>>15);
printf("seed1 :%d\t  seed2: %d\t seed3: %d\n",seed1,seed2,seed3);

and its output was :

size of short: 2
seed1 :63488      seed2: 1       seed3: 23    
seedout1: 8     seedout :382
Process returned 0 (0x0)   execution time : 0.154 s
like image 793
Mrityu Avatar asked Jan 25 '23 10:01

Mrityu


2 Answers

For clarity, you compare

unsigned short seed1 = (seed<<11);
unsigned short seed2 = (seed1>>15);

on one hand and

unsigned short seed3 = ((seed<<11)>>15);

on the other hand.

The first one takes the result of the shift operation, stores it in an unsigned short variable (which apparently is 16 bit on your platform) and shifts this result right again.

The second one shifts the result immediately.

The reason why this is different resp. the bits which are shifted out to the left are retained is the following:

Although seed is unsigned short, seed<<11 is signed int. Thus, these bits are not cut off as it is the case when storing the result, but they are kept in the intermediate signed int. Only the assignment to seed1 makes the value unsigned short, which leads to a clipping of the bits.

In other words: your second example is merely equivalent to

int seed1 = (seed<<11); // instead of short
unsigned short seed2 = (seed1>>15);
like image 90
glglgl Avatar answered Jan 29 '23 06:01

glglgl


Regarding left shifting, type signedness & implicit promotion:

Whenever something is left shifted into the sign bit of a signed integer type, we invoke undefined behavior. Similarly, we also invoke undefined behavior when left-shifting a negative value.

Therefore we must always ensure that the left operand of << is unsigned. And here's the problem, unsigned short is a small integer type, so it is subject to implicit type promotion whenever used in an expression. Shift operators always integer promote the left operand:

C17 6.5.7:

The integer promotions are performed on each of the operands. The type of the result is that of the promoted left operand.

(This makes shifts in particular a special case, since they don't care about the type of the right operand but only look at the left one.)

So in case of a 16 bit system, you'll run into the case where unsigned short gets promoted to unsigned int, because a 16 bit int cannot hold all values of a 16 bit unsigned short. And that's fine, it's not a dangerous conversion.

On a 32 bit system however, the unsigned short gets promoted to int which is signed. Should you left shift a value like 0x8000 (MSB) set 15 bits or more, you end up shifting data into the sign bit of the promoted int, which is a subtle and possibly severe bug. For example, this prints "oops" on my Windows computer:

#include <stdio.h>

int main (void)
{
  unsigned short x=0x8000;
  if((x<<16) < 0) ) // undefined behavior
    puts("oops");
}

But the compiler could as well have assumed that a left shift of x can never result in a value < x and removed the whole machine code upon optimization.

We need to be sure that we never end up with a signed type by accident! Meaning we must know how implicit type promotion works in C.


As for left-shifting unsigned int or larger unsigned types, that's perfectly well-defined as long as we don't shift further than the width of the (promoted) type itself (more than 31 bits on a 32 bit system). Any bits shifted out well be discarded, and if you right shift, it will always be a logical shift where zeroes are shifted in from the right.


To answer the actual question:

Your unsigned short is integer promoted to an int on a 32 bit system. This allows to shift beyond the 16 bits of an unsigned short, but if you discard those extra bits by saving the result in an unsigned short, you end up with this:

383 = 0x17F  
0x17f << 11 = 0xBF800  
0xBF800 truncated to 16 bits = 0xF800 = 63488
0xF800 >> 15 = 0x1

However, if skipping the middle step truncation to 15 bits, you have this instead:

0xBF800 >> 15 = 0x17 = 23

But again, this is only by luck since this time we didn't end up shifting data into the sign bit.

Another example, when executing this code, you might expect to get either the value 0 or the value 32768:

unsigned short x=32768;
printf("%d", x<<16>>16);

But it prints -32768 on my 2's complement PC. The x<<16 invokes undefined behavior, and the >>16 then apparently sign extended the result.

These kind of subtle shift bugs are common, particularly in embedded systems. A frightening amount of all C programs out there are written by people who didn't know about implicit promotions.

like image 43
Lundin Avatar answered Jan 29 '23 07:01

Lundin