# 2020q1 Homework4 (kcalc) contributed by < `nickyanggg` > ###### tags: `linux2020` ## [作業要求](https://hackmd.io/@sysprog/linux2020-kcalc?fbclid=IwAR2b_HwsaQ12hqD7ZdEsfX8ghTi9xMYZJGc2FrkRPs5CeP_0tuwbRcsfDCk#-%E4%BD%9C%E6%A5%AD%E8%A6%81%E6%B1%82) - [自我檢查清單](https://hackmd.io/@sysprog/linux2020-kcalc?fbclid=IwAR2b_HwsaQ12hqD7ZdEsfX8ghTi9xMYZJGc2FrkRPs5CeP_0tuwbRcsfDCk#-%E8%87%AA%E6%88%91%E6%AA%A2%E6%9F%A5%E6%B8%85%E5%96%AE) - livepatch 整合 fib() ## Linux Kernel 使用 Fixed-point 案例 - 參考 [Linux Kernel Load Average 計算分析](http://brytonlee.github.io/blog/2014/05/07/linux-kernel-load-average-calc/)、[loadavg.h](https://github.com/torvalds/linux/blob/345671ea0f9258f410eb057b9ced9cefbbe5dc78/include/linux/sched/loadavg.h) ### Fixed-point - 小數點為固定,因此稱作定點數。 - 以 10 進制為例,若表示小數點後三位,則數字 8.7 在定點數的表示法為 $8.7 \times 10^3 = 8700$ - 定點數之加減法可直接運算。 - 定點數之乘法的結果需除以 $b^n$ (b 為進制,n 為小數的位數)。 - 定點數之除法的結果需乘以 $b^n$ (b 為進制,n 為小數的位數)。 ### 計算 load average - `FSHIFT` 為定點數的小數位數,這邊為 11。 - `FIXED_1` 用來將數字轉為定點數,或者是用來處理定點數乘除法的修正。 - `EXP_{i}` 代表第 i 分鐘的平滑常數 (以定點數表示)。 ```cpp #define FSHIFT 11 /* nr of bits of precision */ #define FIXED_1 (1<<FSHIFT) /* 1.0 as fixed-point */ #define LOAD_FREQ (5*HZ+1) /* 5 sec intervals */ #define EXP_1 1884 /* 1/exp(5sec/1min) as fixed-point */ #define EXP_5 2014 /* 1/exp(5sec/5min) */ #define EXP_15 2037 /* 1/exp(5sec/15min) */ /* * a1 = a0 * e + a * (1 - e) */ static inline unsigned long calc_load(unsigned long load, unsigned long exp, unsigned long active) { unsigned long newload; newload = load * exp + active * (FIXED_1 - exp); if (active >= load) newload += FIXED_1-1; return newload / FIXED_1; } ``` ## 分析 MathEx ### `main` - 呼叫 `expr_create` 並將算式 `s` 轉成 expression data type (`expr`)。 - 呼叫 `expr_val` 來把儲存好的 expression data type 計算出結果。 ```cpp /* * expression.h */ struct expr { int type; union { struct { float value; } num; struct { float *value; } var; struct { vec_expr_t args; } op; struct { struct expr_func *f; vec_expr_t args; void *context; } func; } param; }; /* * test-simple.c */ int main() { const char *s = "x = 40, add(2, x)"; struct expr_var_list vars = { 0 }; struct expr *e = expr_create(s, strlen(s), &vars, user_funcs); if (!e) { printf("Syntax error"); return 1; } float result = expr_eval(e); printf("result: %f\n", result); expr_destroy(e, &vars); return 0; } ``` ### `expr_create` - 呼叫 `expr_next_token` 來抓取下一個 token 的位置。 - 判斷 token 的類型來決定該進入哪個 `if else` block,並用對應到的結構儲存。 ### `expr_eval` - 透過遞迴計算出整個結果。 - 若為單元運算子只取出一個數並做變化。 - 若為二元運算子則是取出 `&e->param.op.args.buf` 中的二數做運算。 - 若為自訂的運算函式如上面的 `main` ,或是其他也被歸類到運算符號的值,也會有相對應的呼叫處理。 ```cpp float expr_eval(struct expr *e) { float n; switch (e->type) { case OP_UNARY_MINUS: return -(expr_eval(&e->param.op.args.buf[0])); case OP_UNARY_LOGICAL_NOT: return !(expr_eval(&e->param.op.args.buf[0])); case OP_UNARY_BITWISE_NOT: return ~(to_int(expr_eval(&e->param.op.args.buf[0]))); case OP_POWER: return powf(expr_eval(&e->param.op.args.buf[0]), expr_eval(&e->param.op.args.buf[1])); case OP_MULTIPLY: return expr_eval(&e->param.op.args.buf[0]) * expr_eval(&e->param.op.args.buf[1]); case OP_DIVIDE: return expr_eval(&e->param.op.args.buf[0]) / expr_eval(&e->param.op.args.buf[1]); ... default: return NAN; } } ``` ## MathEx 實作自訂函數 - `name` 為函式名字。 - `f` 為函式的地址。 ```cpp /* * expression.h */ typedef float (*exprfn_t)(struct expr_func *f, vec_expr_t args, void *context); struct expr_func { const char *name; exprfn_t f; exprfn_cleanup_t cleanup; size_t ctxsz; }; /* * test-simple.c */ static float add(struct expr_func *f, vec_expr_t args, void *c) { float a = expr_eval(&vec_nth(&args, 0)); float b = expr_eval(&vec_nth(&args, 1)); return a + b; } static struct expr_func user_funcs[] = { { "add", add, 0 }, { NULL, NULL, 0 }, }; ``` 而在上面的 `main` 中,除了傳入整個表示式 `s` 之外,`user_funcs` 也會一起被丟入 `expr_create` 中,為了就是在之後呼叫 `expr_func` 來比對表示式 `s` 中是否有出現自訂義的函式。 - 可看出是依函式名字來比對是否有自訂函式。 ```cpp struct expr_func *expr_func( struct expr_func *funcs, const char *s, size_t len) { for (struct expr_func *f = funcs; f->name; f++) { if (strlen(f->name) == len && strncmp(f->name, s, len) == 0) { return f; } } return NULL; } ``` ## 處理 MathEx 負數、overflow ### 除法 - 被除數為負數時結果錯誤,因為在 `while` 這個 block 並沒有考慮 `n1` < 0 的狀況,所以會一直乘以 10 直到 overflow 並且大於 `((1 << 25) - 1)` 才跳出迴圈。 - 處理方式為若發現被除數為負數時,被除數和除數皆乘以 -1,如此 `n1` 再轉換成其他定點數的值才不會出錯。 - 新增 `if (n1 < 0)` 的 block 即可。 ```cpp static int divid(int a, int b) { int frac1 = GET_FRAC(a); int frac2 = GET_FRAC(b); int n1 = GET_NUM(a); int n2 = GET_NUM(b); if (n1 == 0 && n2 == 0) return NAN_INT; if (n2 == 0) return INF_INT; if (n1 < 0) { n1 = -n1; n2 = -n2; } while (n1 * 10 < ((1 << 25) - 1)) { --frac1; n1 *= 10; } int n3 = n1 / n2; int frac3 = frac1 - frac2; return FP2INT(n3, frac3); } ``` ### 乘法 - 處理 `NUM`、`FRAC` overflow 問題。 - 用結果 $\div$ 被乘數是否等於乘數來驗證是否發生 overflow。 - 呼叫 `FP2INT` 並重新觀察它的 `FRAC` 是否合理,若不合理則是發生 overflow。 - overflow 皆回傳 -1。 - 這邊假設參數 `a`、`b` 為合理結果。 ```cpp static int mult(int a, int b) { int frac1 = GET_FRAC(a); int frac2 = GET_FRAC(b); int n1 = GET_NUM(a); int n2 = GET_NUM(b); int n3 = n1 * n2; n3); if (n2 != 0 && n1 != n3 / n2) { printk("Warning: NUM overflow!"); return FP2INT(-1, 0); } int c = FP2INT(n3, (frac1 + frac2)); int frac3 = GET_FRAC(c); if (frac1 > 0 && frac2 > 0 && frac3 < 0) { printk("Warning: FRAC overflow!"); return FP2INT(-1, 0); } if (frac1 < 0 && frac2 < 0 && frac3 > 0) { printk("Warning: FRAC overflow!"); return FP2INT(-1, 0); } return c; } ``` ### 加法 - 和 `mult` 處理 `FRAC` overflow 的方式一樣。 - 這邊假設參數 `a`、`b` 為合理結果。 ```cpp static int plus(int a, int b) { int frac1 = GET_FRAC(a); int frac2 = GET_FRAC(b); int n1 = GET_NUM(a); int n2 = GET_NUM(b); while (frac1 != frac2) { if (frac1 > frac2) { --frac1; n1 *= 10; } else if (frac1 < frac2) { --frac2; n2 *= 10; } } int n3 = n1 + n2; if (n1 > 0 && n2 > 0 && n3 < 0) { printk("Warning: NUM overflow!"); return FP2INT(-1, 0); } if (n1 < 0 && n2 < 0 && n3 > 0) { printk("Warning: FRAC overflow!"); return FP2INT(-1, 0); } return FP2INT(n3, frac1); } ``` ### 減法 - 當被減數比結果大時,減數卻小於 0,則發生 overflow。 - 當被減數比結果小時,減數卻大於 0,則發生 overflow。 - 這邊假設參數 `a`、`b` 為合理結果。 ```cpp static int minus(int a, int b) { int frac1 = GET_FRAC(a); int frac2 = GET_FRAC(b); int n1 = GET_NUM(a); int n2 = GET_NUM(b); while (frac1 != frac2) { if (frac1 > frac2) { --frac1; n1 *= 10; } else { --frac2; n2 *= 10; } } int n3 = n1 - n2; if ((n3 < n1) != (n2 > 0)) { printk("Warning: NUM overflow!"); return FP2INT(-1, 0); } return FP2INT(n3, frac1); } ``` ## Livepatch ### `main.c` 和前面 MathEx 實作自訂函式方式一樣。 ```cpp noinline int myfib(int n) { printk("myfib in main.c"); return 0; } noinline int user_func_fib(struct expr_func *f, vec_expr_t args, void *c) { int n = expr_eval(&vec_nth(&args, 0)); return myfib(n); } static struct expr_func user_funcs[] = { {"nop", user_func_nop, user_func_nop_cleanup, 0}, {"fib", user_func_fib, user_func_fib_cleanup, 0}, {NULL, NULL, NULL, 0}, }; ``` 修改 `test.sh`,並觀看 `dmesg` 結果,結果如預期一樣。 ```shell $ ./scripts/test.sh > /dev/null $ dmesg ... [282890.857900] calc: Received 7 -> fib(4) [282890.857902] myfib in main.c [282890.857902] calc: Result: 0 [282890.857904] calc: Device successfully closed ``` ### `livepatch-calc.c` - 由於參數 `n` 為 fixed-point,因此作運算前要先轉換。 - 回傳之前要將結果再轉回 fixed-point,這邊就沒有去處理 overflow 的問題 (只剩 27 bits 可使用,也就是最大只能到 $2^{27}-1$)。 - 註冊實作出的函式 `livepatch_fib` 來<s>覆蓋</s> 修補 `myfib`。 :::danger 回頭看 Livepatch 運作機制,不是透過「覆蓋」,用語要精準 :notes: jserv ::: ```cpp int livepatch_fib(int n) { pr_err("function nop is now patched\n"); int num = n >> 4; int frac = n & 15; if (frac >= 8) { printk("Warning: Invalid number!"); return -1; } for (int i = 0; i < frac; i++) num *= 10; if (num < 0) return -1; int a = 0, b = 1, t1, t2; int i = 31 - __builtin_clz(num); for (; i >= 0; i--) { t1 = a * (2 * b - a); t2 = b * b + a * a; a = t1; b = t2; if ((num >> i) & 1) { t1 = a + b; a = b; b = t1; } } return a << 4; } static struct klp_func funcs[] = { { .old_name = "user_func_nop", .new_func = livepatch_nop, }, { .old_name = "user_func_nop_cleanup", .new_func = livepatch_nop_cleanup, }, { .old_name = "myfib", .new_func = livepatch_fib, }, {}, }; ``` 驗證後,結果沒有問題。 ```shell $ ./scripts/test.sh ... Testing fib(10) ... 0 livepatch was applied Testing fib(10) ... 55 ```