
                            
                            因收到Google相关通知，网站将会择期关闭。相关通知内容
                            
                            
                            练习Sample跑起来 热点问题答疑第1期
                            你好，我是专栏的“学习委员”孙鹏飞。

专栏上线以来很多同学反馈，说在运行练习Sample的时候遇到问题。由于这些Sample多是采用C/C++来完成的，所以在编译运行上会比传统的纯Java项目稍微复杂一些。今天我就针对第1期～第4期中，同学们集中遇到的问题做一期答疑。设置练习的目的，也是希望你在学习完专栏的内容后，可以快速上手试验一下专栏所讲的工具或方法，帮你加快掌握技术的精髓。所以希望各位同学可以多参与进来，有任何问题也可以在留言区给我们反馈，后面我还会不定期针对练习再做答疑。

编译环境配置

首先是同学们问得比较多的运行环境问题。

前几期的练习Sample大多是使用C/C++开发的，所以要运行起来需要先配置好SDK和NDK，SDK我们一般都是配置好的，NDK环境的配置有一些特殊的地方，一般我们的Sample都会使用最新的NDK版本，代码可能会使用C++11/14的语法进行编写，并且使用CMake进行编译，我这里给出NDK环境的配置项。

首先需要去NDK官网下载最新版本，下载后可以解压到合适的地方，一般macOS可以存放在 ANDROID_SDK_HOME/ndk_bundle目录下，Android Studio可以默认找到该目录。如果放到别的目录，可能需要自己指定一下。

指定NDK目录的方法一般有下面两种。

1.在练习Sample根目录下都会有一个local.properties文件，修改其中的ndk.dir路径即可。

ndk.dir=/Users/sample/Library/Android/sdk/ndk-bundle
 sdk.dir=/Users/sample/Library/Android/sdk


2.可以在Android Studio里进行配置，打开File -> Project Structure -> SDK Location进行修改。



上面两种修改方法效果是一致的。

有些Sample需要降级NDK编译使用，可能需要下载旧版本的NDK，可以从官网下载。

之后需要安装CMake和LLDB。


CMake：一款外部构建工具，可与Gradle搭配使用来构建原生库。

LLDB：一种调试程序，Android Studio使用它来调试原生代码。


这两项都可以在Tools > Android > SDK Manager里进行安装。



这样我们编译所需要的环境就配置好了。

热点问题答疑

01 | 崩溃优化（上）：关于“崩溃”那些事儿

关于第1期的Sample，同学们遇到的最多的问题是使用模拟器运行无法获取Crash日志的问题。

引起这个问题的缘由比较深层，最直观的原因是使用Clang来编译x86平台下的Breakpad会导致运行出现异常，从而导致无法抓取日志。想要解决这个问题，我们需要先来了解一下NDK集成的编译器。

NDK集成了两套编译器：GCC和Clang。从NDK r11开始，官方就建议使用Clang，详情可以看ChangeLog，并且标记GCC为Deprecated，并且从GCC 4.8升级到4.9以后就不再进行更新了。NDK r13开始，默认使用Clang。NDK r16b以后的版本貌似强制开启GCC会引起错误，并将libc++作为默认的STL，而NDK r18干脆就完全删除了GCC。

由于Clang的编译会引起x86的Breakpad执行异常，所以我们需要切换到GCC下进行编译，步骤如下。

1.首先将NDK切换到r16b，你可以从这里下载，在里面找到对应你操作系统平台的NDK版本。

2.在Android Studio里设置NDK路径为ndk-16b的路径。

3.在练习例子源码的sample和breakpad-build的build.gradle配置里进行如下配置。

externalNativeBuild {
   cmake {
     cppFlags "-std=c++11"
     arguments "-DANDROID_TOOLCHAIN=gcc"
   }
}


第二个问题是日志解析工具如何获取。

解析Minidump日志主要是使用minidump_stackwalk工具，配合使用的工具是dump_syms，这个工具可以获取一个so文件的符号表。

这两项工具需要通过编译Breakpad来获取，有部分同学查到的文章是采用Chrome团队的depot_tools来进行工具的源码下载、编译操作。depot_tools是个很好用的工具，但是在国内其服务器是无法访问的，所以我们采用直接下载源码编译的方式相对来说比较方便。

编译Breakpad有一些需要注意的地方，由于Android平台的内核是Linux，Android里的动态链接库的符号表导出工具dump_syms需要运行在Linux下（暂时没有找到交叉编译在别的平台上的办法），所以下面的步骤都是在Linux环境（Ubuntu 18.04）下进行的，步骤如下。

1.先下载源码。

2.由于源码里没有附带上一些第三方的库，所以现在编译会出现异常，我们需要下载lss库到Breakpad源码目录src/third_party下面。

git clone https://chromium.googlesource.com/linux-syscall-support 


3.然后在源码目录下执行。

./configure && make
make install


这样我们就可以直接调用minidump_stackwalk、dump_syms工具了。

第三个问题是如何解析抓取下来的Minidump日志。

生成的Crash信息，如果授予Sdcard权限会优先存放在/sdcard/crashDump下，便于我们做进一步的分析。反之会放到目录/data/data/com.dodola.breakpad/files/crashDump下。

你可以通过adb pull命令拉取日志文件。

adb pull /sdcard/crashDump/


1.首先我们需要从产生Crash的动态库中提取出符号表，以第1期的Sample为例，产生Crash的动态库obj路径在Chapter01/sample/build/intermediates/cmake/debug/obj下。



此处需要注意一下手机平台，按照运行Sample时的平台取出libcrash-lib.so库进行符号表的dump，然后调用dump_syms工具获取符号表。

dump_syms libcrash-lib.so > libcrash-lib.so.sym


2.建立符号表目录结构。首先打开刚才生成的libcrash-lib.so.syms，找到如下编码。

MODULE Linux arm64 322FCC26DA8ED4D7676BD9A174C299630 libcrash-lib.so


然后建立如下结构的目录Symbol/libcrash-lib.so/322FCC26DA8ED4D7676BD9A174C299630/，将libcrash-lib.so.sym文件复制到该文件夹中。注意，目录结构不能有错，否则会导致符号表对应失败。

3.完成上面的步骤后，就可以来解析Crash日志了，执行minidump_stackwalk命令。

minidump_stackwalk crash.dmp ./Symbol > dump.txt 


4.这样我们获取的crash日志就会有符号表了，对应一下之前没有符号表时候的日志记录。



5.如果我们没有原始的obj，那么需要通过libcrash-lib.so的导出符号来进行解析，这里用到的工具是addr2line工具，这个工具存放在$NDK_HOME/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line下。你要注意一下平台，如果是解析64位的动态库，需要使用aarch64-linux-android-4.9下的addr2line（此处是64位的）。

aarch64-linux-android-addr2line -f -C -e  libcrash-lib.so 0x5f8
Java_com_dodola_breakpad_MainActivity_crash


6.可以使用GDB来根据Minidump调试出问题的动态库，这里就不展开了，你可以参考这里。

03 | 内存优化（上）：4GB内存时代，再谈内存优化

针对这一期的Sample，很多同学询问Sample中经常使用的Hook框架的原理。

Sample中使用的Hook框架有两种，一种是Inline Hook方案（Substrate和HookZz），一种是PLT Hook方案（Facebook Hook），这两种方案各有优缺点，根据要实现功能的不同采取不同的框架。

PLT Hook相对Inline Hook的方案要稳定很多，但是它操作的范围只是针对出现在PLT表中的动态链接函数，而Inline Hook可以hook整个so里的所有代码。Inline Hook由于要针对各个平台进行指令修复操作，所以稳定性和兼容性要比PLT Hook差很多。

关于PLT Hook的内容，你可以看一下《程序员的自我修养：链接、装载与库》这本书，而Inline Hook则需要对ARM、x86汇编，以及各个平台下的过程调用标准（Procedure Call Standard）有很深入的了解。

第3期里，还有部分同学询问Sample中的函数符号是如何来的。

首先如果我们要hook一个函数，需要知道这个函数的地址。在Linux下我们获取函数的地址可以通过dlsym函数来根据名字获取，动态库里的函数名称一般都会通过Name Mangling技术来生成一个符号名称（具体细节可以看这篇文章），所以第3期的Sample里出现了很多经过转换的函数名。

void *hookRecordAllocation26 = ndk_dlsym(handle,
"_ZN3art2gc20AllocRecordObjectMap16RecordAllocationEPNS_6ThreadEPNS_6ObjPtrINS_6mirror6ObjectEEEj");

    void *hookRecordAllocation24 = ndk_dlsym(handle,                                        "_ZN3art2gc20AllocRecordObjectMap16RecordAllocationEPNS_6ThreadEPPNS_6mirror6ObjectEj");


这样的函数可以通过c++filt工具来进行反解，我在这里给你提供一个网页版的解析工具。



我们需要阅读系统源码来寻找Hook点，比如第3期里Hook的方法都是虚拟机内存分配相关的函数。需要注意的一点是，要先确认是否存在该函数的符号，很多时候由于强制Inline的函数或者过于短小的函数可能没有对应的符号，这时候就需要使用objdump、readelf、nm或者各种disassembly工具进行查看，根据类名、函数名查找一下有没有对应的符号。

总结

第1期的Breakpad的Sample主要是展示Native Crash的日志如何获取和解读。根据业务的不同，我们平时接触的很多都是Java的异常，在业务不断稳定、代码异常处理逐渐完善的情况下，Java异常的量会逐渐减少，而Native Crash的问题会逐步的显现出来。一般比较大型的应用，都会或多或少包含一些Native库，比如加密、地图、日志、Push等模块，由于多方面的原因，这些代码会产生一些异常，我们需要了解Crash日志来排查解决，又或者说绕过这些异常，进而提高应用的稳定性。

通过Breakpad的源码，以帮你了解到信号捕获、ptrace的使用、进程fork/clone机制、主进程子进程通信、unwind stack、system info的获取、memory maps info的获取、symbol的dump，以及symbol反解等，通过源码我们可以学习到很多东西。

第2期的Sample提供了解决系统异常的一种思路，使用反射或者代理机制来解决系统代码中的异常。需要说明的是FinalizerWatchdog机制并不是系统异常，而是系统的一种防护机制。很多时候我们会遇到一些系统Framework的bug产生的Crash，比如很常见的Toast异常等，这些异常虽然不属于本应用产生的，但也会影响用户的使用，解决这种异常可以考虑一下这个Sample中的思路。

第3期的Sample描述了一个简单的Memory Allocation Trace监控模块，这个模块主要是配合自动性能分析体系来自动发现问题，比如大对象的分配数量监控、分配对象的调用栈分析等。它可以做的事很多，同学们可以根据这个思路，根据自己的业务来开发适合自己的工具。

从第3期的Sample的代码，你可以学习到Inline Hook Substrate框架的使用，使用ndk_dlopen来绕过Android Classloader-Namespace Restriction机制，以及C++里的线程同步等。

欢迎你点击“请朋友读”，把今天的内容分享给好友，邀请他一起学习。

                        
                        
                            