# 2018q3 Homework2 (lab0)
contributed by `<0xff07>`
# 環境
`uname -i`:
```
Linux 4.15.0-34-generic x86_64 GNU/Linux
```
`lscpu`:
```jsx
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 8
On-line CPU(s) list: 0-7
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 70
Model name: Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz
Stepping: 1
CPU MHz: 2289.664
CPU max MHz: 3700,0000
CPU min MHz: 800,0000
BogoMIPS: 4988.61
Virtualization: VT-x
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 6144K
L4 cache: 131072K
NUMA node0 CPU(s): 0-7
```
`cat /etc/lsb-release`:
```jsx
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.1 LTS"
```
# 實作
## `queue_t`
因為要求尾端插入與長度查詢的複雜度是 $O(1)$ ,因此在資料結構中加入 `tail` 與 `size` 分別紀錄尾端與長度:
```c=
typedef struct {
list_ele_t *head;
list_ele_t *tail;
long long size;
} queue_t;
```
## `q_free`
```C=
void q_free(queue_t *q)
{
if (!q)
return;
list_ele_t *tmp = q->head;
while (q->head) {
tmp = q->head;
q->head = q->head->next;
if (tmp->value)
free(tmp->value);
if (tmp)
free(tmp);
}
if (q)
free(q);
}
```
若 `head` 為空,則顯然記憶體已經釋放。若 `head` 非空,則「釋放 `head`」可遞迴地視為「釋放串列第一個元素(迴圈中暫存於 `tmp`) ,並釋放剩下所有元素形成的串列(`q -> head -> next`)」 。
而再釋放串列中的元素之後,再釋放 `q` 即可。
若在 `free` 中傳入空指標,在測試程式中將出現 `Attempt to free NULL` 的錯誤訊息,並且扣分(儘管 `man` 中寫到傳入空指標是合法的)。因此這邊所有的 `free` 都事先檢測是否為空指標。
## `q_insert_head`
```C=
bool q_insert_head(queue_t *q, char *s)
{
list_ele_t *newh = NULL;
char *_s = NULL;
do {
if (!q)
break;
newh = malloc(sizeof(list_ele_t));
if (!newh)
break;
_s = malloc(strlen(s) + 1);
if (!_s)
break;
strcpy(_s, s);
newh->value = _s;
newh->next = q->head;
q->head = newh;
if (!q->tail)
q->tail = newh;
q->size++;
return true;
} while (0);
if (newh)
free(newh);
if (_s)
free(_s);
return false;
}
```
使用 `do{...} while(0)` 配合 `break` ,達到 `goto` 的效果,並進行各種例外處理。並在插入後,將~~屁股~~尾端維護好。正在思考例外處理與判斷的邏輯是否能夠更精減(立刻回去翻上週作業嗚嗚)。
另外發現若將 14. 15 行改成 cpumpound-literal 的寫法:
```C=
*new = (list_ele_t){.value=_s,.next=q->head};
```
在 `git commit` 時似乎會被檢查出 memory leak,跑出:
```=
[queue.c:90]: (error) Memory leak: _s
Fail to pass static analysis.
```
但該作法可以順利通過 `qtest` 。因此不是很確定這種用法是否將造成 memory leak,或是有什麼潛在的不良影響。相關資料待查。
## `q_insert_tail`
```C=
bool q_insert_tail(queue_t *q, char *s)
{
list_ele_t *newh = NULL;
char *_s = NULL;
do {
if (!q)
break;
newh = malloc(sizeof(list_ele_t));
if (!newh)
break;
_s = malloc(strlen(s) + 1);
if (!_s)
break;
strcpy(_s, s);
newh->value = _s;
newh->next = NULL;
if (q->head)
q->tail->next = newh;
else
q->head = newh;
q->tail = newh;
q->size++;
return true;
} while (0);
if (newh)
free(newh);
if (_s)
free(_s);
return false;
}
```
大致上與前一個函數相同,紀錄尾端並在插入後維護好 `tail` 與 `head` 的指標。「新增 `list_ele_t` 」似乎可以寫成一個函式,但發現使用 `strdup` 時,即使 `man` 中提到:
>`The strdup() function returns a pointer to a new string which is a duplicate of the string s. Memory for the new string is obtained with malloc(3), and can be freed with free(3).`
即 `strdup` 中的記憶體由 `malloc` 配置,但使用 `strdup` 時測試程式,仍會判定沒有正確配置記憶體(見「其他」),因此尚未測試。
## `q_remove_head`
依照要求,除了釋放記憶體,必要時得將節點中存的字串進行複製。因此照題目要求寫出來:
```C=
bool q_remove_head(queue_t *q, char *sp, size_t bufsize)
{
if (!q || !q->head)
return false;
list_ele_t *tmp = NULL;
tmp = q->head;
q->head = q->head->next;
if (sp && tmp->value) {
strncpy(sp, tmp->value, bufsize);
sp[bufsize - 1] = 0;
}
q->size--;
if (tmp->value)
free(tmp->value);
if (tmp)
free(tmp);
return true;
}
```
## `q_size`
直接回傳存好的值大小:
```C=
int q_size(queue_t *q)
{
return q ? q->size : 0;
}
```
## `q_reverse`
假定 $L = [1,\dots,k-1,k\dots,n]$ ,則:
1. 若串列為空,或是串列僅有一個元素。則自己就是自己的反轉
2. 若已經有前 $k$ 個元素反轉的串列 $L' = [k-1 \dots 0]$:
$$
[k-1 \dots 0]\cup[k,k+1 \dots n]
$$
則將第 $k$ 個元素加入反轉串列的頭部:
$$
[k,k-1 \dots 0]\cup[k+1 \dots n]
$$
就能構造出前 $k + 1$ 個元素反轉的串列。以此遞推下去。
```C=
void q_reverse(queue_t *q)
{
if (!q || !q->head)
return;
if (q->head == q->tail)
return;
list_ele_t *buf[3] = {q->head, q->head->next, q->head->next->next};
q->head->next = NULL;
q->tail = q->head;
while (buf[2]) {
buf[1]->next = buf[0];
buf[0] = buf[1];
buf[1] = buf[2];
buf[2] = buf[2]->next;
}
buf[1]->next = buf[0];
q->head = buf[1];
}
```
前 2 個 `if` 敘述描述的是第 1. 中的狀況; 而其後的程式用於處理 2. 中的狀況。
我覺得這樣的實作看起來並不一目瞭然。似乎有待改進。
# 測試程式討論
## 記憶體管理
在 `harness.[ch]` 當中,可以看到記憶體管理相關的內容。該記憶體管理機制相關說明如下:
1. 將原先的 `malloc` 與 `free` 使用 `test_malloc` 與 `test_free` 取代。該部份可見 `harness.h` 最後面:
```C=
/* Tested program use our versions of malloc and free */
#define malloc test_malloc
#define free test_free
```
所以實際上,在 `queue.c` 中呼叫 `malloc` 與 `free` 時,其實式分別呼叫 `test_malloc` 與 `test_free` 兩個函式。以下都用 `test_malloc` 與 `test_free` 稱呼該二函式(而不是 `malloc` 與 `free`)。
2. 所有因呼叫 `test_malloc` 得來的記憶體,實際上是紀錄在一個名為 `allocated` 的一個 doubly-linked list 中。該 linked list 結點的結構為 `block_ele_t`,定義在 `harness.c` 中:
```C=
typedef struct BELE {
struct BELE *next;
struct BELE *prev;
size_t payload_size;
size_t magic_header; /* Marker to see if block seems legitimate */
unsigned char payload[0];
/* Also place magic number at tail of every block */
} block_ele_t;
```
在這個結構體中,最後一個成員 `payload[0]` 是實際給呼叫者使用的記憶體開頭。該成員使用到的 GCC 中 "Array of Length Zero" 。該功能簡便處在於:若結構體中的最後一個成員是大小為 0 的陣列,那麼可以透過 `malloc(sizeof(block_ele_t) + 希望 payload 後面接著多少記憶體) ` 來達成「最後一個成員大小可以任意指定」的效果(另外一個好處是 `malloc` 跟 `free` 一次就可以完成,不用替 `payload` 開頭的記憶體再 `malloc` 一次)。`malloc_test` 中可以發現以下程式:
```C=
void *test_malloc(size_t size)
{
if (noallocate_mode) {
report_event(MSG_FATAL, "Calls to malloc disallowed");
return NULL;
}
if (fail_allocation()) {
report_event(MSG_WARN, "Malloc returning NULL");
return NULL;
}
block_ele_t *new_block =
malloc(size + sizeof(block_ele_t) + sizeof(size_t));
...
```
這個技巧表現在程式的 `malloc(size + sizeof(block_ele_t) + sizeof(size_t))` 當中。但除了 `sizeof(block_ele_t)` 與 `size` 之外, 還多了一個 `sizeof(size_t)` 的大小。這是因為使用者實際可使用的記憶體,前後被兩個 `size_t` 整數包著,裡面各自紀錄著兩個 Magic Number,作為驗證這塊記憶體是否是由 `test_malloc` 分配的依據,以及作為記憶體是否有產生越界等不佳使用的依據。
3. 位於記憶體之前的 magic number,實際上就是結構體中 `magic_header` 這個成員,其值會在 `test_malloc` 中被指定為 `MAGICHEADER`,也就是 `0xdeadbeef` ; 在記憶體尾端的 `size_t` 整數,數值必定為 `MAGICFOOTER`,也就是 `0xbeefdead`。
4. 因為要回傳給使用者的指標實際上是 `payload` 的位置,因在程式中另外有 `find_footer` 跟 `find_header` 這兩個函式作為輔助。前這傳入 `block_ele_t*` ,回傳位於 `payload` 尾端 magic number 的位址。如 `test_malloc` 中有一段指定尾端 magic number 的程式:
```C=
*find_footer(new_block) = MAGICFOOTER;
```
而後者則是傳入使用者呼叫後得到的記憶體開頭,反推該記憶體所屬的 `block_ele_t` 的開頭位置。這兩個函式除了用在 `test_malloc` 當中,也會用再 `test_free` 當中。在呼叫 `test_free` 時,使用者傳入的,實際上是 `payload` 的位置,但釋放記憶體時,除了該記憶體之外,該記憶體所屬的結構體,也要一併釋放。因此需要有一個尋找該記憶體所屬開頭的方法。
該二函數的原理不難理解。給定 `block_ele_t`,尾端的 magic number,由 `payload_size` 進行推算即可; 對於反推記憶體所屬的結構體,則是利用 `sizeof(block_ele_t)` 反推,並且尋找反推過後的記憶體是否在 `allocated` 的清單中。若沒有,則推論為錯誤的配置。
而在 `find_header` 中有檢測傳入指標是否為空的設定。
> 我懷疑這是一個不能使用 `calloc` 跟 `strdup` 的理由。但目前還沒確認該二函數中的 `malloc` 是否有被影響到。
>
5. `test_free` 中用的原理類似:首先用 `find_header` 找出使用者準備釋放的記憶體,其所屬的結構體有沒有再 `allocated` 裡面。若傳入的指標為空指標,或是該記憶體不屬於任何一個節點,`find_header` 內部會分別傳出「試圖釋放空指標」或「記憶體損壞」等警告。
6. 若順利找到該記憶體所屬的節點,接著檢驗尾端的 magic number 是否仍為 `MAGICFOOTER`,作為記憶體有沒有被越界存取等狀況的判斷依據。若比對結果發現不合,也會發出該區段記憶體損壞的警告。
7. 若確認結果無誤,則將記憶體區段前後的 magic number 都設成 `MAGICFREE`。並且將記憶體內容都用 `FHILLCHAR` 填充。最後移出 `allocated` 這個 doubly-linked list。
> 不確定為什麼要先把 magic number 都改掉之後再釋放。我猜測是同樣大小的記憶體如果又被 `malloc` ,但裡面仍有 magic number 時,會造成誤判,以為不合法的記憶體其實是合法的。
8. 在分配記憶體時,每呼叫一次 `test_malloc` ,`allocate_count` 就會 +1; 而每呼叫一次 `test_free` ,該數值就會 -1。因此程式即將結束時,判斷該數值,即可知道是否有妥當地釋放記憶體。
## 例外處理
在 `harness.c` 中,有一些使用 `sigsetjmp`, `siglongjmp` 的函式作為例外處理。該函式並未在 `harness.c` 中被其他函式呼叫。因此推測該例外處理用在其他地方。
## shell 實作
# 思考
1. 雖然過了 `qtest`,但應該要更有「品味」一點。決定再把上週的東西仔細看一遍,並嘗試影片裡面的技巧。
3. 程式要怎麼做到「發現記憶體有沒有正確釋放」或「檢查有沒有在不該使用 `malloc` 的時候使用 `malloc`」是一件很神奇的事,決定好好研究一下。
# 其他
## 無法使用 `calloc` 及 `strdup`
使用 `strdup` 或 `calloc` 的話,測試程式會頻繁地跑出如:
>CMU 助教為了測試使用 macro 將 `malloc` 和 `free` 替換成 `test_malloc` 和 `test_free` , 請看 `harness.h` 和`harness.c`, 所以你用其他配置記憶體的函數測試程式沒辦法正確測試
>[name=amikai][color=red]
```
...
+++ TESTING trace trace-01-ops:
# Test of insert_head and remove_head
ERROR: Attempted to free unallocated block. Address = 0x55772899ccf0
ERROR: Attempted to free unallocated or co:rrupted block. Address = 0x55772899ccf0
ERROR: Corruption detected in block with address 0x55772899ccf0 when attempting to free it
ERROR: Segmentation fault occurred. You dereferenced a NULL or invalid pointer
ERROR: Removal from queue failed (1 failures total)
ERROR: Attempted to free unallocated block. Address = 0x55772899cc90
ERROR: Attempted to free unallocated or corrupted block. Address = 0x55772899cc90
ERROR: Corruption detected in block with address 0x55772899cc90 when attempting to free it
ERROR: Segmentation fault occurred. You dereferenced a NULL or invalid pointer
ERROR: Removal from queue failed (2 failures total)
free(): invalid next size (fast)
--- trace-01-ops 0/6
...
```
以及非常低的分數。後來在 `qtest.c` 中才發現:
```
/* Our program needs to use regular malloc/free */
```
雖然說明中有提及: " The function must explicitly allocate space and copy the string into it.",但不是很確定要求「明顯」到什麼程度。以至於花費不少時間懷疑人生。
## `free`
依照 `man` 中的內容:
>`The free() function frees the memory space pointed to by ptr, which must have been returned by a previous call to malloc(), calloc(), or realloc(). Otherwise, or if free(ptr) has already been called before, undefined behavior occurs. If ptr is NULL, no operation is performed.
`
因此若 `ptr` 為 `NULL`,則 `free(ptr)` 應該要合法。但以下實作:
```C=
bool q_insert_head(queue_t *q, char *s)
{
list_ele_t *newh = NULL;
char *_s = NULL;
do {
if (!q)
break;
newh = malloc (sizeof(list_ele_t));
if (!newh)
break;
int len = strlen(s);
_s = malloc(len + 1);
if (!_s)
break;
...
} while (0);
free (newh);
free (_s);
return false;
}
```
在測試時將會出現如:
```shell
+++ TESTING trace trace-07-robust:
# Test operations on NULL queue
Attempt to free NULL
Attempt to free NULL
--- trace-07-robust 0/7
```
的錯誤訊息。