作为一个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
系统环境
源码版本
boot jdk
OpenJdk里除了c、c++的代码以外还有很多java的代码,需要用一个能使用的JDK来编译(这个JDK就叫做boot JDK)。
boot jdk应该使用待编译版本的上一个大版本,但是用和待编译版本相同的版本似乎也可以(理论上也说得通)。我用的就是机器上安装的版本,没出现啥问题。需要注意的是如果没指定boot jdk,默认使用系统jdk。所以如果系统jdk版本不合适的话,需要在运行configure时指定:--with-boot-jdk=你的boot jdk位置。
我在构建过程中遇到的问题(建议先把以下问题解决再编译)
- 缺少依赖
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
- 操作系统检测失败
报错信息:
*** 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%
- 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
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++
- 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:
6. 忽略警告信息
错误信息:
cc1plus: all warnings being treated as errors
解决方法:
vi hotspot/make/linux/makefiles/gcc.make
注释掉如下这行
编译
- 切换到jdk源码目录
cd /path/to/jdk-jdk8-b120
- 配置
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去构建的话可以省略
- 执行make命令
bear make all
调试
- 导入工程
2. 生成Run Configurations and Build Targets
3. Custom Run/Debug configurations
选择刚才创建的make 和 make clean
4. 打断点调试
找到
arguments.cpp 如下位置打个断点,点击debug。
从图中还可以看出来,java.c是java的入口
编译调试linux kernel
下载源码
安装依赖
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脚本只挂载了虚拟文件系统procfs和sysfs,没有挂载磁盘根文件系统,所有调试操作都在内存中进行,不会落磁盘。
$ 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"
至此,就可以安心调试内核了。