编译Linux内核全过程及添加系统调用


编译Linux内核

  1. Linux内核获取
    Linux 官网

    wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.19.10.tar.xz
    

    查看内核列表:

    dpkg --get-selections | grep linux-image
    

    查看内核列表
    查看当前使用的内核:

    uname -r
    

    查看当前使用的内核

  2. 将下载的内核源码压缩包移动到目录 /usr/src (/usr/src:内核源代码默认的放置目录。首先需要切换到 root 账户,或使用 sudo 命令),解压并且换到内核源代码目录下

    mv linux-5.19.10.tar.xz /usr/src
    cd /usr/src
    tar -Jxvf linux-5.19.10.tar.xz
    cd linux-5.19.10
    
  3. 内核源码树目录概览

    目录描述
    arch包括了所有和体系结构相关的核心代码。它下面的每一个子目录都代表一种 Linux 支持的体系结构,例如 i386 就是 Intel CPU 及与之相兼容体系结构的子目录。PC机一般都基于此目录。
    block块设备I/O层
    crypto加密API
    Documentation目录下是一些文档,是对每个目录作用的具体说明。
    drivers系统中所有的设备驱动程序。它又进一步划分成几类设备驱动,每一种有对应的子目录
    firmware保存用于驱动第三方设备的固件
    fsVFS和各种文件系统,存放Linux支持的文件系统代码。不同的文件系统有不同的子目录对应,如ext3文件系统对应的就是ext3子目录。
    include内核头文件,需要提供给外部模块(例如用户空间代码)使用
    initLinux系统启动初始化相关的代码
    ipc进程间通信(IPC)子系统
    kernel内核管理的核心代码放在这里。同时与处理器结构相关代码都放在arch/*/kernel目录下
    lib通用的内核库的代码,不过与处理器结构相关的库代码被放在arch/*/lib/目录下
    mm内存管理子系统和VM。与具体硬件体系结构相关的内存管理代码位于arch/*/mm目录下
    samples示例,示范代码
    net内核的网络部分代码,其每个子目录对应于网络的一个方面
    Kconfig,Kbuild,Makefile,scripts配置和编译内核所用的脚本文件
    securitylinux安全模块
    sound语音子系统
    usr早期的用户空间代码,所谓的initramfs
    tools在linux开发中有用的工具
    virt虚拟化基础结构
    COPYING版权声明
    MAINTAINERS维护者名单
    GREDITSLinux主要的贡献者名单
  4. 安装编译内核需要的环境(下载工具)
    在进行配置时,会执行命令 “make menuconfig”,它需要 libncurses5-dev、flex、bison 等软件。ncurses 库可用于管理字符终端界面,而 bison 和 flex 是生成词法分析器的工具。除此之外,还需要安装好 kernel-package、libssl-dev

    sudo apt-get install git fakeroot build-essential libncurses5-dev xz-utils libssl-dev bc flex libelf-dev bison dwarves libelf-dev
    

    上面的工具编译 4.x 的内核足够了,但是 5.13 之后的还需要一些别的工具

    apt-get install zstd # 5.13以后支持zstd压缩
    
  5. 生成配置文件 .config

    • 方法一:完全手工指定内核的配置选项不现实,一般都是找一个 .config 文件然后以这个文件为基础进行修改。在正式编译内核之前,我们首先必须配置需要包含哪些模块。实际上,有一些非常简单的方式来配置。使用一个命令,可以拷贝当前内核的配置文件,然后使用可靠的 menuconfig 命令来做任何必要的更改。使用如下命令来完成:
      cp /boot/config-$(uname -r)  ./.config
      make menuconfig
      
    • 方法二:使用 localmodconfig 对象获取一个精简的编译配置。为了能够应对各种各样的环境,发布版的内核包含很多内核模块。但是在某个特定机器,例如,大家自己平时使用的PC上实际用到的模块只是其中的极小一部分。重新构建内核时,对不使用的模块进行编译就会浪费时间。将 localmodconfig 作为 make 的目标, kbuild 系统会获取一个当前在用的模块的列表,生成仅以正在使用的内核模块为对象的 .config 文件,从而大幅度减少编译时间。
      # 在源码文件根目录下:
      lsmod > /tmp/lsmod.now
      make LSMOD=/tmp/lsmod.now localmodconfig  
      # localmodconfig 也是使用 /boot/config-$(uname -r) ./.config 作为基准的。输入命令后,两个kbuild会询问对于新内核增加的那些配置的选择
      

    配置 .config:

    make menuconfig   #添加或修改配置  配置完成后选择 Save 保存
    # make xconfig        如果用的是KDE
    # make gconfig        如果用的是GNOME
    
  6. 编译
    提前需要了解的:

    make clean     # 删除大多数的编译生成文件, 但是会保留内核的配置文件.config, 还有足够的编译支持来建立扩展模块
    make mrproper  # 删除所有的编译生成文件, 还有内核配置文件, 再加上各种备份文件
    make distclean # mrproper删除的文件, 加上编辑备份文件和一些补丁文件
    # 对于一个刚刚从 kernel.org 上下载的内核源码包, 可以不用执行 make clean/make mrproper/make distclean, 因为源码包的状态本身就是 clean 的
    # 不需要每次都清理,make会自己判断文件是否为新的
    

    编译内核:

    make -j 4 bzlmage 2>error.log  # 使用4线程编译   数字最好为核心数*2 错误输出到 error.log 文件
    # 编译完成后在 /usr/src/linux-5.19.10/arch/x86/boot 目录下会生成名为 bzlmage 的文件
    # 如果直接输入 make 命令,那么默认执行的是编译内核和模块 : make bzImage 和 make modules
    

内核编译完成
编译完成后显示:Kernel: arch/x86/boot/bzImage is ready (#4)括号里的 #4意思是这个内核的第4次构建。
可能需要提前编辑 .config 配置文件(编译内核时如果报如下错的话):
编译内核报错
将:

CONFIG_SYSTEM_TRUSTED_KEYS="debian/canonical-certs.pem"
CONFIG_SYSTEM_REVOCATION_KEYS="debian/canonical-revoked-certs.pem"

修改为:

CONFIG_SYSTEM_TRUSTED_KEYS=""
CONFIG_SYSTEM_REVOCATION_KEYS=""

可以利用以下命令定位这些项在配置文件中的位置:

cat -n .config|grep CONFIG_SYSTEM_TRUSTED_KEYS # cat 的 -n 选项用于显示行号
# 使用vim编辑的技巧
:set number  #可以显示行号
:number      #可以直接定位到number行

或者直接在vim中使用查找命令:

/CONFIG_SYSTEM_TRUSTED_KEYS    # 正向查找命令 /string   再使用 n 进行前向查找 N 进行反向查找
?CONFIG_SYSTEM_TRUSTED_KEYS    # 反向查找命令 ?string

编译模块:

make -j 4 modules  2>error.log
  1. 安装
    安装模块:
    make -j 4 modules_install
    # 执行后,系统会在 /lib/modules 目录下生成一个与内核名称相同的子目录 (/lib/modules/5.19.10),里面存放着新内核的所有可加载模块(即将编译好的 modules 拷贝到 /lib/modules 下的子目录 5.19.10,注意,此目录是系统目录,不是内核源码目录)
    

安装模块
安装内核:

make install
# 执行后,将复制内核目录中的 .config、vmlinuz、initrd.img、System.map 文件到系统 /boot 目录(不是内核源码目录)
  1. 配置 grub 引导程序:
    update-grub2 # 该命令会修改grub
    
  2. 重启系统:
    sudo reboot
    uname -r  #  启动完成后,查看内核版本号
    

验证新内核是否成功引导
报错解决

FAILED:load BTF from vmlinux:No such file or directory(编译Linux内核时报错)
# 修改 CONFIG_DEBUG_INFO_BTF 的值,将 y 改为 n

添加系统调用


  1. 更新系统调用表: 增加调用号335的系统调用函数,取名为:yajie_syscall64

    cd /usr/src/linux-5.19.10/arch/x86/entry/syscalls
    vim syscall_64.tbl  # 32位系统是 syscall_32.tbl,系统调用表是linux内核中用于关联系统调用号以及其他相应服务例程入口地址的一张表,每个表项记录了某个系统调用的服务例程入口地址,以系统调用号为索引可以快速找到其服务例程
    
    系统调用号ABI应用程序二进制接口(common 32 64)系统调用名称服务例程入口
    0commonreadsys_read(这里不同的内核版本可能不同)
    335commonyajie_syscall64sys_yajie_syscall64

    增加系统调用
    2. 添加系统调用函数声明,修改 arch/x86/include/asm/syscalls.h

    /usr/src/linux-5.19.10/arch/x86/include/asm
    vim syscalls.h 
    # 加 asmlinkage 后,C 函数就会在 stack 取参数,而不是从 register 取参数。使用它是为了保持参数在 stack 中,因为从汇编语言到 C 函数代码参数的传递是通过 stack 的。
    

    添加系统调用声明
    3. 在内核源代码中添加函数定义

    /usr/src/linux-5.19.10/kernel
    vim sys.c  # 每个系统调用都对应一个内核服务例程来实现该系统调用的具体功能
    # SYSCALL_DEFINE 后的数字代表参数个数,这里的例子是0个参数 (void)
    

    添加系统调用服务例程定义

    1. 编译内核并测试新添加的系统调用
      编写测试代码:
    #define _GNU_SOURCE
    #include<unistd.h>
    #include<sys/syscall.h>
    int main(){
       syscall(335);
       return 0;
    }
    

    测试:

    gcc test.c -o test
    ./test
    dmesg | tail -n 4
    

    测试新添加的系统调用

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐