Kikimo's blog

An occasional blog on programming

ptrace 单步执行源码分析

这篇文章分析 x86 平台上 ptrace() 函数单步执行功能的实现以及相关调试寄存器的一些介绍.

1. ptrace() 单步执行(PTRACE_SINGLESTEP)源码分析

1.1 ptrace() 函数的定义: kernel/ptrace.c

ptrace() 函数的实现代码在 Linux 的内核中, 我们以 Linux 4.10.17 版本的代码为例来做分析. ptrace() 函数的定义如下:

1114 SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
1115                 unsigned long, data)
1116 {
1117         struct task_struct *child;
1118         long ret;
...
1149         ret = arch_ptrace(child, request, addr, data);
1150         if (ret || request != PTRACE_DETACH)
1151                 ptrace_unfreeze_traced(child);
1152 
1153  out_put_task_struct:
1154         put_task_struct(child);
1155  out:
1156         return ret;
1157 }

ptrace() 函数的一些操作是平台相关的, 这些平台相关的操作被放置到 arch_ptrace() 函数中, 对于不同的 CPU, arch_ptrace() 函数有不一样的实现. 我们要分析的单步执行功能就是一个平台相关的操作, 所以接下来我们的去看 x86 平台上 arch_ptrace() 函数的实现.

1.2 x86 的 arch_ptrace() 函数: arch/x86/kernel/ptrace.c

arch_ptrace() 中大部分代码和我们要分析单步跟进功能没关系, 第 876 行对ptrace_request() 函数的调用才是我们要关注的, 继续跟踪 ptrace_request() 这个函数.

 766 long arch_ptrace(struct task_struct *child, long request,
 767                  unsigned long addr, unsigned long data)
 768 {
 769         int ret;
 770         unsigned long __user *datap = (unsigned long __user *)data;
 771 
...
 875         default:
 876                 ret = ptrace_request(child, request, addr, data);
 877                 break;
 878         }
 879 
 880         return ret;
 881 }

1.3 ptrace_request() 函数: kernel/ptrace.c

ptrace_request() 函数又辗转回到 kernel/ptrace.c 这个文件中来:

 876 int ptrace_request(struct task_struct *child, long request,
 877                    unsigned long addr, unsigned long data)
 878 {
 879         bool seized = child->ptrace & PT_SEIZED;
 880         int ret = -EIO;
 881         siginfo_t siginfo, *si;
 882         void __user *datavp = (void __user *) data;
 883         unsigned long __user *datalp = datavp;
 884         unsigned long flags;
...
1045 #ifdef PTRACE_SINGLESTEP
1046         case PTRACE_SINGLESTEP:
1047 #endif
1048 #ifdef PTRACE_SINGLEBLOCK
1049         case PTRACE_SINGLEBLOCK:
1050 #endif
1051 #ifdef PTRACE_SYSEMU
1052         case PTRACE_SYSEMU:
1053         case PTRACE_SYSEMU_SINGLESTEP:
1054 #endif
1055         case PTRACE_SYSCALL:
1056         case PTRACE_CONT:
1057                 return ptrace_resume(child, request, data);
...
1088         default:
1089                 break;
1090         }
1091 
1092         return ret;
1093 }

我们可以看到, 单步执行请求会触发(PTRACE_SINGLESTEP) ptrace_resume() 函数的执行, 跳过去看 ptrace_resume() 函数.

1.4 ptrace_resume() 函数: kernel/ptrace.c

 773 static int ptrace_resume(struct task_struct *child, long request,
 774                          unsigned long data)
 775 {
 776         bool need_siglock;
 777 
 778         if (!valid_signal(data))
 779                 return -EIO;
 780 
 781         if (request == PTRACE_SYSCALL)
 782                 set_tsk_thread_flag(child, TIF_SYSCALL_TRACE);
 783         else
 784                 clear_tsk_thread_flag(child, TIF_SYSCALL_TRACE);
 785 
 786 #ifdef TIF_SYSCALL_EMU
 787         if (request == PTRACE_SYSEMU || request == PTRACE_SYSEMU_SINGLESTEP)
 788                 set_tsk_thread_flag(child, TIF_SYSCALL_EMU);
 789         else
 790                 clear_tsk_thread_flag(child, TIF_SYSCALL_EMU);
 791 #endif
 792 
 793         if (is_singleblock(request)) {
 794                 if (unlikely(!arch_has_block_step()))
 795                         return -EIO;
 796                 user_enable_block_step(child);
 797         } else if (is_singlestep(request) || is_sysemu_singlestep(request)) {
 798                 if (unlikely(!arch_has_single_step()))
 799                         return -EIO;
 800                 user_enable_single_step(child);
 801         } else {
 802                 user_disable_single_step(child);
 803         }
 ...
 826         return 0;
 827 }

is_singlestep() 看起来像是用来判断是否为单步执行的测试, 来看 kernel/ptrace.c 中 is_singlestep() 宏的定义:

 755 #ifdef PTRACE_SINGLESTEP
 756 #define is_singlestep(request)          ((request) == PTRACE_SINGLESTEP)
 757 #else
 758 #define is_singlestep(request)          0
 759 #endif

is_singlestep() 宏就是用来判断是不是 PTRACE_SINGLESTEP 请求, 如果是则进入函数 user_enable_single_step() 中.

1.5 user_enable_single_step()函数: ./arch/x86/kernel/step.c

查找 user_enable_single_step() 函数:

$ find . -path ./drivers -prune -o -name '*.c' -print | xargs grep user_enable_single_step | grep x86
./arch/x86/kernel/step.c:void user_enable_single_step(struct task_struct *child)

user_enable_single_step() 函数:

211 void user_enable_single_step(struct task_struct *child)
212 {
213         enable_step(child, 0);
214 }

enable_step() 函数:

193 /*
194  * Enable single or block step.
195  */
196 static void enable_step(struct task_struct *child, bool block)
197 {
198         /*
199          * Make sure block stepping (BTF) is not enabled unless it should be.
200          * Note that we don't try to worry about any is_setting_trap_flag()
201          * instructions after the first when using block stepping.
202          * So no one should try to use debugger block stepping in a program
203          * that uses user-mode single stepping itself.
204          */
205         if (enable_single_step(child) && block)
206                 set_task_blockstep(child, true);
207         else if (test_tsk_thread_flag(child, TIF_BLOCKSTEP))
208                 set_task_blockstep(child, false);
209 }

enable_single_step() 函数:

109 static int enable_single_step(struct task_struct *child)
110 {
111         struct pt_regs *regs = task_pt_regs(child);
112         unsigned long oflags;
113 
114         /*
115          * If we stepped into a sysenter/syscall insn, it trapped in
116          * kernel mode; do_debug() cleared TF and set TIF_SINGLESTEP.
117          * If user-mode had set TF itself, then it's still clear from
118          * do_debug() and we need to set it again to restore the user
119          * state so we don't wrongly set TIF_FORCED_TF below.
120          * If enable_single_step() was used last and that is what
121          * set TIF_SINGLESTEP, then both TF and TIF_FORCED_TF are
122          * already set and our bookkeeping is fine.
123          */
124         if (unlikely(test_tsk_thread_flag(child, TIF_SINGLESTEP)))
125                 regs->flags |= X86_EFLAGS_TF;
126 
127         /*
128          * Always set TIF_SINGLESTEP - this guarantees that
129          * we single-step system calls etc..  This will also
130          * cause us to set TF when returning to user mode.
131          */
132         set_tsk_thread_flag(child, TIF_SINGLESTEP);
133 
134         oflags = regs->flags;
135 
136         /* Set TF on the kernel stack.. */
137         regs->flags |= X86_EFLAGS_TF;
138 
139         /*
140          * ..but if TF is changed by the instruction we will trace,
141          * don't mark it as being "us" that set it, so that we
142          * won't clear it by hand later.
143          *
144          * Note that if we don't actually execute the popf because
145          * of a signal arriving right now or suchlike, we will lose
146          * track of the fact that it really was "us" that set it.
147          */
148         if (is_setting_trap_flag(child, regs)) {
149                 clear_tsk_thread_flag(child, TIF_FORCED_TF);
150                 return 0;
151         }
152 
153         /*
154          * If TF was already set, check whether it was us who set it.
155          * If not, we should never attempt a block step.
156          */
157         if (oflags & X86_EFLAGS_TF)
158                 return test_tsk_thread_flag(child, TIF_FORCED_TF);
159 
160         set_tsk_thread_flag(child, TIF_FORCED_TF);
161 
162         return 1;
163 }

这个函数最重要的操作就是设置 X86_EFLAGS_TF 标志位, 也就是 x86 的 Trap 标志位. 对于 x86 CPU, Trap 标志为将使 CPU 进入单步调试模式. 到这里, 我们已经能明白 ptrace() 单步执行的实现原理了.

1.6 清理 Trap 标志位

ptrace() 在单步执行结束后会回复原来的 Trap 标志位, 具体操作是在 handle_signal() (arch/x86/kernel/signal.c) 函数中:

701 static void
702 handle_signal(struct ksignal *ksig, struct pt_regs *regs)
703 {
704         bool stepping, failed;
705         struct fpu *fpu = &current->thread.fpu;
...
732         /*
733          * If TF is set due to a debugger (TIF_FORCED_TF), clear TF now
734          * so that register information in the sigcontext is correct and
735          * then notify the tracer before entering the signal handler.
736          */
737         stepping = test_thread_flag(TIF_SINGLESTEP);
738         if (stepping)
739                 user_disable_single_step(current);
740 
741         failed = (setup_rt_frame(ksig, regs) < 0);
742         if (!failed) {
743                 /*
744                  * Clear the direction flag as per the ABI for function entry.
745                  *
746                  * Clear RF when entering the signal handler, because
747                  * it might disable possible debug exception from the
748                  * signal handler.
749                  *
750                  * Clear TF for the case when it wasn't set by debugger to
751                  * avoid the recursive send_sigtrap() in SIGTRAP handler.
752                  */
753                 regs->flags &= ~(X86_EFLAGS_DF|X86_EFLAGS_RF|X86_EFLAGS_TF);
754                 /*
755                  * Ensure the signal handler starts with the new fpu state.
756                  */
757                 if (fpu->fpstate_active)
758                         fpu__clear(fpu);
759         }
760         signal_setup_done(failed, ksig, stepping);
761 }

2. 为什么要分析 ptrace() 函数的单步跟进调用

为什么要特地去分析 ptrace() 函数的单步执行功能呢, 这得从我最近在写的一个调试器说起. 我们知道, 调试器中, 可以用 int3 指令来设置软件断点. int3 指令的二进制编码是 0xcc, 要在某一条指令处设置软件断点, 只需要把该指令的第一字节替换成 0xcc 即可. 当被调试程序暂停执行的时候, 触发暂停的原因可能是碰到软件断点, 硬件断点, 也有可能是调用了 ptrace() 单步执行后暂停下来.

在我的调试器中, 对于软件断点都有相应的记录. 最初我用来判断是否碰到软件断点的方法是: 如果当前 IP 寄存器为地址 x, 且有一条软件断点记录的断点地址为 x - 1, 那么我就认为碰到软件断点了. 一般情况下, 这个的判断软件断点的方法是可以正常使用的. 但是, 如果 x 地址处的原始指令长度为一个字节的话就可能出问题了. 比如, 在地址 x - 1 处设置一处软件断点, x - 1 处地址的指令长度为一. 在 x - 1 处的软件点断触发程序暂停后, 此时 IP 寄存器值为 x, 如果要单步执行这条指令, 首先, 得把 IP 寄存器设为 x - 1, 然后把地址 x - 1 处的指令恢复过来, 再单步执行指令. 然而, 执行完指令后, IP 寄存器值又变为 x, 按照我们上面描述的软件断点的判断方法, 这时候会认为又碰到软件断点了. 可能你会说, 只要判断 x - 1 地址处的指令只是不是 0xcc 不就能判断是不是碰到软件断点了嘛? 这个说法看似有理, 但是, 如果 x - 1 处的指令原本就是 int3 指令 0xcc 呢? 因此, 单纯利用 IP 寄存器显然是不能充分判断是否碰到软件断点的.

所以我就开始琢磨 ptrace() 单步执行这个功能是如何实现的. 我模糊记得 x86 的标志寄存器有个标志位可以用来实现指令的单步执行. 在 wik 上查到了 TF 标志位功能的介绍, 我猜测 ptrace() 的单步跟进功能就是利用 TF 标志位来实现的. 写了段测试代码去验证, 这段测试代码主要检测了在执行了 ptrace() 单步执行后 TF 标志位会不会被设置. 我猜想如果 ptrace() 的单步执行功能如果是利用 TF 标志位来实现的话, 在单步执行完后可以检测到 TF 标志为会被设置. 然而最终结果显示 TF 标志位并没有被设置, 这是我非常好奇 ptrace() 的单步跟进到底是如何实现的, 所以也就有了上面的分析.

至于为什么观察不到 TF 标志位被设置, 其原因很可能是 TF 触发程序中断后, 程序从内核态返回用户态前调用了 handle_signal() 函数把 TF 标志位清零了. 更本质的原因应该是一个设计原则的问题: ptrace() 利用 TF 标志位实现了指令的单步执行, 在单步执行结束后, 系统有责任把 TF 标志位恢复原来的设置, 否则要让调用者来负责标志位的恢复工作.

3. 判断被调试程序是不是在单步执行指令

上面解释了为什么要分析 ptrace() 的单步执行功能. 有一个问题还没有说清楚, 那就是 要如何判断被调试程序是否在单步执行指令. 我在翻 kernel 代码的过程中, 发现 "./arch/x86/include/uapi/asm/debugreg.h" 文件中有一个 DR_STEP 宏定义:

 20 #define DR_TRAP0        (0x1)           /* db0 */
 21 #define DR_TRAP1        (0x2)           /* db1 */
 22 #define DR_TRAP2        (0x4)           /* db2 */
 23 #define DR_TRAP3        (0x8)           /* db3 */
 24 #define DR_TRAP_BITS    (DR_TRAP0|DR_TRAP1|DR_TRAP2|DR_TRAP3)
 25 
 26 #define DR_STEP         (0x4000)        /* single-step */

看着 DR_STEP 宏定义周围的代码和注释, 很像是 x86 的调试寄存器相关的. DR_STEP 的注释 single-step 似乎表明这个和指令单步执行有关. 我之前没印象 x86 的调试寄存器和指令单步执行有关. 继续搜了 kernel 代码中对 DR_STEP 的引用:

$ find . -path ./drivers -prune -o -name '*.c' -print | xargs grep 'DR_STEP'
./arch/x86/mm/kmmio.c:  if (val == DIE_DEBUG && (*dr6_p & DR_STEP))
./arch/x86/mm/kmmio.c:          *dr6_p &= ~DR_STEP;
./arch/x86/kernel/traps.c:  if (unlikely(!user_mode(regs) && (dr6 & DR_STEP) &&
./arch/x86/kernel/traps.c:      dr6 &= ~DR_STEP;
./arch/x86/kernel/traps.c:  if ((dr6 & DR_STEP) && kmemcheck_trap(regs))
./arch/x86/kernel/traps.c:  if (WARN_ON_ONCE((dr6 & DR_STEP) && !user_mode(regs))) {
./arch/x86/kernel/traps.c:      tsk->thread.debugreg6 &= ~DR_STEP;
./arch/x86/kernel/traps.c:  if (tsk->thread.debugreg6 & (DR_STEP | DR_TRAP_BITS) || user_icebp)
./arch/x86/kernel/hw_breakpoint.c:  if (dr6 & DR_STEP)
./arch/x86/kernel/kgdb.c:   (*(unsigned long *)ERR_PTR(args->err)) &= ~DR_STEP;

从输出结果上看, 如 "./arch/x86/kernel/traps.c: dr6 &= ~DR_STEP;", DR_STEP 应该是和 dr6 调试寄存器有关. 在 x86 CPU 中 dr6 是作为状态寄存器使用的, wiki 上关于 dr6 寄存器的描述 只提到它的标志位和dr0, dr1, dr2, dr3 寄存器所设置的硬件断点的触发有关. 估计 dr6 还有更丰富的功能, 于是我特地把那本无比厚重的 x86 编程手册 找来, 在 17.2.3 Debug Status Register (DR6) 这一节中找到了 dr6 寄存器的完整说明, 其中有一段和单步执行有关的描述:

BS (single step) flag (bit 14) — Indicates (when set) that the debug exception was triggered by the single-step execution mode (enabled with the TF flag in the EFLAGS register). The single-step mode is the highest-priority debug exception. When the BS flag is set, any of the other debug status bits also may be set.

bit 14 也就是定义 DR_STEP 宏的 0x4000. 当程序执行完指令单步跟进后这个标志位会被设置, 应该可以用 dr6 寄存器的这个标志位来判断程序是否在单步执行指令. 写了段测试代码 验证了下, 果不其然. 所以, 判断是否执行了指令单步跟进, 是可以通过 dr6 寄存器的标志位来判断的.

Django 源码分析: (一) WSGI 规范与 Django 框架

WSGI 是一套定义 Python Web 服务器和 Python Web 应用之间通信接口的规范. Django 框架的本质是一套实现了 WSGI 应用端接口的框架(当然 Django 还提供了 ORM, 模板引擎等诸多常见的 Web 编程组件), WSGI 应用接口可以看做 Django 处理用户请求的入口, 要分析 Django 框架的源码, 我们就从 Django 的 WSGI 应用接口开始. 本文首先简要介绍了 WSGI 规范, 然后介绍 Django 中的 WSGI 定义.

WSGI 规范

Python PEP 3333 提出了 WSGI 的概念. WSGI 的全称是 Web 服务器网关接口(Web Server Gatway Interface), 它是一套接口通信规范, 这套接口规范定义了 Python Web 服务器和 Python Web 应用之间的通信标准. WSGI 规范同时定义了Python Web 服务器端(网关)的接口和 Python Web 应用端(框架)接口. WSGI 服务器接收客户端发送过来的请求, 然后将请求封装好发送给 WSGI 应用处理, WSGI 应用处理完请求后将处理结果发送给 WSGI 服务器, WSGI 服务器进一步将结果发送给客户端. 使用这个规范带来的好处是: 基于 WSGI 规范开发的 Python Web 应用可以运行在任意支持 WSGI 规范的 Web 服务器上. 譬如, 一个基于 WSGI 规范开发的 Python Web 应用既可运行于 gunicorn 服务器上, 又能运行与 usgwi 服务器上, 这两个 Python Web 服务起都支持 WSGI 规范.

WSGI 应用接口是一个可调用对象(Python callable object): 一个函数, 一个方法, 一个实现了 object.call()方法的对象实例. 这个可调用对象必须满足[1]:

  1. 接收两个参数:

    • 一个字典, 包含类似 CGI 的变量;
    • 一个回调函数, 这个回调函数将被用于发送 HTTP 状态码/消息和 HTTP 头部信息给 WSGI Web 服务器.
  2. 返回响应体(response body)给WSGI Web 服务器, 这个响应体应该被封装在一个 Python iterator中.

[1] 这部分关于 WSGI 应用接口的描述引用自 WSGI -- Application Interface

我们来看一个 WSGI 应用接口的例子, wsgiapp.py[2]

# The application interface is a callable object
def application ( # It accepts two arguments:
    # environ points to a dictionary containing CGI like environment
    # variables which is populated by the server for each
    # received request from the client
    environ,
    # start_response is a callback function supplied by the server
    # which takes the HTTP status and headers as arguments
    start_response
):

    # Build the response body possibly
    # using the supplied environ dictionary
    response_body = 'Request method: %s' % environ['REQUEST_METHOD']

    # HTTP response code and message
    status = '200 OK'

    # HTTP headers expected by the client
    # They must be wrapped as a list of tupled pairs:
    # [(Header name, Header value)].
    response_headers = [
        ('Content-Type', 'text/plain'),
        ('Content-Length', str(len(response_body)))
    ]

    # Send them to the server using the supplied function
    start_response(status, response_headers)

    # Return the response body. Notice it is wrapped

    # in a list although it could be any iterable.
    return [response_body]

[2] 这部分关于 WSGI 应用接口的描述也是引用自 WSGI -- Application Interface

这就实现了一个简单的 WSGI Web 应用. WSGI 应用必须跑在 WSGI Web 服务器中. 利用 Python 中的 wsgiref 模块, 我们来构造一个简单的 WSGI Web 服务器:

#! /usr/bin/env python

# Python's bundled WSGI server
from wsgiref.simple_server import make_server

from wsgiapp import application

# Instantiate the server
httpd = make_server (
    'localhost', # The host name
    8000, # A port number where to wait for the request
    application # The application object name, in this case a function
)

# Wait for a single request, serve it and quit
httpd.serve_forever()

执行以上代码, 用浏览器打开页面 http://localhost:8000, 可以看到如下结果:

djserver

Django 中的 WSGI 应用

通常, 想要让我们的 Web 应用能够运行在 WSGI 服务器上的话并不需要我们自己去实现 WSGI 应用接口. 现有的支持 WSGI 规范的 Python Web 框架都已经实现了 WSGI 应用接口部分, 例如 Django, Flask, 我们只要在框架下开发 Web 应用即可. 这些框架中带有 WSGI 应用接口的实现和应用的构造代码. 我们来看看 Django 中的 WSGI 应用的构造.

在一个 Django 项目的 settings.py 文件可以看到一个 WSGI_APPLICATION 变量. 如果我们的 Django 项目名字叫 djtest, 那么 WSGI_APPLICATION 变量的值就会是 djtest.wsgi.application. 打开 djstest/wsgi.py 文件(这个文件是 django-admin 在创建 Django 项目时自动生成的), 可以看到以下内容:

"""
WSGI config for djtest project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djtest.settings")

application = get_wsgi_application()

在以上代码, 我们看到函数 get_wsgi_application() 构造并返回 Django 的 WSGI Web 应用对象, 这个对象被赋值个 application 变量. 利用 Python 的 wsgiref 模块, 我们写个简单的 WSGI Web 服务器 djserver.py, 我们的这个服务器将利用 djtest.wsgi.application 对象来处理用户请求.

from wsgiref.simple_server import make_server
from djtest.wsgi import application

httpd = make_server('localhost', 8000, application)
httpd.serve_forever()

执行 djserver.py 脚本, 在浏览器中打开 http://localhost:8000/, 我们可以看到 Django 项目正常启动的页面:

run django app

这也是我们运行 python manage.py runserver 用浏览器打开页面 http://localhost:8000/ 能看到的结果.

为 Ubuntu 添加系统调用

系统调用是应用程序同操作系统之间的接口. 系统调用在内核态执行, 它可以直接访问内核中的一些关键数据结构(如进程链表等), 执行一些只有在特权级下才能执行的指令(如 io 指令, gdt 寄存器操作等). Ubuntu 是诸多 Linux 系统发行版中比比较常见的一种. 本介绍如何为 Ubuntu 添加系统调用.

1. 获取 Linux 系统源码

添加系统调用前首先得得获取 Linux 的内核源码. 在 Ubuntu 下可以直接通过 apt-get 包管理工具下载内核源码:

apt-get source linux-image-`uname -r`

uname -r 指令获取当前内核的发行版, 以上指令将获取当前内核发行版的源码. 譬如, 我们当前内核的发行版是 4.10.0-33-generic. 代码下载完后则保存在目录 linux-hwe-4.10.0 中.

2. 添加系统调用

Linux 源码中有一份文档详细介绍如何添加系统调用. 在 4.10.0-33-generic 中这份文档是 Documentation/process/adding-syscalls.rst 中 (Documentation/process/ 目录下的这部分文档似乎是用 Sphinx 组织的). 老一些的内核版本如 4.4.0 中则是 Documentation/adding-syscalls.txt.

不同 CPU 架构下添加系统调用的步骤可能有会有细微差别, 这里我们重点介绍如何为 x86 平台添加系统调用. 接下来我们详细介绍为 Ubuntu 添加系统调用的步骤.

2.1 添加系统调用

在 Linux 源码根目录下创建一个新目录 newsys 并进入 newsys 目录:

$ mkdir newsys
$ cd newsys

在 newsys 目录下创建文件 nsys.c, 该文件包含我们的系统调用的主体代码. 在 nsys.c 文件键入以下代码:

#include<linux/kernel.h>
#include<linux/syscalls.h>

SYSCALL_DEFINE0(nsys)
{
        printk(KERN_INFO "greetings from syscall nsys.\n");

        return 0;
}

Linux 的代码提供了一个宏 SYSCALL_DEFINEn, 这个宏是专门用来声明系统调用的. 其中, n 代表这个系统调用的参数个数. 例如, 上面的代码中我们使用的是 SYSCALL_DEFINE0, 这表示我们的这个系统调用没有参数. 这个系统调用的名字叫 nsys, 功能比较简单, 它只是打印一行消息 "greetings from syscall nsys." 到系统日志中.

2.2 修改 Makefile 构建脚本

添加问系统调用的实现部分后, 现在我们需要修改 Linux 的构建脚本, 使得在内核的构建过程中能把这个系统调用也编译进去.

在 newsys 目录下添加一个 Makefile 构建脚本, 内容如下:

obj-y := nsys.o

然后, 打开 Linux 源码根目录下的 Makefile 构建脚本, 找到以下内容:

ifeq ($(KBUILD_EXTMOD),)
core-y          += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/

vmlinux-dirs    := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \

将以下内容:

core-y          += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/

修改为:

core-y          += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/ newsys/

以上的修改可以让我们的系统调用直接被编译到 Linux 内核中. 实际上, Linux 的内核源码还有一套复杂的配置系统. 我们可以通过这个系统添加控制这个系统调动的配置, 如是否要把该系统调用编译到内核中. Linux 配置系统的说明文档在源码目录 Documentation/kbuild 下. 很有意思的一点是, Linux 内核有十几万的可配置选项 (或者一万多, 记不大清 -_-!), 而很多配置项之间又有依赖关系, 如何满足某一个配置项集合约束的条件最终可以转为成了一个求解 SAT 的问题求解.

2.3 修改系统调用表

完成以上步骤后添加系统调用的工作基本上就完成了. 但是, 和其他平台相比, x86 平台下的系统调用还需要一些额外的操作. x86 平台利用一张系统调用表来记录当前的系统调用信息. x86 的系统调用表还分为 32 位的系统调用表和 64 位的系统调用表. 我们现在做的是给 64 位的系统添加系统调用. x86 的 64 位的系统调用表在文件 arch/x86/entry/syscalls/syscall_64.tbl 中. 我们在 syscall_64.tbl 文件中添加一条记录:

326     64      nsys                    sys_nsys

326 是系统调用的编号. 当前内核版本中系统调用的最大编号是 325, 所以我们给新添加的系统调用选取 326 做为他的系统调用编号. 系统调用编号是非常重要的, 用户态程序就是使用系统调用编号来调用某个系统调用.

完成以上几个步骤, 接下来我们可以开始编译内核了.

3. 编译内核

编译 Linux 内核前需要先设置 Linux 内核的配置. 之前我们提过 Linux 内核有非常多可配置项. 设置数量庞大而且复杂的配置项是一件耗时而且容易出错的事情. 一个比较简便的做法就是复用当前系统的内核配置. Linux 内核的构建系统目前支持这样的操作. 我们可以通过在 Linux 内核源码目录下运行以下指令来复制当前系统的内核配置:

make oldconfig

内核配置最终存放在源码目录下的文件 .config 里. 以上指令会把当前系统的内核配置复制到 .config 文件.

make oldconfig 帮我们省去了繁琐复杂的内核配置过程, 但是它有一个潜在的不足. 一般, 我们的电脑装的是某个 Linux 的系统发行版. 这些发行版为了尽可能多的匹配不同型号的硬件设备, 它们会在内核配置中启用的大量的设备驱动. 大部分的这些驱动你可能都用不上. 而编译这些驱动则要花费漫长的时间. 所以, Linux 的构建系统又提供了一个 make localmodconfig 功能. make localmodconfig 会根据当前系统加载的模块来决定编译那些设备驱动. 这样我们就不需要编译大量用不着的内核驱动了, 大大的节省了内核编译的时间.

make localmodconfig 差不多能帮我们搞定大部分的内核配置了. 但是是有时候我们还需要对 Linux 内核的配置做一些微调. 可以直接打开 .config 文件修改配置, 但是这样的修改不是很方便. 比较好的做法是使用 make menucofig, make menuconfig 会弹出一个命令行选择界面, 我们可以根据界面提示来选择内核配置, 如下图:

kernel menu config

make menuconfig 弹出的窗口中, 我们可以按 / 键来搜索配置项(类似在 vi 中执行文本搜索操作). 譬如, 我们想搜索内核调试信息的配置项. 我们可以按先 / 键, 再输入 debug_info, 然后我们可以看到搜索结果:

search config

其中, 第二项就是我们想要的内核调试信息配置项.

关于内核调试信息控制项, 需要提一下的是, 生成内核调试信息会占用非常长的时间. 所以如果不是必要的话可以关闭这个选项.

搞定内核配置, 接下来就要真正开始编译内核了. 由于我们使用的是 Ubuntu 系统, 我们可以直接编译生成内核的 deb 安装, 然后直接利用 dpkg 指令安装新内核. 我们可以利用以下指令编译生成 Ubuntu 内核的 deb 安装包:

fakeroot make -j9 bindeb-pkg LOCALVERSION=-nsys

这里使用了 fackroot, fakeroot 主要是在生成 deb 包的时候需要用到, 具体的解释可以看 stackexchange 上的这个提问.

生成 deb 包使用的 make 目标是 bindeb-pkg. 我也可以使用 deb-pkg 这个目标来生成 deb 包, 不同的是 deb-pkg 目标会先执行 make clean 完全清理我们之前构建的中间结果. make -j9 开启了多任务编译. 我的系统是八核的 CPU, 所以设置 -j9 以最大化利用 CPU 资源. LOCALVERSION 选项设置我们编译出来的内核版本的名称. 最终我们生成的 deb 内核安装包在内核源码的上一级目录, 如下:

$ ls -l ../*.deb 
-rw-r--r-- 1 alex alex   84254 9月   9 15:29 ../linux-firmware-image-4.4.79-nsys_4.4.79-nsys-7_amd64.deb
-rw-r--r-- 1 alex alex 7103108 9月   9 15:30 ../linux-headers-4.4.79-nsys_4.4.79-nsys-7_amd64.deb
-rw-r--r-- 1 alex alex 8579552 9月   9 15:30 ../linux-image-4.4.79-nsys_4.4.79-nsys-7_amd64.deb
-rw-r--r-- 1 alex alex  825380 9月   9 15:30 ../linux-libc-dev_4.4.79-nsys-7_amd64.deb

4. 测试系统调用

现在我们来测试我们新添加的系统调用. 测试系统调用之前, 我们先把编译好的内核安装上去:

$ sudo dpkg -i linux-headers-4.4.79-nsys_4.4.79-nsys-7_amd64.deb linux-image-4.4.79-nsys_4.4.79-nsys-7_amd64.deb

重启系统, 然后编写以下用来代码 syscall.c 来测试我们添加的系统调用:

#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
        int ret;

        ret = syscall(326);
        printf("ret: %d\n", ret);

        return 0;
}

glibc 提供了 Linux 系统中大部分系统调用的封装函数. 例如 fork() 系统调用可以直接在程序中用 fork() 来调用. 我添加的系统没有再做一层封装, 所以这里我们直接使用 syscall() 这个函数来调用我们的系统调用. 注意到这个我们给 syscall() 传入一个 326 的参数, 这个数值就是我们系统调用在系统调用表中的编号. 编译并执行我们的代码, 结果如下:

invoke syscall

注意到系统日志中打印出来的最后一行消息, 可以看到我们的添加的系统调用被成功执行了.