环境
- Arch Linux操作系统
- Xiaomi HyperOS 1.0.6.0
- NDK r26
- CLion Nave
- lldb 17.0.6
- lldb-server for android 17.0.3
- adb 1.0.41
实现原理
在Android中启动lldb-server可执行文件,lldb-server将创建一个套接字,由主机操作系统运行lldb客户端,远程连接到Android中的lldb-server,以执行远程调试。
CLion将使用gdbserver模式启动lldb,所以lldb-server也要使用gdbserver模式,否则将无法连接远程服务器(在此上下文中,服务器永远指代运行lldb-server的Android设备)。
首先介绍一下lldb客户端和lldb-server之间可选的通讯方式。当确认连接可以正常建立之后,在配置CLion不迟。
使用unix-abstract建立连接
首先是,如果使用unix-abstract来创建Unix Domain Socket,那么sock文件实际上不会放在文件系统中,这是一个抽象Unix域。当你在文件系统上找不到sock文件时,你也许会怀疑lldb-server启动失败了,但是不知道什么原因没有报错。实际上lldb-server启动成功了,可以使用如下命令检查:
# 以抽象Unix域启动lldb-server
# sock文件被放置在/data/local/tmp/debug.sock
# 但是这不是一个位于文件系统上的路径,它是抽象Unix域,在文件系统上看不到它
./lldb-server p --server --listen unix-abstract:///data/local/tmp/debug.sock
# 在新的shell中检查
ss -xl | grep debug.sock
如果第二条命令看到了输出,则lldb-server已经成功启动。
然后你可以在主机操作系统上执行如下命令,连接到远程服务器:
lldb #这行命令执行后就会取得lldb客户端的shell,后续命令都由lldb来执行
(lldb) platform select remote-android # 设置目标为远程android操作系统
(lldb) platform connect unix-abstract-connect:///data/local/tmp/debug.sock # 建立连接
如果一切正常,那么连接已经成功建立。然后你可以使用lldb的调试命令来调试可执行文件,但是这超出了这篇博客的范围。实际上我们准备使用CLion来调试,而不是直接使用lldb,所以不需要掌握lldb的调试命令,只要能建立连接就好。
正常情况下这里可以成功建立连接,但是我在这里失败了,因为我要调试的目标必须以root权限运行,所以我的lldb-server是在Android上以root身份运行的,而adbd只具备shell权限,连接将被系统拒绝。
如果能有办法使得adbd使用root权限启动,那么此处不成问题。但是adb root是没有用的,因为adb被设计成只能在debug的系统中以root身份运行。
你当然可以想办法绕过,但是没有必要这么做。因为我们的目标仅仅只是让lldb和lldb-server建立连接,并不局限于unix-abstract,不过如果你没有必须使用root的特殊需求,这是一个极好的方法。
Unix Domain Socket
实际上和刚刚的unix-abstract一样,只不过unix-abstract是抽象Unix域,而Unix Domain Socket的sock文件真真切切的放在文件系统上而已。
建立连接的过程和上面类似,不过使用此方法建立连接对我来说依然是失败的,可能依然是权限问题,不过我没有继续探索了。
TCP forward
虽然无法使用Unix Domain Socket,不过没关系,在计算机和Android设备直接传递数据,使用Unix Domain Socket和TCP forward,数据最后都是通过数据线传递的,效率相差并不高。并且debug过程中,执行慢一点基本无感。
你可以直接使用android设备的内网IP地址来建立连接,但是这样不能编写通用的调试脚本,每一个协作者都必须自己记录自己的内网IP地址,管理相当不便。并且通过网络设备来传递数据,变量太多,受到网络拥堵程度影响。
adb提供了forward端口转发工具,可以将android设备上的某个端口映射到主机上。语法格式如下:
adb forward <主机端口号> <设备端口号>
# 例如将android设备的12345端口转发到主机的54321端口
adb forward tcp:54321 tcp:12345
# 然后,在主机上可以连接到localhost:54321,数据包将被直接转发到android的12345端口。
使用forward来创建一个端口映射。然后使lldb-server监听设备端口,例如
# g表示使用gdbserver模式调试,还记得CLion的要求吗?
# *:12345表示监听本机的12345端口
# ./my_execute是可执行文件的启动命令
./lldb-server g *:12345 ./my_execute
执行后,lldb-server将监听目标端口,并等待客户端连入。
CLion配置
回到CLion,在运行目标中点击“编辑配置”,在弹出界面中,点击左上方加号,选择“远程调试”。
完毕后目标应该如下所示

在右侧有几个配置项需要注意,先看看我的配置。

可以看到有四个内容需要配置,已经使用红色框标注。
首先是调试器,默认是gdb,但是Android NDK在很久之前就已经弃用gdb了,NDK中也没有附带gdb-server。你当然可以针对Android交叉编译,但是没有必要。
下面一行是”process connect url”,也就是要指定lldb如何连接到lldb-server,前文一直在做的就是这件事情。如果你使用的是TCP forward建立的连接,那么此处和我一样即可
你别傻乎乎的真写一模一样,端口号改成你的,忘了端口号就回去在看一遍TCP forward。
然后是符号文件,此处指定CLion从何处得到调试符号。
调试符号是编译器为了方便调试而加在输出的可执行文件之中的信息,包含函数名、文件名、行号、变量名等信息,lldb依赖调试符号来定位代码。不是每一个可执行文件都带有调试符号,要检查是否存在调试符号,执行如下命令:
readelf -S my_execute | grep "debug"
存在输出则说明有调试符号,可以使用。如果没有,阅读下面的章节,我会介绍怎么处理。
将你编译得到的带有调试符号的可执行文件路径,填入”3″处。注意!CLion不会自动同步文件到远端,它没有这个能力,所以你有责任在更改代码后,保证远端和本地的可执行文件版本相同,否则将会引发奇奇怪怪的问题。不在本文讨论范围之内。
sysroot是系统根目录,当远端计算机和本地的操作系统类型不一致时,必须填写。例如使用MacOS远程调试Linux计算机(行为艺术)
Android NDK已经提供了这个目录,在$NDK_PATH/toolchains/llvm/prebuilt/linux-x86_64/sysroot/
到此,配置已经全部完成,在Android上启动lldb-server(还记得怎么启动吗?)然后在CLion中,将目标切换到刚刚新建的远程调试,此时构建按钮将会消失,运行按钮将无法点击。单击调试按钮,CLion将启动lldb,连接到lldb-server。后续部分就和调试本机进程一样了。
没有调试符号?
注意,Release版本的构建目标是没有调试符号的,不过你可以使用-g编译选项来添加(真正的行为艺术!),不过Release版本默认会启用O2优化,可执行文件不一定和源代码一一对应,很可能出现控制流乱飞的神奇景象,不建议这么做。
Debug版本的构建目标默认是带有调试符号的,也不会启用优化。不过如果你添加了-s编译选项(抹除符号表)或者-Ox启用优化,那么就变得和Release构建一样了。
所以,如果你的可执行文件中一直得不到调试符号,请仔细检查你的CMakeLists.txt,确保你使用的是Debug选项,且没有在Debug中添加-s和-Ox编译选项。
如何获得lldb-server?
lldb-server是要运行在Android设备上的,直接从电脑上拷是不行的。有人可能会针对Android交叉编译,但是没有必要,使用Android Studio创建一个带有Native的项目,单击一下调试按钮,然后你就能在/data/data/<PackageName>/lldb/bin 下找到lldb-server可执行文件了,非常方便。
没有root?那么,你无权访问此目录中的lldb-server。但是你的APP可以。你可以编写一段代码,在APP运行后,将lldb-server拷贝到共享存储空间。此处不做赘述。