Purdue CS24000 2022 Summer Final 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 Final 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.

Summer 2022 Final Solutions and Notes

Problem 1 (45 pts)

(a) Which statements in the code

1
2
3
4
5
6
7
8
9
10
typedef struct friend {
char *nickname;
unsigned int year;
} friend_t;

int main() {
friend t *amigo;
amigo->year = 2017;
strcpy(amigo->nickname, "fish");
}

are problematic, likely to trigger segmentation fault? Augment the code by adding calls to malloc() so that the bugs are fixed.

(b) Explain the difference between fun1 and fun2 which are declared as char *fun1(char *) and char (*fun2)(char *), respectively. Code a function fun3 that takes a string as argument and returns the last character of the string. You may assume that the string is of length at least 1 (not counting EOS).

(c) Suppose a user enters the command, %/bin/cp file1 file2, using a shell to copy the content of file1 to file2 on one of our lab machines. From the viewpoint of the shell, from where does it read its input /bin/cp file1 file2? From the viewpoint of the app /bin/cp which is coded in C, how does it access its input which specify the names of two files whose content is to be copied? Before calling execv() what must the shell do to prepare the arguments of execv() so that /bin/cp has access to the two file names?

Problem 1 Solution

(a) Problematic:

1
2
amigo->year = 2017;
strcpy(amigo->nickname, "fish");

The reason is that the pointer amigo has not been initialized to the address of any allocated memory space yet.

Agumentation:

1
2
3
amigo = (friend_t *)malloc(sizeof(friend_t));
amigo->nickname = (char *)malloc(5);
...

(b) fun1 takes as argument a pointer to char and returns a pointer to char. fun2 is a function pointer to a function that takes as argument a pointer to char and returns a value of type char.

1
2
3
4
char fun3(char *s) {    
while (*s != '\0')
s++;
return *(s-1);}

(c) Input /bin/cp file1 file2 is read from stdin.

main(int argc, char *argv) of /bin/cp accesses the two file names via argv[1] and argv[2].

Assuming a variable s is of type, char **s, a shell must allocate sufficient memory for s and copy /bin/cp into s[0]1, file1 into s[1], file2 into s[2], and set s[3] to NULL.

Problem 2 (30 pts)

(a) Code a function, unsigned int countdbl(long), that takes a number of type long as input, counts the number of 0s in the bit representation of the input, and returns 0 if the count is an even number, 1 if odd. Use bit processing techniques to solve the problem. (b) gcc on our lab machine, by default, will insert code to detect stack smashing at run-time. What does gcc's code try to prevent from happening? In the case of reading input from stdin (or file), what is a common scenario and programming mistake that can lead to stack smashing? Provide an example using scanf() (or fscanf()). What issound programming practice that prevents stack smashing?

Problem 2 Solution

(a)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned int countdbl(long x) {
int i;
unsigned int count = 0;
long m = 1;

for(i=0; i<64; i++) {
// Check all bits of long value from lsb to msb.
if ((x & m) == 0) count++;
x = x >> 1;
}

if ((count & 1) == 0) return 0; // Check if count is even.
else return 1; // count is odd
}

(b) When a function is called by another function, gcc tries to detect if the return address has been corrupted and, if so, terminate the running program.This is to prevent the code from jumping to unintended code such as malware.A local variable of a function declared as a 1-D array overflows by input whose length is not checked when reading from stdin (or file).Example: a function contains code

1
2
char buf[100];
scanf("%s", buf);

which may overflow buf[] since scanf() does not check for length of the input.Sound practice: use functions to read from stdin (or file) that check for length.In the above example use fgets() instead of scanf().

Problem 3 (25 pts)

Code a function that takes variable number of arguments, double multnums(char *, ...), multiplies them and returns the result as a value of type double. The fixed argument is a string that specifies how many arguments follow and their type (integer 'd' or float 'f'). For example, in the call multnums("dffd", 3, 88.2, -100.5, 44), the format string "dffd" specifies that four arguments follow where the first character 'd' means the first argument in the variable argument list is of type integer, the second and third 'f' of type float, and the fourth 'd' of type integer. Forgo checking for errors and ignore header files. What would happen in your code if multnums is called as multnums("dffd", 3, 88.2, -100.5, 44, -92, 65)? What about multnums("dffd", 3, 88.2, -100.5)? Explain your reasoning.

Problem 3 Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
double multnums(char *a, ...) {
int x;
double y, val = 1;
va_list arglist;

va_start(arglist, a);
while (*a != '\0') {
// Check the format string, character by character until EOS.
if (*a == 'd') { // Interpret argument as int.
x = va_arg(arglist, int);
val = val * x;
}
else {
// Assumes must be 'f' since forgoing error checking.
// Interpret argument as double (not float).
y = va_arg(arglist, double);
val = val * y;
}
a++;
}
va_end(arglist);
return val;
}

When a C function is defined with a variable number of arguments, it typically uses the va_arg, va_start, and va_end macros from the <stdarg.h> header to handle the variable arguments.

If the input argument count does not match the format string provided to functions like printf or scanf, it can lead to undefined behavior and potentially cause crashes, memory corruption, or incorrect output/input.

Here are some specific scenarios that can occur when there is a mismatch between the input arguments and the format string:

  • Too few arguments:
    • If there are fewer arguments than the number of format specifiers in the format string, the behavior is undefined.
    • The function may attempt to read from uninitialized memory locations or use garbage values, leading to incorrect results or crashes.
  • Too many arguments:
    • If there are more arguments than the number of format specifiers in the format string, the extra arguments will be ignored by the function.
    • However, if the extra arguments are of a different type than expected, it can lead to incorrect interpretation of the data on the stack, potentially causing crashes or memory corruption.

Bonus Problem (10 pts)

Suppose an ASCII file contains lines where each line is a sequence of characters ending with \n but for the last line which ends because the end of file is reached. The goal of main() is to read and store the lines of the ASCII into a variable, char **x, where malloc() is used to allocate just enough memory to store the content of the  file. Using only basic file I/O operations discussed in class, describe in words how your code would work to accomplish this task. Be detailed in how the arguments of malloc() are determined to store the file content in x.

Bonus Problem Solution

  1. Open file, read byte by byte until EOF is reached while counting occurrences of '' to determine the total number of lines (count plus 1). Denote this number of r.Close file.
  2. Use malloc() to allocate 1-D array, int *M, of size r of type int. Open file, read byte by byte, counting for each line the number of bytes. Store the line lengthin 1-D array M. Close file.
  3. Using 1-D array M call malloc() for each line to allocate memory to store the bytes of each line. Point x to the 1-D array of pointers to char.
  4. Open file. Read byte by byte the content of each line into 1-D array of pointersto char pointed to by x.

A sample implementation (not required for this exam) is shown as below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <stdio.h>
#include <stdlib.h>

char **read_file(const char *filename, int *r) {
FILE *file = fopen(filename, "r");
if (file == NULL) {
fprintf(stderr, "Error opening file: %s\n", filename);
return NULL;
}

// 1. Count the number of lines
int c, line_count = 0;
while ((c = fgetc(file)) != EOF) {
if (c == '\n')
line_count++;
}
line_count++; // Account for the last line without newline character
*r = line_count;

rewind(file); // Reset the file pointer to the beginning

// 2. Allocate memory for the line lengths array and store the line lengths
int *M = (int *)malloc((*r) * sizeof(int));
if (M == NULL) {
fprintf(stderr, "Error allocating memory\n");
fclose(file);
return NULL;
}

int i = 0, length = 0;
while ((c = fgetc(file)) != EOF) {
if (c == '\n') {
M[i++] = length;
length = 0;
} else {
length++;
}
}
M[i] = length; // Store the length of the last line

rewind(file); // Reset the file pointer to the beginning

// 3. Allocate memory for the array of character pointers
char **x = (char **)malloc((*r + 1) * sizeof(char *));
if (x == NULL) {
fprintf(stderr, "Error allocating memory\n");
free(M);
fclose(file);
return NULL;
}

// 4. Allocate memory for each line and read the file content
for (i = 0; i < *r; i++) {
x[i] = (char *)malloc((M[i] + 1) * sizeof(char));
if (x[i] == NULL) {
fprintf(stderr, "Error allocating memory\n");
for (int j = 0; j < i; j++)
free(x[j]);
free(x);
free(M);
fclose(file);
return NULL;
}

int j = 0;
while (j < M[i]) {
x[i][j++] = fgetc(file);
}
x[i][j] = '\0'; // Null-terminate the line
}
x[i] = NULL; // Terminate the array of character pointers

free(M);
fclose(file);
return x;
}