版本比较

密钥

  • 该行被添加。
  • 该行被删除。
  • 格式已经改变。
目录
minLevel1
maxLevel4
outlinefalse
stylenone
typelist
printablefalse

背景

在本机远程调试运行在Android环境下的bionic linker64动态库加载过程代码时,发现gdb无法显示STL容器元素内容,只能显示容器结构体数据,另外指针数据只显示指针地址,而非指针所指对象内容。初步分析为bionic linker64使用clang++编译,链接libc++.so库,这些为llvm提供的机制。而gdb为gnu提供的调试工具,其对应gcc编译工具链,链接libstdc++.so。可能是两个编译器对C++实现差异导致。这两种编译器对C++符号修饰一致,因此可兼容执行,但gdb无法解析clang编译的TLS容器元素。采用lldb进行调试,则可显示STL容器元素内容。

vscode调试

针对C/C++调试配置

环境准备

  • 本机:安装vscode

  • 插件:CodeLLDB,C/C++ Extension Pack,ADB Interface for VSCode,Remote-SSH等

  • 调试环境:安装lldb,adb程序(可以是本机,也可以通过remote-SSH插件链接至远程主机)

  • Android环境:安装或从NDK拷贝lldb-server程序,gdbserver64

本地调试配置

用vscode打开被调试程序的源码目录,然后做以下配置:

  1. 创建配置文件:在“Run and Debug”边栏中,点击“create a launch.json file",在标题栏弹出对话框中选择”C++“,即可生成一个launch.json文件

  2. 增加配置模板:在右下方点击”Add Configuration...“,在弹出对话框中选择"{} CodeLLDB: Launch"(没看到就往下拉对话框滚动条),生成lldb调试配置模板。

精简配置

代码块
{
  "type": "lldb", // 重中之重,此类型为"lldb",而非vscode官网上说的"cppgdb"配上"MIMode": "lldb",如果这样做,那就歇菜了
  "request": "launch",
  "name": "Launch", // 可以任性点,改一个自己觉得很酷的名字,做为调试标题,调试时好识别,方便选择调试目标
  "program": "${workspaceFolder}/<program>",  // 被调试的程序路径
  "args": [], // 被调试的程序参数,这个可以没有
  "cwd": "${workspaceFolder}" // 被调试时的工作目录,所有相对路径文件都从该目录开始查找
}

这样本地调试配置就这样简单完成了,在源码中打断点,然后点击”Start Debugging“,正式进入调试阶段。

当然,这些涉及程序与源代码关联的问题。要是在本地工程内调试,不存在该问题。若是拷贝过来的程序,可能存在与源代码文件关联问题。

远程调试配置

通过adb连接至Android环境,需要对Android进行root,不能root,那就麻烦了。然后运行lldb-server,步骤如下:

启动lldb-server

代码块
adb connect 66.66.66.66:5555
adb root
adb forward tcp:5188 tcp:5188  # lldb-server监听端口
adb forward tcp:5189 tcp:5189   # gdbserver监听端口,lldb-server是皮包,真正干活的是gdbserver
adb shell lldb-server platform --listen "*:5188" --server --server --gdbserver-port 5189   # 运行lldb-server

调试配置文件配置,具体选项需参照CodeLLDB手册疑难解答或者LLDB手册

远程调试本地配置

代码块
{
  "name": "hello",
  "type": "lldb",
  "request": "launch",
  "program": "yourprogram",
  "args": [],
  "initCommands": [
    // 选择并连接远程调试平台,在Android环境下调试
    "platform select remote-android", // For example: 'remote-linux', 'remote-macosx', 'remote-android', etc.
    "platform connect connect://localhost:5188",
    "platform settings -w /tmp", // 上传文件至远程的工作目录,文件默认都上传至该目录。
  ],
  "env": {
    "LD_LIBRARY_PATH": "/tmp", // 传给lldb-server的环境变量,动态库搜索路径,可以不配
  },
  "cwd": "/tmp", // 远程调试环境的工作目录,如果默认路径就行,此配置多此一举,就删掉它
  "breakpointMode": "path", // 以文件全路径的方式打断点,而非只是文件名
  "relativePathBase": "${workspaceFolder}/",  // 本地源文件所在的相对目录,千万别配错了,实在不清楚就删掉
}

配置完成好,马马虎虎就能远程调试了。对于没有root的Android环境,上传程序文件至/tmp或者其它可执行权限的目录,上传失败;上传至数据目录,无执行权限,执行失败。

疑难解答

断点无效

一般来说,断点无效说明程序符号与源代码没有关联上。怎样关联呢,那得看程序中符号指定的源码路径与调试环境下的源码路径是否匹配。

您可在vscode下方的“DEBUG CONSOLE”中输入以下内容查看程序符号中源码路径。

代码块
debug_info list # 列出程序加载的所有动态库
debug_info show <module name> # 将module name加上方括号改成你的程序名或库名,其列出该库中所有的编译单元源文件文件路径
 
# 要是程序停不下来,你就点击"Pause"键,程序停下来再输入上述操作。
# 要是程序短小精悍,来不及点就结束了,就使用绝招,在配置中插入括号中的字符串("stopAtEntry": false, ),表示程序已启动就暂停。

既然指导文件路径了,就有两种情况:相对路径和绝对路径。如果是相对路径,还不在一个路径下,鱼和熊掌不可兼得,只能按偏好选一个;如果是绝对路径,多多益善。

代码块
"relativePathBase": "${workspaceFolder}/",  // 想看源码的相对路径
"sourceFileMap": {
  "/IDontKnow/IDontKnow/": "/IKnow/",      // key为程序符号对应的源文件绝对路径前缀,value为调试主机上源文件路径前缀。
  // 可以在此处再加映射。只需要把不同的前缀路径映射一下,后续相同的就不多此一举了
},

一般都程序符号中默认为绝对路径,怎么会有相对路径呢,那是因为编译linker64时,编译工具链配置了编译选项”-fdebug-prefix-map=/proc/self/cwd=“

绝对路径找不到源文件,说明你程序的编译环境与你的调试环境不在一个主机上,所以麻烦一些。

缺少依赖动态库

程序依赖的动态库在远程调试环境中没有,需要自己上传,当然做个配置就好,不需要每次手共上传

代码块
languagejava
// 2. LLDB commands executed to create debug target.
"targetCreateCommands": [
  "target create --no-dependents yourprogram",  // 少不了,代替了"program"选项的程序,也改动了插件的默认操作。
  "target modules add yourmissinglibrary.so",   // 你缺少的动态库,缺少几个,就加几条,程序调试前将动态库上传至远程调试工作目录(/tmp)
],
// 或者更复杂点,向上传至指定的目录,没这个要求,下面内容就不要配置了。过程是先上传至工作目录,再拷贝至指定目录
"preRunCommands": [
  "script lldb.target.module['missinglibraryname'].SetPlatformFileSpec(lldb.SBFileSpec('/otherdir/maybeothername.so'))",
],

友情提示:上传的动态库均无可执行权限

远程调试原理

待续。。。

夹点私货-融合调试环境

调试需要debug版本,bionic的编译环境使用-O2优化,需要禁用该优化。不知道命令参数如何配置,因此使用了一个土办法,直接该linker的编译文件。

linker编译配置修改

代码块
diff --git a/linker/Android.bp b/linker/Android.bp
index 2a071ac6e..bc80fff6a 100644
--- a/linker/Android.bp
+++ b/linker/Android.bp
@@ -33,6 +33,8 @@ cc_object {
         "-Wextra",
         "-Wno-unused",
         "-Werror",
+        "-O0",
+        "-g",
             ],
 
     srcs: [
@@ -78,6 +80,8 @@ cc_defaults {
         "-Wextra",
         "-Wunused",
         "-Werror",
+        "-O0",
+        "-g",
     ],

调试文件准备 

以下是原生Android程序的调试及准备流程,linker64编译等。将bionic库替换成gun库的说明见下下。

环境准备说明

代码块
#!/bin/sh
 
# 测试环境说明:
# 使用一个demo Android程序,依赖3个库,用该程序测试融合链接器
# 将demo的解释器指定融合链接器,在demo运行时加载该融合链接器,从而进行调试
 
DEMO_DIR=${HOME}/test-elf/build/debug
ANDROID_DIR=/IDontKnow/
LINKER_DIR=${ANDROID_DIR}/out/soong/.intermediates/bionic/linker/linker/android_arm64_armv8-a/unstripped/
 
# 一、连接Android环境,并重定向5188端口用于远程调试
# adb connect 10.31.91.92
# adb forward tcp:5188 tcp:5188
 
# 二、更新demo运行依赖路径
# 设置动态库路径,设置linker64为融合版本。与程序上传路径保持一致,免得直接替换系统的linker64,搞不好系统崩了
patchelf --set-rpath /tmp/ ${DEMO_DIR}/demo
patchelf --set-interpreter /tmp/linker64 ${DEMO_DIR}/demo
 
# 三、上传demo程序
# 直接adb push至/tmp报错:remote fchown failed: Operation not permitted
adb push ${DEMO_DIR}/*.so /sdcard/Download/
adb push ${DEMO_DIR}/demo /sdcard/Download/
 
adb shell install -m555 /sdcard/Download/*.so /tmp/
adb shell install -m555 /sdcard/Download/demo /tmp/
 
# 四、准备Android编译环境,每次登录都需做一次
# cd ${ANDROID_DIR}
# source build/envsetup.sh
# lunch 45
 
# 五、编译指定模块linker,可在登录环境下重复运行,第一次运行慢
# 不是一个命令,需要在当前shell下运行,不能在脚本环境下运行
# mmm bionic/linker
 
# 六、上传linker64程序
# adb push ${LINKER_DIR}/linker64 /sdcard/Download/
# adb shell install -m555 /sdcard/Download/linker64 /tmp/
 
# 插个队,linker64在NDK中,记得改改路径再用,运行一次就好
# adb push ${HOME}/Android/Sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/14.0.7/lib/linux/aarch64/lldb-server /sdcard/Download
# adb shell install -m555 /sdcard/Download/lldb-server /tmp/
 
# 七、运行demo程序,启动远程调试程序,每一次调试前都需要执行该命令
# adb shell /tmp/lldb-server platform --listen "*:5188"  --server --gdbserver-port 5189
 
# 八、远程链接调试程序
# lldb
# (lldb) platform select remote-android
# (lldb) platform connect connect://localhost:5188
# (lldb) platform settings -w /tmp
# (lldb) file ${HOME}/test-elf/build/debug/demo
# (lldb) run
 
# 注:每次登录需执行1-5步,每次更新linker代码时执行5-6步

准备gnu库

包括gnu的基础库,如:libc,libstdc++等

准备gnu库环境

代码块
#!/bin/bash
# 测试环境说明:
# 使用一个demo Android程序,依赖几个bionic库,用该程序测试融合链接器
# 将demo的解释器指定融合链接器,在demo运行时加载该融合链接器,从而进行调试
# 最好将gnu库集中在一个目录,方便写脚本
LINUX_LIB_DIR="${HOME}/linux-aarch64"
 
# ld-linux库少不了,但是一个静态库,需要特殊处理
adb push ${LINUX_LIB_DIR}/ld-linux-aarch64.so.1 /sdcard/Download/
adb shell install -m555 /sdcard/Download/ld-linux-aarch64.so.1 /tmp/
 
# 需要拷贝的gnu库列表,觉得不够,可以再加,数量不限
LIBSOS="libc.so.6 libglibc-adapter.so libtest-glibc-adapter.so libstdc++.so.6 libgcc_s.so.1 libm.so.6 libc++.so.1 libc++abi.so.1 libpthread.so.0 librt.so.1"
for libso in ${LIBSOS} ; do
    patchelf --set-rpath /tmp/ ${LINUX_LIB_DIR}/${libso}
    adb push ${LINUX_LIB_DIR}/${libso} /sdcard/Download/
    adb shell install -m555 /sdcard/Download/${libso} /tmp/
done

调试linker

linker调试与其它的有些差异,一是断点不生效,而是上传的linker无可执行权限。

断点无效解决方法

linker在程序运行前运行,不知怎么回事,lldb无法捕获其断点,现在用一个土办法来规避问题。若谁有好主意,请不吝赐教。

基本上是两步,第一步,在运行时暂停;第二步,重新使能断点。

代码块
 "stopOnEntry": true,  // 在运行时暂停,停在linker的_start汇编函数处

在BREAKPOINTS边栏的右上角点击两下红色标记的图标。注意该图标可能隐藏了,鼠标指向该位置时会显示。

...

linker无执行权限解决方法

使用”target modules add“上传的库无可执行权限,因此需要用其它方式,在调试开始前将linker文件上传。

规避方式有两步,第一步,在tasks.json文件中上传和修改权限操作;第二步,在launch.json文件中增加"preLaunchTask"选项("preLaunchTask": "install linker64",)。

tasks和launch json配置

代码块
languagejson
// tasks.json 内容
 
    {
      "label": "push linker64",
      "type": "shell",
      "command": "adb",
      "args": [
        "push",
        "${workspaceFolder}/../out/soong/.intermediates/bionic/linker/linker/android_arm64_armv8-a/unstripped/linker64",
        "/sdcard/Download/"
      ],
    },
    {
      "label": "install linker64",
      "type": "shell",
      "command": "adb",
      "args": [
        "shell",
        "install",
        "-m555",
        "/sdcard/Download/linker64",
        "/tmp"
      ],
      "dependsOn": [
        "push linker64"
      ]
    },
 
// launch.json文件
  "preLaunchTask": "install linker64",