2019-10-22 09:58:16 shensky711 阅读数 193

1. 简介

使用 Android Studio 查看 Android Framework 代码体验非常好,无论是索引还是界面都让人很满意,但是当你跟踪代码,发现进入 native 逻辑时,就会发现 Android Studio 对 native 代码的支持非常不好,不能索引不支持符号搜索不能跳转等,这些让人非常抓狂。那么如何能在 IDE 愉快地查看 native 代码呢?在 Windows 上,Source Insight 的表现也很好,但苦于只有 Windows 平台支持且界面不好,经过一番折腾,还真是找到了方法,下面我们将一步一步打造丝滑的 native 代码阅读环境。

先看一下效果:

2019-10-11-15-02-40.gif

2. CMake

能让 IDE 正确地建立索引,我们需要让 IDE 能正确地知道源文件、头文件、宏定义等各种数据,庆幸的是,我们发现 AOSP 在编译过程中,可以帮我们生成这些数据,详见:http://androidxref.com/9.0.0_r3/xref/build/soong/docs/clion.md

通过文档我们可知,只需要按照以下步骤完成一次编译,即可自动生成各模块对应的 CMake 文件。至于 Cmake 文件是什么,这里就不做赘述了,大家可以自行了解。

  1. 打开以下两个开关,CMakeLists.txt 就会根据编译环境自动生成
export SOONG_GEN_CMAKEFILES=1
export SOONG_GEN_CMAKEFILES_DEBUG=1
  1. 启动编译
make -j16

或者只编译你需要的模块

make frameworks/native/service/libs/ui

生成的文件存放在 out 目录,比如刚刚编译的 libui 模块对应的路径为:

out/development/ide/clion/frameworks/native/libs/ui/libui-arm64-android/CMakeLists.txt
  1. 合并多个模块

生成了 CMake 后,我们发现,CMake 文件是按模块生成的。这样的话,会导致 IDE 只能单独导入一个模块,而我们平时不可能只看一个模块的代码,如果把多个模块都 include 进来呢?
我们可以在 out/development/ide/clion 路径新建一个 CMakeLists.txt 文件,并添加一下内容:

# 指定 CMake 最低版本
cmake_minimum_required(VERSION 3.6)
# 指定工程名,随意
project(aosp)
# 把你需要的模块通过 add_subdirectory 添加进来,注意子目录必须也包含 CMakeLists.txt 文件
add_subdirectory(frameworks/native)
#add_subdirectory(frameworks/base/core/jni/libandroid_runtime-arm64-android)

这样,我们就把多个模块合并在一起了,用 IDE 去打开这个总的 CMake 文件即可

3. 导入 IDE

只要生成 CMake 文件后,剩下的事情就好办了,现在能识别 CMake 工程的 IDE 非常多,大家可以根据个人喜好选择,如:

  • CLion
  • Eclipse
  • Visual Studio

这里以 CLion 为例讲一下如何导入

  1. 打开 CLion
  2. 选择「New CMake Project from Sources」
  3. 指定包含 CMakeLists.txt 的目录,如我们在上一个步骤中说的 out/development/ide/clion(这个目录的 CMakeLists.txt 包含了多个模块,还记得吗?)
  4. 选择「Open Existing Project」
  5. Enjoy your journey …

当然,CLion 也有一个缺点,收费!!如何能免费使用就看大家各显神通了

4. 遇到的一些问题

  • 生成的 CMakeLists.txt 里指定路径可能会使用绝对路径,如: set(ANDROID_ROOT /Volumes/AndroidSource/M1882_QOF7_base),这里大家要注意,如果把 CMakeLists.txt 拷贝到别的工程使用,记得修正一下路径
  • Mac 用户留意,如果你的 CMakeLists.txt 是从 linux 平台生成拷贝过来的,生成的 CMakeLists.txt 里指定的 c++ 编译器 set(CMAKE_CXX_COMPILER "${ANDROID_ROOT}/prebuilts/clang/host/linux-x86/clang-3977809/bin/clang++") 这里指定的是 linux-x86 的编译器,记得替换成 darwin-x86,如果对应目录下没有 clang++,那就从 AOSP 源码拷一个吧
  • 如果 CMake 中列出的源文件在工程中找不到,会导致 CLion 停止索引,如果出现不一致的时候,移除 CMake 中源文件的声明即可

如果使用遇到其他问题,欢迎联系告知,谢谢

5. 总结

所谓工欲善其事,必先利其器。通过这种方法建立的索引包含了 AOSP 所有模块,最重要是它还会根据编译环境,把相关 FLAGS 和宏都设置好。

2018-09-12 09:48:34 cigogo 阅读数 175

调试环境:

    Ubuntu 16.04,win10,android 7.1

    其中,win10主机通过USB与被测试机连接,Ubuntu16.04上有android 7.1 SDK代码及编译环境,通过本地网络与被测试机连接。

第一部分:

代码示例:

test.cpp:

#include <stdlib.h>
#include <iostream>
#include <stdio.h>
#include <unistd.h>

int main() {
  printf("main start...\n");
  int x=5;
  printf("x=%d\n",x);
  while(true)
 {
    printf("%d \n",x);
    x++;
    sleep(1);
 }
  return 0;
}

Android.mk 

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := test
LOCAL_SRC_FILES := test.cpp
LOCAL_CFLAGS := -g
LOCAL_STRIP_MODULE :=false
LOCAL_CPPFLAGS := -g -O0
include $(BUILD_EXECUTABLE)

以上代码可以放到system/core/test目录,然后编译生成test。这边要注意的是,我们需要使用:

out/target/product/***/symbols/system/bin/test 这个bin应用,而非 

out/target/product/***/system/bin/test  目录下的test。然后将test push到被测试机的/system/bin目录:

D:\callen\Downloads {git}
{lamb} adb root

D:\callen\Downloads {git}
{lamb} adb remount
remount succeeded

D:\callen\Downloads {git}
{lamb} adb push test /system/bin
test: 1 file pushed. 1.6 MB/s (20560 bytes in 0.012s)

第二部分:

这边通过android开启wifi,PC端通过网络连接。

首先,配置连接:

Win10:

1). 配置端口:

adb tcpip 5555 

2). 获取IP:

 adb shell ifconfig |grep  "inet addr" 

D:\callen\Downloads {git}
{lamb} adb tcpip 5555
//获取被测试机IP
D:\callen\Downloads {git}
{lamb} adb shell ifconfig |grep  "inet addr"
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet addr:172.16.37.216  Bcast:172.16.37.255  Mask:255.255.255.0

Ubuntu:

连接被测试机:

adb connect 172.16.37.216 

cai@ubuntu:~/N$ adb connect 172.16.37.216
connected to 172.16.37.216:5555

第三部分:

1.android启动gdbserver 来运行被调试应用:

rk3399_mid:/ # gdbserver64 :22335 /system/bin/test
Process /system/bin/test created; pid = 1421
Listening on port 22335

配置在端口22335监听,test的进程号是1421

2.Ubuntu端:

当前路径位于SDK源码根目录。

A:通过gdbclient方式(需要source 编译环境):

Usage: gdbclient <pid|processname> [port number]

cai@ubuntu:~/N$ gdbclient 1421 22335
It looks like gdbserver is already attached to 3592 (process is traced), trying to connect to it using local port=22335
GNU gdb (GDB) 7.11
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from out/target/product/rk3399_mid/symbols/system/bin/test...done.
__dl__start () at bionic/linker/arch/arm64/begin.S:32
32        mov x0, sp

B:如果使用gdb来直接调试:

进入prebuilts/gdb/linux-x86/bin目录,执行:gdb,然后输入:

target remote 172.16.37.216:22335

格式:target remote 被调试机IP:端口

cai@ubuntu:~/N/prebuilts/gdb/linux-x86/bin$ gdb 
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.3) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb) target remote 172.16.37.216:22335
Remote debugging using 172.16.37.216:22335

 

连接上后,Win端的输出如下:

rk3399_mid:/ # gdbserver64 :22335 /system/bin/test
Process /system/bin/test created; pid = 1421
Listening on port 22335
Remote debugging from host 127.0.0.1

执行dir关联下代码:

(gdb) dir system/core/test
Source directories searched: /home/cai/N/system/core/test:$cdir:$cwd

打印堆栈:

(gdb) bt
#0  __dl__start () at bionic/linker/arch/arm64/begin.S:32
#1  0x0000000000000000 in ?? ()
Backtrace stopped: previous frame identical to this frame (corrupt stack?)

查看当前断点情况:

(gdb) info b
No breakpoints or watchpoints.

添加断点:

(gdb) info b
No breakpoints or watchpoints.
(gdb) b system/core/test/test.cpp:7
Breakpoint 1 at 0x5555555690: file system/core/dd/test.cpp, line 7.
(gdb) b system/core/test/test.cpp:13
Breakpoint 2 at 0x55555556d4: file system/core/dd/test.cpp, line 13.

查看断点:

(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000005555555690 in main() at system/core/test/test.cpp:7
2       breakpoint     keep y   0x00000055555556d4 in main() at system/core/test/test.cpp:13

继续执行当前应用:

(gdb) c
Continuing.

Breakpoint 1, main () at system/core/test/test.cpp:7
7         printf("main start...\n");

可见已经在源码的第一个断点停住了。

输入c继续运行,在第二个断点暂停:

(gdb) c
Continuing.

Breakpoint 2, main () at system/core/test/test.cpp:13
13          x++;

打印当前断点变量值:

(gdb) p x
$1 = 5

 

2017-06-16 06:56:17 omnispace 阅读数 449

半年前写了一篇文章,介绍 如何调试Android Framework,但是只提到了Framework中Java代码的调试办法,但实际上有很多代码都是用C++实现的;无奈当时并并没有趁手的native调试工具,无法做到像Java调试那样简单直观(gdb+eclipse/ida之流虽然可以但是不完美),于是就搁置下了。

Android Studio 2.2版本带来了全新的对Android Native代码的开发以及调试支持,另外LLDB的Android调试插件也日渐成熟,我终于可以把这篇文章继续下去了!本文将带来Android Framework中native代码的调试方法。

在正式介绍如何调试之前,必须先说明一些基本的概念。调试器在调试一个可执行文件的时候,必须知道一些调试信息才能进行调试,这个调试信息可多可少(也可以没有)。最直观的比如行号信息,如果调试器知道行号信息,那么在进行调试的时候就能知道当前执行到了源代码的哪一行,如果调试器还知道对应代码的源文件在哪,那么现代IDE的调试器一般就能顺着源码带你飞了,这就是所谓的源码调试。相反,如果没有行号和源码信息,那么只能进行更低级别的调试了,调试器只能告诉你一些寄存器的值;而当前运行的代码也只是PC寄存器所指向的二进制数据,这些数据要么是虚拟机指令,要么是汇编指令;这就是所谓的无源码调试。显然无源码调试相比源码级别的调试要麻烦的多;接下来将围绕这两个方面分别介绍。

用Android Studio进行源码调试

如上文所述,如果需要实现源码调试,必须知道足够的调试信息;在native调试中就是所谓的「调试符号」。但是release版本的动态链接库或者可执行文件一般并不会包含我们需要的调试信息,在Android系统中,/system/lib/* 目录下的那些系统so并没有足够的调试信息,因此如果要进行源码调试,必须自己编译Android源代码,才能获取调试信息,进而让调试器协助我们调试。

Android源码编译是个麻烦事儿,我写过一篇文章介绍 如何使用Docker调试 ;但是,Android版本众多,如果真的需要调试各个版本,在本地进行编译几乎是不可能的——一个版本约占60G空间,如果每个版本都编译,你的Mac还有空间可用吗?因此比较推荐使用云服务进行源码编译;比如使用阿里云的ECS,20M的网速15分钟就能下载完源码;编译速度还勉强,4核8G两个半小时。扯远了 :) 如果你没有精力编译Android源码,我这个 Demo工程 可以让你尝尝鲜,里面包含一些调试的必要文件,可以体会一下Native调试的感觉。

如果我们已经拥有了调试符号,那么还需要保证你的符号文件和设备上真正运行的动态链接库或者可执行文件是对应的,不然就是鸡同鸭讲了。最简单的办法就是使用模拟器。我们编译完源码之后,一个主要的编译产物就是 system.img,这个 system.img会在启动之后挂载到设备的 /system 分区,而system分区包含了Android系统运行时的绝大部分可执行文件和动态链接库,而这些文件就是我们的编译输出,正好可以与编译得到的调试符号进行配合调试。模拟器有一个 -system选项用来指定模拟器使用的 system.img文件;于是这个问题也解决了。

最后一个问题就是,既然是源码调试,当然需要源码了;我们可以在 AOSP 上下载需要的源码即可;需要注意的是,在check分支的时候,必须保证你的分支和编译源码时候的分支是一致的。

需要说明的是,虽然我们使用Android Studio调试,但是其背后的支撑技术实际上是 LLDB。LLDB是一个相当强大的调试器,如果你现在还不知道它为何物,那真的是孤陋寡闻了!建议先简单学习一下 教程

万事俱备,Let’s go!

建立Android Studio工程

实际上任何Android Studio工程都可以进行native源码调试,但是为了方便还是新建一个工程;这个工程是一个空工程,没有任何实际用途;为了体验方便,你可以使用我的这个 Demo 工程,里面包含了调试符号以及模拟器需要使用的system.img。一定要注意Android Studio的版本必须是2.2以上(我的是2.2.3稳定版)。

下载需要调试模块的源码

如果你本地编译了Android源码,那么就不需要这一步了;但是更多的时候我们只是想调试某一个模块,那么只需要下载这个模块的源码就够了。我这里演示的是调试 ART 运行时,因此直接下载ART模块的源码即可,我编译的Android源码版本是 android-5.1.1_r9,因此需要check这个分支的源码,地址在这里:ART-android-5.1.1_r9

运行模拟器

由于我们的调试符号需要与运行时的动态链接库对应,因此我们需要借助模拟器;首先创建一个编译出来的调试符号对应的API版本的模拟器,我这里提供的是5.1.1也就是API 22;然后使用编译出来的 system.img 启动模拟器([Demo]工程的image目录有我编译出来的文件,可以直接使用。):

emulator -avd 22 -verbose -no-boot-anim -system /path/to/system.img

这个过程灰常灰常长!!我启动这个模拟器花了半个多小时,也是醉。现在是2017年,已经是Android创建的第十个年头,ARM模拟器还是烂的一塌糊涂,无力吐槽。一个能让它快一点的诀窍是创建一个小一点的SD card;我设置的是10M。

开始调试

选择native调试模式

首先我们对调试的宿主工程设置一下,选择native调试功能。点击运行下面的按钮 Edit Configuration

然后在debugger栏选择Native:

然后我们点击旁边的 Debug小按钮运行调试程序:

设置调试符号以及关联源码

在运行程序之后,我们可以在Android Studio的状态栏看到,LLDB调试插件自动帮我们完成了so查找路径的过程,这一点比gdb方便多了!在Android Studio的Debug窗口会自动弹出来,如下:

我们点击那个 pause program 按钮,可以让程序暂停运行:

上图左边是正在运行的线程的堆栈信息,右边有两个tab,一个用来显示变量的值;一个是lldb交互式调试窗口!我们先切换到lldb窗口,输入如下命令设置一个断点:

(lldb) br s -n CollectGarbageInternal
Breakpoint 2: where = libart.so`art::gc::Heap::CollectGarbageInternal(art::gc::collector::GcType, art::gc::GcCause, bool), address = 0xb4648c20

可以看到,断点已经成功设置;这个断点在libart.so中,不过现在还没有调试符号信息以及源码信息,我们只知道它的地址。接下来我们设置调试符号以及关联源码。

接下来我们把编译得到的符号文件 libart.so 告诉调试器(符号文件和真正的动态链接库这两个文件名字相同,只不过一个在编译输出的symbols目录) ;在lldb窗口执行:

1
2
3
(lldb) add-dsym /Users/weishu/dev/github/Android-native-debug/app/symbols/libart.so
symbol file '/Users/weishu/dev/github/Android-native-debug/app/symbols/libart.so' \
has been added to '/Users/weishu/.lldb/module_cache/remote-android/.cache/C51E51E5-0000-0000-0000-000000000000/libart.so'

注意后面那个目录你的机器上与我的可能不同,需要修改一下。我们再看看有什么变化,看一下刚刚的断点:

(lldb) br list 2
2: name = ‘CollectGarbageInternal’, locations = 1, resolved = 1, hit count = 0
2.1: where = libart.so`art::gc::Heap::CollectGarbageInternal(art::gc::collector::GcType, art::gc::GcCause, bool) at heap.cc:2124, address = 0xb4648c20, resolved, hit count = 0

行号信息已经加载出来了!!在 heap.cc 这个文件的第2124行。不过如果这时候断点命中,依然无法关联到源码。我们看一下调试器所所知道的源码信息:

(lldb) source info
Lines found in module `libart.so
[0xb4648c20-0xb4648c28): /Volumes/Android/android-5.1.1_r9/art/runtime/gc/heap.cc:2124

纳尼??这个目录是个什么鬼,根本没有这个目录好伐?难道是调试器搞错了?

在继续介绍之前我们需要了解一些关于「调试符号」的知识;我们拿到的调试符号文件其实是一个DWARF文件,只不过这个文件被嵌入到了ELF文件格式之中,而其中的调试符号则在一些名为 .debug_* 的段之中,我们可以用 readelf -S libart.so 查看一下:

编译器在编译libart.so的时候,记录下了编译时候源代码与代码偏移之间的对应关系,因此调试器可以从调试符号文件中获取到源码行号信息;如下:

这下我们明白了上面那个莫名其妙的目录是什么了;原来是在编译libart.so的那个机器上存在源码。那么问题来了,我们绝大多数情况下是使用另外一台机器上的源码进行调试的——比如我提供的那个 Demo工程 包含的带符号libart.so里面保存的源文件信息的目录实际上是我编译的电脑上的目录,而你调试的时候需要使用自己电脑上的目录。知道了问题所在,解决就很简单了,我们需要映射一下;在Android Studio的Debug 窗口的lldb 那个tab执行如下命令:

(lldb) settings set target.source-map /Volumes/Android/android-5.1.1_r9/ /Users/weishu/dev/github/Android-native-debug/app/source/

第一个参数的意思是编译时候的目录信息,第二个参数是你机器上的源码存放路径;设置成自己的即可。

这时候,我们再触发断点(点击demo项目的Debug按钮),看看发生了什么?!

至此,我们已经成功滴完成了在Android Studio中Native代码的源码调试。你可以像调试Java代码一样调试Native代码,step/in/out/over,条件断点,watch point任你飞。你可以借助这个工具去探究Android底层运行原理,比如垃圾回收机制,对象分配机制,Binder通信等等,完全不在话下!

无源码调试

接下来再介绍一下操作简单但是使用门槛高的「无源码调试」方式;本来打算继续使用Android Studio的,但是无奈现阶段还有BUG,给官方提了issue但是响应很慢:https://code.google.com/p/android/issues/detail?id=231116。因此我们直接使用 LLDB 调试;当然,用gdb也能进行无源码调试,但是使用lldb比gdb的步骤要简单得多;不信你可以看下文。

安装Android LLDB工具

要使用lldb进行调试,首先需要在调试设备上运行一个lldb-server,这个lldb-server attach到我们需要调试的进程,然后我们的开发机与这个server进行通信,就可以进行调试了。熟悉gdb调试的同学应该很清楚这一点。我们可以用Android Studio直接下载这个工具,打开SDK Manager:

如上图,勾选这个即可;下载的内容会存放到你的 $ANDROID_SDK/lldb 目录下。

使用步骤

安装好必要的工具之后,就可以开始调试了;整体步骤比较简单:把lldb-server推送到调试设备并运行这个server,在开发机上连上这个server即可;以下是详细步骤。

在手机端运行lldb-server

如果你的调试设备是root的,那么相对来说比较简单;毕竟我们的调试进程lldb-server要attach到被调试的进程是需要一定权限的,如果是root权限那么没有限制;如果没有root,那么我们只能借助run-as命令来调试自己的进程;另外,被调试的进程必须是debuggable,不赘述。以下以root的设备为例(比如模拟器)

  1. 首先把lldb-server push到调试设备。lldb-sever这个文件可以在 `$ANDROID_SDK/lldb/<版本号数字>/android/ 目录下找到,确认你被调试设备的CPU构架之后选择你需要的那个文件,比如大多数是arm构架,那么执行:

    adb push lldb-server /data/local/tmp/

  2. 在调试设备上运行lldb-server。

    adb shell /data/local/tmp/lldb-server platform \
    –server –listen unix-abstract:///data/local/tmp/debug.sock

    如果提示 /data/local/tmp/lldb-server: can’t execute: Permission denied,那么给这个文件加上可执行权限之后再执行上述命令:

    adb shell chmod 777 /data/local/tmp/lldb-server

    这样,调试server就在设备上运行起来了,注意要这么做需要设备拥有root权限,不然后面无法attach进程进行调试;没有root权限另有办法。另外,这个命令执行之后所在终端会进入阻塞状态,不要管它,如下进行的所有操作需要重新打开一个新的终端。

连接到lldb-server开始调试

首先打开终端执行lldb(Mac开发者工具自带这个,Windows不支持),会进入一个交互式的环境,如下图:

  1. 选择使用Android调试插件。执行如下命令:

    platform select remote-android

    如果提示没有Android,那么你可能需要升级一下你的XCode;只有新版本的lldb才支持Android插件。

  2. 连接到lldb-server

    这一步比较简单,但是没有任何官方文档有说明;使用办法是我查阅Android Studio的源码学习到的。如下:

    platform connect unix-abstract-connect:///data/local/tmp/debug.sock

    正常情况下你执行lldb-server的那个终端应该有了输出:

  3. attach到调试进程。首先你需要查出你要调试的那个进程的pid,直接用ps即可;打开一个新的终端执行:

    ~ adb shell ps | grep lldbtest
    u0_a53 2242 724 787496 33084 ffffffff b6e0c474 S com.example.weishu.lldbtest

    我要调试的那个进程pid是 2242,接下来回到lldb的那个交互式窗口执行:

    process attach -p 2242

    如果你的设备没有root,那么这一步就会失败——没有权限去调试一个别的进程;非root设备的调试方法见下文。

    至此,调试环境就建立起来了。不需要像gdb那样设置端口转发,lldb的Android调试插件自动帮我们处理好了这些问题。虽然说了这么多,但是你熟练之后真正的步骤只有两步,灰常简单。

  4. 断点调试

    调试环境建立之后自然就可以进行调试了,如果进行需要学习lldb的使用方法;我这里先演示一下,不关心的可以略过。

    1. 首先下一个断点:

      (lldb) br s -n CollectGarbageInternal
      Breakpoint 1: where = libart.so`art::gc::Heap::CollectGarbageInternal(art::gc::collector::GcType, art::gc::GcCause, bool), address = 0xb4648c20

    2. 触发断点之后,查看当前堆栈:

      1
      2
      3
      4
      5
      
      (lldb) bt
      * thread #8: tid = 2254, 0xb4648c20 libart.so`art::gc::Heap::CollectGarbageInternal(art::gc::collector::GcType, art::gc::GcCause, bool), name = 'GCDaemon', stop reason = breakpoint 1.1
      * frame #0: 0xb4648c20 libart.so`art::gc::Heap::CollectGarbageInternal(art::gc::collector::GcType, art::gc::GcCause, bool)
      frame #1: 0xb464a550 libart.so`art::gc::Heap::ConcurrentGC(art::Thread*) + 52
      frame #2: 0x72b17161 com.example.weishu.lldbtest
      
    3. 查看寄存器的值

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      
      	(lldb) reg read
      General Purpose Registers:
         r0 = 0xb4889600
         r1 = 0x00000001
         r2 = 0x00000001
         r3 = 0x00000000
         r4 = 0xb4889600
         r5 = 0xb4835000
         r6 = 0xb47fcfe4  libart.so`art::Runtime::instance_
         r7 = 0xa6714380
         r8 = 0xa6714398
         r9 = 0xb4835000
         r10 = 0x00000000
         r11 = 0xa6714360
         r12 = 0xb47fbb28  libart.so`art::Locks::logging_lock_
         sp = 0xa6714310
         lr = 0xb464a551  libart.so`art::gc::Heap::ConcurrentGC(art::Thread*) + 53
         pc = 0xb4648c20  libart.so`art::gc::Heap::CollectGarbageInternal(art::gc::collector::GcType, art::gc::GcCause, bool)
       	cpsr = 0x20000030
      

      我们可以看到寄存器 r0的值为 0xb4889600,这个值就是 `CollectGarbageInternal
      函数的第一个参数,this指针,也就是当前Heap对象的地址。在ARM下,r0~r4存放函数的参数,超过四个的参数放在栈上,具体如何利用这些寄存器的信息需要了解一些ARM汇编知识。

    4. 查看运行的汇编代码

      1
      2
      3
      4
      5
      6
      
      (lldb) di -p
      	libart.so`art::gc::Heap::CollectGarbageInternal:
      ->  0xb4648c20 <+0>:  push.w {r4, r5, r6, r7, r8, r9, r10, r11, lr}
      	0xb4648c24 <+4>:  subw   sp, sp, #0x52c
      	0xb4648c28 <+8>:  ldr.w  r9, [pc, #0xa9c]
      	0xb4648c2c <+12>: add    r4, sp, #0x84
      

没有root设备的调试办法

如果没有root权限,那么我可以借助run-as命令。run-as可以让我们以某一个app的身份执行命令——如果我们以被调试的那个app的身份进行attach,自然是可以成功的。

假设被调试的app包名为 com.example.lldb,那么首先想办法把 lldb-server这个文件推送到这个app自身的目录:

  1. adb push直接这么做不太方便(还需要知道userid),我们先push到 /data/local/tmp/

    adb push lldb-server /data/local/tmp/

  2. 然后执行adb shell,连接到Android shell,执行

    run-as com.example.lldb`

  3. 拷贝这个文件到本App的目录,并修改权限;(由于有的手机没有cp命令,改用cat)

    cat /data/local/tmp/lldb-server > lldb-server
    chmod 777 lldb-server

  4. 运行lldb-server

    lldb-server platform –listen unix-abstract:///data/local/tmp/debug.sock

接下来的步骤就与上面root设备的调试过程完全一样了 :)

后记

终于完成了Android调试这一系列的文章,时间跨度长达一年;从Java到C/C++再到汇编级别的调试,从有源码到无源码,从Application层到Framework层,任何代码都可以进行调试。借助强大的IDE以及调试器,我们不仅可以快速定位和解决问题,还可以深入学习任何一个复杂的模块。尤记得用探索用lldb进行native调试的过程,网上没有任何android方面的教程,唯一的学习资料就是Android Studio调试模块的源码以及LLDB Android插件的源码;这其中碰的壁和踩过的坑不计其数。好在最后终于一一解决,可以睡个安稳觉了 ~_~

  1. Android Studio你不知道的调试技巧
  2. 如何调试Android Framework
  3. 如何调试Android Framework Native

原文: http://weishu.me/2017/01/14/how-to-debug-android-native-framework-source/

2016-11-03 13:28:52 songjinshi 阅读数 3794

一、准备工作

1、下载并编译Android系统源码

这里比较灵活,可以下载公司内部机型的代码,也可以下载原生AOSP的代码

环境配置参考

https://source.android.com/source/initializing.html

源码下载参考

https://source.android.com/source/downloading.html

编译运行参考

https://source.android.com/source/building.html

全部编译整个代码工程,中间如果有什么问题可以参考

https://source.android.com/source

2、设置PC端环境

如果要调试自己build的版本,就可以使用out目录下的symbols
PC env setup
这里带一句,symbols是带有debug信息的二进制库或可执行文件,用以调试

除了symbols之外我们还需要gdb(client),可以在源码目录的prebuilts目录下找到他们,为了方便,可以直接设置环境变量
PC env setup2

3、获取调试的权限

最好使用userdebug或者eng build,实在不行也可以使用root后的user build系统,但是要同时关掉selinux

adb shell setenforce 0

否则无法调试

4、设置手机端环境

如果是user build,手机中没有gdbserver,所以需要手动push一个,gdbserver可以去源码目录下的prebuilts目录中搜索一下,但是这里要区分一下gdbserver和gdbserver64
phone env setup
如果是要调试64位的进程就需要gdbserver64
phone env setup2

通过以下命令push到手机中

adb root
adb disable-verity
adb reboot

等待重启完成

adb remount
adb push prebuilts/misc/android-arm/gdbserver/gdbserver /system/bin/

phone env setup3

二、开始调试

这里假设要调试的是zygote进程,首先要知道zygote进程对应的可执行文件,这里是app_process32,64位的zygote64对应的是app_process64

1、gdbserver attach到想要调试的进程

adb shell
ps | grep zygote
gdbserver :1991 --attach 303

这里的端口可以随便指定一个空闲的即可
gdbserver attach

2、gdb client连接到gdbserver

通过执行以下命令进行连接

adb forward tcp:1991 tcp:1991
arm-linux-androideabi-gdb
target remote:1991

这里需要说一下,如果需要调试的进程是64位的,就要用64位的gdb client然后配合gdbserver64

aarch64-linux-android-gdb

3、load对应可执行文件

file /Volumes/1TB-HD/Images/cancro/16.11.01/compressed_cancro_mm-alpha_2016.11.01.18.08_0e62b9421b/out/target/product/cancro/symbols/system/bin/app_process32

4、Set sysroot路径

set sysroot /Volumes/1TB-HD/Images/cancro/16.11.01/compressed_cancro_mm-alpha_2016.11.01.18.08_0e62b9421b/out/target/product/cancro/symbols

gdb client setup

5、设置源码目录

set dir /Volumes/1TB-HD/CodeRoot/CANCRO_ALPHA/

6、设置断点

b frameworks/base/core/jni/fd_utils-inl.h:180

Breakpoint 1 at 0xb6e24f0c: file frameworks/base/core/jni/fd_utils-inl.h, line 180.

7、继续运行

c

Continuing.
[New Thread 5872]
[New Thread 5873]
[New Thread 5874]
[New Thread 5875]

操作并等待运行到断点处

Breakpoint 1, FileDescriptorInfo::Restat (this=0xb4cbd620) at frameworks/base/core/jni/fd_utils-inl.h:182
warning: Source file is more recent than executable.
182       ALOGE("Restat, st_nlink == 8, (%s, fd=%d) : f_stat.st_nlink=%llu,0x%llX file_stat.st_nlink=%llu,0x%llX", 

8、查看变量对应的值

p f_stat.st_nlink

$1 = 1

gdb client setup

三、扩展功能

到这里对于刚接触gdb调试同学也有了入门的知识,对于gdb老手估计是要玩飞的节奏。。。

但是这里要说一下,如果要调试的是framework相关的进程的native代码,可能会受到system server的watchdog的影响,1分钟没有及时响应操作就会触发watchdog而kill到system server进程,zygote也会跟着挂掉,这里有个小技巧可以用一下,就是在调试的过程中,如果需要耗时查看一些运行时状态,可以先执行
adb shell am hang
防止超时重启,查看完毕想要继续执行,就Ctrl+c终止掉am hang即可继续执行,后面就重复这个过程即可。
另外还有一种方式就是用Android Studio在线调试,把断点加在watchdog里面,配置gdb native调试。

2018-01-02 17:25:08 mogoweb 阅读数 1120

对C/C++程序员而言,要说碰到最头疼的问题,无疑就是内存泄漏问题。解决内存泄漏问题似乎很简单,就是秉承一个原则:分配的内存一定要即时释放。然而在实际场景中,随着代码复杂度的增加,要遵守这一原则非常困难,而且随着面向对象、模块化、多线程的引入,更难以判断内存该由谁来释放。为了解决这一难题,C++引入了智能指针和引用计数等。然而引用计数无法解决两个对象相互持有对方引用而引起的内存泄漏。在Android中,引入了强指针(sp)和弱指针(wp)试图解决两个对象相互引用的困境。然而这个皮球又抛到程序员的面前,程序员需要作出决定,何时使用强指针,何时定义弱指针,所以这个方案最终还是不能像Java中的GC(垃圾回收)那样完美的解决内存泄漏问题。

既然内存泄漏似乎无法避免,我们需要做的就是检查程序是否有内存泄漏,并定位到内存泄漏的代码,然后解决之。这个过程不那么简单,特别是对Android C++代码而言。本文探讨的就是如何检查Android C++代码的内存泄漏,文章基于Android官方的一篇文档:Debugging Native Memory Use,但这篇文章写得过于简略,再加上Android系统及Android工具都在快速升级,有些方法在新的环境下不再适用,在参考了一些网络资料及结合实际环境调试之后,现做一个总结。

 1 

有时我们在网上找到一篇文章,照着做却出现这样或者那样的问题,原因就在于计算机技术发展太快,各种软件版本迭代频繁,环境不同了,方法就可能需要做一些修改。本文所写的方法需要两个前提条件:

  1. Android系统已经root,因为需要推送库到system;
  2. 有Android系统对应的版本的Android源码,从Android源码build出我们所需要的库。

我的工作环境为:

  • Ubuntu 16.04 LTS 64位操作系统
  • Android 5.1 源码
  • Nexus 4手机(Android 5.1 AOSP系统)

如果是其它版本的Android系统,理论上也同样适用。

 2 

Android系统使用了一个精简版本的libc,称作bionic,代码位于Android源码的bionic/libc目录。为了调试,需要编译出libc_malloc_debug_leak.so和libc_malloc_debug_qemu.so,这两个库在userdebug版本会被编译出。我们可以在userdebug状态下build系统,或者使用mm命令只编译这个模块。

  1. 将libc_malloc_debug_leak.so和libc_malloc_debug_qemu.so推送到Android设备。

  2. 通过如下命令开启Android设备的malloc debug:

 adb root
 adb shell setprop libc.debug.malloc 1
 adb shell stop
 adb shell start

注意: 开启malloc debug后,系统启动会变慢,请耐心等待,再次重启后会恢复到正常的模式。

  1. 找到开发机HOMEHOME/.android,在里面的ddms.cfg文件下加入一行
    native=true

  2. 下载老版本的Android Tools

最新的Android Studio提供了Android Device Monitor,里面集成了老版本的DDMS,但这个集成版本功能不全, 所以请下载老版本的Android Tools工具,地址:http://dl.google.com/android/repository/tools_r25.2.5-linux.zip

  1. 解压tools_r25.2.5-linux.zip,启动Tools目录下的ddms
  2. 可以看到DDMS界面多了一个Native Heap的tab页,切换到Native Heap页,在左边的进程列表中选择要调试的app进程,然后点击Snapshot Current Native Heap Usage按钮,抓取app native内存的泄漏,包含有泄漏点的调用栈和地址。

image

示例app的源码请参考github: https://github.com/mogoweb/android-testcode ,下面有一个简化的示例malloc_debug_leak。

 3 

在得到内存地址后,我们还需要定位到代码才行。需要注意的是,动态链接库在经过装载和重定位之后,并不能简单的通过addr2line工具来找到代码行。我们需要找到so在内存中装载的基地址。

  1. 使用adb shell ps命令找到app的PID
u0_a52    3509  2047  1530248 47880 ffffffff b6e790dc S com.china_liantong.memoryleaktest
  1. 使用adb shell cat /proc/3509/maps命令找到app的内存映像
a3db7000-a3dcf000 r-xp 00000000 b3:17 276908     /data/app/com.china_liantong.memoryleaktest-2/lib/arm/libnative-lib.so
a3dcf000-a3dd1000 r--p 00017000 b3:17 276908     /data/app/com.china_liantong.memoryleaktest-2/lib/arm/libnative-lib.so
a3dd1000-a3dd2000 rw-p 00019000 b3:17 276908     /data/app/com.china_liantong.memoryleaktest-2/lib/arm/libnative-lib.so
  1. 带有r-xp标记的那一行就是代码在内存中的地址范围,用libnative-lib.so内存泄漏点的地址0xa3dbb2d2减去基地址0xa3db7000即可得到代码地址0x42D2
  2. 使用addr2line工具找出对应的代码行:
alex@alex-ubuntu-dev:~/android/tools$ arm-linux-gnueabi-addr2line -C -f -e /work/myproject/android-testcode/malloc_debug_leak/app/build/intermediates/cmake/debug/obj/armeabi-v7a/libnative-lib.so 0x042d2
Java_com_china_1liantong_memoryleaktest_MainActivity_stringFromJNI
/work/myproject/android-testcode/malloc_debug_leak/app/src/main/cpp/native-lib.cpp:14
  1. 可以看到内存泄漏发生在第14行,native-lib.cpp的源码如下
#include <jni.h>
#include <cstdlib>
#include <cstring>
#include <string>

extern "C"
JNIEXPORT jstring

JNICALL
Java_com_china_1liantong_memoryleaktest_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    char* p = (char*)malloc(1024 * 1024);
    if (p) {
        strcpy(p, "hello");
    }
    return env->NewStringUTF(hello.c_str());
}

 总结 

此malloc的调试原理是:当系统发现我们有libc.debug.malloc的配置时,此时系统会将malloc/free/new/delete 等方法,重新指向到lib_malloc_debug_leak.so里面的对应实现方法,lib_malloc_debug_leak.so里面的方法,多了一些记录信息,将每次的申请时的地址、堆栈等信息记录下来。在需要的时候,通过工具ddms dump出来,分析每个申请的内存,是否正常的释放了,是否出现了内存泄露。

本文给出了一个简单的示例用以说明此方法的可行性,在实际项目中,可能由于memory cache、memory pool的使用,可能找到的泄露点并非真正的内存泄漏,这个需要程序员作出判断。也可能即使找到了泄漏点,解决起来也需要花一番功夫。不管怎么说,找到可疑内存泄漏,也算万里长征迈出了第一步。

没有更多推荐了,返回首页