# LKL: 重用 Linux 核心的成果
> 貢獻者: [jserv](https://wiki.csie.ncku.edu.tw/User/jserv)
[Linux Kernel Library](https://github.com/lkl/linux) (LKL) 設計為 Linux 核心的移植版本,在目錄 `arch/lkl` 中,約有 3500 行的程式碼。LKL 與應用程式連結,以運作於使用者空間,依賴由主機作業系統提供的一組主機端的功能,例如 semaphore, POSIX Threads, malloc、計時器 (timer) 等。LKL 擁有良好定義的外部介面,例如系統呼叫及 virtio-net。LKL 允許 FreeBSD, macOS, Windows, 和(舊版的) Linux 存取各式 Linux 核心的成果。
![LKL](https://hackmd.io/_uploads/S1GVWXMV0.png =70%x)
LKL 執行在非特權的使用者空間,無需記憶體保護機制,只依賴由本機作業系統配置一塊記憶體空間。LKL 的記憶體管理遵循與常規 Linux 核心相同的原則,也就是 SLUB/SLAB 演算法。
## 使用 LKL 的情境
其中一個應用場景是,搭配 [FUSE](https://en.wikipedia.org/wiki/Filesystem_in_Userspace),掛載 Linux 核心支援的多種檔案系統。
![FUSE](https://hackmd.io/_uploads/Sks78EGVC.png)
藉由 LKL,可將 TCP/IP 的處理從核心空間轉移到使用者空間,而不必修改標的應用程式。LKL 還提供使用 [DPDK](https://www.dpdk.org/) 支援的選項,這能提供快速的 TCP/IP 通訊協定堆疊處理。二者都能降低網路通信中的瓶頸。
![image](https://hackmd.io/_uploads/BJeo0x7N0.png)
上圖的左半部分顯示 Node.js 應用程式如何運作於 Linux。右半部分展示改進的方案,包含 LKL 和 DPDK。
此外,LKL 允許在使用者空間中對 Linux 核心程式碼進行模糊測試 (fuzzing),可搭配 [libFuzzer](https://llvm.org/docs/LibFuzzer.html) 使用。該手法的優點包括:
* 在 x86-64 平台上達到高效能的模糊測試。
* 模糊測試工具輕量化,無需借助虛擬機器。
* 容易進行除錯和崩潰重現,還可搭配 gdb。
* 支援硬體模擬,例如 PCI。
示意圖如下:
![lkl-fuzzing](https://hackmd.io/_uploads/r1xSo8rd0.png)
為了讓模糊測試行為更可預測 (deterministic),甚至可在 LKL 中引入特製的排程器,作法是在核心的同步處理機制 (如 spinlock 和 mutex) 前後插入 yield point。
![lkl-sched](https://hackmd.io/_uploads/SySQ6LSuC.png)
延伸閱讀:
* [A Library Version of Linux Kernel](https://archive.fosdem.org/2020/schedule/event/uk_linux/), FOSDEM 2020
> [video](https://youtu.be/EYMqDS1qasg)
* [LKL: A way to reuse Linux kernel in userland](https://static.sched.com/hosted_files/lc32018/e0/lkl-lc3-2018-china.pdf), 2018
* [User Space TCP based on LKL](https://www.netdevconf.info/1.2/slides/oct6/03_jerry_chu_usTCP_LKL.pdf), 2016
> [video](https://youtu.be/xP9crHI0aAU), [paper](https://netdevconf.org/1.2/papers/jerry_chu.pdf)
* [Minimize Node.js I/O Bottlenecks with Linux Kernel Library and Data Plane Developer Kit](https://www.intel.cn/content/www/cn/zh/developer/articles/technical/minimize-nodejs-io-bottlenecks.html)
* [Fuzzing Host-to-Guest Attack Surface in Android Protected KVM](https://kvm-forum.qemu.org/2022/%5BKVM%20Forum%202022%5D%20-%20Virtio%20fuzzing%20%28go_kvm-forum-fuzzing-22%29.pdf), 2022
* [How to Fuzz Your Way to Android Universal Root: Attacking Android Binder](https://androidoffsec.withgoogle.com/posts/attacking-android-binder-analysis-and-exploitation-of-cve-2023-20938/offensivecon_24_binder.pdf), 2024
* [Android Binder Attack Matrix: Fuzzing Binder with Linux Kernel Library (LKL)](https://utkarshcodes.medium.com/android-binder-attack-matrix-fuzzing-binder-with-linux-kernel-library-lkl-article-3-62e931161eb5)
## 編譯和測試
:::warning
:warning: 確保檔案系統有 6 GB 以上的可用空間,否則後續操作會失敗。
:::
取得原始程式碼:
```shell
$ git clone https://github.com/lkl/linux lkl --depth=1
$ cd lkl
```
安裝依賴的套件:
```shell
$ sudo apt-get install fuse3 libfuse3-dev libarchive-dev xfsprogs
```
編譯 LKL:
```shell
$ make -C tools/lkl -j$(nproc)
```
若要執行內建的測試,需要安裝以下套件:
```shell
$ sudo apt-get install btrfs-progs
$ pip3 install yamlish junit_xml
```
執行內建測試:
```shell
$ cd tools/lkl
$ make run-tests
```
參考執行結果:
```
...
TEST ok cleanup
SUITE config
...
Summary: 25 suites run, 151 tests, 135 ok, 0 not ok, 16 skipped
```
在上述的 `tools/lkl` 目錄,預期可見名為 `lklfuse` 的執行檔,以下示範藉由 LKL 掛載 ext2 檔案系統,不依賴作業系統原生的檔案系統實作。
```shell
$ truncate -s 10m /tmp/ext2.img
$ mkfs.ext2 /tmp/ext2.img
$ mkdir /tmp/mnt
$ ./lklfuse -o type=ext2 /tmp/ext2.img /tmp/mnt
```
可將檔案放入掛載的 `/tmp/mnt` 目錄,隨後執行 `umount` 卸載。
## 整合 LKL
前述編譯成功後,預期會在 `tools/lkl` 目錄產生 `liblkl.a` 檔案,我們可用來整合,下方的程式碼和操作也在 `tools/lkl` 目錄中進行。
準備以下 C 程式 (檔名: `min.c`)
```c
#include "lkl_host.h"
#include "lkl.h"
int main()
{
lkl_init(&lkl_host_ops);
lkl_start_kernel("mem=128M");
return 0;
}
```
編譯和連結:
```shell
$ gcc -o min min.c -I./include liblkl.a -lpthread -lrt
```
該程式得以在 Linux (或 LKL 支援的作業系統) 環境中,執行部分 Linux 功能,以下是參考輸出:
```
$ ./min
[ 0.000000] Linux version 6.6.0+ (jserv@node1) (gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1 Mon May 27 22:09:09 CST 2024
[ 0.000000] memblock address range: 0x7f5d04000000 - 0x7f5d0c000000
[ 0.000000] Zone ranges:
[ 0.000000] Normal [mem 0x00007f5d04000000-0x00007f5d0bffffff]
[ 0.000000] Movable zone start for each node
[ 0.000000] Early memory node ranges
[ 0.000000] node 0: [mem 0x00007f5d04000000-0x00007f5d0bffffff
...
[ 0.050217] This architecture does not have kernel memory protection.
[ 0.050223] Run /init as init process
```
當 LKL 停留在最後一行訊息 `Run /init as init process`,就意味著其無法讀取到有效的 `init` 程式,作為 PID=1 的行程 (相當於 systemd 一類的系統程式)。
接著準備以下 C 程式 (檔名: `readfs.c`)
```c
#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "lkl.h"
#include "lkl_host.h"
int main(int argc, const char *argv[])
{
const char *fsimage = argv[1], *fstype = argv[2], *file_to_dump = argv[3];
lkl_init(&lkl_host_ops);
struct lkl_disk disk;
memset(&disk, 0, sizeof(disk));
disk.fd = open(fsimage, O_RDONLY);
assert(disk.fd >= 0);
int disk_id = lkl_disk_add(&disk);
assert(disk_id >= 0);
lkl_start_kernel("mem=128M");
char mpoint[128];
int ret = lkl_mount_dev(disk_id, 0 /* part */, fstype, LKL_MS_RDONLY, NULL,
mpoint, sizeof(mpoint));
if (ret < 0) {
fprintf(stderr, "lkl_mount_dev failed: %s\n", lkl_strerror(ret));
close(disk.fd);
exit(1);
}
struct lkl_dir *dir = lkl_opendir(mpoint, &ret);
struct lkl_linux_dirent64 *dent;
while ((dent = lkl_readdir(dir)))
fprintf(stderr, "Directory entry: %s\n", dent->d_name);
lkl_closedir(dir);
char tmp[256];
char buffer[65536];
snprintf(tmp, sizeof(tmp), "%s/%s", mpoint, file_to_dump);
int fd = lkl_sys_open(tmp, LKL_O_RDONLY, 0);
fprintf(stderr, "fd = %d\n", fd);
assert(fd >= 0);
int count = lkl_sys_read(fd, buffer, sizeof(buffer));
write(STDERR_FILENO, buffer, count);
lkl_sys_close(fd);
return 0;
}
```
編譯並連結:
```shell
$ gcc -o readfs readfs.c -Iinclude liblkl.a -lpthread -lrt
```
接著準備提供給 LKL 掛載為 rootfs 的檔案系統:
```shell
$ mke2fs ext4.img -t ext4 32M
$ mkdir -p /tmp/mnt
$ ./lklfuse -o type=ext4 ext4.img /tmp/mnt
$ echo -e "Hello world\!\nTEST" > /tmp/mnt/test.txt
$ umount /tmp/mnt
```
至此我們終於可指定 rootfs 並啟動 LKL:
```shell
$ ./readfs ext4.img ext4 test.txt
```
參考執行輸出:
```
[ 0.000000] Linux version 6.6.0+ (jserv@node1) (gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1 Mon May 27 22:09:09 CST 2024
[ 0.000000] memblock address range: 0x7fb568000000 - 0x7fb570000000
...
[ 0.047948] Run /init as init process
[ 0.048719] EXT4-fs (vda): mounted filesystem 4cbc1b7f-0e84-4092-b70a-f32cd2398a53 ro with ordered data mode. Quota mode: disabled.
Directory entry: lost+found
Directory entry: test.txt
Directory entry: ..
Directory entry: .
fd = 0
Hello world\!
TEST
```
知曉 LKL 的使用,我們來嘗試傾印 `/proc` 目錄的所有內容。
準備以下 C 程式: (檔名: `cat_proc.c`)
```c
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "lkl.h"
#include "lkl_host.h"
static void close_fd(int fd)
{
int r = lkl_sys_close(fd);
if (r < 0)
lkl_printf("LKL close error: %d\n", r);
}
static int cat_file(char *path)
{
int fd = lkl_sys_open(path, LKL_O_RDONLY, 0);
if (fd < 0) {
lkl_printf("LKL open error: %d\n", fd);
return fd;
}
int ret = 0;
for (;;) {
char buf[1024];
int len = lkl_sys_read(fd, buf, 1024);
if (len < 0) {
lkl_printf("LKL read error: %d\n", len);
ret = len;
goto out;
}
if (len == 0)
break;
for (int written = 0; written < len;) {
int n = write(STDOUT_FILENO, buf + written, len - written);
if (n < 0) {
lkl_printf("write error: %d\n", n);
ret = n;
goto out;
}
written += n;
}
}
out:
close_fd(fd);
return ret;
}
int main(int n, char **args)
{
lkl_init(&lkl_host_ops);
lkl_start_kernel("mem=10M");
int ret = lkl_sys_mkdir("/proc", 0777);
if (ret < 0) {
lkl_printf("LKL mkdir error: %d\n", ret);
return 1;
}
ret = lkl_sys_mount("none", "/proc", "proc", 0, 0);
if (ret < 0) {
lkl_printf("LKL mount error: %d\n", ret);
return 1;
}
char *path = "/proc";
int fd = lkl_sys_open(path, LKL_O_RDONLY | LKL_O_DIRECTORY, 0);
if (fd < 0) {
lkl_printf("LKL open error: %d\n", fd);
return 1;
}
for (;;) {
char buf[1024];
struct lkl_linux_dirent64 *d = (void *) buf;
int n = lkl_sys_getdents64(fd, d, sizeof(buf));
if (n < 0) {
lkl_printf("LKL getdents error: %d\n", n);
return 1;
}
if (n == 0)
break;
char pathbuf[1024];
while ((char *) d < buf + n) {
snprintf(pathbuf, sizeof(pathbuf), "%s/%s", path, d->d_name);
lkl_printf("path: %s\n", pathbuf);
if (d->d_type == DT_REG && strcmp(d->d_name, "kmsg") != 0) {
if (cat_file(pathbuf) < 0)
return 1;
}
d = (void *) ((char *) d + d->d_reclen);
}
}
close_fd(fd);
lkl_sys_halt();
return 0;
}
```
編譯並連結:
```shell
$ gcc -o cat_proc cat_proc.c -I./include liblkl.a -lpthread -lrt
```
參考執行輸出:
```
$ ./cat_proc
[ 0.000000] Linux version 6.6.0+ (jserv@node1) (gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1 Mon May 27 22:09:09 CST 2024
...
183 hw_random
127 vga_arbiter
path: /proc/stat
cpu 0 0 3 0 0 0 0 0 0 0
...
path: /proc/pagetypeinfo
Page block order: 10
Pages per block: 1024
...
[ 0.562821] reboot: Restarting system
```
# 執行緒管理
LKL 不提供建立執行緒的服務給使用它的程式,不過在 LKL 內部仍仰賴執行緒確保 Linux 核心得以正確運作。在 LKL 的早期版本中,藉由內部執行緒來模擬 Linux 核心所需要的機制,但後者必須有單獨的堆疊,而堆疊的配置是由主機作業系統完成,結果 LKL 無法為執行緒配置新的堆疊,不得不將主機作業系統配置的堆疊分成多個部分。因此,每個執行緒的堆疊大小會隨著 LKL 核心中的執行緒數量成比例地減少,對模擬的執行緒數量施加嚴格的限制。開發人員最終決定放棄在 LKL 內部模擬執行緒的想法,倘若 Linux 核心需要執行緒,LKL 會要求本機作業系統建立執行緒,實作二個中介方法:一個呼叫系統呼叫 `thread_create`,另一個呼叫系統呼叫 `thread_exit`。關於 LKL 的主機端操作介面,可見 [arch/lkl/include/uapi/asm/host_ops.h](https://github.com/lkl/linux/blob/master/arch/lkl/include/uapi/asm/host_ops.h), LKL 的主機端操作介面
```c
struct lkl_host_operations {
...
lkl_thread_t (*thread_create)(void (*f)(void *), void *arg);
void (*thread_detach)(void);
void (*thread_exit)(void);
int (*thread_join)(lkl_thread_t tid);
lkl_thread_t (*thread_self)(void);
int (*thread_equal)(lkl_thread_t a, lkl_thread_t b);
void *(*thread_stack)(unsigned long *size);
...
```
[tools/lkl/lib/posix-host.c](https://github.com/lkl/linux/blob/master/tools/lkl/lib/posix-host.c) 是針對 POSIX 相容作業系統的橋接實作,提供上方操作介面的功能:
```c
static lkl_thread_t thread_create(void (*fn)(void *), void *arg)
{
pthread_t thread;
if (WARN_PTHREAD(pthread_create(&thread, NULL, (void* (*)(void *))fn, arg)))
return 0;
else
return (lkl_thread_t) thread;
}
```
不過這種方法在解決堆疊問題的同時,產生新問題:Linux 核心必須確保它控制執行緒切換,但使用上述方法,執行緒切換由主機作業系統控制。為了解決這個問題,LKL 中設計模擬執行緒管理的機制。
如何在 LKL 中實作執行緒管理的模擬?需要施加一個特殊限制:在任何給定時刻,LKL 只能在一個執行緒中運行。這個限制顯著降低核心的性能,但簡化執行緒系統的實作。單執行緒透過在設定檔中指定相應的項目來實作,例如指定核心是針對單處理器架構,也就是取消核心選項 `CONFIG_SMP`。LKL 從本機作業系統建立執行緒,並獲取相應的系統 semaphore。由於此資源是從主機作業系統獲取的,因此透過回呼函式實作。
Linux 排程器在 LKL 改寫,顧及二個層次:一個系統層次(為此我們從系統獲取 semaphore),另一個屬於 LKL 的內部層次(管理 LKL 內部的執行緒邏輯)。
* 在核心的架構相關部分中,Linux 核心的通用結構(lock, semaphore、執行緒)使用經由 `lkl_host_ops` 提供的回呼函式實作。
* 對於核心架構相關部分的內部用途,直接呼叫來自 `lkl_host_ops` 的回呼函式。
為了理解負責執行緒管理如何從主機作業系統獲取外部資源 (執行緒和 semaphore),以下分析排程器的主要函式 `__schedule()`,其程式碼在檔案 `kernel/sched/core.c` 中。該函式是排程器的關鍵操作,進入該函式會發生於以下情況:
* 顯式阻塞:mutex, semaphore, waitqueue
* 中斷和從使用者形成返回。
* 喚醒執行緒不會導致呼叫 `__schedule()`
在切換行程時,排程器除了其他工作外,還處理上下文切換。在 `__schedule()` 函式中,呼叫函式 `context_switch(rq, prev, next)`,然後呼叫在檔案 `linux/include/asm-generic/switch_to.h` 中定義的巨集 `switch_to(prev, next, last)`:
```c
#define switch_to(prev, next, last)
```
在此操作過程中,呼叫函式 `__switch_to(struct task_struct *, struct task_struct *)`,其位於專為 LKL 設計的檔案 `linux/arch/lkl/kernel/threads.c` 中。這就是在 LKL 中的 Linux 核心內部實作執行緒管理邏輯的地方。
當在 LKL 中由 Linux 核心建立執行緒時,呼叫回呼函式如下:
```c
lkl_ops->sem_alloc(0)
```
並將其添加到 `threadinfo` 結構中,該結構定義於 `arch/lkl/include/asm/thread_info.h`
這個結構保存執行緒的信息。結構體中的 `void *sched_sem`,是內部執行緒排程器用來啟動/停止執行緒的 semaphore。在初始化時,每個新執行緒被配置一個由主機作業系統配置的 semaphore,初始值為零:
```c
ti->sched_sem = lkl_ops->sem_alloc(0)
```
因此,新建立的 semaphore 將被阻塞,直到 LKL 排程器決定啟動此執行緒。修改排程器的主要工作實作於函式:
```c
struct task_struct *__switch_to(struct task_struct *prev, struct task_struct *next)
```
該函式的實作在檔案 `arch/lkl/kernel/thread.c` 中。函式的主要三個操作階段是:
1. 保存正在切換的執行緒的 task_struct 指標。
2. 使用對應的回呼函式呼叫解鎖要切換到的執行緒的信號量:
```c
lkl_ops->sem_up(_next->sched_sem);
```
3. 使用以下方式阻塞目前正在切換的執行緒:
```
lkl_ops->sem_down(ei.sched_sem);
```
`__switch_to` 函式返回一個指向呼叫 `__switch_to` 函式的行程的 `task_struct` 的指標。步驟 1 中保存正在切換的執行緒的 task_struct 指標,確保總是返回被切換行程的 `task_struct` 指標。
例如:假設在給定時刻,LKL 中的 Linux 核心在執行緒 1 中運行。`__switch_to` 函式被呼叫,經由 `sem_up` 切換到執行緒 2,然後執行緒 1 使用 `sem_down` 自行阻塞。在此之前,執行緒 1 將其 `task_struct` 指標保存到全域變數 `abs_prev` 中,該變數對所有 `__switch_to` 函式呼叫都是通用的。結果,`__switch_to` 函式將返回指向執行緒 1 的 `task_struct` 的指標。假設在後續工作中,切換到執行緒 3,並且執行緒 3 在其 `__switch_to` 函式呼叫中喚醒執行緒 1。這樣,函式返回指向執行緒 3 的 `task_struct` 的指標,而此時執行緒 1 正在執行。
因此,當內部執行緒排程器需要切換到新執行緒時,它會關閉目前執行緒的外部信號量,並打開所需執行緒的外部信號量。由於所有其他執行緒都被信號量封鎖,主機系統排程器只能啟動內部 LKL 執行緒排程器所需的那一個執行緒。如果不這樣做,外部執行緒排程器可以按照其邏輯啟動執行緒,內部 LKL 執行緒排程器將失去對執行緒啟動順序的控制,破壞 Linux 核心的內部邏輯。與此同時,內部 LKL 層如常運行,封鎖同步的執行緒等。內部 LKL 執行緒排程器根據從其核心接收到的執行緒封鎖/啟動請求打開和關閉外部 lock。
## 待整理
* [LKL.js: Running Linux Kernel on JavaScript](https://retrage.github.io/2018/07/25/lkl-js-en.html/), 2018
> [slides](https://speakerdeck.com/retrage/lkl-dot-js-running-linux-kernel-on-javascript-star-directly-star?slide=14)
* [Playing BBR with a userspace network stack](https://netdevconf.org/2.1/session.html?tazaki), 2017
* [UX/RT - a QNX-like OS based on seL4](https://archive.fosdem.org/2022/schedule/event/awarkentin/), 2022
> LKL for device drivers, disk filesystems, and network stack.
> Linux compatibility environment based on a library/loader and several special filesystems.
* [Booting LKL inside Haiku](https://www.haiku-os.org/blog/lucian/2010-07-08_booting_lkl_inside_haiku/)
* [rkt-io: A Direct I/O Stack for Shielded Execution](https://lsds.doc.ic.ac.uk/sites/default/files/rkt-io-eurosys21.pdf), EuroSys 2021
* [verker: Linux kernel library functions formally verified](https://github.com/evdenis/verker)
* [kBdysch](https://github.com/atrosinenko/kbdysch) is a collection of fast Linux kernel specific fuzzing harnesses supposed to be run in userspace in a guided fuzzing manner
* [node-lkl](https://github.com/balena-io-modules/node-lkl) uses the Linux Kernel Library project to provide access to filesystem drivers from NodeJS in a cross-platform way.
* [ebpf-fuzzer](https://github.com/ssrg-vt/ebpf-fuzzer): a fuzzer for Linux eBPF module, built on top of LKL
* [Mount Btrfs USB disks on non-root Android using the Linux Kernel Library](https://hackweek.opensuse.org/projects/mount-btrfs-usb-disks-on-non-root-android-using-the-linux-kernel-library)
* [Howto: DPDK with LKL](https://github.com/lkl/linux/wiki/Howto:-DPDK-with-LKL)
* [WasmLinux](https://github.com/okuoku/wasmlinux-project): create WebAssembly "native" Linux environment, both kernel and userland.
* LKL 尚未支援 SMP,但有對應的[雛形設計](https://github.com/lkl/linux/issues/370)