# 2019q1 Homework1 (lab0) contributed by < `Superdanby` > ## Environment Setup - Fedora 29 - gcc (GCC) 8.2.1 20181215 (Red Hat 8.2.1-6) 1. `sudo dnf install cppcheck colordiff` ## Function Analysis ### `queue_t *q_new()` #### [Should one type cast malloc?]() The first problem I encountered is whether I should type cast the memory returned from malloc. If I remembered right, there would be a warning/error if one didn't do it. Thus, I searched and found that it is suggested not doing so in C. Note that [C and C++ have different behaviors on this issue](https://stackoverflow.com/questions/605845/do-i-cast-the-result-of-malloc#answer-605858). ### `void q_free(queue_t *q)` Needs to iterate through the queue and release the memory of value entry of each element. ### `bool q_insert_head(queue_t *q, char *s)` Should also link tail if tail is empty. ### `bool q_insert_tail(queue_t *q, char *s)`, O(1) Should also link head if head is empty. ### `bool q_remove_head(queue_t *q, char *sp, size_t bufsize)` The first things I was thinking is that if I allocate `sp` a new block of memory with `malloc`, the caller will not receive the returned string. And then, I saw bufsize, which is when I realized the caller will `malloc` on its side. ### `int q_size(queue_t *q)`, O(1) Add a new entry in queue_t, and this could be done in O(1). ### `void q_reverse(queue_t *q)` Should not allocate or release memory. Needs to traverse the whole queue and reverse each element one by one. ## Version History ### Version 1 - Score: 56/100 - Got lots of : `ERROR: Failed to store removed value` After checking my code, I found that the `if` statement I used to check whether to copy `head->value` to `sp` in `bool q_remove_head(queue_t *q, char *sp, size_t bufsize)` is wrong. Wrong version: ```cpp= // Check sp and bufsize availability if (!sp && (temp->value) && bufsize) ``` Corrected version: ```cpp= // Check sp and bufsize availability if (sp && (temp->value) && bufsize) ``` The reason why I code it wrong is because I wanted to write `sp != NULL`, but I gone lazy and make it `!sp` which is really stupid. I should be more cautious next time. ### Version 2 - Score: 93/100 - Test remove_head with NULL argument results in: `ERROR: Freed queue, but 1 blocks are still allocated` Wrong version: ```cpp= if (sp && (temp->value) && bufsize) { strncpy(sp, temp->value, bufsize - 1); sp[bufsize - 1] = '\0'; // Release value memory free(temp->value); temp->value = NULL; } ``` Corrected version: ```cpp= if (temp->value) { if (sp && bufsize) { strncpy(sp, temp->value, bufsize - 1); sp[bufsize - 1] = '\0'; } // Release value memory free(temp->value); temp->value = NULL; } ``` `temp->value` needs to be cleared even if it couldn't be copied to `sp`. ### Version 3 - Score: 100/100 ## Automated Grading System Looking at `Makefile`, we can see that the `make test` executes `scripts/driver.py`. ### `driver.py` #### `run` method Increments the total score by the corresponding value in `maxScores` if no error detected after executing `runTrace`; the total score will not be incremented otherwise. #### `runTrace` method Executes `qtest` with different arguements composed in `clist` ## `qtest` - Checks memory leaks with `allocation_check()`. - Checks errors with `error_check()`. ### `bool error_check()` Checks if an error had occured and clears `error_occurred`. ### `size_t allocation_check()` Returns the number of blocks of memory still allocated. The number is actually stored in `allocated_count` and is incremented and decremented in `test_malloc` and `test_free` respectively. Thus, we can infer from this fact that all operations for memory allocation / release will be tested with these 2 functions. Searching all files with `find . -exec grep 'test_free\|test_malloc' {} + 2> /dev/null`, we get the following result: ```shell= ./harness.c:void *test_malloc(size_t size) ./harness.c:void test_free(void *p) ./harness.c: void *new = test_malloc(len); ./harness.h:void *test_malloc(size_t size); ./harness.h:void test_free(void *p); ./harness.h:#define malloc test_malloc ./harness.h:#define free test_free Binary file ./queue.o matches Binary file ./qtest matches ``` From the result, we can know that all files which include `harness.h` have there `free()` and `malloc()` from standard library replaced with those of `harness.h`. Next, using `find . -exec grep '#include "harness.h"' {} + 2> /dev/null`, we can see the following: ```shell= ./harness.c:#include "harness.h" ./qtest.c:#include "harness.h" ./queue.c:#include "harness.h" ``` The file `queue.c` we were modifying is also using "harness.h" ### `char *test_strdup(const char *s)` This function utilizes `test_malloc` to allocate memory, copies the contents of `s` to the newly allocated memory, and returns it. `#define strdup test_strdup` is also written in line 61 in `"harness.h"`. It seems that we should use this function in `q_remove_head` to return the string being deleted. **But**, the operation returns a **new** address other than the given one recorded in `sp` of `bool q_remove_head(queue_t *q, char *sp, size_t bufsize)`. Thus, the caller of `bool q_remove_head` would never get `q->head->value` in `sp` if the implementation is `sp = strdup(q->head->value)` ### `void sigsegvhandler(int sig)` Triggers when there is a segementation fault with `SIGSEGV`. ### `void sigalrmhandler(int sig)` Triggers when time limit exceeded with `SIGALRM`. ### `bool exception_setup(bool limit_time)` Saves states and sets up `alarm`. ```cpp= if (sigsetjmp(env, 1)) { /* Got here from longjmp */ jmp_ready = false; if (time_limited) { alarm(0); time_limited = false; } if (error_message) { report_event(MSG_ERROR, error_message); } error_message = ""; return false; } else { /* Got here from initial call */ jmp_ready = true; if (limit_time) { alarm(time_limit); time_limited = true; } return true; } ``` State is saved in line 1. It can later be recovered with `siglongjmp` in `void trigger_exception(char *msg)` if any exception is triggered. From `man alarm`: > `alarm()` arranges for a SIGALRM signal to be delivered to the calling process in `seconds` seconds. > If `seconds` is zero, any pending alarm is canceled. > In any event any previously set `alarm()` is canceled. In line 17, time limit is set with `limit_time`, which is a `bool` type. So, the time limit is 1 second if time limit is set. This function results in the behavior of `ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient` when `--valgrind` option is set, meaning `valgrind` affects the execution time of each operation: ```shell $ ./scripts/driver.py --valgrind 2> /dev/null | grep "TOTAL" --- TOTAL 79/100 $ ./scripts/driver.py 2> /dev/null | grep "TOTAL" --- TOTAL 100/100 ```