--- title: 2025 年 Linux 核心設計課程作業 —— kecho + khttpd image: https://hackmd.io/_uploads/HybjLFz1ll.png description: 檢驗學員對 Linux 核心 kthread 和 workqueue 處理機制的認知 tags: linux2025 --- # N07: ktcp > 主講人: [jserv](https://wiki.csie.ncku.edu.tw/User/jserv) / 課程討論區: [2025 年系統軟體課程](https://www.facebook.com/groups/system.software2025/) :mega: 返回「[Linux 核心設計](https://wiki.csie.ncku.edu.tw/linux/schedule)」課程進度表 ## `khttpd` 程式碼導讀 ### 掛載 `khttpd` 核心模組 掛載 `khttpd` 時,會執行函式 `khttpd_init` ,程式碼如下所示: ```c static int __init khttpd_init(void) { int err = open_listen_socket(port, backlog, &listen_socket); if (err < 0) { pr_err("can't open listen socket\n"); return err; } param.listen_socket = listen_socket; http_server = kthread_run(http_server_daemon, &param, KBUILD_MODNAME); if (IS_ERR(http_server)) { pr_err("can't start http server daemon\n"); close_listen_socket(listen_socket); return PTR_ERR(http_server); } return 0; } ``` `khttpd` 模組初始化的設定和 `kecho` 模組相似,但仍然可發現二者不同之處,最明顯在於 `khttpd` 不使用函式 `alloc_workqueue`,而用系統預設的 workqueue ,因此之後可討論二者之間的效能差異,以下主要將 `khttpd` 分成兩個部份 - `open_listen`: 建立伺服器並等待連線 - `kthread_run`: 用於建立一個立刻執行的執行緒 首先函式 `open_listen` 的部份,建立 socket 連線的步驟都相同,而這邊有個特別的函式 `setsockopt` ,以下節錄部份 `open_listen` 程式碼及 `setsockopt` 程式碼 ```c static int open_listen_socket(ushort port, ushort backlog, struct socket **res) { ... err = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, 1); if (err < 0) goto bail_setsockopt; err = setsockopt(sock, SOL_TCP, TCP_NODELAY, 1); if (err < 0) goto bail_setsockopt; err = setsockopt(sock, SOL_TCP, TCP_CORK, 0); if (err < 0) goto bail_setsockopt; err = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, 1024 * 1024); if (err < 0) goto bail_setsockopt; err = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, 1024 * 1024); if (err < 0) goto bail_setsockopt; ... } static inline int setsockopt(struct socket *sock, int level, int optname, int optval) { int opt = optval; return kernel_setsockopt(sock, level, optname, (char *) &opt, sizeof(opt)); } ``` 這邊要留意判斷 Linux 核心版本,參考 [Support Linux v5.8+ (#5)](https://github.com/sysprog21/khttpd/commit/6312a2dd5e5c5995d0bd27ecfe2264f18d1dfbe4) 及 [net: remove kernel_setsockopt](https://github.com/torvalds/linux/commit/5a892ff2facb4548c17c05931ed899038a0da63e) 發現函式 `kernel_setsockopt` 在 Linux v5.8 之後已被移除,因此在 `khttpd` 模組裡有對應不同 Linux 核心版本的實作 ```c #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 8, 0) ``` 接著研究像是 `SOL_SOCKET` 和 `SOL_TCP` 這類設定的意義,分別參考 [socket(7) - Linux man page](https://linux.die.net/man/7/socket) 及 [tcp(7) — Linux manual page](https://man7.org/linux/man-pages/man7/tcp.7.html) ,以下整理 `khttpd` 所使用到的設定,關於其中 `SO_REUSEADDR`,可對照 [What is the meaning of SO_REUSEADDR (setsockopt option) - Linux?](https://stackoverflow.com/questions/3229860/what-is-the-meaning-of-so-reuseaddr-setsockopt-option-linux) - [ ] `SOL_SOCKET` | Setting | Description | | ------------ | ----------- | | SO_REUSEADDR | 在原本的連線結束後,有使用相同 IP 及 Port 的連線要求出現,讓 socket 可直接重新建立連線 | | SO_RCVBUF | 設定 socket receive buffer 可接收的最大數量 | | SO_SNDBUF | 設定 socket send buffer 可送出的最大數量 | - [ ] `SOL_TCP` | Setting | Description | | ------------ | ----------- | | TCP_NODELAY | 關閉 Nagle's algorithm — 參考 [Best Practices for TCP Optimization in 2019](https://www.extrahop.com/company/blog/2016/tcp-nodelay-nagle-quickack-best-practices/) | | TCP_CORK | 常搭配 TCP_NODELAY 使用,為了避免不斷送出資料量不多 (小於 MSS) 的封包,使用 `TCP_CORK` 可將資料匯聚並且一次發送資料量較大的封包 — 參考 [Is there any significant difference between TCP_CORK and TCP_NODELAY in this use-case?](https://fullstackuser.com/questions/327722/is-there-any-significant-difference-between-tcp-cork-and-tcp-nodelay-in-this-use) | 建立 socket 後,使用函式 `kthread_run` 建立執行緒並執行函式 `http_server_daemon` ```c int http_server_daemon(void *arg) { struct socket *socket; struct task_struct *worker; struct http_server_param *param = (struct http_server_param *) arg; // 登記要接收的 signal allow_signal(SIGKILL); allow_signal(SIGTERM); // 判斷執行緒是否該被中止 while (!kthread_should_stop()) { int err = kernel_accept(param->listen_socket, &socket, 0); if (err < 0) { // 檢查目前執行緒是否有 signal 發生 if (signal_pending(current)) break; pr_err("kernel_accept() error: %d\n", err); continue; } worker = kthread_run(http_server_worker, socket, KBUILD_MODNAME); if (IS_ERR(worker)) { pr_err("can't create more worker process\n"); continue; } } return 0; } ``` 整體程式邏輯類似 `kecho` 核心模組,首先登記 `SIGKILL` 及 `SIGTERM` ,接著使用函式 `kthread_should_stop` 判斷負責執行函式 `http_server_daemon` 的執行緒是否應該中止,使用函式 `kernel_accept` 接受 client 連線要求,成功建立後使用函式 `kthread_run` 建立新的執行緒並且執行函式 `http_server_worker`。 ### 執行 `http_server_worker` 每條連線都由一個子執行緒負責,該執行緒進入 `http_server_worker` 後依序完成下列工作: 1. 設定 HTTP parser 的回呼 (callback) 函式,用於將回應資料傳送給 client 2. 進入事件驅動的主迴圈,藉由 `kthread_should_stop()` 檢查是否需要終止 3. 從 socket 讀取資料 4. 以 `http_parser_execute()` 解析請求內容 5. 連線結束時關閉 socket 並釋放配置的緩衝區等資源 ```c static int http_server_worker(void *arg) { char *buf; struct http_parser parser; // 設定 callback function struct http_parser_settings setting = { .on_message_begin = http_parser_callback_message_begin, .on_url = http_parser_callback_request_url, .on_header_field = http_parser_callback_header_field, .on_header_value = http_parser_callback_header_value, .on_headers_complete = http_parser_callback_headers_complete, .on_body = http_parser_callback_body, .on_message_complete = http_parser_callback_message_complete}; struct http_request request; struct socket *socket = (struct socket *) arg; allow_signal(SIGKILL); allow_signal(SIGTERM); buf = kmalloc(RECV_BUFFER_SIZE, GFP_KERNEL); if (!buf) { pr_err("can't allocate memory!\n"); return -1; } request.socket = socket; // 設定 parser 初始參數 http_parser_init(&parser, HTTP_REQUEST); parser.data = &request; // 判斷執行緒是否該被中止 while (!kthread_should_stop()) { // 接收資料 int ret = http_server_recv(socket, buf, RECV_BUFFER_SIZE - 1); if (ret <= 0) { if (ret) pr_err("recv error: %d\n", ret); break; } // 解析收到的資料 http_parser_execute(&parser, &setting, buf, ret); if (request.complete && !http_should_keep_alive(&parser)) break; } kernel_sock_shutdown(socket, SHUT_RDWR); sock_release(socket); kfree(buf); return 0; } ``` 設定回呼函式的部份,主要是用來送出回應 client 的資料,以下為相關函式 ```c static int http_parser_callback_message_complete(http_parser *parser) { struct http_request *request = parser->data; http_server_response(request, http_should_keep_alive(parser)); request->complete = 1; return 0; } static int http_server_response(struct http_request *request, int keep_alive) { char *response; pr_info("requested_url = %s\n", request->request_url); if (request->method != HTTP_GET) response = keep_alive ? HTTP_RESPONSE_501_KEEPALIVE : HTTP_RESPONSE_501; else response = keep_alive ? HTTP_RESPONSE_200_KEEPALIVE_DUMMY : HTTP_RESPONSE_200_DUMMY; http_server_send(request->socket, response, strlen(response)); return 0; } ``` 而呼叫以下函式的時機在於解析整個資料後,可在函式 `http_parser_execute` 裡找到相關實作。 接著探討整個 `khttpd` 關鍵的函式 `http_parser_execute` ,其功能就是將收到的資料進行解讀,並傳送給 client ```c= size_t http_parser_execute (http_parser *parser, const http_parser_settings *settings, const char *data, size_t len) { ... for (p=data; p != data + len; p++) { ch = *p; if (PARSING_HEADER(CURRENT_STATE())) COUNT_HEADER_SIZE(1); reexecute: switch (CURRENT_STATE()) { ... case s_start_req: { if (ch == CR || ch == LF) break; parser->flags = 0; parser->uses_transfer_encoding = 0; parser->content_length = ULLONG_MAX; if (UNLIKELY(!IS_ALPHA(ch))) { SET_ERRNO(HPE_INVALID_METHOD); goto error; } parser->method = (enum http_method) 0; parser->index = 1; switch (ch) { case 'A': parser->method = HTTP_ACL; break; case 'B': parser->method = HTTP_BIND; break; case 'C': parser->method = HTTP_CONNECT; /* or COPY, CHECKOUT */ break; case 'D': parser->method = HTTP_DELETE; break; case 'G': parser->method = HTTP_GET; break; case 'H': parser->method = HTTP_HEAD; break; case 'L': parser->method = HTTP_LOCK; /* or LINK */ break; case 'M': parser->method = HTTP_MKCOL; /* or MOVE, MKACTIVITY, MERGE, M-SEARCH, MKCALENDAR */ break; case 'N': parser->method = HTTP_NOTIFY; break; case 'O': parser->method = HTTP_OPTIONS; break; case 'P': parser->method = HTTP_POST; /* or PROPFIND|PROPPATCH|PUT|PATCH|PURGE */ break; case 'R': parser->method = HTTP_REPORT; /* or REBIND */ break; case 'S': parser->method = HTTP_SUBSCRIBE; /* or SEARCH, SOURCE */ break; case 'T': parser->method = HTTP_TRACE; break; case 'U': parser->method = HTTP_UNLOCK; /* or UNSUBSCRIBE, UNBIND, UNLINK */ break; default: SET_ERRNO(HPE_INVALID_METHOD); goto error; } UPDATE_STATE(s_req_method); CALLBACK_NOTIFY(message_begin); break; } ... case s_message_done: UPDATE_STATE(NEW_MESSAGE()); CALLBACK_NOTIFY(message_complete); if (parser->upgrade) { /* Exit, the rest of the message is in a different protocol. */ RETURN((p - data) + 1); } break; ... } ... } ... } ``` 函式 `http_parser_execute` 主要是一個很大的迴圈,將讀取到的資料的每個字元進行解讀,這邊特別提到兩種情況,分別是 `s_start_req` 及 `s_message_done` 在第 7 行可看到整個函式的使用,第 15 行可看到 `s_start_req` 的情況,其功能是當一開始進行解析時,會使用第一個字元判斷該要求是屬於那一種的類型,可在第 31 ~ 48 行找到各種的對應 第 57 行可看到 `s_message_done` 的實作,其功能是解析資料完畢後,要給 client 對應的回應,主要是使用以下的巨集進行上面提過的回呼函式呼叫 (位於第 59 行) ```c CALLBACK_NOTIFY(message_complete); ``` ### 比較 `khttpd` 和 CS:APP 給定的網站伺服器 理解 khttpd 的整體流程後,可將其與 [CS:APP](https://hackmd.io/@sysprog/CSAPP) 中介紹的 TINY Web 伺服器進行比較。 ![TINY server flow](https://hackmd.io/_uploads/HJvKdrxJxl.png) 上圖為 CS:APP 教材提供的伺服器流程。可觀察到: 1. 兩者建立 socket 的步驟皆為 `socket` → `bind` → `listen` → `accept` → 資料傳輸 差異在呼叫介面:khttpd 直接使用 Linux 核心 API,而 TINY Web 在使用者空間透過標準系統呼叫 2. I/O 部分,khttpd 採用核心函式 `kernel_recvmsg`、`kernel_sendmsg`;TINY Web 則以自寫的 RIO (可靠 I/O) 套件包裝 `read` 與 `write`,簡化阻塞處理與緩衝管理 其他主要差別: * 執行層級:khttpd 位於 kernel space,TINY Web 屬 user space * 並行策略:khttpd 以多執行緒並行服務多連線,TINY Web 單執行緒依序處理,每次僅服務一位 client ### `khttpd` 實作的缺失 在函式 `http_server_worker` 執行迴圈的部份,如下所示 ```c while (!kthread_should_stop()) { // 接收資料 int ret = http_server_recv(socket, buf, RECV_BUFFER_SIZE - 1); if (ret <= 0) { if (ret) pr_err("recv error: %d\n", ret); break; } // 解析收到的資料 http_parser_execute(&parser, &setting, buf, ret); if (request.complete && !http_should_keep_alive(&parser)) break; } ``` 觀察程式碼後發現,用於接收資料的緩衝區 `buf` 在每次迴圈結束並未清空,殘留內容可能影響下一次解析而產生非預期結果。 為驗證此現象,進行一項簡單測試:先執行 `telnet localhost 8081` 連入伺服器,依序送出 `GET /12345 HTTP/1.1` 和 `GET / HTTP/1.1`。 ``` GET /12345 HTTP/1.1 HTTP/1.1 200 OK Server: khttpd Content-Type: text/plain Content-Length: 12 Connection: Keep-Alive Hello World! GET / HTTP/1.1 HTTP/1.1 200 OK Server: khttpd Content-Type: text/plain Content-Length: 12 Connection: Keep-Alive Hello World! ``` 雖然可見伺服器正常回應,但是查看核心模組相關的訊息 ```shell [186673.227429] khttpd: buf = GET /12345 HTTP/1.1 [186673.338073] khttpd: buf = T /12345 HTTP/1.1 [186673.338083] khttpd: requested_url = /12345 [186733.791423] khttpd: buf = GET / HTTP/1.1 1.1 [186733.918134] khttpd: buf = T / HTTP/1.1 1.1 [186733.918155] khttpd: requested_url = / ``` 實測顯示 `buf` 內容確實會受到前一次輸入影響;雖然此範例未出現錯誤,但無法保證其他情境亦安全。 每送出一個請求時會看到 2 次 `buf =`,原因在於 HTTP 以 2 個 `\r\n` 判斷結束,使用者需按 2 次 Enter 按鍵才形成完整請求。 可在迴圈結束前呼叫 `memset` 清空 `buf`,避免殘留資料干擾下一次解析。 ```diff while (!kthread_should_stop()) { + int ret; + memset(buf, 0, RECV_BUFFER_SIZE); + ret = http_server_recv(socket, buf, RECV_BUFFER_SIZE - 1); if (ret <= 0) { if (ret) pr_err("recv error: %d\n", ret); break; } pr_info("buf = %s", buf); // 解析收到的資料 http_parser_execute(&parser, &setting, buf, ret); if (request.complete && !http_should_keep_alive(&parser)) break; } ``` 接著可再次嘗試上面的實驗,以下為模組輸出的結果 ```shell [187284.736753] khttpd: buf = GET /12345 HTTP/1.1 [187284.849034] khttpd: buf = [187284.849045] khttpd: requested_url = /12345 [187300.646245] khttpd: buf = GET / HTTP/1.1 [187300.784082] khttpd: buf = [187300.784103] khttpd: requested_url = / ``` 可很明顯看到參數 `buf` 已經不會被之前的輸入給影響 ### 減少 `printk` 的使用 在實作之前,先使用 `htstress.c` 測試原本 server 的效能,這裡使用命令 `./htstress http://localhost:8081 -t 3 -c 20 -n 200000` 進行測試 ```shell requests: 200000 good requests: 200000 [100%] bad requests: 0 [0%] socker errors: 0 [0%] seconds: 8.246 requests/sec: 24252.937 ``` 在 `http_server.h` 新增以下結構 ```c enum { TRACE_accept_err = 1, // accept 失敗總數 TRACE_cthread_err, // create thread 失敗總數 TRACE_kmalloc_err, // kmalloc 失敗總數 TRACE_recvmsg, // recvmsg 總數 TRACE_sendmsg, // sendmsg 總數 TRACE_send_err, // send request 失敗總數 TRACE_recv_err, // recv request 失敗總數 }; struct runtime_state { atomic_t accept_err, cthread_err; atomic_t kmalloc_err, recvmsg; atomic_t sendmsg, send_err; atomic_t recv_err; }; extern struct runtime_state states; ``` 而在 `khttpd` 裡,最常呼叫的 `pr_info` 位於函式 `http_server_response` ,以下為修改過程 ```diff static int http_server_response(struct http_request *request, int keep_alive) { char *response; + int ret; - pr_info("requested_url = %s\n", request->request_url); if (request->method != HTTP_GET) response = keep_alive ? HTTP_RESPONSE_501_KEEPALIVE : HTTP_RESPONSE_501; else response = keep_alive ? HTTP_RESPONSE_200_KEEPALIVE_DUMMY : HTTP_RESPONSE_200_DUMMY; ret = http_server_send(request->socket, response, strlen(response)); + if (ret > 0) + TRACE(sendmsg); + return 0; } ``` 這裡將 `pr_info` 移除,改成使用計算送出次數的方式,可避免每次送出資料前,都要先印出的多餘動作,而其他的部份也是做相同的事 最後輸入命令 `./htstress http://localhost:8081 -t 3 -c 20 -n 200000` 並測試: ```shell requests: 200000 good requests: 200000 [100%] bad requests: 0 [0%] socker errors: 0 [0%] seconds: 6.606 requests/sec: 30274.801 ``` 可見伺服器處理效率有明顯上升,再使用命令 `dmesg` 查看實際運作狀況,如下所示 ```shell [164105.005808] khttpd: recvmsg : 200046 [164105.005815] khttpd: sendmsg : 200046 [164105.005817] khttpd: kmalloc_err : 0 [164105.005819] khttpd: cthread_err : 0 [164105.005821] khttpd: send_err : 0 [164105.005823] khttpd: recv_err : 0 [164105.005824] khttpd: accept_err : 0 ``` ## [HTTP keep-alive](https://en.wikipedia.org/wiki/HTTP_persistent_connection) 模式 下圖將 HTTP 傳輸方式分為 2 種:multiple connections 與 persistent connection。 multiple connections 在伺服器回應後關閉連線;persistent connection 則維持同一 TCP 連線,可於其內處理多個請求。 ![](https://hackmd.io/_uploads/HkHudBx1xg.png) 根據 [HTTP 規範](https://en.wikipedia.org/wiki/HTTP_persistent_connection#Operation) 可得: 1. HTTP/1.0 預設採 multiple connections;若需維持連線,標頭須加入 `Connection: keep-alive` 2. HTTP/1.1 預設採 persistent connection,可在單一連線內連續處理多項請求;若想結束連線,需送出 `Connection: close` khttpd 測試步驟:透過 `telnet localhost 8081` 連線,輸入以下命令並觀察伺服器回應。 - [ ] `GET / HTTP/1.0` ``` HTTP/1.1 200 OK Server: khttpd Content-Type: text/plain Content-Length: 12 Connection: Close ``` - [ ] `GET / HTTP/1.1` ``` HTTP/1.1 200 OK Server: khttpd Content-Type: text/plain Content-Length: 12 Connection: Keep-Alive ``` 從回應中的 `Connection:` 欄位可見,khttpd 依 HTTP 版本自動切換連線模式,證實已內建 keep‑alive 支援。 ## Linux 核心如何處理傳遞到核心模組的參數 參考 [Passing Command Line Arguments to a Module](https://sysprog21.github.io/lkmpg/#passing-command-line-arguments-to-a-module) ,發現 kernel 是使用巨集 `module_param` 傳遞參數,接著可在檔案 `main.c` 發現該巨集的使用,可得知 `khttpd` 可讓使用者自己設定 `port` 及 `backlog` ```c static ushort port = DEFAULT_PORT; module_param(port, ushort, S_IRUGO); static ushort backlog = DEFAULT_BACKLOG; module_param(backlog, ushort, S_IRUGO); ``` 接著研究 `module_param` 的實作,可在 [linux/include/linux/moduleparam.h](https://github.com/torvalds/linux/blob/master/include/linux/moduleparam.h) 找到數個定義,將相關定義表示在下方 ```c #define module_param(name, type, perm) \ module_param_named(name, name, type, perm) #define module_param_named(name, value, type, perm) \ param_check_##type(name, &(value)); \ module_param_cb(name, &param_ops_##type, &value, perm); \ __MODULE_PARM_TYPE(name, #type) ``` 可分成 `param_check_##type`, `module_param_cb` 及 `__MODULE_PARM_TYPE` 做討論 ### `param_check_##type` 由於 `khttpd` 的變數是使用 `ushort` 的型態,因此巨集會被展開成 `param_check_ushort` ,以下為相關巨集 ```c #define param_check_ushort(name, p) __param_check(name, p, unsigned short) /* All the helper functions */ /* The macros to do compile-time type checking stolen from Jakub Jelinek, who IIRC came up with this idea for the 2.4 module init code. */ #define __param_check(name, p, type) \ static inline type __always_unused *__check_##name(void) { return(p); } ``` 從註解很明顯可知道 `param_check_##type` 的目的是要在編譯時期就判斷變數 `p` 是否真的是 `type` 型態,方法是藉由回傳 `p` 判斷函式是否回傳相同型態 ### `module_param_cb` ```c /** * module_param_cb - general callback for a module/cmdline parameter * @name: a valid C identifier which is the parameter name. * @ops: the set & get operations for this parameter. * @arg: args for @ops * @perm: visibility in sysfs. * * The ops can have NULL set or get functions. */ #define module_param_cb(name, ops, arg, perm) \ __module_param_call(MODULE_PARAM_PREFIX, name, ops, arg, perm, -1, 0) /* This is the fundamental function for registering boot/module parameters. */ #define __module_param_call(prefix, name, ops, arg, perm, level, flags) \ /* Default value instead of permissions? */ \ static const char __param_str_##name[] = prefix #name; \ static struct kernel_param __moduleparam_const __param_##name \ __used __section("__param") \ __aligned(__alignof__(struct kernel_param)) \ = { __param_str_##name, THIS_MODULE, ops, \ VERIFY_OCTAL_PERMISSIONS(perm), level, flags, { arg } } ``` `__module_param_call` 建立一個型態為 `kernel_param` 且名稱為 `__param_##name` 的結構,並告訴編譯器以下資訊 - 將資料放在 `__param` 區 - 對齊 `__alignof__(struct kernel_param)` 的大小 接著查看結構 `kernel_param` 的宣告 ```c struct kernel_param { const char *name; struct module *mod; const struct kernel_param_ops *ops; const u16 perm; s8 level; u8 flags; union { void *arg; const struct kparam_string *str; const struct kparam_array *arr; }; }; ``` 由以上資訊我們可得到最後 `__module_param_call` 建立的結構,以變數 `port` 作為範例,如以下所示 ```c struct kernel_param _param_name { const char *name = "port"; struct module *mod = THIS_MODULE; const struct kernel_param_ops *ops = &param_ops_ushort; const u16 perm = VERIFY_OCTAL_PERMISSIONS(S_IRUGG); s8 level = -1; u8 flags = 0; void *arg = &port; }; ``` 最後使用命令 `readelf -r khttpd.ko` 查看 `__param` 的區域,的確有 `port` 和 `backlog` 的資料 ```shell Relocation section '.rela__param' at offset 0xc1a48 contains 8 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000000 000700000001 R_X86_64_64 0000000000000000 .rodata + 1248 000000000008 005600000001 R_X86_64_64 0000000000000000 __this_module + 0 000000000010 005400000001 R_X86_64_64 0000000000000000 param_ops_ushort + 0 000000000020 000e00000001 R_X86_64_64 0000000000000000 .data + 4 000000000028 000700000001 R_X86_64_64 0000000000000000 .rodata + 1250 000000000030 005600000001 R_X86_64_64 0000000000000000 __this_module + 0 000000000038 005400000001 R_X86_64_64 0000000000000000 param_ops_ushort + 0 000000000048 000e00000001 R_X86_64_64 0000000000000000 .data + 6 ``` ### `__MODULE_PARM_TYPE` ```c #define __MODULE_PARM_TYPE(name, _type) \ __MODULE_INFO(parmtype, name##type, #name ":" _type) #define __MODULE_INFO(tag, name, info) \ static const char __UNIQUE_ID(name)[] \ __used __section(".modinfo") __aligned(1) \ = __MODULE_INFO_PREFIX __stringify(tag) "=" info #define MODULE_PARAM_PREFIX KBUILD_MODNAME "." #define KBUILD_MODNAME /* empty */ ``` 參考〈[Linux 核心模組掛載機制](https://hackmd.io/@sysprog/linux-kernel-module)〉可知 `__UNIQUE_ID` 的功能 - `__UNIQUE_ID` 會根據參數產生一個不重複的名字,其中使用到的技術是利用巨集中的 `##` 來將兩個參數合併成一個新的字串 - 透過 `__attribute__` 關鍵字告訴編譯器,這段訊息 - 要被放在 `.modinfo` 區 (`__section(".modinfo")`) - 不會被程式使用到,所以不要產生警告訊息 (`__used`) - 最小的對齊格式是 1 bit (`__aligned(1)`) - 巨集 `__stringify` 的目的是為了把參數轉換成字串形式 - 巨集 `MODULE_PARAM_PREFIX` 由巨集 `KBUILD_MODNAME` 和 `"."` 組合而成,簡單來說就只是個字串 最後以變數 `port` 為例,會產生以下巨集 ```c #define __MODULE_INFO(tag, name, info) \ static const char __UNIQUE_ID(name)[] \ __used __section(".modinfo") __aligned(1) \ = ".parmtype=port:ushort." ``` 接著使用命令 `objdump -s khttpd.ko` 查看 `.modinfo` 的區域 ```diff ... Contents of section .modinfo: ... 0070 00706172 6d747970 653d6261 636b6c6f .parmtype=backlo + 0080 673a7573 686f7274 00706172 6d747970 g:ushort.parmtyp + 0090 653d706f 72743a75 73686f72 74007372 e=port:ushort.sr ... ``` 繼續根據〈[Linux 核心模組掛載機制](https://hackmd.io/@sysprog/linux-kernel-module)〉,使用 strace 追蹤 `insmod khttpd.ko` ```shell= $ sudo strace insmod khttpd.ko port=1999 execve("/usr/sbin/insmod", ["insmod", "khttpd.ko", "port=1999"], 0x7fff08f9ff70 /* 25 vars */) = 0 brk(NULL) = 0x5607976a6000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 ... mmap(NULL, 1366608, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa7c761b000 finit_module(3, "port=1999", 0) = -1 EEXIST (File exists) write(2, "insmod: ERROR: could not insert "..., 62insmod: ERROR: could not insert module khttpd.ko: File exists ) = 62 munmap(0x7fa7c761b000, 1366608) = 0 close(3) = 0 exit_group(1) = ? +++ exited with 1 +++ ``` 查看位於第 8 行 `finit_module` 的實作,參考 [kernel/module.c](https://github.com/torvalds/linux/blob/master/kernel/module.c) 及 [finit_module(2) - Linux man page](https://linux.die.net/man/2/finit_module) ```c int finit_module(int fd, const char *param_values, int flags); ``` 對應 strace 的結果 - `fd = 3` - `param_values = "port=1999"` - `flag = 0` ```c SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags) { ... return load_module(&info, uargs, flags); } ``` 函式 `finit_module` 呼叫函式 `load_module` ,接著繼續分析 ```c static int load_module(struct load_info *info, const char __user *uargs, int flags) { /* Now copy in args */ mod->args = strndup_user(uargs, ~0UL >> 1); if (IS_ERR(mod->args)) { err = PTR_ERR(mod->args); goto free_arch_cleanup; } } ``` 從上述程式碼可看到從命令列的輸入參數已經被複製到 `mod->arg` ,且 `mod` 的型態為 `struct mod` ,參考 [include/linux/module.h](https://github.com/torvalds/linux/blob/master/include/linux/module.h) ```c struct module { ... /* The command line arguments (may be mangled). People like keeping pointers to this stuff */ char *args; ... } ``` 找到了 `args` 的宣告,從註解可知道 `args` 的目的就是儲存 command line 的設定參數 回到 `load_module` ,發現了函式 `parse_args` ,從註解可知道是要將 command line 的字串拆解 ```c /* Module is ready to execute: parsing args may do that. */ after_dashes = parse_args(mod->name, mod->args, mod->kp, mod->num_kp, -32768, 32767, mod, unknown_module_param_cb); ``` 進到函式 `parse_args` ,參考 [kernel/params.c](https://github.com/torvalds/linux/blob/master/kernel/params.c) ```c /* Args looks like "foo=bar,bar2 baz=fuz wiz". */ char *parse_args(const char *doing, char *args, const struct kernel_param *params, unsigned num, s16 min_level, s16 max_level, void *arg, int (*unknown)(char *param, char *val, const char *doing, void *arg)) { char *param, *val, *err = NULL; /* Chew leading spaces */ args = skip_spaces(args); if (*args) pr_debug("doing %s, parsing ARGS: '%s'\n", doing, args); while (*args) { int ret; int irq_was_disabled; args = next_arg(args, &param, &val); /* Stop at -- */ if (!val && strcmp(param, "--") == 0) return err ?: args; irq_was_disabled = irqs_disabled(); ret = parse_one(param, val, doing, params, num, min_level, max_level, arg, unknown); if (irq_was_disabled && !irqs_disabled()) pr_warn("%s: option '%s' enabled irq's!\n", doing, param); switch (ret) { case 0: continue; case -ENOENT: pr_err("%s: Unknown parameter `%s'\n", doing, param); break; case -ENOSPC: pr_err("%s: `%s' too large for parameter `%s'\n", doing, val ?: "", param); break; default: pr_err("%s: `%s' invalid for parameter `%s'\n", doing, val ?: "", param); break; } err = ERR_PTR(ret); } return err; } ``` 函式 `parse_args` 做了以下: 1. 使用函式 `skip_spaces` 將字串的第一個字元如果為空白字元,將空白字元全部移除 2. 使用函式 `next_arg` 找到下一個 argument ,參考 [lib/cmdline.c](https://github.com/torvalds/linux/blob/a79cdfba68a13b731004f0aafe1155a83830d472/lib/cmdline.c) 3. 使用函式 `parse_one` 將試著將 argument 加進 module 裡,該函式位於 [kernel/params.c](https://github.com/torvalds/linux/blob/master/kernel/params.c) 最後討論函式 `parse_one` ```c= static int parse_one(char *param, char *val, const char *doing, const struct kernel_param *params, unsigned num_params, s16 min_level, s16 max_level, void *arg, int (*handle_unknown)(char *param, char *val, const char *doing, void *arg)) { unsigned int i; int err; /* Find parameter */ for (i = 0; i < num_params; i++) { if (parameq(param, params[i].name)) { if (params[i].level < min_level || params[i].level > max_level) return 0; /* No one handled NULL, so do it here. */ if (!val && !(params[i].ops->flags & KERNEL_PARAM_OPS_FL_NOARG)) return -EINVAL; pr_debug("handling %s with %p\n", param, params[i].ops->set); kernel_param_lock(params[i].mod); if (param_check_unsafe(&params[i])) err = params[i].ops->set(val, &params[i]); else err = -EPERM; kernel_param_unlock(params[i].mod); return err; } } if (handle_unknown) { pr_debug("doing %s: %s='%s'\n", doing, param, val); return handle_unknown(param, val, doing, arg); } pr_debug("Unknown argument '%s'\n", param); return -ENOENT; } ``` 注意第 17 行的部份, linux 核心逐步尋找符合的參數,並在第 29 行呼叫函式指標 `params[i].ops->set(val, &params[i])` ,將輸入的資料複製到模組的資料裡,以下為其結構宣告 ```c struct kernel_param_ops { /* How the ops should behave */ unsigned int flags; /* Returns 0, or -errno. arg is in kp->arg. */ int (*set)(const char *val, const struct kernel_param *kp); /* Returns length written or -errno. Buffer is 4k (ie. be short!) */ int (*get)(char *buffer, const struct kernel_param *kp); /* Optional function to free kp->arg when module unloaded. */ void (*free)(void *arg); }; ```