Functions and Pointer Parameters

Preparation

As a security measure, many operating systems randomize the layout of a program's memory address space, which makes any vulnerabilities more difficult to discover and exploit across different runs of the program. Such randomization also makes it more difficult for beginning students (such as yourself) to learn and understand memory layout. As a result, we'll need to disable address space layout randomization (ASLR) for the purposes of this lab.

To start a new shell (the program that actually is running in your Terminal window, listening to and obeying your commands) with ASLR disabled, run the following command in your Terminal:

setarch `uname -m` -R /bin/bash

Make sure you use this Terminal window to complete Exercises 1–6. If your work is interrupted (i.e., by the end of classtime), simply log in again and re-run the command above in your Terminal before continuing.

If you forget or neglect to disable ASLR, you'll see different pointer addresses for different runs of the same program. For our purposes, this is fine, but you might find it a bit confusing as you work through the lab.

Finding the Perimeter and Area of a Rectangle

In the previous lab, you were asked to write two functions related to circles: one computed the circumference given the radius, and the second computed the area given the radius. Following the same approach, program perim-area-1.c computes the perimeter and area of a rectangle, given the lengths of the two sides.

  1. Copy perim-area-1.c to your account. Compile and run it, and review how the program works.
    1. Following the approach of the previous lab, draw a schematic diagram of main memory after the function calculate_perimeter has been called just before it returns; and draw a second schematic diagram of main memory after the function calculate_area has been called, just before it returns. (For now, omit any addresses from your diagram.)
    2. In C, the address operator (&) allows one to determine the location or address in main memory where a variable is located. Make the following insertions into perim-area-1.c:

      Insert into calculate_perimeter just before the return statement:

      printf ("parameter side1:  location: %p, value: %lf\n",
             &side1, side1);
      printf ("parameter side2:  location: %p, value: %lf\n", 
             &side2, side2);
      printf ("local lengthPlusWidth:  location: %p, lengthPlusWidth: %lf\n", 
             &lengthPlusWidth, lengthPlusWidth);

      Insert into calculate_area just before the return statement:

      printf ("parameter side1:  location: %p, value: %lf\n", 
             &side1, side1);
      printf ("parameter side2:  location: %p, value: %lf\n", 
             &side2, side2);

      Insert into main just before the return statement:

      printf ("variable length:  location: %p, value: %lf\n",
             &length, length);
      printf ("variable width:   location: %p, value: %lf\n",
             &width, width);
      printf ("variable perim:   location: %p, value: %lf\n",
             &perimeter, perimeter);
      printf ("variable area:    location: %p, value: %lf\n", 
             &area, area);

      Note: The %p format string instructs printf to interpret the argument as a pointer, which is an address, which is actually an integer. The size of such an integer is platform-dependent, so it is best to let the standard I/O library handle the interpretation, rather than casting the address to an unsigned long int, or whatever the equivalent integer size may be.

      Rather than displaying the address as a decimal number, the valueis printed in so-called hexadecimal format, where each position in the number is a power of 16 (instead of the usual power of 10). Because we want each digit to represent a single power, numbers larger than 9 must be represented by the characters af, where a represents 10, b is 11, and so on up to f as 15 (that being the largest possible digit value in a base sixteen number system, much like 9 is the largest digit value in the ordinary base ten, or decimal, number system). Hex values are typically preceded by the 0x prefix.

    3. Recompile and run your program. To interpret the output, suppose that the first printf statement in the calculate_perimeter function produced the output:

      parameter side1:  location: 28580, value: 5.00000

      This indicates that the parameter side1 corresponds to memory location 28580, and the value 5.0 is stored there. This part of the memory diagram might look like:


      main
      Partial schematic memory diagram for program perim-area-1 showing part of the stack frame for the function perimeter.


    4. Use the address information from the inserted print statements to annotate the memory diagrams from Step 1a with the actual locations or addresses where each parameter and variable was stored. Rather than write the entire hexadecimal address, you may write the last four digits of each address.

Program perim-area-1.c computed perimeter and area in two separate functions, because each function can only return one value. To obtain more than one result from a function, we have to change the nature of the parameters, as illustrated in the next exercises.

Passing Values and Addresses as Parameters

Be sure you have completed the reading on passing values and addresses as parameters before continuing with this lab!

  1. Copy perim-area-2.c, compile and run it, and check that it produces exactly the same output as perim-area.1.c

    To understand how perim-area-2.c works, several print statements have been added to yield program perim-area-2a.c. Compile and run this program.

    When the program was run on one machine, the program generated the following output:

    working with a rectangle of width 7.000000 and length 5.000000
    compute:  addresses, values, and pointer references
                 side1:  address: 640370600, value: 5.000000
                 side2:  address: 640370592, value: 7.000000
       lengthPlusWidth:  address: 640370616, value: 12.000000
       pPerimeter:       address: 640370584, value: 640370664, *pPerimeter: 24.000000
       pArea:            address: 640370576, value: 640370656, *pArea: 35.000000
    the rectangle's perimeter is 24.000000
    the rectangle's area is 35.000000
    main:  variable addresses and values
       length:      address: 640370680, value: 5.000000
       width:       address: 640370672, value: 7.000000
       perimeter:   address: 640370664, value: 24.000000
       area:        address: 640370656, value: 35.000000

    This information gives rise to the following diagram for main memory that would have been encountered immediately before the compute procedure finished.


    main
    Schematic memory diagram for perim-area-2.c with function compute as the active stack frame.


    As we shall discover later in the course, each double requires 8 units (technically called bytes) of memory, so many of the locations given are 8 numbers apart.

    1. Explain why the values stored in main memory for side1 and side2 duplicate the numbers stored in main memory for length and width, respectively.
    2. The printf statement involving pPerimeter in compute indicates
      • the address of pPerimeter (i.e., &pPerimeter) is 640370584; that is the variable pPerimeter is stored at location 640370584
      • the value stored for pPerimeter is 640370664; note that this is the location of the perim variable in main
      • the value referenced by the location stored in pPerimeter (i.e., the value stored in perimeter) is 24.00000.
      Write similar statements about what is printed regarding the parameter area.
  2. In the previous lab, you wrote two functions that computed the circumference and the area of a circle. Write a new version of your solution, so that the program has just one procedure compute_circle_geometry that has three parameters, the radius of a circle, the circumference, and the area. Your function should have a void return type, but takes the radius as input and returns the circumference and area as changed parameters. (That is, you will need to pass in the addresses of the circumference and area variables from your main procedure.)
  3. Copy the program pointers-1.c to your account.
    1. Examine the program and predict the output it will produce.
    2. Compile and run this program. Verify your prediction and explain each value printed.
    3. Draw a schematic memory diagram, showing variables and their values just before pointersFunctionOne finishes and returns.
  4. Copy the program pointers-2.c to your account.
    1. Examine the program and predict the output you expect it to produce.
    2. Compile and run the program. Verify your prediction and explain each value printed.
    3. Using this output as an aid, draw a schematic memory diagram, showing variables and their values just before pointersFunctionTwo finishes and returns.
    4. Add the declaration int w = 100; as the first statement in the main procedure (before the declaration int x = 3;).
      • How do you predict the output will change?
      • Compile and run the program. Verify your prediction and explain each value printed.
      • Does the result depend upon the value assigned to w? Why or why not?
    5. Add the declaration int z = 25; immediately after the declaration of variable y in main.
      • How do you predict the the output will change?
      • Compile and run the program. Verify your prediction and explain each value printed.
      • Does the result depend upon the value assigned to z? Why or why not?
  5. Copy the program pointers-3.c to your account.
    1. Examine the program and predict the output it will produce.
    2. Compile and run this program. Verify your prediction and explain each value printed.
    3. Draw a schematic memory diagram, showing variables and their values just before pointersFunctionThreeInner finishes and returns.

For those with extra time

Setting a Max

Write the function

void
set_max (int n, int m, int * p_max);

that sets the variable pointed to by p_max to the larger of n and m.

Dividing the time

Write the function

void
split_time (unsigned long total_seconds, unsigned int* p_hours, 
            unsigned int * p_minutes, unsigned int * p_seconds);

where total_seconds is a time represented as the number of seconds since midnight. The other parameters are pointers to variables in which the function will store the equivalent time in hours (0-23), minutes (0-59) and seconds (0-59), respectively.

Exchange

Write the function

void
exchange ( double * p, double * q);

where p and q are pointers to two values that are exchanged after the procedure is run. For example,

double a = 543.21;
double b = 123.45;
exchange (&a,&b);
printf ("a=%lf, b=%lf\n",a,b);

should print

a=123.45, b=543.21
Problem set_max is adapted from Exercise 5 in Chapter 11 (p. 108) of Study Guide for C Programming: A Modern Approach by Manuel E. Bermudez. Problem split_time is adapted from Exercise 5 in Chapter 11 (p. 254) of C Programming: A Modern Approach by K. N. King.