编译调试jdk和linux内核

1,094 阅读4分钟

作为一个javaer,调试c/c++项目一直是未曾涉足的领域。这几天踩了些坑,调试了一下linux kernel和openjdk。作下记录(基本都是网上文章拼凑的),留以备查。

背景

gdb

ptrace 介绍

GDB 中的魔法般的操作底层都是通过 ptrace 调用来实现的, 在介绍 GDB 的具体实现细节前, 我们先来好好了解下 ptrace 调用.

从名字就可以看出 ptrace 系统调用是用于进程跟踪的, 当进程调用了 ptrace 跟踪某个进程之后:

  • 调用 ptrace 的进程会变成被跟踪进程的父进程;
  • 被跟踪进程的进程状态被标记为 TASK_TRACED;
  • 发送给被跟踪子进程的信号 (SIGKILL 除外) 会被转发给父进程, 而子进程会被阻塞;
  • 父进程收到信号后, 可以对子进程进行检查和修改, 然后让子进程继续执行;

在 man ptrace 中可以找到 ptrace 的定义原型:

#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

其中 request 参数指定了我们要使用 ptrace 的什么功能, 常用的有两种:PTRACE_ATTACH 或 PTRACE_TRACEME :

  • PTRACE_TRACEME 是被跟踪子进程调用的, 表示让父进程来跟踪自己, 通常是通过 GDB 启动新进程的时候使用;
  • PTRACE_ATTACH 是父进程调用 attach 到已经运行的子进程中; 这个命令会有权限的检查, non-root 的进程不能 attach 到 root 进程中;

参数 pid 表示的是要跟踪进程的 pid, addr 表示要监控的被跟踪子进程的地址.

gdb 断点的实现原理

当我们用 GDB 设置断点时, GDB 会把断点处的指令修改成 int 3, 同时把断点信息及修改前的指令保存起来. 当被调试子进程运行到断点处时, 便会执行 int 3命令, 从而产生 SIGTRAP 信号. 由于 GDB 已经用 ptrace 和调试进程建立了跟踪关系, 此时的 SIGTRAP 信号会被发送给 GDB, GDB 通过和已有的断点信息做对比 (通过指令位置) 来判断这次 SIGTRAP 是不是一个断点.

如果是断点的话, 就回等待用户的输入以做进一步的处理. 如果用户的命令是继续执行的话, GDB 就会先恢复断点处的指令, 然后执行对应的代码.

GDB支持将debug信息编译进执行文件里(本文中linux 内核的做法),也支持分开存放(本文中openjdk的做法)。可参见 官方手册

简而言之,gdb通过动态修改程序代码和数据来改变程序的运行轨迹。java的jdwp可能也是这样,但是java只需要改变字节码即可。这么看其实debug的原理与动态代理差不多。而最新的GraalVM已经支持将java代码直接编译成平台相关的可执行文件,并且支持GDB调试

clion导入makefile项目

clion默认支持的构建工具是cmake,对于Makefile的支持并不完善,所以建议使用compilation database去import project。在linux上,需要装个bear(apt install bear),然后使用bear make $target,bear会在当前目录生成compile_commands.json文件,在clion中打开这个文件即可导入 project。

clion对Makefile项目支持已经相当完善,尤其是一些知名项目,本文的两个Makefile项目直接导入即可。

操作

编译调试 openjdk

系统环境

Screenshot from 2021-08-08 22-21-34.png

Screenshot from 2021-08-08 22-26-18.png

源码版本

github.com/openjdk/jdk…

boot jdk

OpenJdk里除了c、c++的代码以外还有很多java的代码,需要用一个能使用的JDK来编译(这个JDK就叫做boot JDK)。 boot jdk应该使用待编译版本的上一个大版本,但是用和待编译版本相同的版本似乎也可以(理论上也说得通)。我用的就是机器上安装的版本,没出现啥问题。需要注意的是如果没指定boot jdk,默认使用系统jdk。所以如果系统jdk版本不合适的话,需要在运行configure时指定:--with-boot-jdk=你的boot jdk位置。

我在构建过程中遇到的问题(建议先把以下问题解决再编译)

  1. 缺少依赖

sudo apt-get update && apt-get install -y curl ssh zip unzip vim ant git mercurial build-essential ccache cpio g++ gcc gdb libx11-dev libxext-dev libxrender-dev libxtst-dev libxt-dev libcups2-dev libfreetype6-dev libasound2-dev libelf-dev

  1. 操作系统检测失败

报错信息:

*** This OS is not supported: Linux Lincoln 4.15.0-130-generic #134-Ubuntu SMP Tue Jan 5 20:46:26 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

make[5]: *** [check_os_version] Error 1

/home/parallels/workspace/openjdk/jdk-jdk8-b120/hotspot/make/linux/Makefile:234: recipe for target 'check_os_version' failed

解决方法:

vi hotspot/make/linux/Makefile

修改:

SUPPORTED_OS_VERSION = 2.4% 2.5% 2.6% 3%

SUPPORTED_OS_VERSION = 2.4% 2.5% 2.6% 3% 4% 5%
  1. make语法不兼容

报错信息:

/usr/bin/make: invalid option -- '8'

/usr/bin/make: invalid option -- '-'

/usr/bin/make: invalid option -- '1'

/usr/bin/make: invalid option -- '2'

/usr/bin/make: invalid option -- '0'

/usr/bin/make: invalid option -- '/'

/usr/bin/make: invalid option -- 'a'

/usr/bin/make: invalid option -- '/'

/usr/bin/make: invalid option -- 'c'

解决方法:

vi hotspot/make/linux/makefiles/adjust-mflags.sh

添加一个大写I

c3ea793ff5ac5f10543f8220d8b5a6a1.png 4. gcc版本太高

错误信息:

generation.hpp:421:17: error: invalid suffix on literal; C++11 requires a space between literal and string macro [-Werror=literal-suffix]

....省略

left operand of shift expression ‘(-1 << 1)’ is negative [-fpermissive]

解决方法:

根据官方文档()hg.openjdk.java.net/jdk8/jdk8/r… 4.3。

安装gcc和g++ 4.9版本

修改apt源:

sudo vi /etc/apt/sources.list

添加以下内容到文件末尾:

#install gcc 4.9

deb http://dk.archive.ubuntu.com/ubuntu/ xenial main

deb http://dk.archive.ubuntu.com/ubuntu/ xenial universe

更新源:

sudo apt-get update

安装4.9版本:

sudo apt install gcc-4.9

sudo apt install g++-4.9

管理多版本gcc:

sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-7 50
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-7 50
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.9 20
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.9 20

切换到4.9版本:

sudo update-alternatives --config gcc
sudo update-alternatives --config g++
  1. nashorn构建错误

错误信息:

Exception in thread "main" java.lang.VerifyError:
  class jdk.nashorn.internal.objects.ScriptFunctionImpl overrides final method setPrototype.(Ljava/lang/Object;)V

解决办法:

修改:

vim nashorn/make/BuildNashorn.gmk

80行原来 -cp 修改为:-Xbootclasspath/p:

519520-20180725205337849-1733655828.png 6. 忽略警告信息

错误信息:

cc1plus: all warnings being treated as errors

解决方法:

vi hotspot/make/linux/makefiles/gcc.make

注释掉如下这行

c3dbd34b63717d41269221db205507a5.png

编译

  1. 切换到jdk源码目录
cd /path/to/jdk-jdk8-b120
  1. 配置
sh ./configure --with-target-bits=64 --with-jvm-variants=server --with-debug-level=slowdebug --disable-zip-debug-info --with-boot-jdk=/path/to/bootjdk

参数解释:

--with-target-bits=64: 构建64的JDK

--with-jvm-variants=server :构建server模式的JDK

--with-debug-level=slowdebug: slowdebug带有更多的调试信息

--disable-zip-debug-info:禁止压缩调试信息,可以方便调试

--with-boot-jdk=/path/to/bootjdk :表示boot jdk的路径,如果用系统jdk去构建的话可以省略
  1. 执行make命令
bear make all

调试

  1. 导入工程

1dd02c47fc0a8854be91f8ecb090af24.png

c2e1878416f9192cb25e835cff3e9a45.png 2. 生成Run Configurations and Build Targets

ae6940ffe52f8f7b2d3a2da97dbf5abd.png

cf23cb5d7d1158319a769620016d5017.png

542727c9a7f898f388f4f0f3bc87f204.png

c64edeb2ccbd4d29dda3fa20e6198831.png 3. Custom Run/Debug configurations

a33ddae651a8414c8f4bd628acf0cf38.png

选择刚才创建的make 和 make clean

5617c99a523bcd86251c0474a9e18c07.png

dd2a2b5d97545da99fa6774ac7883c82.png 4. 打断点调试

0e5be217d398349fc3eb69b6a04fc906.png 找到arguments.cpp 如下位置打个断点,点击debug。

从图中还可以看出来,java.c是java的入口

编译调试linux kernel

下载源码

github.com/torvalds/li…

安装依赖

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

配置

如果是linux系统,可以直接copy当前系统配置

cp /boot/config-$(uname -r) .config

然后 make menuconfig开启debug参数

Kernel hacking  ---> 
    [*] Kernel debugging
    Compile-time checks and compiler options  --->
        [*] Compile the kernel with debug info
        [*]   Provide GDB scripts for kernel debugging

编译可能会碰到错误

make[1]: *** No rule to make target 'debian/canonical-certs.pem', needed by 'certs/x509_certificate_list'.  Stop.

解决方法:

vi .config

修改CONFIG_SYSTEM_TRUSTED_KEYS,将其置空。

CONFIG_SYSTEM_TRUSTED_KEYS=""

执行编译、安装(需要一些时间):

$ make -j 12

构建initramfs根文件系统 Linux系统启动阶段,boot loader加载完内核文件vmlinuz后,内核紧接着需要挂载磁盘根文件系统,但如果此时内核没有相应驱动,无法识别磁盘,就需要先加载驱动,而驱动又位于/lib/modules,得挂载根文件系统才能读取,这就陷入了一个两难境地,系统无法顺利启动。于是有了initramfs根文件系统,其中包含必要的设备驱动和工具,boot loader加载initramfs到内存中,内核会将其挂载到根目录/,然后运行/init脚本,挂载真正的磁盘根文件系统。

这里借助BusyBox 构建极简initramfs,提供基本的用户态可执行程序。

编译BusyBox,配置CONFIG_STATIC参数,编译静态版BusyBox,编译好的可执行文件busybox不依赖动态链接库,可以独立运行,方便构建initramfs。

$ cd busybox-1.28.0
$ make menuconfig

选择配置项:

Settings  --->
    [*] Build static binary (no shared libs)

执行编译、安装:

$ make 
$ make install

会安装在_install目录:

$ ls _install 
bin  linuxrc  sbin  usr

创建initramfs,其中包含BusyBox可执行程序、必要的设备文件、启动脚本init。这里没有内核模块,如果需要调试内核模块,可将需要的内核模块包含进来。init脚本只挂载了虚拟文件系统procfssysfs,没有挂载磁盘根文件系统,所有调试操作都在内存中进行,不会落磁盘。

$ mkdir initramfs
$ cd initramfs
$ cp ../_install/* -rf ./
$ mkdir dev proc sys
$ sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/
$ rm linuxrc
$ vim init
$ chmod a+x init
$ ls
$ bin   dev  init  proc  sbin  sys   usr

init文件内容:

#!/bin/busybox sh         
mount -t proc none /proc  
mount -t sysfs none /sys  

exec /sbin/init

打包initramfs:

$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz

调试

$ cd busybox-1.28.0
$ qemu-system-x86_64 -s -kernel ./linux-4.4.203/arch/i386/boot/bzImage -initrd ./initramfs.cpio.gz -nographic -append "console=ttyS0 nokaslr "
  • s是-gdb tcp::1234缩写,监听1234端口,在GDB中可以通过target remote localhost:1234连接;
  • kernel指定编译好的调试版内核;
  • initrd指定制作的initramfs;
  • nographic取消图形输出窗口,使QEMU成简单的命令行程序;
  • append "console=ttyS0"将输出重定向到console,将会显示在标准输出stdio。nokaslr 禁用 KASLR

启动后的根目录, 就是initramfs中包含的内容:

/ # ls                    
bin   dev  init  proc  root  sbin  sys   usr

启动GDB:

$ cd linux-4.14
$ /usr/local/bin/gdb vmlinux
(gdb) target remote localhost:1234

在函数cmdline_proc_show设置断点,虚拟机中运行cat /proc/cmdline命令即会触发。

(gdb) b cmdline_proc_show                           
Breakpoint 1 at 0xffffffff81298d99: file fs/proc/cmdline.c, line 9.                                      
(gdb) c                                             
Continuing.                                         

Breakpoint 1, cmdline_proc_show (m=0xffff880006695000, v=0x1 <irq_stack_union+1>) at fs/proc/cmdline.c:9
9               seq_printf(m, "%s\n", saved_command_line);                                              
(gdb) bt
#0  cmdline_proc_show (m=0xffff880006695000, v=0x1 <irq_stack_union+1>) at fs/proc/cmdline.c:9
#1  0xffffffff81247439 in seq_read (file=0xffff880006058b00, buf=<optimized out>, size=<optimized out>, ppos=<optimized out>) at fs/seq_file.c:234
......(此处省略)
(gdb) p saved_command_line
$2 = 0xffff880007e68980 "console=ttyS0"

至此,就可以安心调试内核了。

reference

GDB 实现原理介绍

OpenJDK8编译调试

使用QEMU和GDB调试Linux内核