JNI代码调试初体验

  |   0 评论   |   0 浏览

背景

在用Java调用C++的代码时,通过JNI方式时,遇到了未预期的情况,需要调试。

于是问题来了?Java的代码可以通过设置断点,在IDE中来调试。

但是C++的native方式怎么样调试呢?

JNI代码调试初体验

我这里的java代码是运行在服务器上的,但是IDE是在本机电脑上。

于是呢?

第一步,是通过jdwp的方式,在本机IDE上远程调度服务器上的Java代码。

第二步,是通过gdb,在服务器上进入Java进程。

具体如下:

JDWP远程调试Java代码

Java代码启动时,增加参数

-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y

这样程序启动后,会等待IDE远程连接上后,再继续运行。

在eclipse中新建调度项目

Debug Configurations -> Remote Java Application -> New 来创建一个新的项目

在 Connection Properties中,在 Host 和 Port 中填写服务器的 IP 和 Port

启动 java 服务

先在 eclipse 中设置断点,然后启动程序。程序运行至断点处时,会中断。

gdb调试JNI代码

找到程序pid

jps

attach到pid

gdb -p ${pid}

attach到指定的进程

先通过jstack找到指定的nid,比如:

184 "main" #1 prio=5 os_prio=0 tid=0x0000ffff9404e000 nid=0xb336 at breakpoint[0x0000ffff9ac1e000]
185 java.lang.Thread.State: RUNNABLE

这里的nid为0xb336,即45878

print 0xb336
45878

然后在gdb中找到对应的thread

(gdb) info threads
Id Target Id Frame

  • 74 Thread 0xffff9ac1f1e0 (LWP 45878) "java" 0x0000ffff9be5c008 in pthread_cond_wait@@GLIBC_2.17 () from /lib64/libpthread.so.0

这里找到了thread为45878,其Id为74,再切换到对应的thread线程

(gdb) thread 74
[Switching to thread 74 (Thread 0xffff9ac1f1e0 (LWP 45878))]
#0 0x0000ffff9be5c008 in pthread_cond_wait@@GLIBC_2.17 () from /lib64/libpthread.so.0

(gdb) bt
#0 0x0000ffff9be5c008 in pthread_cond_wait@@GLIBC_2.17 () from /lib64/libpthread.so.0
#1 0x0000ffff9b3db8f4 in os::PlatformEvent::park() () from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.201.b09-2.el7_6.aarch64/jre/lib/aarch64/server/libjvm.so

ok,虽然到了这里了。但是是看不到变量的。所以我们还得尝试其它的办法。

再次体验

JNI库中加入调试代码

从头开发一个自己的JavaCPP presets,具体为:

修改pom.xml,如下文所示,增加compilerOption配置。

  <plugin>
    <groupId>org.bytedeco</groupId>
    <artifactId>javacpp</artifactId>
  <configuration>
        <compilerOptions>
            <compilerOption>-O0</compilerOption>
            <compilerOption>-g3</compilerOption>
        </compilerOptions>
     </configuration>
  </plugin>

其实这样还是不够的,因为默认的-O3参数会把函数参数给优化掉。所以呢,还是用笨办法吧。

false false false

可以通过将javacpp.compiler.skip来设置成true,来跳过自动编译so包过程。

什么?那怎么手动编译so包?好吧,还是用自动编译so包为模板,来手动改改g++命令的参数吧。

直接用gdb运行java程序

gdb --args java ...

如果代码中没有用到过多的环境变量的话,这样是OK的。

否则的话,还是直接先启动一下java服务。再通过gdb attach到对应的pid上吧。

什么?还没有attach,服务已经报错退出了?

聪明的你自然可以猜到,可以在java服务中加一些sleep操作嘛。

通过阅读上面的g++中编译的cpp文件,找几个需要打断点的地方,然后在gdb中设置断点,比如:

break Java_org_bytedeco_javacpp_als_00024WordUnit_text

break Java_org_bytedeco_javacpp_als_00024Result_alternates__

break Java_org_bytedeco_javacpp_als_00024Phrase_words__

gdb常见命令

  • 回车键:重复上一命令
  • help (h):帮助
  • run (r):重新开始运行
  • start:运行程序,停在第一执行语句
  • list (l):查看原代码,
  • set: 设置变量的值
  • next (n):单步向下
  • step (s): 单步进入
  • backtrace (bt): 查看调用栈帧和层级
  • frame (f): 切换函数栈帧
  • info (i):查看局部变量的数值
  • finish: 结束当前函数,返回到函数调用点
  • continue (c): 继续运行
  • print (p): 打印
  • quit (q): 退出gdb
  • break +num: 在第num行设置断点
  • info breakpoints: 查看设置的所有断点
  • delete (d) breakpoints num: 删除第num个断点
  • display: 追踪查看具体变量值
  • undisplay:取消追踪观察变量
  • watch:被设置观察点的变量发生修改时,打印显示
  • i watch:显示观察点
  • enable breakpoints:启用断点
  • disable breakpoints:禁用断点
  • x:查看内存x/20xw 显示20个单元,16进制,4字节每单元
  • run argv[1] argv[2]:调试时命令行传参
  • set follow-fork-mode child#Makefile项目管理:选择跟踪父子进程(fork())

参考