Sandeep 's World >> C Musings

C programs are organized as statements, blocks and funtions. A statement is the smallest component and is always ended using a ';' character. Blocks are collections of statements grouped within curly braces ('{' and '}'). One or more such blocks comprise functions. And 'C' is a procedure oriented language. i.e., every C program is divided into small functionalities and implemented as functions or procedures.

So where does data fit in here ? All the statements, blocks and functions are essentialy instructions or set of instructions. Nevertheless they act on data. And 'data' in C (or for that matter programs written in any language) is classified in to two. Stack data and Heap data. In 8086 terminology, these are the stack and data segments.

In 'C' language there are generally two ways u can see data being declared. Declaration of data is nothing but allocating memory in stack or heap. These declarations could be found at the beginning of a block (local declarations) or outside all functions (global declarations). All local declarations allocate memory in the stack, while global declarations reserve memory in the heap. To examine how memory is allocated in stack we'll dissect the following 'C' code:

    {
      int i = 10;
      {
        int i = 20;
        {
          int i = 30;
        }
      }
    }
    
If u do a gcc -s of this code, u'll get the following assembly in 8086:

    ....
    pushl   %ebp
    movl    %esp, %ebp
    subl    $12, %esp
    movl    $10, -4(%ebp)
    movl    $20, -8(%ebp)
    movl    $30, -12(%ebp)
    ....
    
The first line means, copy the stack pointer to base pointer. Second line means subtract stack pointer by 12 (this is to create space for three 4-byte integers) and the next three lines are initializing the three 'i' variables in the stack. Obviously the three 'i's have 4 bytes each allocated in the stack.In global declarations there is hardly any complications. The data quitely goes in to the heap segment and the variable is addressed using its name. Check out this 'C' program:

    int i;
    int main()
    {
      i = 10;
    }
    
On a gcc -S it will give:

    movl    %esp, %ebp
    movl    $10, i
    .....
    comm   i,4,4
    
The .comm statement is to allocate 4 byte for 'i' in the heap, while in the movl $10, i, u can see that 'i' is being addressed by name. What is actually happening is that the variable 'i' is added to the table of exported symbols from this binary.

Time to examine 'extern' and 'static' variables. An 'extern' declaration does not allocate memory for any variables, but just adds an unresolved symbol to a binary, which is resolved during linkage time. Like I said, a variable declared as global will normally be added to the symbol table. These variables can be accessed from another file using an extern declaration for the same variable. But this is true, only if it is not declared static. For example if file a.c has the following code:

    int i;
 
    int main()
    {
      i = 20;
    }
    
and file b.c has:

    extern int i;
    
and file b.c has:

    static int i;
    
b.c will show an error at the linking stage, unless 'i' is defined somewhere else.

Another exception with the static keyword is static variables inside a function (or a block in general). Variables declared inside a block is usually local and stored in stack; unless its declared as 'static'. static 'local' variables are stored in the heap and is alive throughout the life of the process. Only that they are not 'visible' from other functions. Almost like a static 'global' variable is not 'visible' from other files. Just try writing a C program with a static 'local' variable and examine its assembly to see whats happening.

As a side note on 'normal' local variables, their life cycle is limited to the time when the block in which its declared is active. This will be clearer once we have a look at what function calls and return does to the stack. In case u r curious what happens to the local variables allocated after exitting from a block, say when a function returns, check this 'C' program:

    void func1(void)
    {
      int i[10], j;
      for ( j = 0; j < 10; ++j )
        i[j] = j * j;
      printf( "&i[0] = %pn", i );
    }
    void func2()
    {
      int i[10], j;
      for ( j = 0; j < 10; ++j )
        printf( "&i[%d] = %p, i[%d] = %dn", j, &i[j], j, i[j] );
    }
    int main()
    {
      func1();
      func2();
    }
    
This is the output I got:

    &i[0] = 0xbffffb80
    &i[0] = 0xbffffb80, i[0] = 0
    &i[1] = 0xbffffb84, i[1] = 1
    &i[2] = 0xbffffb88, i[2] = 4
    &i[3] = 0xbffffb8c, i[3] = 9
    &i[4] = 0xbffffb90, i[4] = 16
    &i[5] = 0xbffffb94, i[5] = 25
    &i[6] = 0xbffffb98, i[6] = 36
    &i[7] = 0xbffffb9c, i[7] = 49
    &i[8] = 0xbffffba0, i[8] = 64
    &i[9] = 0xbffffba4, i[9] = 81
    
What u saw is that the values written to the stack by func1() is retrieved from func2(). This means that the space used to allocate variables in func1() was reused by func2().

This is no secret, once u get the concept of stack growth. Every time a function is called, the stack grows. And shrinks back when the function returns. Try "gcc -S" to see what I meant.

Some of u may have used the stdarg library. If no, have a look at functions like printf() which will take variable number and types of arguments. Ever wondered how ? Once u have a good picture of the stack and the local variables allocated and passed through the stack, it shudnt be difficult. Check the following 'C' program:

    void func(int n, ...)
    {
      int i, *ptr = &n;
      printf( "&n = %p, &i = %pn", &n, &i );
      for ( i = 0; i < n; ++i )
        printf( "ptr = %p, ptr[%d] = %dn", ptr, i, ptr[i] );
    }
    int main()
    {
      func( 3, 1, 2, 3 );
      func( 5, 5, 4, 3, 2, 1 );
    }
    
Here, func() is a variable argument function (as signified by ... in the arg list) with a mandatory int argument 'n', which specifies the number of additional parameters. The function uses the address of 'n' to access the additional parameters. If u really understood what I had been talking about, it shud not be difficult to guess the output. Anyways, lemme include the output I got:

    &n = 0xbffffbb0, &i = 0xbffffba4
    ptr = 0xbffffbb4, ptr[0] = 1
    ptr = 0xbffffbb8, ptr[1] = 2
    ptr = 0xbffffbbc, ptr[2] = 3
    &n = 0xbffffba0, &i = 0xbffffb94
    ptr = 0xbffffba4, ptr[0] = 5
    ptr = 0xbffffba8, ptr[1] = 4
    ptr = 0xbffffbac, ptr[2] = 3
    ptr = 0xbffffbb0, ptr[3] = 2
    ptr = 0xbffffbb4, ptr[4] = 1
    
Verify that the function func() was able to retrieve the additional arguments in both the cases. Today I was thinking about structures and alignments, in particular zero sized array elements inside structures. So here we go, compile the following program and check the output:

    struct s
    {
      int array[0];
    };
    int main()
    {
      printf( "sizeof(struct s) = %d\n", sizeof(struct s) );
      return 0;
    }
    
U mite think y somebody would need such a structure ? These type of structures are useful when u have a variable size data element to send as a PDU across networks. When u define the structure, u will consider only the fixed size and leave a field like 'char variableSizeData[0]' in the end. When memory is allocated for this structure (through malloc) u can consider the size required for the variable size data and allocate accordingly. But, ANSI disagrees. Try compiling the above program with a "-ansi -pedantic" option, it will give a warning.

Well, I was actually planning to discuss something else. Its about the size of a structure. Is the size of structure always same as the sum of the sizes of the individual elements ? may not be. See the following structure:

    struct
    {
      char c;
      int i;
      short s;
    }
    
Do a sizeof() on this structure and see the result. U shud get 12!!! Now re-arrange it to:

    struct
    {
      int i;
      short s;
      char c;
    }
    
U will get 8 this time, When the sum of individual sizes is 7. Why is this disparity?

Thats because an integer is always four byte aligned. That means an integer pointer will be always a multiple of sizeof(int). Similarly a short pointer will be a multiple of sizeof(short). Now, there is a pre-processor declaration '#pragma pack(n)'. Add a "#pragma pack(1)" to the top of ur file and do a sizeof() on the structure. U'll get exactly the sum of individual sizes. This is a 'pragma' derivative to pack the structure with only 1-byte alignment. This is handy in case u want to save memory.This alignment-mania of the compiler can also be demonstrated by declaring stack variables (of random types) and printing the pointer values of these variables. U can verify that when integers are allocated in the stack, the alignment is always taken care of. Hey ... btw .... "pragma pack" is not going to work for stack variables :(

A useful macro when u r experimenting with the offsets of structure members is:

    #define OFFSET(type, member) (&((type *)0)->member)
    
Figure out how it works :)

Check out this program:

    int main()
    {
      float f=0.0f;
      int i;
      for(i=0;i<10;i++)
        f += 0.1f;
      if(f==1.0f)  printf("f is 1.0f\n");
      else  printf("f is NOT 1.0f\n");
      return 0;
    }
    
Obviously, the output should be "f is 1.0f\ndifference is 0.0\n". Someone disagree?

Well .... u shud know a thing or two about the representation of binary numbers.

Exercise number one ... try converting 0.25 to binary.

Okie ... the answer is 1.0 * 10 ^ (-10)

If u doubt this ... I am tempted to quote this famous sentence - "There are only 10 type of people. Those who understand binary and those who dont" :)

Now a bit more difficult one ... convert 0.375 to binary.

It is ... 1.1 * 2 ^ (-2)

So ... what is 0.1 decimal in binary?

2 ^ (-4) * 1.100110011... and never ends ... almost like "PI", "e" ... etc ... but sadly a float has only 32 bits to represent this number. And obviously there will be a loss of accuracy. Now, to verify that try this ...:

    int main()
    {
      float f=0.0f;
      int i;
      for(i=0;i<16;i++)
        f += 0.0625f;
      if(f==1.0f)  printf("f is 1.0f\n");
      else  printf("f is NOT 1.0f\n");
      return 0;
    }
    
Here, I've used 0.0625 instead of 0.1. And 0.0625 decimal is 1.0 * 10 ^ (-10000) in binary.

Another thing on precision ... but slightly different type ...

u mite have seen a general malloc statement. It will look like:

    Record *record;
    record = (Record *) malloc(sizeof(Record));
    
Now y do u need the type caste ? Somebody may tell u that it is to avoid compiler warnings since u cant convert what malloc returns to "Record *". If u check up some manuals (man malloc) u can see malloc returns "void *". So this argument must be wrong. To understand what i've said, put these lines in a C file and compile:

    Record *record;
    void *pointer;
    record = pointer;
    
No ... warnings right? That means u can assign a "void *" to any other pointer. Then y do you need to typecast? Okie ... write a program doing malloc (without typecast). One important thing is ... dont use an "#include"s in ur program. Go compile ... and this is what I got:

    warning: assignment makes pointer from integer without a cast
    
Why ?

Now include and compile. Its gone !!!!

If you havent understood the reason yet, this is because the default return type of a function (to be precise, an undeclared function) in 'C' is "int". So what ? Just that u cannot assign an int to a pointer. When u dont include stdlib.h, the declaration of malloc() is missing and the return type is assumed to be int (instead of void *) and the compiler cribs.

Now u might say ... big deal ... lets typecast and it should work fine. Yes, it will work fine in most of the machines ... where the word size (read it as pointer size) is same as sizeof(int). This is true in most of the 32 bit processors. But how about 64 bit processors (ofcourse, the OS also shud be running in 64 bit mode) ? There, a 64 bit pointer is going to be converted to a 32 bit integer (since the default return value is int and only that much is picked up from the register/stack when malloc returns) and then type casted to a 64 bit pointer. During this process, the higher (or lower depending on endianness) 32 bits is gone and the pointer value may be invalid !!! A nice way for segmentation violations and core dumps!!!!!!

So people ... dont typecast what malloc returns. If u want to avoid compiler warnings, #include . Thats the way to go !!!



© 2018 Sandeep Unnimadhavan