JNI代码调试初体验

  |   2 评论   |   223 浏览

背景

在用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中,在 HostPort 中填写服务器的 IPPort

启动 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

这里的nid0xb336,即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

这里找到了thread45878,其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参数会把函数参数给优化掉。所以呢,还是用笨办法吧。

  <properties>
    <javacpp.cppbuild.skip>false</javacpp.cppbuild.skip> <!-- To skip execution of cppbuild.sh: -Djavacpp.cppbuild.skip=true -->
    <javacpp.parser.skip>false</javacpp.parser.skip>     <!-- To skip header file parsing phase: -Djavacpp.parser.skip=true  -->
    <javacpp.compiler.skip>false</javacpp.compiler.skip> <!-- To skip native compilation phase: -Djavacpp.compiler.skip=true -->
  </properties>

可以通过将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())

参考

评论

  • flowaters @泛音青年 回复»

    自己在外面排好版,然后复制进去的,通过 ``` 标签来保持格式。

  • 泛音青年 回复»

    hello,请问笔者你的solo博客编辑器支持markdown吗?代码片段怎么添加的?我把代码片段放进

    的标签,没有缩进

发表评论

validate