# 2021q1 Homework1 (lab0)
contributed by < `shanihsu` >
###### tags: `linux2021`
> [作業要求](https://hackmd.io/@sysprog/linux2021-lab0)
## 開發環境
```
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04.1 LTS (Focal Fossa)"
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
```
## 實作程式
> [GitHub](https://github.com/shanihsu/lab0-c)
### 定義 `queue_t`
為了使 `q_insert_tail` 和 `q_size` 滿足 O(1) 時間複雜度,加入`tail`跟`size`。
* `tail` : queue的尾端記憶體位置
* `size` : queue的元素個數
```c=
typedef struct {
list_ele_t *head;
int size;
list_ele_t *tail;
} queue_t;
```
### 實作 `q_new`
* 將 `size` 跟 `tail` 初始化
* 當 `malloc` 回傳 NULL 時,代表記憶體分配空間錯誤,則 `q_new` 所指向的位置為 NULL。
```c=
queue_t *q_new()
{
queue_t *q = malloc(sizeof(queue_t));
if (!q)
return NULL;
q->head = NULL;
q->tail = NULL;
q->size = 0;
return q;
}
```
### 實作 `q_free`
* 先檢查 `q` 是否為空,若 `q` 有指向的記憶體位置,則至 `queue` 的 `head` 開始搜尋可釋放的空間。
* 使用 while loop ,從 `queue` 的 `head` 開始,依次將 `queue` 中的元素及儲存 `value` 的記憶體釋放掉。
* 最後再將 `q` 釋放。
```c=
void q_free(queue_t *q)
{
if (q) {
while (q->head) {
list_ele_t *tmp;
tmp = q->head;
free(tmp->value);
q->head = tmp->next;
free(tmp);
}
free(q);
}
}
```
### 實作 `q_insert_head`
* 當 `q` 非空時, `malloc` 一段記憶體空間給 `queue` ,並確保記憶體空間足夠,同時以 `newh` 儲存 `queue` 的起始記憶體位置。
* 計算字串所需長度:字元 `s` 長度,加上一個結束字元 `\0`
* `malloc` 一段記憶體空間儲存欲插入元素的 `value`,若 `malloc`錯誤,需將先前儲存 `newh` 的記憶體區段釋放,以避免 memory leak 。
* 若 `q->head` 是 NULL ,表示 queue 中沒有元素,插入元素 `newh` 後,該 queue 的 `head` 跟 `tail` 均為此新元素 `tail` 。
```c=
bool q_insert_head(queue_t *q, char *s)
{
list_ele_t *newh;
if (!q)
return false;
if (!(newh = malloc(sizeof(list_ele_t))))
return false;
size_t len = strlen(s) + 1;
if (!(newh->value = malloc(len * sizeof(char)))) {
free(newh);
return false;
}
memcpy(newh->value, s, len);
newh->next = q->head;
if (!q->head)
q->tail = newh;
q->head = newh;
q->size++;
return true;
}
```
### 實作`q_insert_tail`
* 新增 `tail` 在 struct `queue_t` 中,以達成 O(1) 時間複雜度
* 在 insert 新的元素時,可直接將 `queue` 的 `tail->next` 設成新元素,不須每次重新搜尋 `queue` 的尾端
```c=
bool q_insert_tail(queue_t *q, char *s)
{
list_ele_t *newh;
if (!q)
return false;
if (!(newh = malloc(sizeof(list_ele_t))))
return false;
size_t len = strlen(s) + 1;
if (!(newh->value = malloc(len * sizeof(char)))) {
free(newh);
return false;
}
memcpy(newh->value, s, len);
newh->next = NULL;
if (!q->head) {
q->head = newh;
} else {
q->tail->next = newh;
}
q->tail = newh;
q->size++;
return true;
}
```
### 實作 `q_remove_head`
* 檢查 `q` 跟 `q->head` 是否為空
* 檢查 `sp` 是否為空,並比較 `head->value` 字元長度與 `bufsize` 大小,並將 `head->value` 字元儲存到 `sp` 。
* 最後再將 `head` 及儲存 `head->value` 的記憶體釋放掉
```c=
bool q_remove_head(queue_t *q, char *sp, size_t bufsize)
{
if (!q || !q->head)
return false;
size_t len = strlen(q->head->value);
if (sp) {
if (len < bufsize) {
memcpy(sp, q->head->value, len);
sp[len] = '\0';
} else {
memcpy(sp, q->head->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
}
list_ele_t *tmp;
tmp = q->head;
q->head = q->head->next;
free(tmp->value);
free(tmp);
q->size--;
return true;
}
```
* 當 `head->value` 字元長度小於 `bufsize` 時,字元 `head->value` 在存入 `sp` 後,需加上 `\0` ,也就是上面程式碼中的第9行,若沒有加,會導致 `bufsize` 找不到終止字元不斷輸出,而產生以下情況:
```
$ ./qtest
cmd> new
q = []
cmd> ih monkey
q = [monkey]
cmd> rh
Removed monkeyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX from queue
q = []
```
* 原本在上述程式碼中的第17,18行中間有加入以下程式碼:
```c=
if (!q->head){
free(q->tail->value);
free(q->tail);
}
```
`make` 後執行得到以下錯誤訊息:
```
$ ./qtest
cmd> new
q = []
cmd> ih monkey
q = [monkey]
cmd> rh
ERROR: Attempted to free unallocated block. Address = 0x5555555555555555
Bus error (core dumped)
```
由上述錯誤訊息得知,當 `queue` 中只剩下一個元素時,其 `head` 跟 `tail` 指向的記憶體位置相同,若將 `tail` free 掉之後,再 free `head` 會導致 free unallocated block 。
### 實作 `q->size`
* 檢查 `q` 跟 `q->head` 是否為空,若為空,直接 return 0,若非空,則 return `q->size` 。
```c=
int q_size(queue_t *q)
{
if (!q || !q->head) {
return 0;
}
return q->size;
}
```
* 原先在上述程式碼中第4行是寫:
```c=4
q->size = 0;
```
`make` 後執行得到以下錯誤訊息:
```
$ ./qtest
cmd> free
q = NULL
cmd> size
Warning: Calling size on null queue
Segmentation fault occurred. You dereferenced a NULL or invalid pointer
Aborted (core dumped)
```
由上述得知,因為 `queue` 是 NULL,所以 `q->size` 尚未被宣告,將其賦值會出錯。
### 實作 `q_reverse`
* 當 `q` 及 `q->head` 及 `q->head->next`都非空時,需要做 reverse
* 使用 while loop ,反覆執行將 node 斷開後重新反向鏈結,直到 `q->head->next` 為空, while loop 中的變數意義如下:
* `prev` : 儲存當下已完成 reverse 的 `queue` 之 `head`
* `q->head` : 儲存當下欲斷開 link 的 node 位置
* `current` : 儲存當下欲斷開 link 的 node 所連接的下一個 node 位置
```c=
void q_reverse(queue_t *q)
{
if (!(!q || !q->head || !q->head->next)) {
list_ele_t *current = q->head->next;
list_ele_t *prev = q->head;
prev->next = NULL;
q->tail = prev;
q->head = current;
while (q->head->next) {
current = q->head->next;
q->head->next = prev;
prev = q->head;
q->head = current;
}
q->head->next = prev;
}
}
```
* 開發初期,第七行程式碼漏加, reverse 後沒有設定好 `tail` ,導致 queue 在 reverse 後, cmd 中執行 `size` 指令結果跟 `queue` 中實際的元素個數不同,其後執行的指令均有誤,且最後 `free` 時,無法完全釋放記憶體,造成以下錯誤訊息:
```
$ ./qtest
cmd> new
q = []
cmd> ih dog
q = [dog]
cmd> ih cat
q = [cat dog]
cmd> ih monkey
q = [monkey cat dog]
cmd> reverse
q = [dog cat monkey]
cmd> it fish
q = [dog fish]
cmd> size
Queue size = 4
q = [dog fish]
cmd> free
q = NULL
ERROR: Freed queue, but 4 blocks are still allocated
```
### 實作 `q_sort`
* 使用 merge sort 排序,排序後將 `tail` 重新設置
* merge sort 分成 split 跟 merge排序 兩部份,在 split 部份使用 recursive ,在 merge 排序部份使用 while loop
* 使用 strcmp 函式先比 `value` 大小,再由此順序排序
```c=
void q_sort(queue_t *q)
{
if (!q || !q->head || !q->head->next) {
return;
}
q->head = merge_sort(q->head);
list_ele_t *tmp = q->head;
while (tmp->next) {
tmp = tmp->next;
}
q->tail = tmp;
}
list_ele_t *merge_sort(list_ele_t *head)
{
if (!head || !head->next)
return head;
list_ele_t *fast = head->next;
list_ele_t *slow = head;
// split list
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
fast = slow->next;
slow->next = NULL;
// sort each list
list_ele_t *lhs = merge_sort(head);
list_ele_t *rhs = merge_sort(fast);
// merge sorted lhs and sorted rhs
list_ele_t *tmp = NULL;
if (lhs && rhs) {
if (strcmp(lhs->value, rhs->value) < 0) {
head = lhs;
lhs = lhs->next;
} else {
head = rhs;
rhs = rhs->next;
}
}
tmp = head;
while (lhs && rhs) {
if (strcmp(lhs->value, rhs->value) < 0) {
tmp->next = lhs;
tmp = tmp->next;
lhs = lhs->next;
} else {
tmp->next = rhs;
tmp = tmp->next;
rhs = rhs->next;
}
}
if (lhs)
tmp->next = lhs;
if (rhs)
tmp->next = rhs;
return head;
}
```
* 最一開始參考[作業要求](https://hackmd.io/@sysprog/linux2021-lab0)中的 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 資料,將 merge sort 的 merge 部份也用 recursive 完成,實作後發現會 segmentation fault ,改用 while loop 完成 merge 部份,即可通過評分。
* 下方為 merge 部份使用 recursive 完成的程式碼:
```c=
void q_sort(queue_t *q)
{
if (!q || !q->head || !q->head->next) {
return;
}
q->head = merge_sort(q->head);
list_ele_t *tmp = q->head;
while (tmp->next) {
tmp = tmp->next;
}
q->tail = tmp;
}
list_ele_t * merge(list_ele_t *lhs, list_ele_t *rhs)
{
// merge with recursive
if (!rhs){
return lhs;
}
if (!lhs){
return rhs;
}
if (strcmp(lhs->value, rhs->value) < 0) {
lhs->next = merge(lhs->next, rhs);
return lhs;
} else {
rhs->next = merge(lhs, rhs->next);
return rhs;
}
}
list_ele_t *merge_sort(list_ele_t *head)
{
if (!head || !head->next)
return head;
list_ele_t *fast = head->next;
list_ele_t *slow = head;
// split list
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
fast = slow->next;
slow->next = NULL;
// sort each list
list_ele_t *lhs = merge_sort(head);
list_ele_t *rhs = merge_sort(fast);
return merge(lhs, rhs);
}
```
`make` 後執行結果,在 `queue` 元素個數小於 1000000 個時,均可正常排序,但在 `queue` 元素個數大於 1000000 個時(例如 : trace-15-perf.cmd 檔案),會造成 Segmentation fault ,使用 Valgrind 追蹤 trace-15-perf.cmd 這個檔案,結果如下:
```
$ valgrind -q --leak-check=full ./qtest -v 3 -f traces/trace-15-perf.cmd
==9406== ./.valgrindrc was not read as it is either not a regular file,
==9406== or is world writeable, or is not owned by the current user.
==9406== Invalid read of size 8
==9406== at 0x10B09B: do_sort (qtest.c:564)
==9406== by 0x10CE65: interpret_cmda (console.c:221)
==9406== by 0x10D2FC: interpret_cmd (console.c:244)
==9406== by 0x10DB3F: cmd_select (console.c:594)
==9406== by 0x10DE0C: run_console (console.c:667)
==9406== by 0x10C2BF: main (qtest.c:788)
==9406== Address 0x0 is not stack'd, malloc'd or (recently) free'd
==9406==
Segmentation fault occurred. You dereferenced a NULL or invalid pointer
Aborted (core dumped)
```
:::warning
由上述錯誤訊息得知,造成 segmentation fault 的原因是 `Invalid read of size 8` ,試圖讀取一個非法的區域,更進一步追蹤結果如下:
:::
```
$ scripts/driver.py -p /tmp/qtest.p1jRLH --valgrind -t 15
--- Trace Points
+++ TESTING trace trace-15-perf:
==10126== ./.valgrindrc was not read as it is either not a regular file,
==10126== or is world writeable, or is not owned by the current user.
==10126== Memcheck, a memory error detector
==10126== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==10126== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==10126== Command: /tmp/qtest.p1jRLH -v 1 -f ./traces/trace-15-perf.cmd
==10126==
# Test performance of insert_tail, size, reverse, and sort
==10126== Stack overflow in thread #1: can't grow stack to 0x1ffe801000
==10126== Stack overflow in thread #1: can't grow stack to 0x1ffe801000
==10126== Can't extend stack to 0x1ffe8010a8 during signal delivery for thread 1:
==10126== no stack segment
==10126==
==10126== Process terminating with default action of signal 11 (SIGSEGV)
==10126== Access not within mapped region at address 0x1FFE8010A8
==10126== Stack overflow in thread #1: can't grow stack to 0x1ffe801000
==10126== at 0x10E589: merge (queue.c:257)
==10126== If you believe this happened as a result of a stack
==10126== overflow in your program's main thread (unlikely but
==10126== possible), you can try to increase the size of the
==10126== main thread stack using the --main-stacksize= flag.
==10126== The main thread stack size used in this run was 8388608.
==10126== Stack overflow in thread #1: can't grow stack to 0x1ffe801000
==10126==
==10126== Process terminating with default action of signal 11 (SIGSEGV)
==10126== Access not within mapped region at address 0x1FFE801F70
==10126== Stack overflow in thread #1: can't grow stack to 0x1ffe801000
==10126== at 0x4831134: _vgnU_freeres (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_core-amd64-linux.so)
==10126== If you believe this happened as a result of a stack
==10126== overflow in your program's main thread (unlikely but
==10126== possible), you can try to increase the size of the
==10126== main thread stack using the --main-stacksize= flag.
==10126== The main thread stack size used in this run was 8388608.
==10126==
==10126== HEAP SUMMARY:
==10126== in use at exit: 207,010,436 bytes in 4,000,050 blocks
==10126== total heap usage: 4,000,094 allocs, 44 frees, 207,015,514 bytes allocated
==10126==
==10126== LEAK SUMMARY:
==10126== definitely lost: 0 bytes in 0 blocks
==10126== indirectly lost: 0 bytes in 0 blocks
==10126== possibly lost: 0 bytes in 0 blocks
==10126== still reachable: 207,010,436 bytes in 4,000,050 blocks
==10126== suppressed: 0 bytes in 0 blocks
==10126== Rerun with --leak-check=full to see details of leaked memory
==10126==
==10126== For lists of detected and suppressed errors, rerun with: -s
==10126== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
--- trace-15-perf 0/6
--- TOTAL 0/6
```
:::warning
由上述追蹤結果發現, 錯誤訊息顯示 `Stack overflow in thread #1: can't grow stack to 0x1ffe801000` ,懷疑其是因為執行 recursive 過程, stack 不斷堆疊沒有釋放,導致記憶體不足而 segmentation fault ,為了驗證此推論,使用 Massif 工具加以分析,得到以下的圖。
:::
![](https://i.imgur.com/646qfHI.png)
:::warning
* 由上圖可看出記憶體使用狀況,在 recursive 過程不斷升高,但因在 trace-15-perf.cmd 檔案中的指令本身就沒有 `free` ,所以這樣的記憶體使用狀況是合理的,無法由此看出 segmentation fault 的原因。
* 因為暫時無法找出哪裡出錯,我將 merge sort 中 merge 的部份改用 while loop 代替 recursive 實作,以通過評分。
:::
:::info
疑問 :
目前還無法肯定導致 segmentation fault 的原因,只能歸納出以下兩種可能 :
1. merge sort 中 merge 的部份如果使用 recursive 來撰寫,會影響其記憶體使用空間。
2. 在我上述已撰寫的程式碼中, function `merge` 邏輯考慮不完整,導致讀取一個非法的區域。
:::
## 以 Valgrind 分析記憶體問題
### 安裝
```
$ sudo apt install massif-visualizer
```
### 測試
```
$ make valgrind
```
執行後某部份結果如下 :
```
+++ TESTING trace trace-17-complexity:
==19887== ./.valgrindrc was not read as it is either not a regular file,
==19887== or is world writeable, or is not owned by the current user.
==19887== Memcheck, a memory error detector
==19887== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==19887== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==19887== Command: /tmp/qtest.lRmLYV -v 1 -f ./traces/trace-17-complexity.cmd
==19887==
# Test if q_insert_tail and q_size is constant time complexity
Probably constant time
Probably constant time
==19887==
==19887== HEAP SUMMARY:
==19887== in use at exit: 311 bytes in 21 blocks
==19887== total heap usage: 94,736,463 allocs, 94,736,442 frees, 4,922,653,226 bytes allocated
==19887==
==19887== LEAK SUMMARY:
==19887== definitely lost: 0 bytes in 0 blocks
==19887== indirectly lost: 0 bytes in 0 blocks
==19887== possibly lost: 0 bytes in 0 blocks
==19887== still reachable: 311 bytes in 21 blocks
==19887== suppressed: 0 bytes in 0 blocks
==19887== Rerun with --leak-check=full to see details of leaked memory
==19887==
==19887== For lists of detected and suppressed errors, rerun with: -s
==19887== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
--- trace-17-complexity 5/5
--- TOTAL 100/100
```
由上述執行結果可知
* 在 HEAP SUMMARY 中, `total heap usage: 94,736,463 allocs, 94,736,442 frees, 4,922,653,226 bytes allocated` ,由此可看出,==還有 `94,736,463 - 94,736,442 = 21` 的記憶體沒有被釋放,但程式並沒有因此出錯,是為何呢?==
* 在 LEAK SUMMARY 中, `still reachable: 311 bytes in 21 blocks` ,表示程式結束時有未釋放的記憶體,==較疑惑的是,既然記憶體沒有被完全釋放,為何評分程式會給過呢?==
也可使用以下指令測試單一個 case , `<tid>` 為 cmd 檔案的編號。
```
$ scripts/driver.py -p /tmp/qtest.lRmLYV --valgrind -t <tid>
```
### 搭配視覺化工具 Massif
* 執行以下指令可產生一個 massif.out 檔, e.g. massif.out.20796
* 參數 <test-data> 為欲 test 的 file 名稱, e.g. traces/trace-0x-ops.cmd
```
$ valgrind --tool=massif ./qtest -f <test-data>
```
* 執行以下指令,將上述指令產生的 massif.out 檔視覺化
* 參數 <file-name> 為上述指令產生的 massif.out 檔案名稱, e.g. massif.out.20796
```
$ massif-visualizer <file-name>
```
執行結果如下 :
![](https://i.imgur.com/stHf1po.png)
:::info
當程式執行結束時,記憶體需要被完全釋放,故此圖最終點應落在 0。
:::