\chapter{sysfs: 同你的模块交互}
\label{sec:sysfs}
-\emph{sysfs} 允许你在用户空间,通过读或设置模块中的变量,来与正在运行的内核进行%
-交互。调试进程时,或作为应用程序或脚本的一个接口,这很有用。你可以在你的系统上%
+\emph{sysfs} 允许你在用户空间,通过读或设置模块中的变量,与正在运行的内核进行%
+交互。出于调试目的,或作为应用程序或脚本的一个接口,这很有用。可以在你的系统上%
的 \verb|/sys| 目录下找到 sysfs 目录与文件。
\begin{codebash}
ls -l /sys
\end{codebash}
-在文件系统中,对于 kobjects 的属性可以用普通文件的形式,被输出。Sysfs 将文件 I/O %
-æ\93\8dä½\9c转å\8f\91å\88°ä¸ºå±\9eæ\80§å®\9aä¹\89ç\9a\84æ\96¹æ³\95ï¼\8c提供读取和写入内核属性的手段。
+在文件系统中,对于 kobjects 的属性可以通过普通文件的形式被输出。Sysfs 将文件 I/O %
+æ\93\8dä½\9c转å\8f\91å\88°ä½\9c为å±\9eæ\80§è¢«å®\9aä¹\89ç\9a\84æ\96¹æ³\95ï¼\8cè¿\99äº\9bæ\96¹æ³\95提供读取和写入内核属性的手段。
-简单的属性定义:
+一个属性定义非常简单:
\begin{code}
struct attribute {
\end{code}
要读或写属性,当声明属性时,\cpp|show()| 或 \cpp|store()| 方法必须被指定。对大%
-多数情况 \src{include/linux/sysfs.h} 提供易于的宏(\cpp|__ATTR|,\cpp|__ATT_RO|,%
-\cpp|__ATTR_WO|,等等。)以简化属性定义,同时令代码更精简与可读。
+多数情况 \src{include/linux/sysfs.h} 提供了易于被使用的宏(\cpp|__ATTR|,%
+\cpp|__ATT_RO|,以及 \cpp|__ATTR_WO|,等等。)以简化属性定义,并让代码更为精简%
+且更具可读性。
-一个 hello world 示例如下结出,该示例包含可由 sysfs 访问的一个变量的创建。
+一个 hello world 模块如下所示,其包含通过如下 sysfs 访问的一个变量的创建。
\samplec{examples/hello-sysfs.c}
sudo insmod hello-sysfs.ko
\end{codebash}
-检查模块的存在:
+检查模块是否存在:
\begin{codebash}
sudo lsmod | grep hello_sysfs
cat /sys/kernel/mymodule/myvariable
\end{codebash}
-设置 \cpp|myvariable| 的值并检查它的改变。
+设置 \cpp|myvariable| 的值并检查它已被改变。
\begin{codebash}
echo "32" > /sys/kernel/mymodule/myvariable
sudo rmmod hello_sysfs
\end{codebash}
-å\9c¨ä¸\8aé\9d¢ç\9a\84æ\93\8dä½\9cè¿\87ç¨\8bä¸ï¼\8cæ\88\91们ç\94¨ä¸\80个ç®\80å\8d\95 kobject æ\9d¥å\9c¨ sysfs ä¸\8bå\88\9b建ä¸\80个ç\9b®å½\95ï¼\8cå¹¶与它的%
-属性进行通讯。自从 Linux v2.6.0 \cpp|kobject| 数据结构初露峥嵘。它最初的目的是%
-作为一种统一管理引用计数对象的内核代码的简单方法。经过一些任务的扩展之后,它现%
-å\9c¨æ\88\90为å°\86设å¤\87模å\9e\8bç\9a\84大é\83¨å\88\86å\8f\8aå\85¶ sysfs æ\8e¥å\8f£ç²\98å\90\88å\9c¨ä¸\80èµ·ç\9a\84ç²\98å\90\88å\89\82ã\80\82对äº\8eæ\9b´å¤\9aå\85³äº\8e kobject %
-与 sysfs 的信息,查看 \src{Documentation/driver-api/driver-model/driver.rst} 与%
-\url{https://lwn.net/Articles/51437/}。
+å\9c¨ä¸\8aé\9d¢ç\9a\84æ\83\85å\86µä¸ï¼\8cæ\88\91们ç\94¨ä¸\80个ç®\80å\8d\95 kobject æ\9d¥å\9c¨ sysfs ä¸\8bå\88\9b建ä¸\80个ç\9b®å½\95ï¼\8cå¹¶ä¸\94与它的%
+属性进行通讯。自从 Linux v2.6.0 \cpp|kobject| 数据结构初露峥嵘。设计它最初的意%
+图是作为一种统一管理引用计数对象的,内核代码的简单方法。经过一些任务的扩展之后,%
+å®\83ç\8e°å\9c¨æ\88\90为å°\86设å¤\87模å\9e\8bç\9a\84大é\83¨å\88\86å\8f\8aå\85¶ sysfs æ\8e¥å\8f£ç²\98å\90\88å\9c¨ä¸\80èµ·ç\9a\84ç²\98å\90\88å\89\82ã\80\82对äº\8eæ\9b´å¤\9aå\85³äº\8e %
+kobject 与 sysfs 的信息,查看 \src{Documentation/driver-api/driver-model/driver.rst} %
+与 \url{https://lwn.net/Articles/51437/}。
\chapter{与设备文件对话}
\label{sec:device_files}
-设备应该表示物理设备。大多数物理设备都用于输出和输入,因此内核中的设备驱动程序%
-必须有某种机制来获取输出,以及,从进程发送到设备。通过以输出与写入为目标,对于%
-设å¤\87æ\96\87ä»¶ç\9a\84æ\89\93å¼\80æ\93\8dä½\9cç\9a\84å\81\9aæ³\95ï¼\8c就象å\90\91ä¸\80个æ\96\87ä»¶å\86\99å\85¥æ\95°æ\8d®ã\80\82以ä¸\8b示ä¾\8bï¼\8cé\80\9aè¿\87 \cpp|device_write| %
-而被执行。
+设备应该表示物理设备。大多数物理设备被用于输出及输入,因此对内核中的设备驱动来%
+说,必须有一些机制,实现从得到输出,到从进程发送数据到设备。通过以输出与写入自%
+身为ç\9b®ç\9a\84ï¼\8cæ\89\93å¼\80设å¤\87æ\96\87ä»¶æ\9d¥å\81\9aå\88°ï¼\8c就象å\86\99å\85¥ä¸\80个æ\96\87ä»¶ã\80\82å\9c¨ä¸\8bé\9d¢ç\9a\84示ä¾\8bï¼\8cå\85¶é\80\9aè¿\87 %
+\cpp|device_write| 而被执行。
-大多数物理设备被用于输出及输入,因此对内核中的设备驱动来说,必须有一些机制,实%
-现从得到输出,到从进程发送数据到设备。通过以输出与写入自身为目的,打开设备文件%
-来做到,就象写入一个文件。在下面的示例,其通过 \cpp|device_write| 而被执行。
-
-这样远远不够。想象你有一个串行端口被连接到一个 modem(甚至你有一个内部 modem,%
+这样远远不够。想象你有一个串行端口被连接到一个 modem\,(甚至你有一个内部 modem,%
从 CPU 的视角看来,它依然被作为一个串行端口连接到一个 modem 来被执行,因此不必%
为你的想象力税赋犯难)。自然的做法是使用设备文件将内容写入 modem (或者 modem 命%
令,或者是通过电话线传送的数据)并从 modem 读取内容(或者是命令的应答,或者是通过%
电话线接收的数据)。然而,这留下了一个问题:当您需要与串行端口本身通信时该怎么办,%
例如配置发送和接收数据的速率。
-在 Unix 中答案是使用一个被称为 \cpp|ioctl|(输入输出控制的缩写) 的特殊函数。每个%
-设备可拥有自己的 \cpp|ioctl| 命令,可以读取 ioctl(从一个进程发送信息到内核),或%
-写入 ioctl(向进程返回信息),同时执行两者,或同时不执行两者。注意这里的读和写的%
-角色又再次相反了,因在 ioctl 中的读是发送信息到内核,且写入是从内核接收数据。
+在 Unix 中答案是使用一个被称为 \cpp|ioctl|\,(\,输入输出控制的缩写) 的特殊函数。%
+每个设备可拥有自己的 \cpp|ioctl| 命令,可以读取 ioctl\,(\,从一个进程发送信息到%
+内核\,),或写入 ioctl\,(\,向进程返回信息\,),同时执行两者,或同时不执行两者。注%
+意这里的读和写的角色又再次变反了,因在 ioctl 中的读是发送信息到内核,且写入是从%
+内核接收数据。
ioctl 函数被调用时,使用三个参数:对应设备文件的文件说明符,ioctl 数字,还有一%
-个参数,它是一个 long 类型,因此你可利用强制转换来传递任何数据。在这种方法中,%
-你不能传递一个结构,但你可以传递一个指向结构的指针。
+个参数,它是一个 long 整数类型,因此你可利用强制类型转换来传递任何数据。在这种%
+方法中,你不能传递一个结构,但你可以传递一个指向结构的指针。
这里有一个示例:
\samplec{examples/ioctl.c}
-你可以看到,在 \cpp|test_ioctl_ioctl()| 函数中,被称为 \cpp|cmd| 的参数被调用。%
-它是一个 ioctl 数字。ioctl 数字编码主设备号,ioctl 的类型,命令,与参数类型。%
-ioctl 数字在头文件中,通常通过一个宏调用(\cpp|_IO|,\cpp|__IOR|,\cpp|_IOW| 或 %
-\cpp|_IOWR|,其依赖于类型)来创建。这个头文件然后应当同时被使用 ioctl 的程序(因%
-此可以产生相应的 ioctl)包含,并且还被内核模块(因此它可理解相关操作)包含。在下面%
-的示例中,头文件是 \verb|chardev.h| 并且程序在 \verb|userspace_ioctl.c| 文件中%
-使用它。
+你可以看到,在 \cpp|test_ioctl_ioctl()| 函数中有一个参数被称为 \cpp|cmd|。%
+它是一个 ioctl 数字。ioctl 数字对主设备号,ioctl 的类型,命令,与参数类型时行%
+编码。ioctl 数字在头文件中,通常通过一个宏调用(\cpp|_IO|,\cpp|__IOR|,\cpp|_IOW| %
+或 \cpp|_IOWR|,其依赖于类型)来创建。这个头文件之后应当同时被使用 ioctl 的程序(因%
+此可以产生相应的 ioctl),以及内核模块(因此它可以理解它)包含。在下面的示例中,头%
+文件是 \verb|chardev.h| 并且程序在 \verb|userspace_ioctl.c| 文件中使用它。
-如果你想在你自己的内核中使用 ioctl,最好接收一个官方 ioctl 参数,因此如果你意外%
-收到其它人的不同 ioctl,或如果他们收到你的 ioctl,你将知道有地方出错了。更多信息,%
-查看内核源代码树中 \src{Documentation/userspace-api/ioctl/ioctl-number.rst}。
+如果你想在自己的内核中使用 ioctl,最好接收一个官方 ioctl 参数,因为你如果意外%
+收到其它人的不同 ioctl,或他们如果收到你的 ioctl,你将知道有地方出错了。更多信息,%
+查看内核源代码树中 \src{Documentation/userspace-api/ioctl/ioctl-number.rst} 的内容。
另外,我们需要小心对共享资源的并发访问,将导致竞争条件产生。解决方法是使用原子 %
Compare-And-Swap (CAS),我们在 \ref{sec:chardev_c} 中提到了它,用它来强制执行%
\chapter{系统调用}
\label{sec:syscall}
-到目前为止,我们所做的唯一的一件事情,就是使用定义良好的内核机制来注册 \verb|/proc| %
+到目前为止,我们只作了一件事,就是用良好定义的内核机制来注册 \verb|/proc| %
文件和设备处理程序。如果你想做内核程序员认为值得做的事,例如编写设备驱动程序,%
那么这很好。但是,如果你想做一些不寻常的事情,以某种方式改变系统的行为,该怎么%
办?然后,你就只能靠自己了。
如果您不明智地使用虚拟机,那么这就是内核编程可能变得危险的地方。在编写下面的示%
例时,我杀死了 \cpp|open()| 系统调用。这意味着我无法打开任何文件,无法运行任何%
程序,也无法关闭系统。我不得不重新启动虚拟机。没有重要的文件被消灭,但如果我在%
-ä¸\80äº\9bå®\9eæ\97¶ä»»å\8a¡å\85³é\94®ç³»ç»\9fä¸\8aæ\89§è¡\8cæ¤æ\93\8dä½\9cï¼\8cé\82£ä¹\88è¿\99å\8f¯è\83½æ\98¯ä¸\80个å\8f¯è\83½ç\9a\84ç»\93æ\9e\9cã\80\82è¦\81ç¡®ä¿\9dä¸\8dè¦\81丢失任%
-何文件,甚至在一个测试环境中,请在执行 \sh|insmod| 和 \sh|rmmod| 命令之前运行 %
-\sh|sync| 命令。
+ä¸\80äº\9bå®\9eæ\97¶ä»»å\8a¡å¾\88å\85³é\94®ç\9a\84ç³»ç»\9fä¸\8aæ\89§è¡\8cæ¤æ\93\8dä½\9cï¼\8cé\82£ä¹\88è¿\99æ\88\96许æ\98¯ä¸\80个å\8f¯è\83½ç\9a\84ç»\93æ\9e\9cã\80\82è¦\81ç¡®ä¿\9dä¸\8dè¦\81丢%
+失任何文件,甚至在一个测试环境中,请在执行 \sh|insmod| 和 \sh|rmmod| 命令之前%
+运行 \sh|sync| 命令。
忘记 \verb|/proc| 文件,忘记设备文件。它们只是次要的细节。浩瀚宇宙的细节。真正%
-的进程到内核的通信机制,即所有进程都使用的机制,是 \emph{系统调用}。当进程向内%
+的进程到内核的通信机制,即所有进程都使用的机制,是{\bf{系统调用}\/}。当进程向内%
核请求服务时(例如打开文件,或请求更多内存),这就是被用到的机制。如果你想以有趣%
-的方式改变内核行为,这里就是你可以做到的地方。顺便说一句,如果你想查看程序使用%
+的方式改变内核行为,这里就是你可以施展的地方。顺便说一句,如果你想查看程序使用%
哪个系统调用,请运行 \sh|strace <arguments>|。
-总之,一个进程不被支持有能力去访问内核。它不能访问内核内存,并且它不能调用内核%
-函数。CPU 的硬件增强这方面的功能(这就是为什么它被称为``保护模式''或``页面保护''%
-的原因)。
+总之,无情的现实是,一个进程被废除了访问内核的能力。它不能访问内核内存,并且它%
+不能调用内核函数。CPU 的硬件增强这方面的功能(这就是为什么它被称为``保护模式''或%
+``页面保护''的原因)。
系统调用是对通用规则的一个例外。在进程用适当的值填充寄存器,然后调用一条特殊指%
-令,该指令跳转到内核中先前定义的位置(当然,该位置可以由用户进程读取,但不能写%
-入)。在 Intel CPU 上,这是通过中断 0x80 完成的。硬件知道,一旦你跳转到这个位置,%
-你就不再运行受限用户模式下,而是作为操作系统内核运行,因此你可以做任何你想做的%
-事情。
-
+令,该指令跳转到内核中先前定义的位置(当然,该位置可以由用户进程读取,但不能被%
+它们写入)。在 Intel CPU 上,这是通过中断 0x80 完成的。硬件知道,一旦你跳转到这个%
+位置,你就不再运行于受限用户模式下,而是作为操作系统内核运行,因此你可以做任何你%
+想做的事情。
进程在内核中可以跳转到的位置被称为 \verb|system_call|。该位置的例程检查系统调用%
-号,它告诉内核进程请求什么服务。然后,它查看系统调用表(\cpp|sys_call_table|)以%
-查看要调用的内核函数地址。然后它调用该函数,并在返回后执行一些系统检查,然后返%
-å\9b\9eå\88°è¿\9bç¨\8b(æ\88\96è\80\85è¿\94å\9b\9eå\88°å\8f¦ä¸\80个è¿\9bç¨\8bï¼\8cå¦\82æ\9e\9cè¿\9bç¨\8bæ\97¶é\97´ç\94¨å®\8c)ã\80\82å¦\82æ\83³è¯»è¯¥ä»£ç \81ï¼\8cç\9c\8b %
-\verb|arch/$(architecture)/kernel/entry.S|,中 \cpp|ENTRY(system_call)|。%
-行后的代码。
+号,该系统调用号告诉内核,进程请求什么服务。然后,它查看系统调用表 %
+(\cpp|sys_call_table|\,)\,以查看要调用的内核函数地址。然后它调用该函数,并在返%
+å\9b\9eå\90\8eæ\89§è¡\8cä¸\80äº\9bç³»ç»\9fæ£\80æ\9f¥ï¼\8cç\84¶å\90\8eè¿\94å\9b\9eå\88°è¿\9bç¨\8b(æ\88\96è\80\85è¿\94å\9b\9eå\88°å\8f¦ä¸\80个è¿\9bç¨\8bï¼\8cå¦\82è¿\9bç¨\8bè\80\97å°½ CPU å\88\86é\85\8d%
+给它的占用时间片)。如想读这些代码,查看 \verb|arch/$(architecture)/kernel/entry.S| %
+中 \cpp|ENTRY(system_call)| 行后的代码。
所以,如果我们想要改变某个系统调用的工作方式,我们需要做的就是编写自己的函数来%
-实现它(通常是添加一点我们自己的代码,然后调用原来的函数)。然后改变 \cpp|sys_call_table| %
-处的指针,指向我们的函数。因为我们可能被移除,并且我们不想让系统处于不稳定状态,%
-所以这对于 \cpp|cleanup_module| 恢复表到它是初状态很重要。
+实现它\,(\,通常是添加一点我们自己的代码,然后调用原来的函数)。然后改变 %
+\cpp|sys_call_table| 处的指针,指向我们的函数。因为我们的函数可能被移除,并且%
+我们不想让系统处于不稳定状态,所以这对于让 \cpp|cleanup_module| 恢复系统调用表%
+到最初状态来说,很重要。
-要修改 \cpp|sys_call_table| 的内容,我们需要考虑控制寄存器。一个控制寄存器是改变%
+要修改 \cpp|sys_call_table| 的内容,我们需要斟酌控制寄存器。一个控制寄存器是改变%
或控制 CPU 一般行为的处理器寄存器。对于 x86 架构,\verb|cr0| 寄存器具有修改处理%
-器基本操作的各种控制标志。在 \verb|cr0| 中的 \verb|WP| 标志代表写保护。一旦\verb|WP| %
-标记被设置,处理器禁止未来试图对只读小节的数据的写入操作。因此,我们必须在更改 %
+器基本操作的各种控制标志。在 \verb|cr0| 中的 \verb|WP| 标志代表写保护。一旦 %
+\verb|WP| 标记被设置,处理器禁止之后试图对只读段的写入操作。因此,我们必须在更改 %
\cpp|sys_call_table| 之前禁止 \verb|WP| 标记。因此 Linux v5.3 开始,\cpp|write_cr0| %
-由于 \verb|cr0| 比特敏感,无法使用该函数,由于安全问题所锁定的位,攻击者可能会%
-写入 CPU 控制器寄存器,以禁用写保护等 CPU 保护。因此,我们必须提供自定义汇编例%
-程来绕过它。
+函数不能被使用,由于 \verb|cr0| 比特位的敏感性带来的安全问题,攻击者可能会%写入 CPU %
+控制器寄存器,以禁用写保护等 CPU 保护。因此,我们必须提供自定义汇编例程来绕过它。
然而,\cpp|sys_call_table| 符号未导出以防止误用。但是获取符号的方法很少,手动符%
-号查找和 \cpp|kallsyms_lookup_name|。这里我们使用两者取决于内核版本。
+号查找和 \cpp|kallsyms_lookup_name| 可用的两种方法。这里我们使用两者取决于内核%
+版本。
由于 \textit{control-flow-integrity},这是一种防止攻击者重定向执行代码的技术,%
-以确保间接调用到达预期地址并且返回地址不被更改。从 Linux v5.7 开始,内核为 x86 %
-修补了一系列 \textit{control-flow-enforcement} (CET)。并且 GCC 的某些配置,例如%
+以确保间接调用到达预期地址,并且返回地址不被更改。从 Linux v5.7 开始,内核为 x86 %
+修补了一系列 \textit{control-flow-enforcement} (CET)。并且 GCC 的某些配置,例如 %
Ubuntu Linux 中的 GCC 版本\,9\,和\,10,默认情况下会在内核中添加\,CET\, (\verb|-fcf-protection| %
-选项)。使用该 GCC 在关闭 retpoline 时编译内核,可能会导致在内核中启用了 CET。%
+选项)。使用该 GCC 在关闭 retpoline 选项时编译内核,可能会导致在内核中启用了 CET。%
你可以使用以下命令查看 \verb|-fcf-protection| 选项是否启用:
\begin{verbnobox}[\fontsize{7pt}{7pt}\selectfont]
\end{verbnobox}
但在内核中 CET 不应被打开,开启后可能中断 Kprobes 与 bfp。最后,CET 从 v5.11 开%
-始被禁止。为保证手动符号查找有效,我们最多只能使用 v5.4。
+始被禁止。为保证手动符号查找有效,我们最多只能使用 v5.4 版本内核。
-不幸的是,自从 Linux v5.7 \cpp|kallsyms_lookup_name| 也不被输出,需要一定的技巧%
-才能获取 \cpp|kallsyms_lookup_name| 的地址。如果 \cpp|CONFIG_KPROBES| 被打开,%
-我们可以方便地通过 Kprobes 检索函数地址,从而动态地侵入特定的内核例程。Kprobes %
-通过替换探测指令的第一个字节在函数入口处插入断点。当 CPU 到达断点时,寄存器被存%
-å\82¨ï¼\8cæ\8e§å\88¶æ\9d\83å°\86ä¼ é\80\92ç»\99 Kprobesã\80\82å®\83å°\86ä¿\9då\98ç\9a\84å¯\84å\98å\99¨ç\9a\84å\9c°å\9d\80å\92\8c Kprobe ç»\93æ\9e\84ä¼ é\80\92ç»\99ä½ å®\9aä¹\89ç\9a\84%
-处理程序,然后执行它。Kprobes 可以通过符号名称或地址来注册。在符号名称中,地址%
-将由内核处理。
+不幸的是,自从 Linux v5.7 开始 \cpp|kallsyms_lookup_name| 也不被输出,需要一定%
+的技巧才能获取 \cpp|kallsyms_lookup_name| 的地址。如果 \cpp|CONFIG_KPROBES| 选%
+项被打开,我们可以方便地通过 Kprobes 检索函数地址,从而动态地侵入特定的内核例程。%
+Kprobes 通过替换探测指令的第一个字节在函数入口处插入断点。当 CPU 到达断点时,寄%
+å\98å\99¨è¢«å\98å\82¨ï¼\8cæ\8e§å\88¶æ\9d\83å°\86ä¼ é\80\92ç»\99 Kprobesã\80\82å®\83å°\86ä¿\9då\98ç\9a\84å¯\84å\98å\99¨ç\9a\84å\9c°å\9d\80å\92\8c Kprobe ç»\93æ\9e\84ä¼ é\80\92ç»\99%
+你定义的处理程序,然后执行它。Kprobes 可以通过符号名称或地址来注册。在符号名称%
+中,地址将由内核处理。
否则,从 \verb|/proc/kallsyms| 和 \verb|/boot/System.map| 指定 \cpp|sys_call_table| %
的地址进入 \cpp|sym| 范围。以下是 \verb|/proc/kallsyms| 的示例的用法:
件 \verb|/boot/System.map| 中列出的地址,将会偏移一些熵(随机值)。KASLR 的目的是%
防止攻击者对内核空间的攻击。没有 KASLR,攻击者可以非常容易地发现固定地址的攻击%
地址。那么攻击者就可以利用面向返回的编程方式,插入一些恶意代码,通过被篡改的指针%
-来执行或接收目标数据。KASLR 治愈这种攻击,因为攻击者不能立即知道目标地址,但一%
-个暴力破解攻击还可以实施攻击。如果在 \src{/proc/kallsyms} 一个符号的地址与大文件 %
-\verb|/boot/System.map| 中的地址不同,KASLR 通过你系统运行的内核被启用。
+来执行或接收目标数据。KASLR 缓和此攻击的危害,因为攻击者不能立即知道目标地址,%
+但一个暴力破解攻击还可以实施攻击。如果在 \src{/proc/kallsyms} 中,一个符号的地%
+址与在文件 \verb|/boot/System.map| 中的地址不同,说明你使用的内核中KASLR 已经被%
+打开。
\begin{verbatim}
$ grep GRUB_CMDLINE_LINUX_DEFAULT /etc/default/grub
$ sudo grep sys_call_table /proc/kallsyms
ffffffff86400300 R sys_call_table
\end{verbatim}
-如果 \verb|KASLR| 被启用,每次我们启动时,我们必须小心从 \verb|/proc/kallsyms| %
-的地址。为了使用来自 \verb|/boot/System.map| 的地址,确保 \verb|KASLR| 被禁用。%
-在下次启动时,你可以添加 \verb|nokaslr| 内核启动参数来禁止 \verb|KASLR|:
+
+如果 \verb|KASLR| 选项被启用,每次我们启动时,来自 \verb|/proc/kallsyms| 的地址,%
+必须被我们注意。为了使用来自 \verb|/boot/System.map| 的地址,确保 \verb|KASLR| %
+被禁用。在下次启动时,你可以添加 \verb|nokaslr| 内核启动参数来禁止 \verb|KASLR|:
+
\begin{verbnobox}[\fontsize{8pt}{8pt}\selectfont]
$ grep GRUB_CMDLINE_LINUX_DEFAULT /etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"
\item \href{https://lwn.net/Articles/569635/}{Kernel address space layout randomization}
\end{itemize}
-这里有源代码是这样一个内核模块的示例。我们想``监视''某个用户,每当用户打开一个%
+这里的源代码是一个此种内核模块的示例。我们想``监视''某个用户,每当用户打开一个%
文件时,并且用 \cpp|pr_info()| 打印一条信息。为此,我们用自己命名为 \cpp|our_sys_openat| %
-的函数替换系统调用。这个函数检查当前进程的 uid(用户的id),且如果它等于我们监控%
-的用户的 uid,它调用 \cpp|pr_info()| 来显示被打开的文件的名字。然后,无论哪种方%
-式,它都会用相同的参数调用原始的 \cpp|openat()| 函数,来实际上打开文件。
+的函数替换系统调用。这个函数检查当前进程的 uid\,(\,用户的id\,),并且,如果它等%
+于我们监控的用户的 uid,它调用 \cpp|pr_info()| 来显示被打开的文件的名字。然后,%
+无论哪种方式,它都会用相同的参数调用原始的 \cpp|openat()| 函数,来实际上打开文件。
\cpp|init_module| 函数在 \cpp|sys_call_table| 中替换适当位置并在一个变量中保存%
最初的指针。\cpp|cleanup_module| 函数使用那个变量来恢复所有数据到之前正常状态。%
前它就不再指向 \cpp|A_openat|。不幸的是,\cpp|B_openat| 仍会尝试调用 \cpp|A_openat|,%
它已经不在内存中了,因此即使不删除 B,系统调用也会崩溃。
-请注意,所有相关问题使得系统调用窃取在生产使用中不可行。为了防止人们做潜在有害%
-的事情,\cpp|sys_call_table| 不再被输出。这意味着,如果你想要做的不仅仅是这个示%
-例的空运行,你将必须修补当前内核,才能拥有 \cpp|sys_call_table| 被输出。
+请注意,所有相关问题使得系统调用窃取的花招在生产使用中不可行。为了防止人们做潜%
+在有害的事情,\cpp|sys_call_table| 不再被输出。这意味着,如果你想要做的不仅仅%
+是这个示例的空运行,你将必须修补当前内核,才能拥有 \cpp|sys_call_table| 被输出。
\samplec{examples/syscall.c}
\label{sec:blocking_process_thread}
\section{睡眠}
\label{sec:sleep}
-当某人请你帮忙,而你恰巧现在不能帮时,你会怎么做?如果你是一个人,并被另一个人%
-打扰,你唯一能说的是:``\emph{不是现在,我正忙,走开!}''。但如果你有一个内核模块%
-å¹¶ä¸\94ä½ è¢«ä¸\80个è¿\9bç¨\8bæ\89\93æ\89°ï¼\8cä½ è¿\98æ\9c\89å\8f¦å¤\96ä¸\80ç§\8då\8f¯è\83½æ\80§ã\80\82ä½ å\8f¯ä»¥è®©è¯¥è¿\9bç¨\8bè¿\9bå\85¥ä¼\91ç\9c ç\8a¶æ\80\81ï¼\8cç\9b´å\88°å\8f¯ä»¥%
-为其提供服务为止。毕竟,进程一直被内核置于睡眠状态并被唤醒(这就是多个进程在单个 %
-CPU 上同时运行的方式)。
+当某人请你帮忙,而你恰巧现在腾不出手,你会怎么做?如果你是一个人,并被另一个人%
+打扰,你唯一能说的是:``\emph{不是现在,我正忙,走开!}''。但如果你有一个内核模块,%
+å¹¶ä¸\94ä½ è¢«ä¸\80个è¿\9bç¨\8bæ\89\93æ\89°ï¼\8cä½ è¿\98æ\9c\89å\8f¦å¤\96ä¸\80ç§\8dåº\94ä»\98ç\9a\84å\8f¯è\83½æ\80§ã\80\82ä½ å\8f¯ä»¥è®©è¯¥è¿\9bç¨\8bè¿\9bå\85¥ä¼\91ç\9c ç\8a¶æ\80\81ï¼\8c%
+直到可以为该进程提供服务为止。毕竟,进程一直被内核置于睡眠状态并被唤醒(这就是%
+多个进程在单个 CPU 上同时运行的方式)。
-内核模块就是这种工作方式的一个示例。这个文件(叫作 \verb|/proc/sleep|)只能被单一%
-进程在一个时刻被打开。如果该文件已经打开,内核模块称为 \cpp|wait_event_interruptible|。%
-保持文件打开的最简单方法是使用以下命令打开:
+此内核模块就是这种处理方式的一个示例。该文件\,(\,叫作 \verb|/proc/sleep|\,)\,%
+只能被单一进程在一个时刻被打开。如果该文件已经打开,内核模块调用 %
+\cpp|wait_event_interruptible|。保持文件打开的最简单方法是使用以下命令打开:
\begin{codebash}
tail -f
\end{codebash}
-函数修改一个任务(一个任务是内核数据结构,它保存有关进程及其所在系统调用的信息,%
-如果有的话)的状态到 \cpp|TASK_INTERRUPTIBLE|,这意味着该任务在某时被唤醒之前,一%
-直不会动行,并将该任务添加到 WaitQ,它是等候访问文件的任务的队列。然后,该函数%
-调用调度程序将上下文切换到另一个进程,该进程对 CPU 有一定使用。
+函数修改一个 task (\,一个 task 是内核数据结构,它保存有关进程及其所在系统调用的%
+信息,如果有的话\,)\,的状态到 \cpp|TASK_INTERRUPTIBLE|,这意味着该任务在某时被%
+唤醒之前,一直不会动行,并将该任务添加到 WaitQ,它是等候访问文件的任务的队列。%
+然后,该函数调用调度程序将上下文切换到另一个进程,该进程对 CPU 有一定使用。
当一个进程处理完文件时,它关闭文件,并且 \cpp|module_close| 被调用。这个函数唤%
醒在队列(没有机制只唤醒其中一个队列内的进程)中的所有进程。它然后返回,并且刚刚%
当其它进程得到一片 CPU 占用时间片,它将看到该全局变量,并返回睡眠状态。
因此我们将使用 \sh|tail -f| 来保持在后台打开,同时尝试用另一个进程访问它(再次在%
-后台,这样我们就不需要切换到不同的 vt)。一旦第一个后台进程被使用命令 kill \%1 %
+后台,这样我们就不需要切换到不同的终端)。一旦第一个后台进程被使用命令 kill \%1 %
所杀死,第二个进程被唤醒,它能访问文件并最终终止。
为使得我们的生活更为有趣,\cpp|module_close| 不垄断唤醒等待访问文件的进程。一个%
\section{完成}
\label{sec:completion}
-å\9c¨ä¸\80人有多线程的模块中,有时一件事应在另一件事之前发生。而不是使用 \sh|sleep| %
+å\9c¨ä¸\80个æ\8b¥有多线程的模块中,有时一件事应在另一件事之前发生。而不是使用 \sh|sleep| %
命令,内核有另一种方法可以做到这一点,它允许超时或中断发生。在下面的示例中,两%
个线程已经开始,但一个需要在另一个之前开始。
状态被更新,并且 \cpp|wait_for_completion| 被飞轮线程使用,来确保它不会过早的%
开始。
-因此即使 \cpp|flywheel_thread| 首先启去吧,你应当注意到是否你加载这个模块并运行 %
-\sh|shidmesg|,它总是首先发生,因为 flywheel 线程等待它执行完成。
+因此即使 \cpp|flywheel_thread| 首先启动,你应当注意到是否加载这个模块,并运行 %
+\sh|shidmesg| 命令,该命令使得转动曲轴线程总是首先发生,因为 flywheel 线程会等%
+待曲轴进程完成任务。
\cpp|wait_for_completion| 函数还有其它变体,包括超时或被中断,但这种基本机制足%
以满足许多常见情况,而不会增加很多复杂性。
\chapter{避免冲突和死锁}
\label{sec:synchronization}
-如果进程运行于不同的 CPU 上或在不同的线程中试图访问相同的内存位置,然后奇怪的事%
-情可能发生,你的系统或许被锁住。要避免这样,可以使用各种类型的互斥内核函数。这%
-些指示一段代码是否被``锁定''或``解锁'',以便不会发生同时尝试运行这些代码的状况。
+如果进程运行于不同的 CPU 上,或在不同的线程中,试图访问相同的内存位置,然后奇%
+怪的事情可能发生,你的系统或许被锁住。要避免这样,可以使用各种类型的互斥内核%
+函数。这些指示一段代码是否被``锁定''或``解锁'',以便不会发生同时尝试运行这些代%
+码的状况。
\section{互斥}
\label{sec:mutex}
-你可使用内核互斥体(互斥),其使用方式与在用户态中部署它们的方式大致相同。在大多%
-数情况下,这可能是避免碰撞所需的全部内容。
+你可使用内核互斥体(相互排斥),其使用方式与你在用户态中部署它们的方式大致相同。%
+在大多数情况下,这可能是避免冲突所需的全部情况。
\samplec{examples/example_mutex.c}
\section{自旋锁}
\label{sec:spinlock}
-顾名思义,自旋锁会锁定代码正在运行其上的 CPU 并占用其 100\% 的资源。因此你只该%
-对è¿\90è¡\8cæ\97¶é\97´å\8f¯è\83½ä¸\8dä¼\9aè¶\85è¿\87å\87 毫ç§\92ç\9a\84代ç \81段ä¸\8a使ç\94¨è\87ªæ\97\8bé\94\81æ\9cºå\88¶ï¼\8cå\9b æ¤ä»\8eç\94¨æ\88·è§\92度æ\9d¥ç\9c\8bä¸\8dä¼\9aæ\98\8e%
-显减慢任何速度。
+顾名思义,自旋锁会锁定代码到正在运行此代码的 CPU 之上,并占用其 100\% 的资源。%
+å\9b æ¤ä½ å\8fªè¯¥å¯¹è¿\90è¡\8cæ\97¶é\97´å\8f¯è\83½ä¸\8dä¼\9aè¶\85è¿\87å\87 毫ç§\92ç\9a\84代ç \81段ï¼\8c使ç\94¨è\87ªæ\97\8bé\94\81æ\9cºå\88¶ï¼\8cå\9b æ¤ä»\8eç\94¨æ\88·è§\92度%
+æ\9d¥ç\9c\8bä¸\8dä¼\9aæ\98\8eæ\98¾å\87\8fæ\85¢ä»»ä½\95é\80\9f度ã\80\82
在这里的示例是 \verb|``irq safe''|,这意味着如果在锁定期间发生中断,那么它们不%
会被遗忘,并且在解锁发生时将被激活,使用 \cpp|flags| 变量来保持它们的状状。
\label{sec:print_macros}
\section{替换}
% FIXME: cross-reference
-在 \ref{sec:preparation} 小节,值得注意的是,X Window 系统和内核模块编程不利于%
-集成。这在内核模块开发过程中还有效。然而,在实际场景中,需要将消息中断到发出模%
-å\9d\97å\8a è½½å\91½ä»¤ç\9a\84 tty(ç\94µä¼ æ\89\93å\97æ\9cº)。
+在 \ref{sec:preparation} 节,值得注意的是,X Window 系统和内核模块编程不利于%
+集成。这在内核模块开发过程中还有效。然而,在实际场景中,需要紧急将加模块加载%
+å\91½ä»¤äº§ç\94\9fç\9a\84æ¶\88æ\81¯è½¬å\8f\91å\88°tty\,(\,teletype\,)。
术语``tty''源于 \emph{teletype},它最初指的是用于 Unix 系统通信的组合键盘打印%
机。今天,它表示 Unix 程序使用的文本流抽象,包括物理终端,X 显示中的 xterm,以%
\section{闪轹键盘LED}
\label{sec:flash_kb_led}
-在某些情况下,你可能想有一种更简单、更直接的方式与外部世界进行交流。闪烁的键盘 %
-LED 可以是这样的解决方案:它是吸引注意力或显示状态条件的直接方法。每个硬件上都%
-有键盘 LED,它们始终可见,不需要任何设置,并且与写入 tty 或文件相比,它们的使用%
-相当简单,而且具有非侵入性。
-
-从 v4.14 到 v4.15,计时器(timer) API 发生一系列改变以提高内存安全性。一个 \cpp|timer_list| %
-结构区域中的一个缓存溢出,或许有能力覆盖 \cpp|function| 与 \cpp|data| 字段,为%
-攻击者提供一种使用返回对象编程(ROP)来调用内核中的任意函数的方法。另外,回调的函%
-数原型,包含 \cpp|unsigned long| 参数,将阻止任何类型检查工作。此外,函数原型为 %
-\cpp|unsigned long| 参数可能是 \textit{control-flow integrity} 的前向边缘保护的%
-障碍。因此,最好使用唯一的原型来与采用 \cpp|unsigned long| 参数的集群分开。
-
-计时器回调应该传递一个指向 \cpp|timer_list| 结构的指针,而不是一个 \cpp|unsigned long| %
-å\8f\82æ\95°ã\80\82ç\84¶å\90\8eï¼\8cå®\83å\8c\85è£\85æ\89\80æ\9c\89å\9b\9eè°\83æ\89\80é\9c\80ä¿¡æ\81¯ï¼\8cå\8c\85æ\8b¬ \cpp|container_of| å®\8fï¼\8cæ\9b¿æ\8d¢ \cpp|unsigned long| %
-值。更多信息,请参阅: \href{https://lwn.net/Articles/735887/}{Improving the kernel timers API}.
-
-在 Linux v4.14 以前,\cpp|setup_timer| 被用来初始化定时器,并且 \cpp|timer_list| 结%
-构看起来如下:
+在某些情况下,你可能想有一种更简单、更直接的方式与外部世界进行交流。闪烁键盘 %
+LED 可以是此需求的解决方案:它是吸引注意力或显示状态条件的直接方法。每个硬件上%
+都有键盘 LED,它们始终可见,不需要任何设置,并且与写入 tty 或文件相比,它们的%
+使用相当简单,而且具有非侵入性。
+
+从 v4.14 到 v4.15,计时器(timer) API 发生一系列改变,以提高内存安全性。一个 %
+\cpp|timer_list| 区域中的一个缓存溢出,或许可能覆盖 \cpp|function| 与 \cpp|data| %
+字段,这为攻击者提供一种使用返回对象编程(ROP)来调用内核中的任意函数的方法。另外,%
+回调的函数原型,包含 \cpp|unsigned long| 参数,将阻止任何类型检查工作。此外,%
+函数原型为 \cpp|unsigned long| 参数可能是 \textit{control-flow integrity} 的前%
+向边缘保护的障碍。因此,最好使用唯一的原型来与采用 \cpp|unsigned long| 参数的%
+集群分开。计时器回调应该传递一个指向 \cpp|timer_list| 结构的指针,而不是一个 %
+\cpp|unsigned long| 参数。然后,它包装所有回调所需信息,包括 \cpp|container_of| %
+å®\8fï¼\8cæ\9b¿æ\8d¢ \cpp|unsigned long| å\80¼ã\80\82æ\9b´å¤\9aä¿¡æ\81¯ï¼\8c请å\8f\82é\98\85: %
+\href{https://lwn.net/Articles/735887/}{Improving the kernel timers API}.
+
+Linux v4.14 以前,\cpp|setup_timer| 被用来初始化定时器,并且 \cpp|timer_list| %
+结构看起来如下:
\begin{code}
struct timer_list {
unsigned long expires;
unsigned long data);
\end{code}
-自从 Linux v4.14 开始,\cpp|timer_setup| 被采用,并且内核逐步转从结构 %
+从 Linux v4.14 开始,\cpp|timer_setup| 被采用,并且内核逐步转从结构 %
\cpp|setup_timer| 逐步转换到 \cpp|timer_setup|。API 变更的原因之一是需要与旧版%
本接口共存。此外,\cpp|timer_setup| 首先通过 \cpp|setup_timer| 被执行。
如果本章中的示例都不符合你的调试需求,可能还可以尝试一些其它技巧。有没有想过 %
\cpp|CONFIG_LL_DEBUG| 是什么?在 \sh|make menuconfig| 中有什么好处?如果激活%
它,你将获得对串行端口的低级别访问权限。虽然这本身听起来不是很强大,但你可以%
-修补 \src{kernel/printk.c} 或任何其它必要的系统调用来打印 ASCII 字符,从而可%
-以è·\9f踪代ç \81é\80\9aè¿\87串è¡\8c线路æ\89§è¡\8cç\9a\84å\87 ä¹\8eæ\89\80æ\9c\89æ\93\8dä½\9cã\80\82ä½ å\8f\91ç\8e°è\87ªå·±å°\86å\86\85æ ¸ç§»æ¤\8då\88°ä¸\80äº\9bæ\96°ç\9a\84å\92\8c以%
-前不受支持的体系结构,这通常是首先应该实现的事情之一。通过网络控制台登录也可%
-能值的一试。
+对 \src{kernel/printk.c} 打补丁, 或任何其它必要的系统调用来打印 ASCII 字符,%
+ä»\8eè\80\8cå\8f¯ä»¥è·\9fè¸ªä½ ä»£ç \81å\9c¨ä¸\80æ\9d¡ä¸²è¡\8c线路ä¸\8aå\81\9aç\9a\84æ\89\80æ\9c\89äº\8bæ\83\85ã\80\82ä½ å\8f\91ç\8e°è\87ªå·±å°\86å\86\85æ ¸ç§»æ¤\8då\88°ä¸\80äº\9bæ\96°%
+的和以前不受支持的体系结构,这通常是首先应该执行的事情之一。通过网络控制台登%
+录也可能值的一试。
虽然你在这里看到了很多可用于帮助调试的内容,但仍有一些事情需要注意。调试几乎%
总是侵入性的。添加调试代码可以改变情况,足以使错误看起来消失。因此,你应该将%
\chapter{调度任务}
\label{sec:scheduling_tasks}
-运行任务有两种主要方式:tasklets 与工作队列。Tasklet 是一种安排单个函数运行的%
-快速而简单的方法。例如,当从中断触发时,工作队列更复杂,但也更适合按顺序运行%
-多个事物。
+运行任务有两种主要方式:tasklets 与工作队列(work queues)。Tasklet 是一种安排单%
+个函数运行的快速而简单的方法。例如,当从中断触发时,工作队列更复杂,但也更适合%
+按顺序运行多个事物。
\section{Tasklets}
\label{sec:tasklet}
在最近的内核中,tasklet 可以被工作队列,定时器,或线程中断所替换。\footnote{线%
程中断的目标是将更多的工作推送到不同的线程,以便对一个中断的最小需要被减少,并%
因此处理该中断(在同一时刻不能处理其它任何中断)花费的时间被减少了。%
-查看\url{https://lwn.net/Articles/302043/}。}
-
-虽然删除 tasklets 仍然是一个长期的目标,但当前内核包含一百多种 tasklet 的使用。%
-现在开发人员正在继续进行 API 更改和宏 \cpp|DECLARE_TASKLET_OLD| 存在是为了兼容%
-性。要了解更多信息,查看 \url{https://lwn.net/Articles/830964/}。
+查看\url{https://lwn.net/Articles/302043/}。}虽然删除 tasklets 仍然是一个长期%
+目标,但当前内核包含一百多种 tasklet 的使用。开发人员正在继续进行 API 更%
+改,并且宏 \cpp|DECLARE_TASKLET_OLD| 存在是为了兼容性。要了解更多信息,查看 %
+\url{https://lwn.net/Articles/830964/}。
\section{工作队列}
\label{sec:workqueue}
-要将任务添加到调度程序,我们可以使用工作队列。然后,内核使用完全公平调度程序(%
-CFS)来执行队列中的工作。
+要将任务添加到调度程序,我们可以使用工作队列。然后,内核使用完全公平调度\,(\,%
+Completely Fair Scheduler 缩写为 CFS\,)\,来执行队列中的工作。
\samplec{examples/sched.c}