# 2017q3 Homework4 (sandbox) ###### tags: `sysprogram` contributed by <`st9007a`, `ian910297`> ## [Github](https://github.com/ian910297/moxiebox) ## 解析 moxiebox ### Registers 首先透過 [moxiebox architecture](http://moxielogic.org/blog/pages/architecture.html) 可以得知 moxiebox 有 16 個 32-bit 的 registers: - $sp (stack pointer) - $fp (frame pointer) - $r0 ~ $r13 以及只能用 `gsr` 跟 `ssr` 這兩個指令來存取的 special registers,在 `moxie.h` 可以得知它們的數量為 256 個: ```clike enum { NUM_MOXIE_REGS = 17, /* Including PC */ NUM_MOXIE_SREGS = 256, /* The special registers */ PC_REGNO = 16, }; ``` 除了上述以外,透過 gdb 發現另外兩個 registers: - $pc (program counter) - $cc (condition code register) ### Memory 根據 [sandbox execution environment](https://github.com/ian910297/moxiebox/blob/master/sandbox-design.md) 的敘述,虛擬記憶體從低到高分別為: - elf:1 個或以上,存放 moxiebox program 的 machine code - data:0 個或以上,從外部載入的資料 - stack:1 個,大小為 64k - memory descriptor:查了一下,memory descriptor 的功用是將 virtual address 映射到 physical address 接著透過 sandbox 執行 `tests/basic`,可以看到 virtual memory 的分配狀況: ``` $ src/sandbox -e tests/basic ep 00001000 ro 00000f8c-0000146c elf0 rw 0000156c-000019d8 elf1 rw 000029d8-000129d8 stack ro 000139d8-00013a78 mapdesc ``` 最前面的 ro 代表 read-only,rw 代表 read and write,elf1 的權限為 rw,用 `moxie-none-moxiebox-objdump` 查看了一下,`0x156c` 對映到 data section: ``` $ moxie-none-moxiebox-objdump tests/basic -h Idx Name Size VMA LMA File off Algn ... 4 .data 00000430 0000156c 0000156c 000004e0 2**2 ... ``` 最後 heap 的資訊可以在 `src/sandbox.h` 找到: ```clike machine() { startAddr = 0; tracing = false; profiling = false; heapAvail = 0xfffffffU; } ``` `heapAvail` 代表可以使用的 heap 的長度,分配 heap 的程式碼可以在 `moxie.cc` 的 `sim_mmap()` 找到 比較值得注意的是,一塊記憶體被劃分後,透過 `mapInsert()` 這個 function 加入機器中,`mapInsert()` 內部實作會先空出一個 page size 的空間,再把劃好的記憶體放到這個空間之後: ```clike bool machine::mapInsert(addressRange *rdr) { addressRange *ar = memmap.back(); rdr->start = ar->end + MACH_PAGE_SIZE; rdr->end = rdr->start + rdr->length; memmap.push_back(rdr); return true; } ``` >> 這裡不太懂為什麼要空出一個 page size 的空間 >> [name="st9007a"] ### Library 由於 moxiebox 為隔離環境,能使用的現成的 function 有限,能夠使用的工具在 `runtime` 裡面找到,可以參照 [sandbox execution environment](https://github.com/ian910297/moxiebox/blob/master/sandbox-design.md) 的 ABI Summary: - Runtime environment: - moxie_memmap - Global variable, pointer to list of struct moxie_memory_map_ent, which describes the execution environment's input data. - setreturn(3) - Pointer to environment's output data buffer. This is the data returned from the sandbox to the user. - stdlib.h: abort(3), exit(3) - string.h: memchr(3), memcmp(3), memcpy(3), memset(3) - string.h: strchr(3), strcmp(3), strcpy(3), strlen(3), strncpy(3), strcpy(3), strstr(3) - Runtime environment - crypto: - sha256: sha256_init(), sha256_update(), sha256_final() - System calls: - mmap(2) - MAP_PRIVATE|MAP_ANONYMOUS to allocate heap memory. - _exit(2) - End process 這裡要注意的是 `mmap` 劃分的空間有條件限制,可以在 `moxie.cc` 的 `sim_mmap()` 看到,這個 function 是用來處理 mmap 的 system call: ```clike static void sim_mmap(machine &mach) { ... if ((addr != 0) || (length < MACH_PAGE_SIZE) || (length & MACH_PAGE_MASK) || ((uint32_t) length > mach.heapAvail) || (!(prot & MOXIE_PROT_READ)) || (!(prot & MOXIE_PROT_WRITE)) || (!(prot & MOXIE_PROT_EXEC)) || (!(flags & MOXIE_MAP_PRIVATE)) || (!(flags & MOXIE_MAP_ANONYMOUS))) { cpu.asregs.regs[2] = -EINVAL; return; } ... } ``` mmap 劃分的記憶體大小必須是 page size 的整數倍才行,否則會回傳錯誤碼:EINVAL (Invalid Argument) ## 修正 sandbox 在研究 sandbox 的期間,在 `src/sandbox.cc` 的 `gdb_main_loop()` 中發現了兩個 FIXME: ```clike case 'c': wrc = write(newsockfd, "+", 1); sim_resume(mach); // FIXME.. assuming BREAK for now sendGdbReply(newsockfd, "S05"); mach.cpu.asregs.regs[16] -= 2; i += 4; ... if (!p) { /*FIXME: implement error handling */ sprintf(reply, "OK"); goto cmd_m_out; } ``` 第 1 個 FIXME 要求是當執行中斷或結束時回傳正確的 signal,原本的程式碼不管執行是中斷或結束都一律回傳 "S05",對應的 signal 是 SIGTRAP,代表 gdb 遇到了一的 breakpoint 我的作法是在 `sim_resume()` 結束時將 cpu exception 回傳出來,並且根據回傳的 exception 來回應正確的 signal,程式碼可以參照 [github](https://github.com/ian910297/moxiebox/commit/c15de9ddf9e6227d3669e81a93822fd302a6d3d0) 接著第 2 個 FIXME 要求實作 Error Handling,首先修正前完整程式碼如下: ```clike case 'm': { uint32_t addr = readDelimitedHexValue(buffer, &++i); uint32_t length = readDelimitedHexValue(buffer, &i); char *p = (char *) mach.physaddr(addr, length); if (!p) { sprintf(reply, "OK"); goto cmd_m_out; } reply[0] = 0; while (length-- > 0) { int c = *p++; char buf[3]; strcat(reply, lowByteToHex(buf, c)); } cmd_m_out: sendGdbReply(newsockfd, reply); i += 2; } break; ``` 根據 [The GDB remote serial protocol - Communication protocol](ftp://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_129.html#SEC134) 知道 `m` 用於 read memory,如果執行成功會回應 "OK",失敗會回應 "ENN",其中 NN 代表錯誤碼 再來根據 [Howto: GDB Remote Serial Protocol](http://www.embecosm.com/appnotes/ean4/embecosm-howto-rsp-server-ean4-issue-2.html) 的 4.7.7 提到 read memory error 會回應 "E01",所以將原本的 "OK" 改成 "E01",相關程式碼可以參照 [github](https://github.com/ian910297/moxiebox/commit/3d08b79b262e5959ca5a8f5723730278088d3e25) ## Fibonacci 實作 **TODO**:補上 Q-Matrix 實作 這裡用 C 語言實作出 iterative, recursive, tail recursion, fast doubling 版本的 Fibonacci 由於 sandbox 只能透過 `-d` 的 flag 將外部資料載入記憶體,所以在實作 Fibonacci 之前先寫一支輔助程式用來產生二進位資料的檔案: ``` $ tool/write -h Description: Generate the input data for sandbox It write 4 byte to output one times Usage: -i [input integer]: input data -o [output file]: output file -a : append input data to output file ``` ``` $ tool/write -i 40 -o tmp/fib.in $ hexdump -C tmp/fib.in 00000000 28 00 00 00 |(...| 00000004 ``` 相關程式碼放在 [github](https://github.com/ian910297/moxiebox/blob/master/tool/write.c) 接著實作 Fibonacci,這裡統一將所有版本的方法寫在同一支程式裡,在編譯時利用 `-D` 帶入不同 macro 產生對應版本的 Fibonacci 實作 由於程式碼放在共筆上相對稍長,所以不附上程式碼,相關程式碼可以參照 [github](https://github.com/ian910297/moxiebox/blob/master/tests/fib.c) ## 嘗試使用 gprof 在分析之前,先嘗試操作 gprof,首先使用 sandbox 的 `-p` option 來產生 profile 檔案 ``` $ src/sandbox -e tests/fib -d tmp/fib.in -o tmp/fib.out -p tmp/fib.prof ``` 這裡用 iterative 的 Fibonacci 來測試,輸入的資料為 40 接著用 moxie-none-moxiebox-gprof 來查看 profile 檔 ``` $ moxie-none-moxiebox-gprof tests/fib tmp/fib.prof -l <unknown>:0: (register_tm_clones:0x10a6) 1 executions <unknown>:0: (frame_dummy:0x114c) 1 executions ~/project/sysprog/build-toolchain/.build/moxie-none-moxiebox/src/newlib/newlib/libc/stdlib/__atexit.c:121: (__register_exitproc:0x1230) 1 executions ~/project/sysprog/build-toolchain/.build/moxie-none-moxiebox/src/newlib/newlib/libc/stdlib/__atexit.c:164: (__register_exitproc:0x1270) 1 executions ~/project/sysprog/moxiebox/runtime/memset.c:92: (memset:0x14b6) 40 executions ~/project/sysprog/moxiebox/runtime/strstr.c:53: (strstr:0x14fe) 2 executions ~/project/sysprog/moxiebox/runtime/strstr.c:57: (strstr:0x14ee) 3 executions ~/project/sysprog/moxiebox/runtime/strstr.c:62: (strstr:0x14da) 6 executions ~/project/sysprog/moxiebox/runtime/strstr.c:113: (strstr:0x14d2) 1 executions ~/project/sysprog/moxiebox/tests/fib.c:15: (find_input:0x141c) 3 executions ~/project/sysprog/moxiebox/tests/fib.c:39: (fib_iterative:0x1498) 39 executions ~/project/sysprog/moxiebox/tests/fib.c:107: (main:0x1436) 1 executions ``` 這裡顯示了 execution 的數量,查了一下,是指 basic block 的 execution 數量,拿倒數第二行的 `fib.c:39 (fib_iterative:0x1498) 39 executions` 來看,第 39 行附近程式碼如下: ```clike 38 for (int i = 1; i < n; i++) { 39 sum = f0 + f1; 40 f0 = f1; 41 f1 = sum; 42 } ``` 所以就是這個 for loop 被編譯成一個 basic block,然後在這次的執行中被執行了 39 次,跟迴圈停止條件吻合 `i < 40` >> 試了其他幾個 gprof 的 options 後,都沒辦法讓 profile 檔案 dump 出其他的資訊,不確定 moxie-none-moxiebox-gprof 是不是還能顯示 execution count 以外的資訊 >> [name="st9007a"] ## 分析各版本 Fibonacci ### Iterative 透過上一節我們可以知道 iterative 版本的程式碼中有一個 basic block,在輸入數值為 40 的狀況下會被執行 39 次 接著利用 perf 來重複執行 100 次查看 instructions, cycles, 以及執行時間 ``` $ perf stat --repeat 100 -e instructions,cycles src/sandbox -e tests/fib -o tmp/fib.out -d tmp/fib.in Performance counter stats for 'src/sandbox -e tests/fib -o tmp/fib.out -d tmp/fib.in' (100 runs): 330,2641 instructions # 1.30 insns per cycle ( +- 0.07% ) 253,3844 cycles ( +- 0.11% ) 0.064130528 seconds time elapsed ( +- 2.00% ) ``` ### Recursive 一樣輸入數值 40 來查看 basic block 的 execution count ``` <unknown>:0: (register_tm_clones:0x10a6) 1 executions <unknown>:0: (frame_dummy:0x114c) 1 executions ~/project/sysprog/build-toolchain/.build/moxie-none-moxiebox/src/newlib/newlib/libc/stdlib/__atexit.c:121: (__register_exitproc:0x1230) 1 executions ~/project/sysprog/build-toolchain/.build/moxie-none-moxiebox/src/newlib/newlib/libc/stdlib/__atexit.c:164: (__register_exitproc:0x1270) 1 executions ~/project/sysprog/moxiebox/runtime/memset.c:92: (memset:0x14e0) 40 executions ~/project/sysprog/moxiebox/runtime/strstr.c:53: (strstr:0x1528) 3 executions ~/project/sysprog/moxiebox/runtime/strstr.c:57: (strstr:0x1518) 4 executions ~/project/sysprog/moxiebox/runtime/strstr.c:62: (strstr:0x1504) 6 executions ~/project/sysprog/moxiebox/runtime/strstr.c:113: (strstr:0x14fc) 1 executions ~/project/sysprog/moxiebox/tests/fib.c:15: (find_input:0x1466) 4 executions ~/project/sysprog/moxiebox/tests/fib.c:56: (fib_recursive:0x1416) 165580140 executions ~/project/sysprog/moxiebox/tests/fib.c:107: (main:0x1480) 1 executions ``` 可以看到 basic block 數量超過 $10 ^ 8$,接著用 perf 來看看 ``` $ perf stat --repeat 5 -e instructions,cycles src/sandbox -e tests/fib -o tmp/fib.out -d tmp/fib.in Performance counter stats for 'src/sandbox -e tests/fib -o tmp/fib.out -d tmp/fib.in' (5 runs): 6800,4980,1761 instructions # 2.35 insns per cycle ( +- 0.00% ) 2887,8397,5877 cycles ( +- 0.17% ) 80.571199833 seconds time elapsed ( +- 0.17% ) ``` 在輸入為 40 的狀況下,執行時間為 80 秒,是原本 iterative 版本的 1000 倍以上 ### Tail Recursion profile 的結果如下: ``` <unknown>:0: (register_tm_clones:0x10a6) 1 executions <unknown>:0: (frame_dummy:0x114c) 1 executions ~/project/sysprog/build-toolchain/.build/moxie-none-moxiebox/src/newlib/newlib/libc/stdlib/__atexit.c:121: (__register_exitproc:0x1230) 1 executions ~/project/sysprog/build-toolchain/.build/moxie-none-moxiebox/src/newlib/newlib/libc/stdlib/__atexit.c:164: (__register_exitproc:0x1270) 1 executions ~/project/sysprog/moxiebox/runtime/memset.c:92: (memset:0x14aa) 40 executions ~/project/sysprog/moxiebox/runtime/strstr.c:53: (strstr:0x14f2) 2 executions ~/project/sysprog/moxiebox/runtime/strstr.c:57: (strstr:0x14e2) 3 executions ~/project/sysprog/moxiebox/runtime/strstr.c:62: (strstr:0x14ce) 6 executions ~/project/sysprog/moxiebox/runtime/strstr.c:113: (strstr:0x14c6) 1 executions ~/project/sysprog/moxiebox/tests/fib.c:15: (find_input:0x141c) 3 executions ~/project/sysprog/moxiebox/tests/fib.c:69: (fib_tail_recursion:0x148c) 40 executions ~/project/sysprog/moxiebox/tests/fib.c:107: (main:0x1436) 1 executions ``` 在 `fib_tail_recursion()` 裡,basic block 執行了 40 次,查看程式碼 69 行附近,正好是 tail call ```clike 69 fib_tail_recursion(n - 1, result, b, a + b); ``` 原本想透過 gdb 查看 stack 狀況,不過編譯時,`fib_tail_recrsion()` 似乎沒有被編譯成 subrountine,利用 gdb 的 `info functions` 也沒有看到: ``` File fib.c: int main(); File memset.c: void *memset(void *, int, size_t); File strstr.c: char *strstr(const char *, const char *); ``` 接著使用 perf 來查看 ``` $ perf stat --repeat 100 -e instructions,cycles src/sandbox -e tests/fib -d tmp/fib.in -o tmp/fib.out Performance counter stats for 'src/sandbox -e tests/fib -d tmp/fib.in -o tmp/fib.out' (100 runs): 330,0576 instructions # 1.30 insns per cycle ( +- 0.06% ) 253,3873 cycles ( +- 0.11% ) 0.062247430 seconds time elapsed ( +- 1.84% ) ``` 其執行速度幾乎與 iterative 版本差不多 ### Fast Doubling profile 結果如下: ``` <unknown>:0: (register_tm_clones:0x10a6) 1 executions <unknown>:0: (frame_dummy:0x114c) 1 executions ~/project/sysprog/build-toolchain/.build/moxie-none-moxiebox/src/gcc/libgcc/libgcc2.c:709: (__clzsi2:0x11a2) 1 executions ~/project/sysprog/build-toolchain/.build/moxie-none-moxiebox/src/newlib/newlib/libc/stdlib/__atexit.c:121: (__register_exitpr$c:0x128e) 1 executions ~/project/sysprog/build-toolchain/.build/moxie-none-moxiebox/src/newlib/newlib/libc/stdlib/__atexit.c:164: (__register_exitpr$c:0x12ce) 1 executions ~/project/sysprog/moxiebox/runtime/memset.c:92: (memset:0x154c) 40 executions ~/project/sysprog/moxiebox/runtime/strstr.c:53: (strstr:0x1594) 3 executions ~/project/sysprog/moxiebox/runtime/strstr.c:57: (strstr:0x1584) 4 executions ~/project/sysprog/moxiebox/runtime/strstr.c:62: (strstr:0x1570) 6 executions ~/project/sysprog/moxiebox/runtime/strstr.c:113: (strstr:0x1568) 1 executions ~/project/sysprog/moxiebox/tests/fib.c:15: (find_input:0x147a) 4 executions ~/project/sysprog/moxiebox/tests/fib.c:81: (fib_fast_doubling:0x14ea) 1 executions ~/project/sysprog/moxiebox/tests/fib.c:87: (fib_fast_doubling:0x1508) 4 executions ~/project/sysprog/moxiebox/tests/fib.c:90: (fib_fast_doubling:0x1536) 4 executions ~/project/sysprog/moxiebox/tests/fib.c:107: (main:0x1494) 1 executions ``` 跟前面比較不一樣的是 `fib_fast_doubling()` 有三個 basis block 分別被執行了 1, 4, 4 次,查看其對應的程式碼: ```clike ... 81 int cbs = 30 - __builtin_clz(n); ... 86 while (cbs >= 0) { 87 sum = (f2 * 2 - f1) * f1; 88 sum_1 = f1 * f1 + f2 * f2; 89 f2 = sum_1; 90 f1 = sum; 91 92 if ((n >> cbs) & 1) { 93 f1 = f2; 94 f2 = sum_1 + sum; 95 } 96 cbs--; 97 } ``` 第 1 個 basic block 呼叫 `__builtin_clz()`,第 2 個是迴圈,第 3 個將 90 行與下面的 if 條件判斷視為 1 個 basic block,查看它的組合語言如下: ``` 0x1536 <main+248>: mov $r0, $r3 0x1538 <main+250>: jmpa 0x1528 <main+234> 0x153e <memset>: mov $r3, $r0 0x1540 <memset+2>: mov $r4, $r0 0x1542 <memset+4>: add $r4, $r2 0x1544 <memset+6>: mov $r2, $r4 0x1546 <memset+8>: cmp $r3, $r2 0x1548 <memset+10>: bne 0x154c <memset+14> 0x154a <memset+12>: ret ``` 可以看到這裡執行了兩道指令後就進入了 `memset()` 中,所以第 3 個 basic block 只有兩個指令 接著使用 perf: ``` $ perf stat --repeat 100 -e instructions,cycles src/sandbox -e tests/fib -d tmp/fib.in -o tmp/fib.out Performance counter stats for 'src/sandbox -e tests/fib -d tmp/fib.in -o tmp/fib.out' (100 runs): 331,1244 instructions # 1.30 insns per cycle ( +- 0.07% ) 254,2927 cycles ( +- 0.12% ) 0.058379399 seconds time elapsed ( +- 1.76% ) ``` 執行時間比 iterative, tail recursion 版本快了一點點,但是差距不明顯,其原因在於 fast doubling 在第 2 個 basic block 上執行指令較多 分別查看 iterative, tail recursion, fast doubling 的 basic block 如下,這裡 basic block 的判斷方式為從 gprof 給的虛擬位址到遇到一個 j-type 指令為止將其視為 1 個 basic block: - iterative ``` 0x1498 <main+184>: mov $r1, $r5 0x149a <main+186>: add $r1, $r3 0x149c <main+188>: inc $r2, 0x1 0x149e <main+190>: mov $r5, $r3 0x14a0 <main+192>: mov $r3, $r1 0x14a2 <main+194>: jmpa 0x147e <main+158> ``` - tail recursion ``` 0x148c <main+172>: dec $r6, 0x1 0x148e <main+174>: mov $r3, $r1 0x1490 <main+176>: add $r3, $r2 0x1492 <main+178>: mov $r1, $r2 0x1494 <main+180>: mov $r2, $r3 0x1496 <main+182>: jmpa 0x1472 <main+146> ``` - fast doubling ``` 0x1508 <main+202>: mov $r3, $r1 0x150a <main+204>: add $r3, $r1 0x150c <main+206>: sub $r3, $r0 0x150e <main+208>: mul $r3, $r0 0x1510 <main+210>: mul $r0, $r0 0x1512 <main+212>: mul $r1, $r1 0x1514 <main+214>: add $r0, $r1 0x1516 <main+216>: mov $r1, $r0 0x1518 <main+218>: mov $r0, $r8 0x151a <main+220>: ashr $r0, $r2 0x151c <main+222>: and $r0, $r5 0x151e <main+224>: cmp $r0, $r6 0x1520 <main+226>: beq 0x1536 <main+248> 0x1522 <main+228>: mov $r0, $r1 0x1524 <main+230>: add $r3, $r1 0x1526 <main+232>: mov $r1, $r3 0x1528 <main+234>: dec $r2, 0x1 0x152a <main+236>: cmp $r2, $r4 0x152c <main+238>: bne 0x1508 <main+202> 0x152e <main+240>: st.l ($r7), $r0 0x1530 <main+242>: jmpa 0x14d4 <main+150> ``` 可以注意到 iterative 跟 tail recursion 的 basic block 只用了 6 道指令,而 fast doubling 有 21 道指令,中間還有 `beq`, `bnq` 的 branch 相關指令,其行為較 iterative, tail recursion 複雜 再來還需要考慮到執行時間也包含了啟動 sandbox 的時間,所以在輸入數值為 40 的狀況下差距較小 ## 移植演算法並驗證 ### constant time memcmp() function #### 原理 參考自 [github - cst_time_memcmp](https://github.com/chmike/cst_time_memcmp/blob/master/README.md) 原本的非常數時間 memcmp function 如下: ```clike int memcmp(const unsigned char *m1, const unsigned char *m1, size_t n) { size_t i; for (i = 0; i < n; ++i ) { int diff = m1[i] - m2[i]; if (diff != 0) return (diff < 0) ? -1 : +1; } return 0; } ``` 原本的演算法用一個迴圈將兩個記憶體位址逐個 byte 比較內容,如果內容不相同就回傳 1 或 -1,整個迴圈跑完後表示這兩段記憶體的內容相同,回傳 0 常數時間的 memcmp function 如下: ```clike int cst_time_memcmp(const unsigned char *m1, const unsigned char *m1, size_t n) { int res = 0, diff; if (n > 0) { do { --n; diff = m1[n] - m2[n]; if (diff != 0) res = diff; } while (n != 0); } return (res > 0) - (res < 0); } ``` 概念上就是原本的演算法是從頭比較每個 byte,常數時間的演算法則是從記憶體的最後一個 byte 往前比較,直到比完最前面的 byte 為止,即使中間就發現 byte 的內容不一樣也不會馬上回傳,整個迴圈都跑完才回傳 接著必須避免 conditional branch,所以將 `if` 替換成 bit-wise operator ```clike if (diff != 0) res = diff; ``` 改成 ```clike res = (res & -!diff) | diff; ``` 當 diff = 0 時,-!diff = 0xffffffff,所以程式碼就會變成 `res = (res & 0xffffffff) | 0x0`,res 的值會保持,若 diff 不為 0,res 的值就會改變 接著在某些硬體架構下,編譯 `!diff` 時可能會產生 branching instruction,所以可以再修改: ```clike res = (res & -!diff) | diff; ``` 改成 ```clike res = (res & (((diff - 1) & ~diff) >> 8)) | diff; ``` 修改後的程式碼在執行上會比原本慢,但是可以保證編譯後不會產生 branching instruction 另一個解法是利用一個 static table 記下所有取代 -!diff: ```clike static signed char tbl[256] = {-1, 0, ... 0 }; res = (res & tbl[(unsigned char)diff]) | diff; ``` 但是這個做法的執行速度會被 table 的 cache 影響,導致間接透漏記憶體比較的狀況,因此不推薦 最後程式碼最後的回傳 `return (res > 0) - (res < 0);` 也可能編譯出 branching instruction,所以可以改成如下: ```clike return ((res - 1) >> 8) + (res >> 8) + 1; ``` 所以完整的演算法如下: ```clike int cst_time_memcmp_safest1(const void *m1, const void *m2, size_t n) { const unsigned char *pm1 = (unsigned char)m1; const unsigned char *pm2 = (unsigned char)m2; int res = 0, diff; if (n > 0) { do { --n; diff = pm1[n] - pm2[n]; res = (res & (((diff - 1) & ~diff) >> 8)) | diff; } while (n != 0); } return ((res - 1) >> 8) + (res >> 8) + 1; } ``` #### 移植 在 `runtime` 資料夾中創立一個 `cst_memcmp.c`,寫下程式碼: ```clike #include <stddef.h> int cst_memcmp(const void *m1, const void *m2, size_t n) { const unsigned char *pm1 = (const unsigned char *) m1; const unsigned char *pm2 = (const unsigned char *) m2; int diff, res = 0; if (n > 0) { do { --n; diff = pm1[n] - pm2[n]; res = (res & (((diff - 1) & ~diff) >> 8)) | diff; } while (n != 0); } return ((res - 1) >> 8) + (res >> 8) + 1; } ``` 接著在 `sandboxrt.h` 加入: ```clike extern int cst_memcmp(const void *m1, const void *m2, size_t n); ``` `Makefile` 加入對應的 obj 檔名,就可以將 cst_memcmp 加入 runtime library 了 這邊要 commit 的時候發現 cppcheck 會跳錯誤 `Shifting a negative value is unbefined behavior`,所以將程式最後一行改成: ```clike return (((res - 1) & 0xff000000) | ((unsigned int)(res - 1)) >> 8) + ((res & 0xff000000) | (unsigned int)(res >> 8)) + 1; ``` #### 驗證 首先要確定沒有編譯出 branching instruction,所以用 gdb 來查看: ``` ... 0x00001574 <+10>: beq 0x15d4 <cst_memcmp+106> ... 0x000015a0 <+54>: bne 0x157e <cst_memcmp+20> ... 0x000015d6 <+108>: jmpa 0x15a2 <cst_memcmp+56> ``` 被編譯出來的組合語言有三個 branching instruction,其中 `beq` 跟 `jmpa` 是 if 產生出來的,`bne` 則是迴圈,其餘部分沒有任何 branch 出現 接著驗證結果正確性,這邊用 3 個 test case 來測試 `cst_memcmp()` 的回傳結果: ```clike #define TEST_START int pass = 0; #define GET_TEST_RESULT(total, success) \ total = 3; \ success = pass; #define TEST_RULE(m1, m2, expected) \ do { \ pass += cst_memcmp(m1, m2, MAP_SIZE); == expected; \ } while (0); #define TEST_CASE_1(m1, m2) \ do { \ memset(m1, 0, MAP_SIZE); \ memset(m2, 0, MAP_SIZE); \ TEST_RULE(m1, m2, 0); \ } while (0); #define TEST_CASE_2(m1, m2) \ do { \ memset(m1, 1, MAP_SIZE >> 1); \ memset(m2, 0, MAP_SIZE); \ TEST_RULE(m1, m2, 1); \ } while (0); #define TEST_CASE_3(m1, m2) \ do { \ memset(m1, 0, MAP_SIZE); \ memset(m2, 1, MAP_SIZE >> 1); \ TEST_RULE(m1, m2, -1); \ } while (0); int main () { ... TEST_START; TEST_CASE_1(a, b); TEST_CASE_2(a, b); TEST_CASE_3(a, b); GET_TEST_RESULT(p[0], p[1]); setreturn(res, sizeof(int) * 2); _exit(0); return 0; } ``` 用 hexdump 查看結果: ``` 00000000 03 00 00 00 03 00 00 00 |........| 00000008 ``` 前 4 個 bytes 代表測試的 case 數量,後 4 bytes 代表通過測試的 case 數量,可以看到回傳結果都正確,程式實作上沒有問題 再來要驗證是否為常數時間: 1. 由於sandbox 中無法取得系統時間,所以使用 perf 對整個 sandbox 執行 100 次,觀察其 cycle 數量以及執行的秒數 2. 準備三組測試資料: - **測試資料 1**:1Mb,每個 bit 皆為 0 - **測試資料 2**:1Mb,前 512kb 每個 bit 為 1,後 512kb 每個 bit 為 0 - **測試資料 3**:1Mb,每個 bit 皆為 1 3. 每組測試資料皆用於與 1Mb 每個 bit 皆為 0 的靜態資料比較 4. 確保準備測試資料的時間為常數時間,所以測試資料預先準備好,於 sandbox 執行時載入 完整程式碼如下: ```clike #include <stddef.h> #include "sandboxrt.h" #define PAGE_SIZE 0x1000 #define MAP_SIZE 0x100000 #define MMAP_FAILED ((void *) -22) static int prot = MOXIE_PROT_READ | MOXIE_PROT_WRITE | MOXIE_PROT_EXEC; static int flags = MOXIE_MAP_PRIVATE | MOXIE_MAP_ANONYMOUS; static struct moxie_memory_map_ent *data; static void find_input(void) { data = moxie_memmap; while (data->addr) { if (strstr(data->tags, "data0,")) return; data++; } _exit(1); } int main() { find_input(); void *mem = mmap(NULL, MAP_SIZE, prot, flags, 0, 0); if (mem == MMAP_FAILED) { _exit(1); } memset(mem, 0, MAP_SIZE); cst_memcmp(mem, data->addr, MAP_SIZE); _exit(0); return 0; } ``` 在這個測試程式中除了 `cst_memcmp()` 待驗證以外,排除所有非常數時間的程式碼 perf 執行結果: - 測試資料 1 ``` Performance counter stats for 'src/sandbox -e tests/cst_memcmp_time_test -d tmp/0k.in' (100 runs): 8,0428,2638 cycles ( +- 0.01% ) 0.225285735 seconds time elapsed ( +- 0.04% ) ``` - 測試資料 2 ``` Performance counter stats for 'src/sandbox -e tests/cst_memcmp_time_test -d tmp/512k.in' (100 runs): 8,0465,0895 cycles ( +- 0.01% ) 0.225444945 seconds time elapsed ( +- 0.05% ) ``` - 測試資料 3 ``` Performance counter stats for 'src/sandbox -e tests/cst_memcmp_time_test -d tmp/1M.in' (100 runs): 8,0434,6874 cycles ( +- 0.01% ) 0.225341900 seconds time elapsed ( +- 0.04% ) ``` 可以看到 3 組測試資料的執行時間非常接近,其彼此的差距約略在 $0.02\%\sim0.03\%$,自己執行 100 次的差距也在 $10 ^ {-4}$ 的層級,故可以確認 `cst_memcmp()` 執行時間為常數時間 #### 應用場合 constant time memcmp 可以應用於機密資料的比較,比如雜湊或加密資料比較,在 [linux man pages - memcmp(3)](http://man7.org/linux/man-pages/man3/memcmp.3.html) 中提到 `Do not use memcmp() to compare security critical data`,原因在於 memcmp 從頭逐個 byte 比較,一找到不一樣就會馬上回傳 Timing Attack 就是利用這個概念根據 memcmp 的執行時間把被保護的資料猜出來,比如一個系統的資料庫存著 hash 過的密碼,每次都使用 memcmp 來比較送過來的 hash 以及密碼的 hash,攻擊者就能根據執行的時間猜出密碼的 hash 原本參考的 [github repo](https://github.com/chmike/cst_time_memcmp) 中有提供 NetBSD 所使用的 constant time memcmp ### cn_string #### 原理 cn_string 的主要目的是想要在 c 語言裡模擬 c++ 的 string 物件,使用 typedef 定義一個新的資料結構 `CN_STRING` ,並實作 string 物件的功能 ```clike typedef unsigned int cn_uint; typedef unsigned char cn_byte; typedef struct cn_string { cn_byte *data; cn_uint len; } * CN_STRING; ``` #### 移植 移植最關鍵的問題在於 `moxiebox` 並沒有實作 `malloc`, `calloc` 這些 function ,必須重新實作控制記憶體的部分 以 memory pool 的概念實作,先使用 `moxiebox` 提供的 `mmap` 取得一段可以使用的記憶體, `malloc`, `calloc` 的記憶體都將從這段記憶體裡取出 ```clike typedef struct { void *ptr; size_t used; size_t size; } MEMPool; MEMPool *pool; int main() { ... // initial memory pool pool = mmap(NULL, PAGE_SIZE, prot, flags, 0, 0); pool->ptr = mmap(NULL, MAP_SIZE, prot, flags, 0, 0); pool->used = 0; pool->size = MAP_SIZE; ... } ``` ```clike void *my_malloc(size_t size) { if (pool->used + size <= pool->size) { void *p = pool->ptr + pool->used; pool->used += size; return p; } return NULL; } void *my_calloc(size_t num, size_t size) { if (pool->used + (num * size) <= pool->size) { void *p = pool->ptr + pool->used; memset(p, 0, num * size); pool->used += num * size; return p; } return NULL; } ``` #### 驗證 將 `CN_STRING` 裡的資料使用 `setreturn` 匯出,並用 `hexdump` 驗證結果是否正確 ```clike int main() { void *res = mmap(NULL, PAGE_SIZE, prot, flags, 0, 0); ... // Starting with initial string CN_STRING str_i = cn_string_from_cstr("This is also a test."); // Output to check result int i; char *p = res; for (i = 0; i < str_i->len; i++) { p[i] = str_i->data[i]; } setreturn(res, sizeof(char) * str_i->len); ... } ``` ```shell $ ../src/sandbox -e cn_string -o ../tmp/cn_string.out $ hexdump -C ../tmp/cn_string.out 00000000 54 68 69 73 20 69 73 20 61 6c 73 6f 20 61 20 74 |This is also a t| 00000010 65 73 74 2e |est.| 00000014 ``` #### 應用場合 若須撰寫大量使用到字串處理的程式,有以下優點 * 查詢字串長度時間恆定,只需呼叫 `str->len` 即可取得字串長度,是一種使用空間換取時間的設計理念 * 較安全的指標使用,如果遇到 NULL 資料,會回傳 1 個 byte 的空字串,並將字串長度設為 0 ,不會出現 NULL pointer ## 參考資料 [moxiebox architecture](http://moxielogic.org/blog/pages/architecture.html) [sandbox execution environment](https://github.com/ian910297/moxiebox/blob/master/sandbox-design.md) [Understanding the Stack](https://www.cs.umd.edu/class/sum2003/cmsc311/Notes/Mips/stack.html) [Microsoft - using MDLs](https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/using-mdls) [The GDB remote serial protocol - Communication protocol](ftp://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_129.html#SEC134) [Howto: GDB Remote Serial Protocol](http://www.embecosm.com/appnotes/ean4/embecosm-howto-rsp-server-ean4-issue-2.html) [Program Error Signals](https://www.gnu.org/software/libc/manual/html_node/Program-Error-Signals.html) [Interpreting gprof's Output](https://ftp.gnu.org/old-gnu/Manuals/gprof-2.9.1/html_chapter/gprof_5.html) [GCC - Optimize Option](https://gcc.gnu.org/onlinedocs/gcc-4.7.4/gcc/Optimize-Options.html#Optimize-Options) [stack exchange - Why should memcmp not be used to compare security critial data ?](https://security.stackexchange.com/questions/160808/why-should-memcmp-not-be-used-to-compare-security-critical-data) [linux man pages - memcmp(3)](http://man7.org/linux/man-pages/man3/memcmp.3.html)