--- tags: linux2025 --- # [2025q1](https://wiki.csie.ncku.edu.tw/linux/schedule) 第 4 週測驗題 :::info 目的: 檢驗學員對 [bitops](https://hackmd.io/@sysprog/c-bitwise) 及數值系統的認知 ::: ==[作答表單: 測驗 `1`](https://docs.google.com/forms/d/e/1FAIpQLSdgTPAv4jwn4dCNepDTjLoavgP5ljyWfETrLVP5ROiO2ayhTg/viewform?usp=dialog)== ==[作答表單: 測驗 `2`, `3`](https://docs.google.com/forms/d/e/1FAIpQLSd2uyZy_O8WVo5gXg0vTf5tNi61cJ75lEnqc1dOmk_jhoJSUg/viewform?usp=dialog)== ### 測驗 `1` [CRC](https://en.wikipedia.org/wiki/Cyclic_redundancy_check) (循環冗餘校驗,Cyclic Redundancy Check) 是種用於網路與儲存設備的錯誤檢測演算法。發送端指定一個數值作為除數,並對訊息進行除法運算來取得餘數,然後將餘數附加到訊息的末尾。接收端對該訊息執行相同的運算來驗證是否有錯誤,如果餘數不為零,則表示訊息有誤。由於這種方法不會修改訊息內容 (冗餘性),且計算方式只是將被除數位移後再相減 (循環碼),因此稱為 CRC。 當 CRC 演算法的除數 (正式名稱為 check value) 長度為 n 個位元時,稱為 n-bit CRC。CRC32C 是 CRC32 的變體,其除數為 32 位元的二進位數。多項式如下: $$ P(x) = x^{32} + x^{28} + x^{27} + x^{26} + x^{25} + x^{23} + x^{22} + x^{20} + x^{19} + x^{18} + x^{14} + x^{13} + x^{11} + x^{10} + x^9 + x^8 + x^6 + 1 $$ 對應的十六進位表示為 `0x1EDC6F41` CRC 不把資料當作數字,而是當作多項式係數來處理。例如,8-bit 資料 `11010101` 可以表示為: $$ 1 \cdot x^7 + 1 \cdot x^6 + 0 \cdot x^5 + 1 \cdot x^4 + 0 \cdot x^3 + 1 \cdot x^2 + 0 \cdot x^1 + 1 \cdot x^0 $$ 即: $$ x^7 + x^6 + x^4 + x^2 + x^0 $$ 計算 CRC 時,使用資料的多項式去除以預先指定的 CRC 多項式,取餘數作為校驗碼。例如,給定資料 `11010101`(8-bit),若用 `0x1EDC6F41` 來除,就會得到一個 32-bit 餘數,這就是 CRC32 的值。 CRC 計算時,不是使用標準的數字除法,而是透過 XOR 運算來模擬二進位多項式的除法。最後留下來的 32-bit 餘數,就是 CRC32 校驗碼。 接下來我們採用位元反轉方式。至於使用該方法的原因,可見 [Fast CRC32](https://create.stephan-brumme.com/crc32/)。 最初的實作: ```c uint32_t crc32_naive(uint32_t crc, uint8_t v) { crc ^= v; for (int bit = 0; bit < 8; bit++) { if (crc & 1) crc = (crc >> 1) ^ UINT32_C(0x82f63b78); else crc = (crc >> 1); } return crc; } ``` 現代編譯器可以將三元運算子最佳化為條件移動 (conditional move,簡稱 cmov) 指令,以避免分支。因此,我們可將 if-else 改為三元運算子: ```c for (int bit = 0; bit < 8; bit++) crc = (crc & 1) ? ((crc >> 1) ^ UINT32_C(0x82f63b78)) : (crc >> 1); ``` 接著我們可發現 P 的哪些位元會進行 XOR 運算,完全由 `crc` 較低 8 個位元所決定。因此,可改寫為: ```c crc = (crc >> 1) ^ A; crc = (crc >> 1) ^ B; ... crc = (crc >> 1) ^ H; ``` 將其重寫為單一運算式: ```c (crc >> 8) ^ (A >> 7) ^ (B >> 6) ^ (C >> 5) ^ (D >> 4) ^ (E >> 3) ^ (F >> 2) ^ (G >> 1) ^ H ``` 將 A 至 H 合併為單一值 T,則可簡化為: ```c (crc >> 8) ^ T ``` 我們可預先計算 T,因為它僅由 256 種排列組合組成。知名的模擬器專案 QEMU 就採用該手法,參見 [util/crc32c.c](https://github.com/qemu/qemu/blob/907209e3111dd62a553a19319b422ff8aba5b9c0/util/crc32c.c#L40) ```c static const uint32_t crc32c_table[256] = { 0x00000000L, 0xF26B8303L, 0xE13B70F7L, 0x1350F3F4L, ... 0xBE2DA0A5L, 0x4C4623A6L, 0x5F16D052L, 0xAD7D5351L }; uint32_t crc32c(uint32_t crc, const uint8_t *data, unsigned int length) { while (length--) { crc = crc32c_table[(crc ^ *data++) & 0xFFL] ^ (crc >> 8); } return crc ^ 0xffffffff; } ``` 上述方式需要佔用 1 KiB 的空間來保存表格。 參見 [Fast CRC32](https://create.stephan-brumme.com/crc32/) 提到的 "Half-Byte Algorithm",我們可將 8-bit 表格查詢拆分為二次 4-bit 查詢,產生器如下: ```c /* CRC32C polynomial */ #define POLY 0xEDB88320 void generate() { printf("static const uint32_t crc32_table[16] = {\n"); for (size_t i = 0; i < 16; i++) { uint32_t crc = i; for (uint32_t j = 0; j < 4; j++) crc = (crc >> 1) ^ (-(int)(crc & 1) & POLY); printf(" 0x%08X%s", crc, (i % 4 == 3) ? ",\n" : ", "); } printf("};\n"); } ``` > [godbolt](https://godbolt.org/z/WxaPnqqqM) 以下程式碼 (部分遮蔽) 具備與 `crc32_naive` 完全等效的行為。 ```c uint32_t crc32_u8(uint32_t crc, uint8_t v) { crc ^= v; static const uint32_t crc32_table[] = { 0x00000000, 0x105ec76f, 0x20bd8ede, 0x30e349b1, 0x417b1dbc, 0x5125dad3, 0x61c69362, 0x7198540d, 0x82f63b78, 0x92a8fc17, 0xa24bb5a6, 0xb21572c9, AAAA, BBBB, CCCC, DDDD, }; crc = (crc >> 4) ^ crc32_table[crc & 0x0F]; crc = (crc >> 4) ^ crc32_table[crc & 0x0F]; return crc; } ``` 補完程式碼,使其運作符合預期,AAAA 到 DDDD 均為十六進位數值,亦即以 `0x` 開頭,小寫字母。 :::success 延伸問題: 1. 在 Linux 核心原始程式碼中找到 CRC32 的應用案例至少 3 處,並探討其原始程式碼 2. 設計實驗來分析 [Fast CRC32](https://create.stephan-brumme.com/crc32/) 提出的手法 (要涵蓋 branchless 和查表的變形),並與硬體的 CRC32 指令效能進行比較。 3. 以 x86-64 或 Arm64 為探討標的,說明 Linux 核心如何使用硬體加速的 CRC32 指令 ::: --- ### 測驗 `2` 以下程式碼實作小型的模組化合成器 (Synthesizer 或 Synth),得以處理多個「聲音」及其內部互連的元件來產生音訊。程式的核心架構是由代表振盪器、[envelope](https://vocus.cc/article/64ed72c5fd89780001569b27)、濾波器和混音器的各個節點所構成。每個聲音都包含這些節點,它們連接一起以便產生並調整聲音的特性。 程式碼使用定點數,而用 [Q notation](https://en.wikipedia.org/wiki/Q_(number_format)),以下是產生正弦波表格的 Python 程式: ```python import math # Generate 128 sample values for a sine wave lut = [] for i in range(128): angle = (i / 128.0) * (2 * math.pi) lut.append(round(math.sin(angle) * 127)) # Append an extra value (0) to make interpolation easier, as in the C version. lut.append(0) # Print the lookup table values as a comma-separated string (without [ and ]) print(', '.join(map(str, lut))) ``` 該輸出結果導向至 `sine-table.h` 檔案。 程式碼輸出多種[波形](https://en.wikipedia.org/wiki/Square_wave_(waveform)): ![image](https://hackmd.io/_uploads/Hka-nZD2kg.png =70%x) 對應的函式原型: * `q15_t sawtooth_wave(q15_t input)` * `q15_t sine_wave(q15_t input)` * `q15_t square_wave(q15_t input)` 音訊合成器的程式碼可見 [main.c](https://gist.github.com/jserv/061b2ac9be06b446f31087357b8e4054) (部分遮蔽),編譯並執行: ```shell $ cc -O2 -o main -Wall main.c $ ./main ``` 預期會輸出 `out.wav`,其內容為〈[一閃一閃亮晶晶](https://zh.wikipedia.org/zh-tw/%E4%B8%80%E9%96%83%E4%B8%80%E9%96%83%E4%BA%AE%E6%99%B6%E6%99%B6)〉,可用 [MPlayer](https://help.ubuntu.com/community/MPlayer) 一類的工具播放。 補完程式碼以符合預期,作答規範: * AAAA 到 EEEE 為十六進位數值 (即 `0x` 開頭),大寫英文字母,最精簡的方式書寫 * FFFF 為 C 語言表示式,使用一致的縮排 (注意空白字元),不能用除法和取餘數運算子,且 FFFF 應有 hexadecimal integer literal (即 `0x` 開頭的數值) * GGGG 和 HHHH 為 `Q15_` 開頭的巨集名稱 :::success 延伸問題: 1. 解釋上述程式碼運作原理,搭配「[信號與系統](https://ocw.aca.ntu.edu.tw/ntu-ocw/ocw/cou/103S207)」教材 2. 探討定點數的使用和其精確度 3. 改進 MIDI 一閃一閃亮晶晶的音效輸出 ::: --- ### 測驗 `3` [chat.c](https://gist.github.com/jserv/3294f32b2824076ac17683fdac2daf24) (部分遮蔽) 實作精簡的聊天伺服器,可用 TCP 連線工具 (如 telnet) 存取。 作答規範: * IIII 和 JJJJ 為 C 語言表示式 :::success 延伸問題: 1. 解釋上述程式碼運作原理 2. 學習 [jserv/ttt](https://github.com/jserv/ttt) 程式碼,在不使用 (n)curses 一類現成的函式庫前提下,改進網路聊天室的文字呈現介面 3. 在既有的程式碼之上,實作帳號登入 (login) 及基本管理功能,並該有線上與否的提示,留意 [ping value](https://www.business.att.com/resources/knowledge-center/what-is-latency-impact-on-gaming-streaming.html) 的處理機制 :::