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.
- Course Title: Programming in C
- Textbook: The C Programming Language (2nd Edition); Kernighan and Ritchie; March 1988
- 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 | int a, *b, *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 | char r[4]; |
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:
- prints “Hi” to stdout.
- 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 | int abc(void) { |
three times. Explain what values are returned to main() in each of
the three calls to abc().
(b) Suppose the code snippet
1 | float m, **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:
- First call returns 4. The if-statement checks 4 > 2 and
a++returns 4 before incrementinga. - Second call returns 4. Before the if-statement, the static variable
bbecomes 2 since it preserves the previous value from the first call. So the if-statement checks 4 > 3. Hencea++returns 4. - Third call return 5. Now the static variable
bbecomes 3 at the beginning of the call, and the if-statement checks 4 > 4. So the program goes to the else-part which incrementsbagain and returnsb. 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 | int readpasswd() { |
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 |
|
Bonus Problem (10 pts)
Suppose you are given the code in main.c
1 | int s[5]; |
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 | int x, *y, *z; |
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 | x = g(s); |
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 | void fun(float 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 | FILE *f; |
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:
fopen()may fail and return NULL.fscanf()may overflow 1-D array r if the character sequence indata.datexceeds 100 bytes.
To fix these, do the following modifications:
1 | f = fopen("data.dat", "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 | int main() { |
📝Notes: Freeing memory of such a 2-D integer array also needs two steps:
1 | void free_2d_array(int **array, int rows) { |
(b) The solution code can be seen below
1 | unsigned int x, m = 1; |
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.
