# 2022q1 Homework5 (quiz8) contributed by < [`leewei05`](https://github.com/leewei05) > ## [測驗 1](https://hackmd.io/@sysprog/linux2022-quiz8/https%3A%2F%2Fhackmd.io%2F%40sysprog%2FHyg5nxO79#%E6%B8%AC%E9%A9%97-1) > [完整程式碼](https://github.com/leewei05/playground/blob/main/linux2022-quiz8/quiz1/memchr.c) memchr_opt 初步理解為以下: :::warning 注意用詞: * bit 的翻譯是「位元」 * byte 的翻譯是「位元組」 尊重資訊科技前輩的篳路藍縷,儘量使用傳統詞彙。 :notes: jserv ::: 首先會判斷帶入的字串長度是否比 8 個位元組還要長,如果比 8 個位元組還短即比對各個位元組 (byte) 搜尋指定的 char。如果比字串比 8 個位元組還要長,那就會進入 `if (!TOO_SMALL(length))` 的判斷式。 與其一個位元組接著一個位元組比對,memchr 會根據 `unsigned char d` (欲搜尋的字元) 製作一個 8 個位元組的 bytemask。 ```c void *memchr_opt(const void *src_void, int c, size_t length) { const unsigned char *src = (const unsigned char *) src_void; unsigned char d = c; while (UNALIGNED(src)) { if (!length--) return NULL; if (*src == d) return (void *) src; src++; } if (!TOO_SMALL(length)) { /* If we get this far, we know that length is large and * src is word-aligned. */ /* The fast code reads the source one word at a time and only performs * the bytewise search on word-sized segments if they contain the search * character, which is detected by XORing the word-sized segment with a * word-sized block of the search character and then detecting for the * presence of NULL in the result. */ unsigned long *asrc = (unsigned long *) src; unsigned long mask = d << 8 | d; mask = mask << 16 | mask; for (unsigned int i = 32; i < LBLOCKSIZE * 8; i <<= 1) mask = (mask << i) | mask; while (length >= LBLOCKSIZE) { // check if char matches if (DETECT_CHAR(*asrc, mask)) { // if yes, check char one by one until we find the char src = (unsigned char *) asrc; while (length--) { if (*src == d) return (void *) src; src++; } } // if no char matches, reduce length by LBLOCKSIZE length -= LBLOCKSIZE; // move forward by LBLOCKSIZE because there were no matching // charater in this block asrc += 1; // check if source array is shorter than LBLOCKSIZE if (DETECT_NULL(*asrc)) break; } /* If there are fewer than LBLOCKSIZE characters left, then we resort to * the bytewise loop. */ src = (unsigned char *) asrc; } while (length--) { if (*src == d) return (void *) src; src++; } return NULL; } int main() { const char str[] = "http://wiki.csie.ncku.edu.tw"; const char ch = '.'; const char str2[] = "http://wiki.csie.ncku.edu.tw"; const char ch2 = 'p'; const char str3[] = "http://wiki.csie.ncku.edu.tw"; const char ch3 = 'u'; char *ret = memchr_opt(str, ch, strlen(str)); printf("String after |%c| is - |%s|\n", ch, ret); char *ret2 = memchr_opt(str2, ch2, strlen(str2)); printf("String after |%c| is - |%s|\n", ch2, ret2); char *ret3 = memchr_opt(str3, ch3, strlen(str3)); printf("String after |%c| is - |%s|\n", ch3, ret3); return 0; } ``` 執行結果如下: ```shell String after |.| is - |.csie.ncku.edu.tw| String after |p| is - |p://wiki.csie.ncku.edu.tw| String after |u| is - |u.edu.tw| ``` ### 除錯過程 一開始很快就 parse 出 `.`,但加了另外兩個 case 發現都會回傳 NULL。透過 gdb 檢查之後發現這行寫錯了。原本是要位移一個 pointer to long,但一開始寫的版本是取回一個位元組,所以才會出錯。也因為為了熟悉 gdb 的關係,花了不少時間,但學會使用 gdb 除錯真的節省不少時間以及排除用感覺寫程式的壞習慣。 ```c // WRONG *asrc = *asrc + 8; // CORRECT asrc += 1; ``` --- ## [測驗 2](https://hackmd.io/@sysprog/linux2022-quiz8/https%3A%2F%2Fhackmd.io%2F%40sysprog%2FHyg5nxO79#%E6%B8%AC%E9%A9%97-2) --- ## [測驗 3](https://hackmd.io/@sysprog/linux2022-quiz8/https%3A%2F%2Fhackmd.io%2F%40sysprog%2FHyg5nxO79#%E6%B8%AC%E9%A9%97-3) :::info - [x] 列出完整且有效的 periodic_routine 函式程式碼,並附上註解 - [ ] 解釋上述程式碼運作原理,應探討 Linux 核心內部 ptrace 系統呼叫和 signal 的實作方式 - [ ] 研讀 The race to limit ptrace 一類的材料,探討行程避免被追蹤的手法 ::: [完整程式碼](https://github.com/leewei05/playground/blob/main/linux2022-quiz8/quiz3/dont_trace.c) ### 理解並實作程式碼 首先從核心模組的 init function 開始看起。查閱 [Linux Kernel 原始碼](https://github.com/torvalds/linux/blob/33fb42636a938be01d951b4cee68127a59a1e7e4/include/linux/workqueue.h),`dont_trace_init` 基本上就是建立一個 work queue,並且設定 work queue 的延遲時間(1 Jiffy)。 exit function 則是停止 work queue 的 routine 並清除先前定義的 work queue。 ```c static int __init dont_trace_init(void) { wq = create_workqueue(DONT_TRACE_WQ_NAME); queue_delayed_work(wq, &dont_trace_task, JIFFIES_DELAY); loaded = true; pr_info("Loaded!\n"); return 0; } static void __exit dont_trace_exit(void) { loaded = false; /* No new routines will be queued */ cancel_delayed_work(&dont_trace_task); /* Wait for the completion of all routines */ flush_workqueue(wq); destroy_workqueue(wq); pr_info("Unloaded.\n"); } ``` 除了 `periodic_routine` 以外需要實作,其他四個函式分別為: - `kill_task`: 刪除 Kernel space 的行程 - `is_tracer`: 判斷行程是否有追蹤其他行程 - `kill_tracee`: 刪除所有 tracer 追蹤的行程 - `check`: 檢查所有的行程的 ptraced,也就是追蹤的行程。如果 `is_tracer` 返回 true,則刪除所有追蹤行程以及該行程。 ```c /* * 'ptraced' is the list of tasks this task is using ptrace() on. * * This includes both natural children and PTRACE_ATTACH targets. * 'ptrace_entry' is this task's link on the p->parent->ptraced list. */ struct list_head ptraced; ``` 首先,`likely` 巨集是由 gcc extension `__builtin_expect` 組成。此用途是可提示編譯器產生對 branch predictor 友善的程式碼,因為工程師可以透過 `__builtin_expect` 來告知編譯器 `!!(x) == 1` 的機率很大,也就是 x 很可能為 1。`!!` 是確保 `!!(x)` 的結果一定為 0 或是 1。 > 6.5.3.3 Unary arithmetic operators > The result of the logical negation operator ! is 0 if the value of its operand compares unequal to 0, 1 if the value of its operand compares equal to 0. The result has type int. The expression !E is equivalent to (0==E) ```c # define likely(x) __builtin_expect(!!(x), 1) ``` 什麼時候需要會需要執行 `check()`? A: 掛載核心模組的時候。 可以使用 init function 所定義的 `loaded = true;`,因為大部分的時間 `loaded == true`。 ```c static void periodic_routine(struct work_struct *ws) { // check each process if dont_trace module is loaded if (likely(loaded)) check(); // run work task again after JIFFIES_DELAY schedule_delayed_work(&dont_trace_task, JIFFIES_DELAY); } ``` 測試程式碼 ```shell # 確認沒有掛載模組 $ lsmod | grep dont # 編譯模組 $ make make -C /lib/modules/`uname -r`/build M=quiz3 modules make[1]: Entering directory '/usr/src/linux-headers-5.13.0-39-generic' CC [M] quiz3/dont_trace.o MODPOST quiz3/Module.symvers CC [M] quiz3/dont_trace.mod.o LD [M] quiz3/dont_trace.ko BTF [M] quiz3/dont_trace.ko Skipping BTF generation for quiz3/dont_trace.ko due to unavailability of vmlinux make[1]: Leaving directory '/usr/src/linux-headers-5.13.0-39-generic' $ yes >/dev/null & [1] 21169 $ sudo gdb -q --pid=`pidof yes` Attaching to process 21169 Reading symbols from /usr/bin/yes... (No debugging symbols found in /usr/bin/yes) Reading symbols from /lib/x86_64-linux-gnu/libc.so.6... Reading symbols from /usr/lib/debug//lib/x86_64-linux-gnu/libc-2.31.so... Reading symbols from /lib64/ld-linux-x86-64.so.2... (No debugging symbols found in /lib64/ld-linux-x86-64.so.2) 0x00007f399da860a7 in __GI___libc_write (fd=1, buf=0x555c63a3b510, nbytes=8192) at ../sysdeps/unix/sysv/linux/write.c:26 26 ../sysdeps/unix/sysv/linux/write.c: No such file or directory. (gdb) signal SIGINT Continuing with signal SIGINT. Program terminated with signal SIGINT, Interrupt. The program no longer exists. (gdb) quit [1]+ Interrupt yes > /dev/null $ make load sudo insmod dont_trace.ko $ lsmod | grep dont dont_trace 16384 0 $ yes >/dev/null & [1] 21450 $ sudo gdb -q --pid=`pidof yes` Attaching to process 21450 [1]+ Killed yes > /dev/null Killed $make unload sudo rmmod dont_trace ``` ### Ptrace (process trace) & signal 當一個行程追蹤了另一個行程,被追蹤的行程 (tracee) 需要成為追蹤行程 (tracer) 的子行程,tracer 擁有 tracee 的控制權,包括設定 break point,檢視 tracee 的記憶體位址,攔截系統呼叫等等。GDB 即是透過 ptrace 實現上述的功能。 Ptrace 的限制 - Ptrace 沒有符合 POSIX 標準,在每個平台的實作方式都不同 - 為了追蹤某一個行程,tracer 一定要是 tracee 的父行程 參考文件 [Ptrace manual](https://man7.org/linux/man-pages/man2/ptrace.2.html) [Intercepting and Emulating Linux System Calls with Ptrace](https://nullprogram.com/blog/2018/06/23/) [Ptrace, Utrace, Uprobes: Lightweight, Dynamic Tracing of User Apps](https://landley.net/kdocs/ols/2007/ols2007v1-pages-215-224.pdf)