Stuff About C

  • Jian Huang, referencing notes of Dr. Jim Plank
  • CS360
  • Programs are in ~huangj/cs360/notes/CStuff
  • Url: http://www.cs.utk.edu/~huangj/cs360/360/notes/CStuff/lecture.html

    What about C?

    C has been THE language for systems programming. It achieved its fame by being the first and yet the best language that "puts programmers close to the system". There are a few enabling fundamentals in the C programming language. I summarize four of the most major ones as the following:

    These functionalities have been ingeneously designed and optimized, although they appear to be natural and straight-forward to use. However, not knowing the necessary intrinsics, codes tend to be very buggy and even a moderately sized system could fall flat on its face as a daily ritual.

    Before dwelling on details, let's also point out some not so "good" things about C. One might notice that, given C's prevalent popularity, a lot scientific pracitioners still program in Fortran. There are obviously reasons. First, C does not have a built-in constructs like complex numbers, vectors and matrices, etc. It is not so easy to program sophisticated math routines using C. Second, C does not have a high quality scientific or numerical libraries. Indeed, until very recently, all of C's math libraries are in double precision only. While one might argue that there is nothing wrong with always using the highest precision, we all know perfectly well that great performance gains could result if we can afford to lose a few of the minor digits. Practically speaking, 30% to 50% performance gains are usually possible. For simulations that would take days of time, this difference carries great significance.

    Of course, C was mainly designed as a systems programming language. It is unfair and unrealistic to expect C to perform equally well for other applications. Just that, for us, the pupils of computer science, we need to understand this distinction.


    TYPES in C

    In C, there are three kinds of types that variables can have -- scalars, aggregates, and pointers. Half of the game in getting things right in C is keeping yourself from being confused about types. This lecture tries to elaborate on this a little.

    Scalar Types

    There are 7 scalar types in C:
  • char -- 1 byte
  • short -- 2 bytes
  • int -- 4 bytes
  • long -- 4 bytes (8 on some systems)
  • float -- 4 bytes
  • double -- 8 bytes
  • (pointer -- 4 bytes (8 on some systems))

    What's listed on the right are the size of each variable in the corresponding type on a common system. The proper way to obtain the size of each type is to use the macro sizeof, like in: sizeof(int).

    These should all be familiar to you. You can declare a scalar variable in one of three places: As a global variable, as a procedure parameter, and as a local variable. For example, look at the program below in p1.c:

    (In this and all other lecture notes, you can copy the programs and the makefile into your own directory, and then compile them by using make. E.g. to make the program p1, you say ``make p1'').

    #include < stdio.h >
    
    int i;
    
    main(int argc, char **argv)
    {
      int j;
    
      j = argc;
      i = j;
      printf("%d\n", i);
    }
    
    There are three scalar int variables here -- i, j, and argc. I is a global variable. J is a local variable, and argc is a parameter. Scalars are pretty straightforward. You can pass them as parameters to procedures, and return them from procedures without worrying about anything going awry.

    Aggregate Types

    Arrays and structures are aggregate types in C. They are more complex than scalars. You can statically declare an array as a global or local variable. I.e.:
    #include < stdio.h >
    
    char s1[15];
    
    main(int argc, char **argv)
    {
      char s2[4];
      
    }
    
    S1 is a global array of 15 chars and s2 is a local array of 4 chars.

    If an array has been statically declared, then you cannot assign it to another array. For example, look at p2.c:

    #include < stdio.h >
    
    char s1[15];
    
    main(int argc, char **argv)
    {
      char s2[4];
      
      s2 = "Jim";
    }
    
    The statement ``s2 = "Jim"'' is illegal in C, because s2 has been statically declared. If you try to compile this program, gcc will give you an error:
    UNIX> gcc -o p2 p2.c
    p2.c: In function `main':
    p2.c:10: incompatible types in assignment
    UNIX>
    
    This is a good rule to bear in mind -- if x is a statically declared aggregate type (a struct or an array), then you can NEVER say ``x = something''. It will always give you an error.

    However, you can say ``something = x''. We'll discuss this below:


    Pointers

    Pointers are where most people mess up in C. A pointer is simply a pointer to memory. Memory can be allocated in one of two ways -- by declaring variables, or by calling malloc(). Whenever memory has been allocated, you can set a pointer to it.

    You can view memory as one huge array of bytes (chars). This array has 2147483648 (or some other huge number) elements. Usually, we consider the indices to this array in hexidecimal. In other words, the array goes from 0x0 to 0x7fffffff. What is this number? Why is this number important?

    A pointer is simply an index of this array. Whenever we allocate x bytes of memory, we are reserving x contiguous elements from the memory array. If we set a pointer to these bytes, then that pointer will be the index of the first allocated byte in memory.

    For example, look at the following program (in p3.c):

    main()
    {
      int i;
      char j[14];
      int *ip;
      char *jp;
    
      ip = &i;
      jp = j;
    
      printf("ip = 0x%x.  jp = 0x%x\n", ip, jp);
    }
    
    This program allocates one integer (i), an array of 14 characters (j), and two pointers (ip and jp). It then sets the pointers so that they point to the memory allocated for i and j. Finally, it prints out the values of those pointers -- these are indices into the memory array. When we run it, we get:
    UNIX> p3
    ip = 0xefffe924.  jp = 0xefffe910
    UNIX> 
    
    What this means is that when we view memory as an array, elements 0xefffe924, 0xefffe925, 0xefffe926, and 0xefffe927 are allocated for the local variable i, and elements 0xefffe910 through 0xefffe91d are allocated for the array j.

    Note that I said ``jp = j'' and not ``jp = &j''. This is because when treated as an expression, an array is equivalent to a pointer. The only difference is that you cannot assign a value to an array variable. Thus, you can say ``jp = j'', but you cannot say ``j = jp''. Moreover, you cannot take the address of an array variable -- saying ``&j'' is illegal.

    Pointers are a little like scalars -- they too can be declared as globals, locals or parameters, and can be assigned values, passed as parameters, and returned from procedures. On our machines all pointers are 4 bytes. Thus, in p3.c, there are 26 bytes of local variables allocated in the main() procedure -- 4 for i, 14 for j, 4 for ip, and 4 for jp.


    Address Space

    Here is an illustration of BSD style organization of an address space for a program.

    BSD defines segments to be contiguous regions of virtual space (like everybody else). A process address space is composed of five primary segments: (i) the text segment, holding the executable code; (ii) the initialized data segment, containing those data that are initialized to specific non-zero values at process start-up; (iii) the bss segment, containing data initialized as zero at process start-up; (iv) the heap segment, containing uninitialized data and the processs heap; and (v) the stack.

    Beyond the stack is a region holding the kernel's stack (used when executing system calls on behalf of this process, for example) and the user struct, a kernel data structure holding a large quantity of process-specific information.

    This is a illustration of BSDs per process virtual memory space. Text, initialized data, bss and heap are contiguously put in memory address, starting from the lowest address. Stack, however, starts from the highest address space, expanding downward in address space. The empty chunk between the top of heap and bottom of stack are allocated as needed.

    Where and how a memory space is allocated affect which segment this memory allocation resides.


    Type Casting (sometimes called ``type coercion'')

    There are times when you would like to take x bytes of memory of a certain type, and assign it to y bytes of memory of another type. This is called ``type casting''. A simple example is when you want to turn a char into an int, or an int into a float as in p4.c:
    main()
    {
      char c;
      int i;
      float f;
    
      c = 'a';
      i = c;
      f = i;
      printf("c = %d (%c).   i = %d (%c).  f = %f\n", c, c, i, i, f);
    }
    
    The statement `i = c' is a type cast, as is the statement `f = i'.

    Some type castings, like the one above, are very natural. The C compiler will do these for you without complaining. Most others, however, the C compiler will complain about, unless you specifically tell it that you are doing a type cast (this is a way of telling the compiler ``Yes, I know what I'm doing.'').

    For example, think about the procedure call: malloc(n). It allocates and returns n bytes of memory to the programmer. Look at the program p5.c:

    main()
    {
      char *s;
    
      s = malloc(10);
      strcpy(s, "Jim");
      printf("s = %s\n", s);
    }
    
    When you try to compile p5.c, you get a warning from the C compiler:
    UNIX> gcc -o p5 p5.c
    p5.c: In function `main':
    p5.c:5: warning: assignment makes pointer from integer without a cast
    UNIX>
    
    What's going on? Well, all procedures in C are assumed to return integers unless they are specified otherwise. Thus, the statement ``s = malloc(10)'' is trying to set s, which is a pointer, to the return value of malloc, which is assumed to be an integer. The compiler actually does create p5, but it lets you know that you're doing something strange -- that is, assign a pointer to an integer.

    What's the proper thing to do here? Well, you should really declare malloc() as returning a char *, as in p6.c:

    extern char *malloc();
    
    main()
    {
      char *s;
    
      s = malloc(10);
      strcpy(s, "Jim");
      printf("s = %s\n", s);
    }
    
    This tells the compiler that you are using the procedure malloc() which returns a char *, and which is defined elsewhere. You'll note that p6.c compiles without any warnings, but that both p5 and p6 do the same thing when you run them:
    UNIX> p5
    s = Jim
    UNIX> p6
    s = Jim
    UNIX>
    
    Most people do not write code like p6.c, though. Instead, they write it as in p7.c:
    main()
    {
      char *s;
    
      s = (char *) malloc(10);
      strcpy(s, "Jim");
      printf("s = %s\n", s); 
    }
    
    This says to the compiler ``Yes, I know malloc() is returning an int, but I want it to be treated like a char *''. You'll notice that p7.c compiles without warning and runs just like p5 and p6.

    Before we go on to other subjects, you need to understand that, however, p7.c is still not how you should write code. The following would be a lot more favorable:

    #include < stdlib.h >
    
    main()
    {
      char *s;
    
      s = malloc(10);
      strcpy(s, "Jim");
      printf("s = %s\n", s);
    }
    

    In this piece of code, including stdlib.h effectively adds the line of code "void * malloc (size_t size);" before the main function. This makes sure the compiler knows the return value of malloc is actually not an integer, but instead a pointer of the type void *. Similar to how a char type can always be implicitly cast to an integer, a pointer of void * can always be coerced to be an char * type.


    Portability Issues

    You should also notice that on our machines, both pointers and ints are 4 bytes. This has led many people to treat pointers and ints as interchangable. For example, look at the code in p8.c:

    main()
    {
      char s[4];
      int i;
      char *s2;
    
      strcpy(s, "Jim");
      i = (int) s;
      printf("i = %ld (0x%lx)\n", i, i);
      printf("s = %ld (0x%lx)\n", s, s);
    
      i++;
      s2 = (char *) i;
      printf("s = 0x%lx.  s2 = 0x%lx, i = 0x%lx, s[0] = %c, s[1] = %c, *s2 = %c\n",
              s, s2, i, s[0], s[1], *s2);
    }
    
    This is a bad assumption, however, because on some machines, like the DEC alpha, ints are 4 bytes and pointers are 8. Thus, when you run p8.c on an alpha you get an error instead of a correct program run. This is because when we said ``i = (int) s'', we lost 4 bytes of the pointer s. Then when we said ``s2 = (char *) i'', the four extra bytes of s2 were set to zero, giving us different addresses for *s2 and s[1]. In fact, s2 becomes an illegal address, which results in a segmentation fault:

    On our sparcs:

    UNIX> p8
    i = -268441312 (0xefffe920)
    s = -268441312 (0xefffe920)
    s = 0xefffe920.  s2 = 0xefffe921, i = 0xefffe921, s[0] = J, s[1] = i, *s2 = i
    
    On the alpha:
    UNIX> p8
    i = 536864720 (0x1fffe7d0)
    s = 4831832016 (0x11fffe7d0)
    Segmentation fault (core dumped)
    
    If we instead use a long for i instead of an int, everything works fine on the alpha, since longs and pointers are both 8 bytes:

    On the alpha:

    UNIX> p9
    i = 4831832016 (0x11fffe7d0)
    s = 4831832016 (0x11fffe7d0)
    s = 0x11fffe7d0.  s2 = 0x11fffe7d1, i = 0x11fffe7d1, s[0] = J, s[1] = i, *s2 = i
    UNIX>
    
    In this class we will assume that we are always working on a machine where ints and pointers are always 4 bytes. However, in general you should always be sure that your code will work when pointers and ints are different sizes.

    Be Extremely Careful with Pointer Arithmetic

    A hallmark feature of high level programming language is to have types and have type checks. Try this:
    main()
    {
      char * s;
      int  array[2] = { 10000, 1000000};
      s = array;
      s++;
      printf("my array has %d and %d\n",array[0], (int)(*s));
      printf("array = %lx and s=%lx\n",array,s);
    }
    

    The output looks like this:

    my array has 10000 and 0
    in address space, array = ffbef848, s= ffbef849
    

    Pointers to Functions

    Since everything is in a giant array of bytes (so do executable texts), we can obviously get the entry address of each function. With this, let's introduce pointers to functions. These pointers can be passed to other functions as parameters, assigned, placed in arrays, returned by functions just like other variables.

    You specify function pointers types by giving the return type, and type of each input parameters. Like this:
      int (*) (void *, void *)
    
    This is a function pointer that can point to any function that takes two void pointers as input and returns an integer. Note here, that the above specifies a type! Let's look at the following:
    #include < stdio.h >
    
    int i = 0;
    int main(void)
    {
      int (*myfunc) (void);
      int (*foo) (void);
    
      myfunc = main;
      printf("main = %lx, myfunc = %lx, i = %d\n",main,myfunc,i);
      i ++;
      if (i == 5) return 0;
      
      foo = (i%2) ? myfunc : main;
      foo();
    }
    
    The output would look like:
    main = 106d8, myfunc = 106d8, i = 0
    main = 106d8, myfunc = 106d8, i = 1
    main = 106d8, myfunc = 106d8, i = 2
    main = 106d8, myfunc = 106d8, i = 3
    main = 106d8, myfunc = 106d8, i = 4
    

    Segmentation Violations and Bus Errors

    Memory can be viewed as a giant array of bytes. However, certain parts of this array are not accessible. For example, elements 0 to 0x1000 are inaccessible. When you try to access an inaccessible element, you generate a segmentation violation, and your program dumps core so that you can debug it. The reason element 0 is inaccessible is that it's a common bug to forget to initialize a pointer. When that happens, the pointer's value is zero, and when you try to dereference it, you'll generate a segmentation violation, which helps you find the bug.

    The program pa.c generates a segmentation violation by trying to dereference NULL:


    #include < stdio.h >
    
    main()
    {
      char *s;
    
      s = NULL;
    
      printf("%d\n", s[0]);
    }
    

    Whenever you access a scalar type, its value in memory must be aligned. What this means is that if the type is 4 bytes, its location in memory must start at a memory index that is a multiple of 4. For example, if i is an (int *), then if i is not a multiple of 4, dereferencing i will be an error. This error is manifested by a bus error and another core dump. For example, program pb.c makes the error of trying to dereference memory location 1 as an integer. This will generate a bus error (therefore bus errors have precedence over core dumps). By the way, if you change the int below to char, then you get segmentation fault, since alignment is not an issue for char type. If you know the hardware address decoder's setup, you can trigger it to throw bus error too, by accessing memory segment that does not exist. But this is system dependent and much harder to show with a simple program. Refer to the lecture notes on the fundamentals for details on this.
    main()
    {
      int *i;
    
      i = (int *) 1;
    
      printf("%d\n", *i);
    }
    

    Note, malloc() always returns pointers that are multiples of 8 -- therefore using malloc() to allocate any scalar type or array of scalar types will generate pointers that are correctly aligned.

    Moreover, the compiler always lays out structs so that the fields are aligned. Thus, in the following struct:

    struct {
      char b;
      int i;
    }
    
    The whole struct will be 8 bytes -- 1 for b, 3 unused, and 4 for i. The 3 bytes are necessary so that i will be aligned. The compiler does not shuffle around the fields so that they pack into memory better. So, for example, if you have:
    struct {
      char b1;
      int i1;
      char b2;
      int i2;
    }
    
    The struct will be 16 bytes:
  • 1 for b1
  • 3 unused
  • 4 for i1
  • 1 for b2
  • 3 unused
  • 4 for i2

    However, if you order them differently, you can get all of those fields into 12 bytes:

    struct {
      char b1;
      char b2;
      int i1;
      int i2;
    }
    
    Now the struct will have:
  • 1 byte for b1
  • 1 byte for b2
  • 2 unused bytes
  • 4 bytes for i1
  • 4 bytes for i2

    Three Common Type Bugs

    The first one looks idiotic, but it is at the heart of all type bugs. Look at pc.c:
    main()
    {
      char c;
      int i;
      int j;
    
      i = 10000;
      c = i;
      j = c;
    
      printf("I: %d,   J: %d,       C: %d\n", i, j, c);
      printf("I: 0x%04x,  J: 0x%04x,   C: 0x%04x\n", i, j, c);
    }
    

    Since c is a char, it cannot hold the value 10000. It will instead hold the lowest order byte of i, which is 16 (0x10). Then when you set j to c, you'll see that j becomes 16. Make sure you understand this bug.

    The second bug is a typical one when you deal with math routines. If you say ``man log10,'' you'll see that it takes a double and returns a double:

         double log10(double x);
    
    So pd.c tries to take the log of 100.0, which should be two:
    main()
    {
      double x;
    
      x = log10(100);
    
      printf("%lf\n", x);
    }
    

    When you compile it, you have to include -lm on the linking line so that it includes the math libraries. When you do this, you'll see a weird result:
      
    UNIX> pd
    -1035.000000
    
    Why? This is because you didn't include math.h in your C program, and therefore the compiler assumed that you were passing log10 an integer, and that it returned an integer. And the compiler doesn't worry about casting int's to double's. So you get the bug. C's linker does not worry about types of parameters at all, in fact, it only checks function name for matches. That's one of the main reasons that you cannot overload function names like in C++. Note here though, the comipler, once given a function declaration, checks for number of parameters you are passing too. So, that's some added security. You can fix this by including math.h, as in pe.c, this also causes C compiler to do an implicit type cast for you, that is integer to double. Some other languages have stronger mechanism for type checking though.
    #include < math.h >
    
    main()
    {
      double x;
    
      x = log10(100);
    
      printf("%lf\n", x);
    }
    
    UNIX> pd 2.00000

    In fact, this following program also compiles:

    #include < stdio.h >
    main ()
    {
      double x;
      x = log10();
      printf("x = %lf\n",x);
    }
    
    This use of log10 does not even have an input parameter. But when you compile and supply the linker flag "-lm", it compiles. The output I get is:
    x = -1036.000000
    
    But when I include math.h, i.e.:
    #include < stdio.h >
    #include < math.h >
    main ()
    {
      double x;
      x = log10();
      printf("x = %lf\n",x);
    }
    
    This time I get a compile error:
    prototype mismatch: 0 args passed, 1 expected
    
    To sum up, compiler and linker can do strange things, if you do not have a decent understanding of what is going on. Some weird errors may cost a lot of time to figure out. Actually, all three common bugs we are talking about here has to do with type casting, more specifically, whether an implicit type casting is triggered or not. Be very careful.

    Finally pf.c displays another common type bug:


    main()
    {
      double x;
      int y;
      int z;
    
      x = 4000.0;
      y = 20;
      z = -17;
    
      printf("%d %d %d\n", x, y, z);
      printf("%f %d %d\n", x, y, z);
      printf("%lf %d %d\n", x, y, z);
      printf("%lf %lf %lf\n", x, y, z);
    }
    
    UNIX> pf 1085227008 0 20 4000.000000 20 -17 4000.000000 20 -17 4000.000000 0.000000 0.000000

    Typically you see the first bug in line one. You try to print out a double as an int. Not only does it get the value wrong, but it gets x and y wrong as well. You'll learn why later. Lines two and three are fine, but line 4 is now wrong, because you try to print all three quantities as double's. Again, you'll see the reason why later, but you should be aware of this kind of bug now, since you may well see it again.

    An even more interesting situation occurs in the following example pg.c.

    #include < stdio.h >
    
    main()
    {
      double x;
      int y;
      char s[4] = "Jim";
    
      x = 4000.0;
      y = 20;
    
      printf("%lf %lf %s\n", x, y, s);
    }
    

    Running this code, the output is sometimes:


    UNIX> pg
    Bus error
    
    But at other times, you may get:
    4000.000000 0.00000
    
    Or even:
    4000.000000 0.00000 ????%?????&H???'H????(H???)H?
    

    One way to catch many of such bugs is to always set the warning level of the compiler to the highest and truly treat each warning as a WARNING, and try any compilers as you get on your hand might help as well.