--- tags: sysprog21 --- # 2021q1 Homework1 (lab0) contributed by < akamayu-ouo > # Environment ```shell $ uname -a Linux ... 5.10.15-1-MANJARO #1 SMP PREEMPT Wed Feb 10 10:42:47 UTC 2021 x86_64 GNU/Linux $ gcc --version | head -1 gcc (GCC) 10.2.0 $ valgrind --version valgrind-3.16.1 ``` # Overview - [x] C programming lab - [x] Fix `qtest` - [ ] Use Massif to visualize "simulation" - [ ] Add coroutine into `qtest` - [ ] Explain the use of "select" in this program - [ ] Explain how `linenoise` works - [ ] Read the paper - [ ] Find defect in the system, then try to fix it # C Programming Lab - [Github repo](https://github.com/akamayu-ouo/lab0-c) - PDF: {%pdf https://www.cs.cmu.edu/afs/cs/academic/class/15213-s20/www/labs/cprogramminglab.pdf %} ## Memory management 將記憶體管理集中以減少重複的程式碼,出錯時也容易檢查。在這份作業中 `queue_t` 以及 `list_ele_t` 都需要動態配置記憶體,前者的記憶體管理由 `q_new` 和 `q_free` 兩個函式負責;後者由我自己增加的兩個函式負責。分別是: 1. `list_ele_t *new_element(char *, list_ele_t *)`: 配置記憶體並初始化新的 `list_ele_t*` ,若輸入的字串為 `NULL` 或有其他錯誤,都輸出 `NULL`。 ```c= inline static list_ele_t *new_element(char *s, list_ele_t *next) { list_ele_t *new_ele = NULL; if (!s || !(new_ele = malloc(sizeof(list_ele_t))) || !(new_ele->value = strdup(s))) free(new_ele); return NULL; } new_ele->next = next; return new_ele; } ``` 2. `list_ele_t *del_element(list_ele_t *e)`: 釋放 `e` 與 `e->value` 的記憶體,回傳 `e->next` 以方便使用。這個函式假設輸入不會是 `NULL` ```c= inline static list_ele_t *del_element(list_ele_t *e) { list_ele_t *next = e->next; free(e->value); free(e); return next; } ``` 值得注意的是根據 [free(3)](https://linux.die.net/man/3/free) > 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. `free` 在輸入為 `NULL` 時不會出問題,因此不做檢查。 ## `queue_t` structure 增加兩個變數: `size` 與 `tail` ```language=c struct { size_t size; list_ele_t *head; list_ele_t **tail; } queue_t; ``` 動態地更新 `size` 與 `tail` 以達成常數時間的 `q_size` 以及 `q_insert_tail`。 ## `q_new` 與 `q_free` `q_new` 只有在配置記憶體成功的時候初始化內容。`q_free` 則從 `head` 開始依序使用 `del_element` 返還記憶體,最後再釋放佇列本身的空間。 ## Element Insertion 定義一個 static 函數來簡化實做。參數 `link` 指向「要新增元素的記憶體位置」。 - 備份 `*link` - 嘗試取得新的元素 - 失敗:恢復 `*link` 並回傳 `false` - 成功:更新佇列大小與 `tail`,回傳 `true` ```c= inline static bool insert_at(queue_t *q, list_ele_t **link, char *s) { list_ele_t *orig = *link; if (!(*link = new_element(s, orig))) { *link = orig; return false; } ++q->size; if (*q->tail) q->tail = &(*q->tail)->next; return true; } ``` 如此一來, `q_insert_head` 與 `q_insert_tail` 都能簡化成一行 ```c= bool q_insert_head(queue_t *q, char *s) { return q && insert_at(q, &q->head, s); } bool q_insert_tail(queue_t *q, char *s) { return q && insert_at(q, q->tail, s); } ``` ## Element Removal `q_remove_head` 將佇列的第一個元素去除,並交由 `del_element` 釋放其空間。並在 `sp` 不為 `NULL` 的時候將元素中的字串複製過去。 :::warning 這邊有個地方不是很清楚,當 `sp` 不為 `NULL` 但元素字串是 `NULL` 時應該將 `sp` 更新為 `NULL` 、空字串 `\0` 抑或是完全不動它呢?~~還是我們能保證字串一定存在?但插入函式也都沒有定義字串不存在時的行為?~~ 我目前的想法是:只要我保證佇列的 API 不會產生沒有字串的 `list_ele_t` ,那遇到這個情形就~~一定是使用者亂搞~~,什麼都不動就好。 附上原始的註解如下: > If sp is non-NULL and an element is removed, copy the removed string to *sp (up to a maximum of bufsize-1 characters, plus a null terminator. ::: ```c= bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { if (0 == q_size(q)) return false; if (sp) { strncpy(sp, q->head->value, bufsize - 1); sp[bufsize - 1] = '\0'; } q->head = del_element(q->head); if (--q->size <= 1) q->tail = (q->head) ? &q->head->next : &q->head; return true; } ``` ## Queue size 可以利用 `q_size(q)<n` 將「佇列不存在」與「佇列長度小於 n 」的檢查合而為一,讓 edge cases 的處理更加簡潔。 ## Reverse 先將 linked list 反轉,再更新 `head` 與 `tail`。 ## Sort 基本上使用 [merge sort](https://en.wikipedia.org/wiki/merge_sort) 實做(一開始嘗試用 [quick sort](https://en.wikipedia.org/wiki/quick_sort) 硬幹,但忘記它最差情況會需要花 $O(n^2)$ 的時間,測試直接爆掉)。主要分為三個步驟: 1. 將 linked list 分為兩段 使用類似 [龜兔賽跑](https://hackmd.io/@sysprog/c-linked-list?type=view#circular-linked-list) 的方法,從 `head` 出發,當兔子跑過整條 linked list 的時候烏龜會停在 linked list 中央。 3. 分別進行排序(遞迴呼叫) 4. 將兩段排序好的 linked list 合成一條 實做中另外定義了一個 struct `range_t` ```c= struct { list_ele_t* head; list_ele_t** tail; } range_t; ``` 這樣在傳遞參數的時候比較簡潔,程式碼也比較整齊。 ```c= inline static bool too_close(list_ele_t *const head, list_ele_t *const *const tail) { /* Order matters, the first clause ensures `head` * would not be NULL */ return head == *tail || &head->next == tail; } inline static range_t split(list_ele_t *const head, list_ele_t *const *const tail) { list_ele_t *slow = head; for (list_ele_t *fast = head->next; !too_close(fast, tail);) { fast = fast->next->next; slow = slow->next; } return (range_t){.head = slow->next, .tail = &slow->next}; } inline static range_t merge(range_t r1, range_t r2) { list_ele_t *merged = NULL, **tail = NULL; for (tail = &merged; r1.head && r2.head; tail = &((*tail)->next)) { list_ele_t **source = (cmp_elem(r1.head, r2.head) < 0) ? (&r1.head) : (&r2.head); *tail = *source; *source = (*source)->next; } range_t *result = r1.head ? &r1 : &r2; *tail = result->head; result->head = merged; return *result; } static range_t merge_sort(list_ele_t *head, list_ele_t **tail) { if (too_close(head, tail)) return (range_t){.head = head, .tail = tail}; range_t slit = split(head, tail); *slit.tail = *tail = NULL; return merge(merge_sort(head, slit.tail), merge_sort(slit.head, tail)); } void q_sort(queue_t *q) { if (q_size(q) <= 1) return; range_t range = merge_sort(q->head, q->tail); q->head = range.head; q->tail = range.tail; } ``` # Fix `qtest` ## Sanitizer ### 問題敘述 用 `make SANITIZER=1` 編譯 `qtest` 。再於 `qtest` 中執行 `help` 得到了以下的錯誤: ```= ================================================================= ==4193211==ERROR: AddressSanitizer: global-buffer-overflow on address 0x5590791906c0 at pc 0x55907917a112 bp 0x7ffeb4778e10 sp 0x7ffeb4778e00 READ of size 4 at 0x5590791906c0 thread T0 #0 0x55907917a111 in do_help_cmd /.../sysprog21/Week1/lab0-c/console.c:306 #1 0x55907917a225 in interpret_cmda /.../sysprog21/Week1/lab0-c/console.c:220 #2 0x55907917a9f3 in interpret_cmd /.../sysprog21/Week1/lab0-c/console.c:243 #3 0x55907917c0ca in run_console /.../sysprog21/Week1/lab0-c/console.c:659 #4 0x559079178da3 in main /.../sysprog21/Week1/lab0-c/qtest.c:783 #5 0x7f8a969a1b24 in __libc_start_main (/usr/lib/libc.so.6+0x27b24) #6 0x5590791765ed in _start (/.../sysprog21/Week1/lab0-c/qtest+0x85ed) 0x5590791906c1 is located 0 bytes to the right of global variable 'echo' defined in 'console.c:59:13' (0x5590791906c0) of size 1 SUMMARY: AddressSanitizer: global-buffer-overflow /.../sysprog21/Week1/lab0-c/console.c:306 in do_help_cmd Shadow bytes around the buggy address: 0x0ab28f22a080: f9 f9 f9 f9 04 f9 f9 f9 f9 f9 f9 f9 04 f9 f9 f9 0x0ab28f22a090: f9 f9 f9 f9 00 f9 f9 f9 f9 f9 f9 f9 00 f9 f9 f9 0x0ab28f22a0a0: f9 f9 f9 f9 00 f9 f9 f9 f9 f9 f9 f9 00 00 00 00 0x0ab28f22a0b0: 04 f9 f9 f9 f9 f9 f9 f9 00 00 00 00 00 00 00 00 0x0ab28f22a0c0: 00 00 f9 f9 f9 f9 f9 f9 01 f9 f9 f9 f9 f9 f9 f9 =>0x0ab28f22a0d0: 01 f9 f9 f9 f9 f9 f9 f9[01]f9 f9 f9 f9 f9 f9 f9 0x0ab28f22a0e0: 04 f9 f9 f9 f9 f9 f9 f9 04 f9 f9 f9 f9 f9 f9 f9 0x0ab28f22a0f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0ab28f22a100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0ab28f22a110: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0ab28f22a120: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb Shadow gap: cc ==4193211==ABORTING ``` 從輸出第四行可知錯誤發生在 console.c 的第 306 行,並且是錯誤地存取了在 `echo` 這個變數的附近的地址。 ```c=305 while (plist) { report(1, "\t%s\t%d\t%s", plist->name, *plist->valp, plist->documentation); plist = plist->next; } ``` 變數 `echo` 宣告在第 59 行。 ```c=59 static bool echo = 0; ``` 檢查程式碼,發現在 `init_cmd` 中 `bool*` 被轉成 `int*` ,造成之後在印出時被當作 `int` 讀取,讀到沒有配置的空間。 ```c void init_cmd() { ... add_param("simulation", (int *) &simulation, "Start/Stop simulation mode", NULL); add_param("verbose", &verblevel, "Verbosity level", NULL); add_param("error", &err_limit, "Number of errors until exit", NULL); add_param("echo", (int *) &echo, "Do/don't echo commands", NULL); ... } ``` 會需要轉成 `int*` 的原因推測是因為 `PELE` 中的 `valp` 是整數指標。 ```c typedef struct PELE param_ele, *param_ptr; struct PELE { char *name; int *valp; char *documentation; /* Function that gets called whenever parameter changes */ setter_function setter; param_ptr next; }; ``` ### 解決方法 除了 `echo` 以外,還有一個變數 `simulation` 也有同樣的情況。將它們都改宣告為 `int` ,並移除型別轉換就好了。 然而這顯現了另一個問題,若將上述的兩個變數設定成其他整數,程式也不會出錯。但既然是「開關」行為的變數,值域應該只有 `0` 與 `1`。要解決這個問題,可以在 parse 命令的時候檢查,也可以對有 `bool` 數值的選項另外增加 struct,但這兩個選項都會需要更動很多地方。 ## Valgrind ### 問題敘述 測試後發現當 `.cmd_history` 有東西的時候使用 `qtest -f <FILENAME>` 會出現 memory leak 。 ``` $ cat .cmd_history quit $ cat dummy_input $ valgrind ./qtest -f dummy_input Freeing queue ==4063868== 5 bytes in 1 blocks are still reachable in loss record 1 of 2 ==4063868== at 0x483E77F: malloc (vg_replace_malloc.c:307) ==4063868== by 0x4A4A5FE: strdup (in /usr/lib/libc-2.33.so) ==4063868== by 0x10F992: linenoiseHistoryAdd (linenoise.c:1240) ==4063868== by 0x110488: linenoiseHistoryLoad (linenoise.c:1329) ==4063868== by 0x10BD9A: main (qtest.c:770) ==4063868== ==4063868== 160 bytes in 1 blocks are still reachable in loss record 2 of 2 ==4063868== at 0x483E77F: malloc (vg_replace_malloc.c:307) ==4063868== by 0x10F952: linenoiseHistoryAdd (linenoise.c:1228) ==4063868== by 0x110488: linenoiseHistoryLoad (linenoise.c:1329) ==4063868== by 0x10BD9A: main (qtest.c:770) ==4063868== ``` ### 錯誤發生原因 從錯誤訊息推測是 `history` 沒有被釋放。也能發現出問題的記憶體是由 `linenoiseHistoryAdd` 配置。 有可能會釋放 `history` 的函式只有 `linenoise.c` 中的 `freeHistory` 以及前文提到的 `linenoiseHistorySetMaxLen` 。後者只有在 `history` 不為 `NULL` 時會重新配置記憶體,而它在主程式中被呼叫時 `history` 還未被初始化,因此它並不會對記憶體進行操作,僅僅是更新 `history_max_len` 而已。因此只有可能是 `freeHistory` 沒有被呼叫造成錯誤。 ```c= ine linenoiseHistorySetMaxLen(int len) { char **new; ... if(history) { ... new = malloc(sizeof(char *) * len); ... free(history); history = new; } ... } ``` ```graphviz digraph { rankdir=LR; node[shape=oval] freeHistory -> linenoiseAtExit -> enableRawMode ; enableRawMode:e -> linenoiseRaw:w; node[shape=legend] linenoiseRaw:e -> linenoise:w; enableRawMode:e -> linenoisePrintKeyCodes:w; subgraph cluster1{ label="linenoise.c" freeHistory linenoiseAtExit enableRawMode linenoisePrintKeyCodes linenoiseRaw linenoise } subgraph cluster2 { label="console.c" run_console; linenoise -> run_console; } subgraph cluster3 { label="main.c" run_console -> main:w; } } ``` 上圖是有機會執行到 `freeHistory` 的函式的函式 ,橢圓形表示該函式是 static 函式。 其中函式 `enableRawMode` 將 `linenoiseAtExit` 交給 `atexit` 註冊 ```c if(!atexit_registered) { atexit(linenoiseAtExit); atexit_registered = 1; } ``` 根據 [man page](https://man7.org/linux/man-pages/man3/atexit.3.html) ,經由 `atexit` 註冊的函式會在主程式正常結束時被呼叫。 > The **atexit**() function registers the given *function* to be called at normal process termination, either via **exit**(3) or via return from the program's *main*(). Functions so registered are called in the reverse order of their registration; no arguments are passed. 換句話說,若要將 `history` 釋放掉,程式執行期間至少要呼叫 `enableRawMode` 一次。否則 `linenoiseAtExit` 不會被註冊,主程式結束時也不會將空間歸還,造成 memory leak。 因為 `enableRawMode` 只有透過 `linenoisePrintKeyCodes` 或 `linenoise` 才能呼叫到,但前者沒有在任何地方被呼叫,後者只被 `run_console` 呼叫。然而`run_console` 只在沒有輸入檔案時會呼叫到 `linenoise`。 ```c bool run_console(char *infile_name) { ... if (!has_infile) { char *cmdline; while ((cmdline = linenoise(prompt)) != NULL) { ... } } else { while (!cmd_done()) cmd_select(0, NULL, NULL, NULL, NULL); } return err_cnt == 0; } ``` #### 驗證 以下是使用 `gprof` 紀錄到被呼叫的函式名稱(輸入內容都是 `./trace-13-perf.cmd`)。 - 以檔案輸入: `$ ./qtest -f <FILENAME>` ``` [25] add_cmd [41] finish_cmd [49] q_free [30] add_param [29] free_array [11] q_insert_head [36] add_quit_helper [20] free_block [15] q_insert_tail [37] allocation_check [22] free_string [16] q_new [28] calloc_or_fail [32] get_int [9] q_reverse [1] cmd_select [42] init_cmd [50] queue_quit [38] delta_time [43] init_time [31] report [39] do_comment_cmd [2] interpret_cmd [19] report_noreturn [8] do_insert_head [3] interpret_cmda [4] run_console [12] do_insert_tail [24] linenoiseHistoryAdd [34] set_cautious_mode [14] do_new [44] linenoiseHistoryLoad [51] set_echo [33] do_option_cmd [45] linenoiseHistorySetMaxLen [35] set_noallocate_mode [40] do_quit_cmd [46] linenoiseSetCompletionCallback [52] set_verblevel [7] do_reverse [21] malloc_or_fail [6] show_queue [17] error_check [10] new_element [23] strsave_or_fail [26] exception_cancel [47] pop_file [18] test_free [27] exception_setup [48] push_file [13] test_malloc ``` - 從 `STDIN` 輸入: `$ cat <FILENAME> | ./qtest` ``` [24] add_cmd [19] free_block [51] q_free [32] add_param [22] free_string [10] q_insert_head [38] add_quit_helper [34] get_int [14] q_insert_tail [39] allocation_check [44] init_cmd [15] q_new [28] calloc_or_fail [45] init_time [7] q_reverse [40] delta_time [1] interpret_cmd [52] queue_quit [41] do_comment_cmd [2] interpret_cmda [33] report [8] do_insert_head [27] linenoise [18] report_noreturn [11] do_insert_tail [30] linenoiseFree [3] run_console [13] do_new [21] linenoiseHistoryAdd [36] set_cautious_mode [35] do_option_cmd [46] linenoiseHistoryLoad [53] set_echo [42] do_quit_cmd [31] linenoiseHistorySave [37] set_noallocate_mode [5] do_reverse [47] linenoiseHistorySetMaxLen [54] set_verblevel [16] error_check [48] linenoiseSetCompletionCallback [6] show_queue [25] exception_cancel [20] malloc_or_fail [23] strsave_or_fail [26] exception_setup [9] new_element [17] test_free [43] finish_cmd [49] pop_file [12] test_malloc [29] free_array [50] push_file ``` 可以看到前者沒有呼叫到 `linenoise`。 ### 解決方法 若讀檔作為輸入,那就沒有紀錄歷史的必要,也不需要自動補全命令。在 `qtest.c` 中不呼叫自動補全以及跟歷史紀錄有關的設定。只要不拿記憶體,就不會有忘記釋放的問題。 ```diff= - /* Trigger call back function(auto completion) */ - linenoiseSetCompletionCallback(completion); + if (!infile_name) { + /* Trigger call back function(auto completion) */ + linenoiseSetCompletionCallback(completion); + linenoiseHistorySetMaxLen(HISTORY_LEN); + linenoiseHistoryLoad(HISTORY_FILE); /* Load the history at startup */ + } - linenoiseHistorySetMaxLen(HISTORY_LEN); - linenoiseHistoryLoad(HISTORY_FILE); /* Load the history at startup */ ``` # 伴程(Coroutine) TODO # 解釋 select 系統呼叫在本程式的使用方式,並分析 console.c 的實作,說明其中運用 CS:APP RIO 套件 的原理和考量點 我們先來看 `select` 的用途。根據 [select(2)]() , `select` 是用於等待 **檔案描述符**(File descriptor)可動作的函式。 > **select**() allows a program to monitor multiple file descriptors, waiting until one or more of the file descriptors become "ready" for some class of I/O operation (e.g., input possible). A file descriptor is considered ready if it is possible to perform a corresponding I/O operation (e.g., read(2), or a sufficiently small write(2)) without blocking. `select()` 能在時限(參數 `timeout` )中等待「讀」、「寫」以及「例外」三組檔案描述符(分別為參數 `readfds` 、 `writefds` 與 `exceptfds` ),而變數 `nfd` 則是那三組描述符的數值上界。 當 `select()` 執行完成時,三組描述符中都只會剩下可動作的描述符(使用巨集 `FD_ISSET` 確認),回傳值則是總共可動作的描述符的數目。 `console.c` 中的 `cmd_select()` 是 `select()` 的 wrapper 。 它將 `buf_stack` 最上層的檔案描述符加入 `readfds`,再使用 `select()` 確認其是否準備好了。若該描述符已經可以被讀取,當下又是在讀取檔案的話,`cmd_select` 會直接讀取並執行命令。 而 `readline()` 顧名思義就是從檔案描述符中讀取命令的函式。其中使用到與 CS:APP RIO 套件相同的邏輯——每次使用核心函式取得一塊輸入,暫存在記憶體中,在用完前都在 user space 讀取,不夠再去跟核心要資料,達到減少呼叫核心函式的目的。 ```c= static char *readline() { ... for (cnt = 0; cnt < RIO_BUFSIZE - 2; cnt++) { /* 暫存的輸入都用完了 */ if (buf_stack->cnt <= 0) { /* 使用核心函式 `read` 讀取輸入 */ buf_stack->cnt = read(buf_stack->fd, buf_stack->buf, RIO_BUFSIZE); buf_stack->bufptr = buf_stack->buf; ... } /* 暫存區還有輸入 */ c = *buf_stack->bufptr++; *lptr++ = c; buf_stack->cnt--; if (c == '\n') break; } ... return linebuf; } ``` ## 測試 使用 [strace](https://man7.org/linux/man-pages/man1/strace.1.html) 來觀察程式呼叫核心函式的行為 ```shell= $ wc --bytes ./traces/trace-13-perf.cmd 125 ./traces/trace-13-perf.cmd $ strace -e trace=read ./qtest -v 0 -f ./traces/trace-13-perf.cmd read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\200\272\0\0\0\0\0\0"..., 832) = 832 read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20\35\2\0\0\0\0\0"..., 832) = 832 read(3, "# Test performance of insert_tai"..., 8192) = 125 read(3, "", 8192) = 0 +++ exited with 0 +++ ``` 先忽略輸出的前兩行,可以看到共呼叫 `read` 兩次,第一次讀取了 125 個位元組,這與輸入檔案的大小相同。而 8192 則是暫存區大小(定義於 `console.c` 的第 39 行)。 前兩行的 `read` 是動態載入器( dynamic loader )在載入動態函式庫。 ```shell= $ strace ./qtest -v 0 -f ./traces/trace-13-perf.cmd ... openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libm.so.6", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\200\272\0\0\0\0\0\0"..., 832) = 832 ... openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20\35\2\0\0\0\0\0"..., 832) = 832 ... ``` 其中 `libm.so.6` 是數學函式庫; `libc.so.6` 是 [GNU 的 C 函式庫](https://man7.org/linux/man-pages/man7/libc.7.html)。檔案內容中的 [ELF](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format) 來自「Executable and Linking Format」,是 Unix 與 類 Unix 系統的執行檔格式。 ## 自製終端命令解譯器 TODO ## 參考資料 - [CS:APP Lecture 10](https://scs.hosted.panopto.com/Panopto/Pages/Viewer.aspx?id=f107c2ce-79d5-4529-baeb-2bb495d8c11a) - [linux read system call produced by strace - how to understand pointer to buffer value?](https://stackoverflow.com/questions/58049023/linux-read-system-call-produced-by-strace-how-to-understand-pointer-to-buffer) To Be Continued ... ---