Purdue CS24000 2022 and 2023 Summer Midterm Exam Solutions

Purdue University CS24000 is an undergraduate-level course that teaches students programming principles and techniques for problem-solving in the C programming language. Here are the solutions and study notes for the 2022 and 2023 Midterm exams.

Introduction

Below are extracted from the Summer 2023 CS24000 course homepage:

Disclosure: This blog site is reader-supported. When you buy through the affiliate links below, as an Amazon Associate, I earn a tiny commission from qualifying purchases. Thank you.

  • Reference: Beej’s Guide to C Programming; Brian “Beej” Hall; 2007
  • Lecture Subjects
    • Roles of C compiler, C preprocessor, linker, loader.
    • Main memory: addresses and their content, meaning of variables.
    • Reading from stdin and writing to stdout.
    • Fundamental difference between printf() and scanf(): need to pass addresses in scanf().
    • Pointers and indirection.
    • Global vs. local variables.
    • Function calls and passing arguments.
    • Passing by value vs. reference, their typical usage.
    • Basic methods for run-time debugging.
    • Memory layout of 1-D arrays, indexing using pointer notation.
    • Segmentation fault, silent run-time errors.
    • Array overrun, stack smashing and gcc intervention.
    • Scope of global and local variables, properties of static variables.
    • Memory layout of 2-D integer arrays, indexing using pointer notation.
    • Basic string processing.
    • Function pointers.
    • Basic file I/O.
    • Controlling the number of bytes read to prevent stack smashing.
    • Using the make tool to help automate code maintenance.
    • Bit processing techniques, common applications.
    • Basic dynamic memory allocation using malloc(), 1-D and 2-D array examples.
    • Applications of 2-D tables, limitation and caution regarding the use of variable length arrays.
    • Command-line argument support in main(), loader invocation and passing arguments using execl().
    • Applications of command-line arguments.
    • Composite data types using struct, its memory structure, and applications.
    • Conversion/casting of data types.
    • Variadic functions: structure and applications.
    • Application of passing function pointers: responding to events via callback functions (i.e., throwing and catching exceptions).
    • union and enum: structure and applications.
    • Role of const qualifier in argument passing.
    • Basic structure of concurrent client/server apps, shell as an example app.
    • Additional features and applications of file I/O.

Summer 2022 Midterm Solutions and Notes

Problem 1 (36 pts)

(a) Consider the code snippet

1
2
3
4
5
int a, *b, *c;
a = 3; b = &a;
printf("%d", *b);
*c = 5;
printf("%d", *c);

Explain in detail what is likely to happen if the code snippet is compiled and executed.

(b) What are the possible outcomes if the code snippet

1
2
3
4
char r[4];
r[0] = 'H';
r[1] = 'i';
printf("%s", r);

is compiled and executed? Explain your reasoning.

(c) Suppose we have a 2-D array, int x[2][3], wherein 6 integers are stored. What array expression is *(*(x+1)+2) equivalent to, and why?

Problem 1 Solution

(a) The first printf() outputs 3 since b is a pointer to integer variable a. *c = 5 is likely to generate a segmentation fault since the code does not place a valid address in c before this assignment. The second printf() is likely not reached due to a segmentation fault from *c = 5 which terminates the running program.

(b) There are two possible outcomes:

  1. prints "Hi" to stdout.
  2. prints "Hi" followed by additional byte values.

Explanation: If the memory location r[2] contains EOS ('\0') then the first outcome results. Otherwise, printf() will continue to print byte values (not necessarily ASCII) until a byte containing 0 (i.e.,EOS) is reached.

(c) Equivalent to x[1][2].

Explanation: In our logical view of 2-D arrays: x points to the location in memory where the beginning addresses of two 1-D integer arrays are located. Therefore x+1 points to the beginning address of the second 1-D integer array. *(x+1) follows the pointer to the beginning address of the second 1-D integer array. *(x+1)+2 results in the address at which the third element of the second 1-D integer array is stored. *(*(x+1)+2) accesses the content of the third element of the second 1-D integer array. Hence equivalent to x[1][2].

Problem 2 (32 pts)

(a) Suppose main() calls function

1
2
3
4
5
int abc(void) {
int a = 3, static int b = 1;
if(++a > ++b) return a++;
else return ++b;
}

three times. Explain what values are returned to main() in each of the three calls to abc().

(b) Suppose the code snippet

1
2
3
4
5
float m, **n;
m = 3.3;
printf("%f", m);
**n = 5.5;
printf("%p", n);

is compiled and executed. What is likely to happen, and why? How would you modify the code (involving printf() calls) to facilitate ease of run-time debugging?

Problem 2 Solution

(a) Here are the three return values for each call and the explanation:

  1. First call returns 4. The if-statement checks 4 > 2 and a++ returns 4 before incrementing a.
  2. Second call returns 4. Before the if-statement, the static variable b becomes 2 since it preserves the previous value from the first call. So the if-statement checks 4 > 3. Hence a++ returns 4.
  3. Third call return 5. Now the static variable b becomes 3 at the beginning of the call, and the if-statement checks 4 > 4. So the program goes to the else-part which increments b again and returns b. Hence the function call returns 5.

(b) Since we did not assign a valid address to n, **n is likely to reference an invalid address that triggers a segmentation fault which terminates the running program.

Although the first printf() call was successful, 3.3 will likely will not be output to stdout (i.e., display) due to abnormal termination of the program and buffering by stdio library functions.

Adding a newline in the first printf() call, or calling fflush(stdout) after the first printf() call will force 3.3 in the stdout buffer to be flushed before the program terminates due to segmentation fault.

Problem 3 (32 pts)

(a) Suppose you are supervising a team of C programmers. One of the team members is responsible for coding a function, int readpasswd(void), that reads from stdin a new password and checks that it contains upper case letters, special characters, etc. per company policy. The team member shows you part of the code

1
2
3
4
5
int readpasswd() {
char secret[100];
scanf("%s", secret);
/* code follows to check validity of password */
}

that reads a password from stdin and stores it in local variable secret for further processing. Explain why you would be alarmed by the code. How would you rewrite to fix the problem in the code?

(b) Code main() that reads a file, test.out, byte by byte using fgetc() and counts how many bytes are ASCII characters. main() outputs the count to stdout. Focus on making sure that your code is robust and does not crash unexpectedly.

Problem 3 Solution

(a) The scanf() does not prevent user input that exceeds buffer size (100 characters) from overwriting memory in readpasswd()'s stack frame, potentially modifying its return address. This can lead to the execution of unintended code such as malware.

Alternate: The scanf() functions can lead to a buffer overflow if used improperly. Here in this function, it does not have bound checking capability and if the input string is longer than 100 characters, then the input will overflow into the adjoining memory and corrupt the stack frame.

📝Notes: This is a major security flaw in scanf family (scanf, sscanf, fscanf ..etc) esp when reading a string because they don't take the length of the buffer (into which they are reading) into account.

To fix this, the code should explicitly check that no more than 100 characters are read from stdin to prevent overflow over secret[100]. This can be done by reading character by character using getchar() in a loop until a newline is encountered or 100 characters have been read.

(b) A sample solution can be seen below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
int main() {
FILE *fp;
int c, count;

if ((fp = fopen("test.out","r")) == NULL) {
fprintf(stderr,"opening file blog.dat failed\n");
exit(1);
}

count = 0;
while ((c = fgetc(fp)) != EOF) {
if (0 <= c <= 127) {
// it's an ASCII character, increment count
count++;
}
}
printf("count = %d\n", count); //output result fclose(fp);
fp = NULL;
}

Bonus Problem (10 pts)

Suppose you are given the code in main.c

1
2
3
4
5
6
int s[5]; 
int main() {
int i;
for (i=0; i<50; i++)
s[i] = 0;
}

which is compiled using gcc and executed. What are the two possible outcomes? Explain your answer.

Bonus Problem Solution

  • Outcome 1: The for-loop overwrites global memory following s[5] which may, or may not, corrupt program data and computation but does not crash the running program (i.e., silent run-time bug).
  • Outcome 2: The for-loop overwrites global memory following s[5] which exceeds the running program's valid memory, resulting in a segmentation fault.

Summer 2023 Midterm Solutions and Notes

Problem 1 (30 pts)

(a) Consider the code snippet

1
2
3
4
5
6
int x, *y, *z;
x = 5;
y = &x;
*y = 10;
printf("%d %p\n", x, y);
*z = 3;

Explain what is likely to happen if the code snippet is compiled and executed as part of main().

(b) Explain what the declarations of g and h mean:

1
char *g(char *), (*h)(char *);

For the two assignment statements to be meaningful

1
2
x = g(s);
h = y;

what must be the types of x and y? Provide the C statements for their type declarations.

Problem 1 Solution

(a) printf() will output 10 (for x) and the address of x (in hexadecimal notation) which is contained in y. Assignment statement *z = 3 will likely trigger a segmentation fault since a valid address has not been stored in z.

(b) g is a function that takes a single argument that is a pointer to char (i.e., char *), and g returns a pointer to char (i.e., address that points to char). h is a function pointer that takes a single argument that is a pointer to char, and h returns a value of type char.

x is a pointer to char, i.e., char *x. y is a function that takes an argument that is a pointer to char and returns a value of type char, i.e., char y(char *).

Problem 2 (30 pts)

(a) For the function

1
2
3
4
5
void fun(float a) {
float x[5], i;
for (i=0; i<8; i++)
x[i] = a;
}

explain what is likely to happen if fun() is called by main(). Explain how things change if 1-D array x is made to be global.

(b) What are potential issues associated with code snippet

1
2
3
4
FILE *f;
char r[100];
f = fopen("data.dat", "r");
fscanf(f, "%s", r);

Provide modified code that fixes the issues.

Problem 2 Solution

(a) Calling fun() will likely generate a stack smashing error. This is so since x is local to fun() and overflowing the 1-D array (by 3 elements, i.e., 12 bytes) is likely to cause the canary (bit pattern) inserted by gcc (to guard the return address) to be changed. If x is made global, gcc does not insert a canary, hence stack smashing will not occur. However, overflowing x may, or may not, trigger a segmentation fault.

(b) Two potential issues:

  1. fopen() may fail and return NULL.
  2. fscanf() may overflow 1-D array r if the character sequence in data.dat exceeds 100 bytes.

To fix these, do the following modifications:

1
2
3
4
5
6
f = fopen("data.dat", "r");
if (f == NULL) {
printf("error opening data.dat");
exit(1);
}
fscanf(f, "%99s", r);

Problem 3 (40 pts)

(a) A 2-D integer array, int d[100][200], declaration is restrictive in that it hardcodes the number of rows and columns to fixed values 100 and 200, respectively. Suppose two integers N and M are read from stdin that specify the number of rows and columns of a 2-D integer array which is then used to read N x M integers from stdin into main memory. Provide C code main() that uses malloc() to achieve this task. Your code should be complete but for including header files.

(b) Provide code that reads a value of type unsigned int from stdin, then uses bit processing techniques to count how many of the 32 bits contain bit value 0. Annotate your code to note what the different parts are doing.

Problem 3 Solution

(a) The complete code is shown below (Note we skip the NULL check for the return of malloc(), add that after each such call if required)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {
int **d;
int N, M, i, j;

scanf("%d %d", &N, &M);
d = (int **)malloc(N * sizeof(int *));

for(i=0; i<N; i++) {
// can also use d[i] on the left below
*(d + i) = (int *)malloc(M * sizeof(int));
}
for (i=0; i<N; i++) {
for (j=0; j<M; j++) {
scanf("%d", &d[i][j]);
}
}
}

📝Notes: Freeing memory of such a 2-D integer array also needs two steps:

1
2
3
4
5
6
7
void free_2d_array(int **array, int rows) {
for (int i = 0; i < rows; i++) {
// equivalent to free(array[i])
free(*(array+i));
}
free(array);
}

(b) The solution code can be seen below

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned int x, m = 1;
int i, count = 0;

scanf("%u", &x);

for (i=0; i<32; i++) {
if ((x & m) == 0) {
count++;
}
x = x >> 1;
}

printf("%d", count);

Bonus Problem (10 pts)

Explain why printf("%d", x) passes argument x by value whereas scanf("%d", &x) passes the argument by reference. Can one code printf() so that it passes x by reference? If so, why is it not done?

Bonus Problem Solution

printf() only needs a copy of the value of x to do its work of printing the value to stdout. scanf() needs the address of x so that the value entered through stdin (by default, keyboard) can be stored at the address of x. Yes, since following the address of x allows printf() to access its value. It is not necessary to reveal the address of x to printf() since it only requires its value.