---
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, ¶m, 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 伺服器進行比較。

上圖為 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 連線,可於其內處理多個請求。

根據 [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, ¶m_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 = ¶m_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, ¶m, &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(¶ms[i]))
err = params[i].ops->set(val, ¶ms[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, ¶ms[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);
};
```