记一次诡异的性能bug

虽然说Android 10 patchoat已不再使用,但是仍想把这个事记录下,能发现原生Android art问题也是件有趣的事,本身发现问题,解决问题过程中,也可以学到一些新东西。

最后呢,还是应证了那句话,所有诡异的问题到最后都是简单的bug,如果到最后列入了玄学,那多半是你不够深入。

起因

[基于Android 8.1平台 相关代码等已做脱敏处理]

事情的背景是这样的,平台有几个应用从点击然后会黑3s,然后才显示出来(被杀了,然后再启动该应用也这样慢),虽然这个应用有3D一些东西但这个启动速度肯定是无法让人接受的,

那咋办,解决呗!

那…咋解决(优化)? 是从cpu, io, memory,调度等角度入手吗?

还好,用同一平台别的项目机器发现不存在该问题,然后回退该项目的版本,早期版本是好的,最终定位到某个patch,

然而,诡异的是,该同事提交的接口没人用,是hardware下的HWBinder相关的服务(android.hardware.noname-V1.0-java),和该应用也八杆了都打不着,
但是确确实实回退该代码就好了。

那咋搞了呢?代码肯定最终是要要的——尽管现在没人调用,那还是得从根上找找原因。

这个时候性能的同事也上场了,发现那会儿cpu占用老高了,还有一些JIT相关的啥啥啥……,这个时候呢,从性能找原因也是个方向,说不定能成长为大牛,替掉art,干掉安卓,某果啥的性能都是小菜……(想多了)

事情的转机在测试的同事报了个patchoat相关的 signal 6 (SIGABRT)fault addr 问题,
然后好的版本是没问题的,进一步发现是 /data/dalvik-cache/arm 没有生成(那个应用是32位的),而/data/dalvik-cache/arm64有生成,在正常的版本上/data/dalvik-cache/arm都是好的,我们知道这个东西也是谷歌为了性能优化搞的一些东东,所以现在呢,

调查方向由性能调查转为了dalvik cache没有生成,也就是patchoat为啥crash了。

性能 -> dalvik cache -> patchoat crash

同时呢,也有了

  • 问题1: 别人的HW Binder服务都好好的,为啥该服务(android.hardware.noname-V1.0-java)还与dalvik-cach这些扯上关系了?
  那是因为android.hardware.noname-V1.0-java加到了PRODUCT_BOOT_JARS里,然后呢会生成boot...oat之等东西,
  为了加速性能和安全,开机后patchoat会根据/system/framework/{arch}/下boot.art和其他boot-*进行重定位
  可看下我平台下的这些boot*文件
  $ ls /system/framework/arm/    
  ......
  boot-android.hardware.noname-V1.0-java.art
  boot-android.hardware.noname-V1.0-java.oat
  boot-android.hardware.noname-V1.0-java.vdex
  boot-framework.art
  boot-framework.oat
  boot-framework.vdex
  ......
  boot.art
  boot.oat
  boot.vdex

然后另外一同事经过一翻斗争,提出了如下的代码解决了

INoNameHwDevice.hal
-    setTestVall(int32_t value) generates (Result setVolumeRet);
+    setTestVall(int32_t value) generates (Result setTestVallRet);

可是为啥会解决问题呢?猜测是因为和另外一个函数返回值一样

setVolume(TestChannelType type, int32_t value) generates (Result setVolumeRet);

啥?HW Binder服务两个函数返回值还不能一样?这个bug的确是有些诡异…那是不是这么回事呢?

作为一好奇宝宝,我觉得吧,太诡异了,得研究研究看为啥,是不是HWBinder实现带的原生坑,如果是这样的话以后好避坑。
反正还有点空闲,那就看看吧,然后我就查看了HW Binder工具HIDL生成的代码(HIDL和AIDL差不多,都是谷歌用来帮生成进程间通信代码的一个工具,只是HIDL是更底层的一些服务,也会生成C++的),

INoNameHwDevice.hal -> INoNameHwDevice.java
out/target/common/gen/JAVA_LIBRARIES/....../INoNameHwDevice.java
@Override
 public void onTransact(int _hidl_code, android.os.HwParcel _hidl_, final android.os.HwParcel _hidl_reply, int _hidl_flags)
         throws android.os.RemoteException {
 ......
         case 6 /* setVolume */:
         {
             _hidl_request.enforceInterface(evice.kInterfaceName);
             int type = _hidl_request.readInt32();
             int value = _hidl_request.readInt32();
             int _hidl_out_setVolumeRet = setVolume(type, value); // 返回值 _hidl_out_setVolumeRet
             _hidl_reply.writeStatus(.os.HwParcel.STATUS_SUCCESS);
             _hidl_reply.writeInt32(_hidl_out_setVolumeRet);
             _hidl_reply.send();
             break;
         }
......
         case 8 /* setTestVall */:
         {
             _hidl_request.enforceInterface(evice.kInterfaceName);
             int value = _hidl_request.readInt32();
             int _hidl_out_setVolumeRet = setTestVall(value); // 返回值 _hidl_out_setVolumeRet
             _hidl_reply.writeStatus(.os.HwParcel.STATUS_SUCCESS);
             _hidl_reply.writeInt32(_hidl_out_setVolumeRet);
             _hidl_reply.send();
             break;
         }

看了代码,好像说这是这两个返回值一样导致的好像也有可能(JAVA里同一个函数下不允许有相同名的多个变量,即使是他们的作用域不同,c/c++里是允许的),在java代码里一个函数有两个同名变量会编译出错,可是HIDL居然没报错,正常生成了代码,还能正常运行,这实在是另人费解。

而且,

前面提到回退另一同事的patch也能修复该问题,回退后生成的代码也是两个同名变量,这似乎又不能解释通,为啥两个同名变量一个能工作一个又不能工作。

我一度认为回退的patch是不是回退错了,多回退了,但是我自己回退也确定是能工作的。

我也不知道同事是如何定位到该问题,然后咋就想到这,然后偏偏又修复好了该问题,那就实战一把吧,自己动手查查该问题。

经过

分析patchoat crash分析

从logcat里,我们提取如下一些信息

// 运行的命令, 注意input-image-location和instruction-set, 该命令意思是会根据/system/framework/arm/boot.art信息,重定位Image, 成功后输出在/data/dalvik-cache/arm/目录下
01-01 09:39:10.148   500   500 I zygote  : RelocateImage: /system/bin/patchoat --input-image-location=/system/framework/boot.art --output-image-file=/data/dalvik-cache/arm/system@[email protected] --instruction-set=arm --base-offset-delta=-14499840

// crash信息
01-01 09:39:10.174   540   540 W patchoat: Could not reserve sentinel fault page
01-01 09:39:10.716   540   540 F patchoat: hash_set.h:218] Check failed: num_elements_ <= num_buckets_ (num_elements_=134217728, num_buckets_=0) 
01-01 09:39:10.730   540   540 F patchoat: runtime.cc:523] Runtime aborting...
01-01 09:39:10.730   540   540 F patchoat: runtime.cc:523] Dumping all threads without appropriate locks held: thread list lock
01-01 09:39:10.730   540   540 F patchoat: runtime.cc:523] All threads:
..... 一些dump信息,略

第一个信息是运行的patchoat完整命令,也可以在手机上试试看能正常生成dalvik-cache不,直接用该命令就不用编ROM,刷机了,也可以加快调试。

第二个信息是从crash dump里能看出在哪儿挂了,及原因说明(num_elements_ <= num_buckets_)

hash_set.h:218] Check failed: num_elements_ <= num_buckets_ (num_elements_=134217728, num_buckets_=0)

从crash信息里能看出大体的调用关系,要想具体定位某一行,可以根据关系分析,也可借助工具

  • stack

该工具源码位置在 development/scripts/stack
用之前需要 编译下对应的相关代码,也可全编译整个源码,

使用方法就是运行该工具,然后把信息粘贴下,然后再按 Ctrl+Shift+d
然后该工具就会给你分析出结果。

更多c/c++ bug定位方法,可参看
https://blog.csdn.net/qq_33750826/article/details/98872326

示例(堆栈不重要,可略过,只是为了说下stack工具):

$ development/scripts/stack
Reading native crash info from stdin
01-01 08:09:57.659   537   537 F libc    : Fatal signal 6 (SIGABRT), code -6 in tid 537 (main), pid 537 (main)
01-01 08:09:57.714   545   545 I crash_dump32: obtaining output fd from tombstoned, type: kDebuggerdTombstone
01-01 08:09:57.715   545   545 E libc    : failed to connect to tombstoned: No such file or directory
01-01 08:09:57.715   545   545 I crash_dump32: performing dump of process 537 (target tid = 537)
01-01 08:09:57.715   545   545 F DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
01-01 08:09:57.715   545   545 F DEBUG   : Build fingerprint: 'Android/.../...:8.1.0/.../941:userdebug/test-keys'
01-01 08:09:57.715   545   545 F DEBUG   : Revision: '0'
01-01 08:09:57.715   545   545 F DEBUG   : ABI: 'arm'
01-01 08:09:57.715   545   545 F DEBUG   : pid: 537, tid: 537, name: main  >>> /system/bin/patchoat <<<
01-01 08:09:57.715   545   545 F DEBUG   : signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
01-01 08:09:57.718   545   545 F DEBUG   : Abort message: 'hash_set.h:218] Check failed: num_elements_ <= num_buckets_ (num_elements_=134217728, num_buckets_=0) '
01-01 08:09:57.718   545   545 F DEBUG   :     r0 00000000  r1 00000219  r2 00000006  r3 00000008
01-01 08:09:57.718   545   545 F DEBUG   :     r4 00000219  r5 00000219  r6 ffe65064  r7 0000010c
01-01 08:09:57.718   545   545 F DEBUG   :     r8 00000000  r9 ef2f52f8  sl ef2f530c  fp ef2f531c
01-01 08:09:57.718   545   545 F DEBUG   :     ip 00000002  sp ffe65050  lr ef07dc7b  pc ef0776ac  cpsr 200f0030
01-01 08:09:57.726   545   545 F DEBUG   : 
01-01 08:09:57.726   545   545 F DEBUG   : backtrace:
01-01 08:09:57.726   545   545 F DEBUG   :     #00 pc 0001a6ac  /system/lib/libc.so (abort+63)
01-01 08:09:57.726   545   545 F DEBUG   :     #01 pc 0035fdaf  /system/lib/libart.so (art::Runtime::Abort(char const*)+262)
01-01 08:09:57.726   545   545 F DEBUG   :     #02 pc 00007d8d  /system/lib/libbase.so (android::base::LogMessage::~LogMessage()+452)
01-01 08:09:57.727   545   545 F DEBUG   :     #03 pc 001e0291  /system/lib/libart.so (art::HashSet, art::InternTable::GcRootEmptyFn, art::InternTable::StringHashEquals, art::InternTable::StringHashEquals, std::__1::allocator>>::HashSet(unsigned char const*, bool, unsigned int*)+332)
01-01 08:09:57.727   545   545 F DEBUG   :     #04 pc 001dfcd9  /system/lib/libart.so (art::InternTable::Table::AddTableFromMemory(unsigned char const*)+28)
01-01 08:09:57.727   545   545 F DEBUG   :     #05 pc 001dfc9f  /system/lib/libart.so (art::InternTable::AddTableFromMemory(unsigned char const*)+50)
01-01 08:09:57.727   545   545 F DEBUG   :     #06 pc 00005145  /system/bin/patchoat (art::PatchOat::PatchImage(bool)+180)
01-01 08:09:57.727   545   545 F DEBUG   :     #07 pc 000042df  /system/bin/patchoat (art::PatchOat::Patch(std::__1::basic_string, std::__1::allocator> const&, long, std::__1::basic_string, std::__1::allocator> const&, art::InstructionSet, art::TimingLogger*)+4610)
01-01 08:09:57.727   545   545 F DEBUG   :     #08 pc 000072d3  /system/bin/patchoat (main+730)
01-01 08:09:57.727   545   545 F DEBUG   :     #09 pc 0007a435  /system/lib/libc.so (__libc_init+48)
01-01 08:09:57.727   545   545 F DEBUG   :     #10 pc 00002ffc  /system/bin/patchoat (_start_main+88)

// Ctrl+Shift+d后输出,路径做了脱敏处理
Reading symbols from /home/.../out/target/product/.../symbols
signal 6 (SIGABRT), code -6 in tid 537 (main), pid 537 (main)
Revision: '0'
pid: 537, tid: 537, name: main  >>> /system/bin/patchoat <<<
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: 'hash_set.h:218] Check failed: num_elements_ <= num_buckets_ (num_elements_=134217728, num_buckets_=0) '
     r0 00000000  r1 00000219  r2 00000006  r3 00000008
     r4 00000219  r5 00000219  r6 ffe65064  r7 0000010c
     r8 00000000  r9 ef2f52f8  sl ef2f530c  fp ef2f531c
     ip 00000002  sp ffe65050  lr ef07dc7b  pc ef0776ac  cpsr 200f0030
Using arm toolchain from: /home/.../prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.9/bin/

// 具体输出的代码行看右边FILE:LINE就行了
Stack Trace:
  RELADDR   FUNCTION                                                               ......               FILE:LINE
  v------>  inline_tgkill(int, int, int)                                           ......               bionic/libc/bionic/abort.cpp:?
  0001a6ac  abort+64                                                               ......               bionic/libc/bionic/abort.cpp:68
  0035fdaf  art::Runtime::Abort(char const*)+262                                   ......               art/runtime/runtime.cc:553
  v------>  std::__1::function::operator()(char const*) const  ......               external/libcxx/include/functional:1915
  00007d8d  android::base::LogMessage::~LogMessage()+452                           ......               system/core/base/logging.cpp:433
  001e0291  art::HashSet, art::InternTable::GcRoot......d int*)+332    art/runtime/base/hash_set.h:218 // <-- 这里进行比较,之后会有crash信息生成
  001dfcd9  art::InternTable::Table::AddTableFromMemory(unsigned char const*)+28   ......               art/runtime/intern_table.cc:368
  v------>  art::InternTable::AddTableFromMemoryLocked(unsigned char const*)       ......               art/runtime/intern_table.cc:312
  001dfc9f  art::InternTable::AddTableFromMemory(unsigned char const*)+50          ......               art/runtime/intern_table.cc:308
  v------>  art::PatchOat::PatchInternedStrings(art::ImageHeader const*)           ......               art/patchoat/patchoat.cc:476
  00005145  art::PatchOat::PatchImage(bool)+180                                    ......               art/patchoat/patchoat.cc:600
  000042df  art::PatchOat::Patch(std::__1::basic_string  art::patchoat_image(art::TimingLogger&, art::InstructionSet, std::__1::......, bool, bool)  art/patchoat/patchoat.cc:781
  v------>  art::patchoat(int, char**)                                             ......               art/patchoat/patchoat.cc:853
  000072d3  main+730                                                               ......               art/patchoat/patchoat.cc:872
  0007a435  __libc_init+48                                                         ......               bionic/libc/bionic/libc_init_dynamic.cpp:126
  00002ffc  _start_main+88                                                         ......               external/libunwind_llvm/src/libunwind.cpp:?

根据调用堆栈信息和出错信息,整理的代码调用如下,

main()/(art/patchoat/patchoat.cc)
+ art::patchoat(argc, argv);
  + patchoat_image()
    + PatchOat::Patch()
      + p.PatchImage(i == 0)
        + PatchArtFields(image_header);
        | PatchArtMethods(image_header);
        | PatchImTables(image_header);
        | PatchImtConflictTables(image_header);
        | PatchInternedStrings(image_header);
        | + const auto& section = image_header->GetImageSection(ImageHeader::kSectionInternedStrings);
        | + temp_table.AddTableFromMemory(image_->Begin() + section.Offset());
        |   + InternTable::Table::AddTableFromMemory() (art/runtime/intern_table.cc)
        |     + UnorderedSet set(ptr, /*make copy*/false, &read_count);
        |       + HashSet(const uint8_t* ptr, bool make_copy_of_data, size_t* read_count) (art/runtime/base/hash_set.h)
        |          uint64_t temp;
        |          size_t offset = 0;
        |          offset = ReadFromBytes(ptr, offset, &temp); // 从ptr, offset为0开始读64位,即8字节
        |          num_elements_ = static_cast(temp); // 读出值给num_elements_
        |          offset = ReadFromBytes(ptr, offset, &temp); // 再读64位
        |          num_buckets_ = static_cast(temp); // 给 num_buckets_
        |          CHECK_LE(num_elements_, num_buckets_); // <-- 这里比较大小,num_elements_ <= num_buckets_,报错
        + PatchClassTable(image_header);

从代码分析,HashSet()构造时从某个地址里读了两个8字节的数,结果这两数有问题(num_elements_=134217728, num_buckets_=0),那就继续沿着这两个数调查,
这个时候我调查又走了弯路,我去调查 为什么 num_buckets_ 为零 ,而不是比 134217728(即0x8000000)大的数,这两个数代表的是啥意思?
然后呢看 PatchInternedStrings(image_header) --> image_header->GetImageSection(ImageHeader::kSectionInternedStrings);
时,看到 kSectionInternedStrings 定义处有几个相关的定义

art/runtime/image.h
enum ImageSections {
  kSectionObjects,
  kSectionArtFields,
  kSectionArtMethods,
  kSectionRuntimeMethods,
  kSectionImTables,
  kSectionIMTConflictTables,
  kSectionDexCacheArrays,
  kSectionInternedStrings,
  kSectionClassTable,
  kSectionImageBitmap,
  kSectionCount,  // Number of elements in enum.
};

觉得应该是ELF或者art格式的段之类相关的,又去查了下ELF或者art相关的东西(几年前看了ELF相关的,现在都忘了; art格式布局也没找到太好的资料,当然也可以从代码里整理出,但是我懒得整理)....

我也怀疑是不是编译生成的InternedStrings因为啥原因导致数出错了,一度怀疑是不是得回头把编译原理捡起来看看……

调查方向:
性能 -> dalvik cache -> patchoat crash -> num_elements_ <= num_buckets_ -> 为啥num_buckets_不是大于0x8000000的数 -> ELF/art格式 -> InternedStrings -> 编译原理?

总之呢,走了一些弯路,加了好些log, 发觉事情也不对,走下去就是个死胡同了,我突然想看看 OK情况下这两个数为多少 才让事情有了转机,
这些弯路里虽然也学了新东西,我觉得最有用的还是发觉修改了
android.hardware.noname-V1.0-java后只需要push而且是全部push boot*后就可生效,这样也可以加快实验,节约时间。

 push boot*
 adb push out/target/product/.../system/framework/arm/boot* /system/framework/arm/

让我们回到正常情况下num_elements_和num_buckets_为多少继续说,看加的log里,这两个数都为0,
那么这时候应该猜测,不是写入的num_elements_和num_buckets_有问题,而是 有可能读的时候,读到的是后面段地址的数据,
后面地址的数据是谁的数据呢?这个数据是本来就是0x8000000还是读的时候指针飞了?
因为每次出错都是0x8000000,所以读飞了情况不太可能,那就看看是不是重定位写入了脏数据还是原始的文件就是这样,
在这之前,我们得搞清楚是重定位 哪个文件,哪个地址时出问题了,这中间的一些过程和弯路导致数据对不上重复看代码的过程就不说了,
简单说下就是
/system/bin/patchoat --input-image-location=/system/framework/boot.art
运行时会根据boot.art里信息,然后挨个处理boot-*, 最终定位到确实是我们的HWBinder服务有问题,对应的文件名为
/system/framework/arm/boot-android.hardware.noname-V1.0-java.art

简化的流程为

main()/(art/patchoat/patchoat.cc)
+ art::patchoat(argc, argv);
  + patchoat_image()
    + PatchOat::Patch()
      + for (size_t i = 0; i < spaces.size(); ++i) { // <--之前没注意到这有个for循环处理文件
      |     p.PatchImage(i == 0)
      |     + PatchInternedStrings(image_header);
      |      ......
      |           CHECK_LE(num_elements_, num_buckets_); // <-- 这里比较大小,num_elements_ <= num_buckets_,报错
      + }

具体代码为:

art/patchoat/patchoat.cc
bool PatchOat::Patch(const std::string& image_location,
                       off_t delta,
                       const std::string& output_directory,
                       InstructionSet isa,
                       TimingLogger* timings) {
......
    // 得到BootImage Spaces
    std::vector spaces = Runtime::Current()->GetHeap()->GetBootImageSpaces();
........
    // Symlink PIC oat and vdex files and patch the image spaces in memory.
    // 挨个处理
    for (size_t i = 0; i < spaces.size(); ++i) {
      gc::space::ImageSpace* space = spaces[i]; // space
      std::string input_image_filename = space->GetImageFilename(); // <-- 注意可得到具体文件的名字

      std::string input_vdex_filename =
          ImageHeader::GetVdexLocationFromImageLocation(input_image_filename);
      std::string input_oat_filename =
          ImageHeader::GetOatLocationFromImageLocation(input_image_filename);
      std::unique_ptr input_oat_file(OS::OpenFileForReading(input_oat_filename.c_str()));
......// 添加的log
+    LOG(ERROR) << "testlog open input image file at " << input_image_filename;
+    LOG(ERROR) << "testlog open input oat file at " << input_oat_filename;
......
      PatchOat& p = space_to_patchoat_map.emplace(space, // <-- space, 通过这确定了input_image_filename和p.PatchImage可以一一对应起来
                                                  PatchOat(
                                                      isa,
                                                      space_to_memmap_map.find(space)->second.get(),
                                                      space->GetLiveBitmap(),
                                                      space->GetMemMap(),
                                                      delta,
                                                      &space_to_memmap_map,
                                                      timings)).first->second;
......
      if (!p.PatchImage(i == 0)) {


void PatchOat::PatchInternedStrings(const ImageHeader* image_header) {
  const auto& section = image_header->GetImageSection(ImageHeader::kSectionInternedStrings);
......
  // 添加的log
+   PLOG(ERROR) << "testlog Begain:" << std::hex << image_->Begin() << " Offset:" << section.Offset();
  temp_table.AddTableFromMemory(image_->Begin() + section.Offset());

日志输出:

// 文件名
01-01 12:35:04.539   539   539 E patchoat: testlog open input image file at /system/framework/arm/boot-android.hardware.noname-V1.0-java.art
01-01 12:35:04.539   539   539 E patchoat: testlog open input oat file at /system/framework/arm/boot-android.hardware.noname-V1.0-java.oat
01-01 13:46:18.520   539   539 E patchoat: testlog Begain:art
// 偏移0x2000
01-01 13:46:18.520   539   539 E patchoat: 046 Offset:2000: No such file or directory
01-01 12:35:04.541   539   539 E patchoat: testlog elements 134217728 bukets 0

肯定了文件(boot-android.hardware.noname-V1.0-java.art)和位置(0x2000)之后,就应该看是本来文件数据就有问题,还是重定位写入了脏数据。

调查方向:
性能 -> dalvik cache -> patchoat crash -> num_elements_ <= num_buckets_ -> 为啥num_elements_不为0 -> 确定文件和偏移 -> 文件本身问题?脏数据?

这时候就需要两个工具了oatdump和 二进制查看工具 vim -b

  • oatdump

这个工具就是用于dump art oat文件信息的,方便查看(对于dex格式的可用dexdump),
这个工具电脑或手机侧都可以用,因为我的代码都编译过,所以在电脑上使用快些也方便,
具体的手法可以

oatdump -h 查看下

$ oatdump -h
Usage: oatdump [options] ...
    Example: oatdump --image=$ANDROID_PRODUCT_OUT/system/framework/boot.art
    Example: adb shell oatdump --image=/system/framework/boot.art

  --oat-file=: specifies an input oat filename.
      Example: --oat-file=/system/framework/boot.oat

  --image=: specifies an input image location.
      Example: --image=/system/framework/boot.art
  .....

要说的 oatdump使用的坑

1.
就是后面接的路径.../system/framework/boot.art,得结合--instruction-set,所以
.../system/framework/boot.art 最终会找.../system/framework/arm/boot.art

2.
不知道为啥参数不能直接用boot-android.hardware.noname-V1.0-java.art,似乎只能是boot.art
,所以呢导致速度慢。

相关的dump信息如下,

从中可以看到 InternedStrings ClassTable ImageBitmap 三个的起始地址都是重叠的

oatdump --image=$ANDROID_PRODUCT_OUT/system/framework/boot.art --instruction-set=arm

 IMAGE LOCATION: /home/.../out/target/product/.../system/framework/boot-android.hardware.noname-V1.0-java.art
 
 IMAGE BEGIN: 0x70810000
 
 IMAGE SIZE: 8192
......
 IMAGE SECTION SectionDexCacheArrays: size=7880 range=312-8192
 
 IMAGE SECTION SectionInternedStrings: size=0 range=8192-8192 //这是我们要关心的段信息,range 8192即为0x2000
 
 IMAGE SECTION SectionClassTable: size=0 range=8192-8192 // 注意和ClassTable重叠了
 
 IMAGE SECTION SectionImageBitmap: size=4096 range=8192-12288 // 注意和ImageBitmap起始地址也是重叠的
 
 OAT CHECKSUM: 0xfc8b0837

因为dump一次太慢了,所以对工具进行了修改,只需要dump文件名和Sections就行。

修改代码如下:

art/oatdump/oatdump.cc
 static int DumpImage(gc::space::ImageSpace* image_space,
     fprintf(stderr, "Invalid image header %s\n", image_space->GetImageLocation().c_str());
     return EXIT_FAILURE;
   }
+  fprintf(stderr, "testlog ---->\n");
+  image_space->Dump(*os); // 这个会打出文件名
+  fprintf(stderr, "\n");
+  image_space->DumpSections(*os); // 打印出Section信息
+  fprintf(stderr, "testlog <----\n");
+
+  bool test = false;
   ImageDumper image_dumper(os, *image_space, image_header, options);
-  if (!image_dumper.Dump()) {
+  if (test && !image_dumper.Dump()) { // 后面就不再dump很具体的信息了
     return EXIT_FAILURE;
   }

用修改后的工具dump出来信息格式不太一样,但是也表明
InternedStrings ClassTable ImageBitmap 三个的起始地址都是重叠的

// 文件名在name里
SpaceTypeImageSpace begin=0x70810000,end=0x70810138,size=312B,name="/home/.../out/target/product/.../system/framework/arm/boot-android.hardware.noname-V1.0-java.art"]
SectionObjects 0x70810000-0x70810138
......
SectionIMTConflictTables 0x70810138-0x70810138
SectionDexCacheArrays 0x70810138-0x70812000
SectionInternedStrings 0x70812000-0x70812000 // 偏移即为0x70812000-0x70810000 = 0x2000
SectionClassTable 0x70812000-0x70812000 // 偏移也为0x2000
SectionImageBitmap 0x70812000-0x70813000 // 偏移也为0x2000

那接下来就用vim看下0x2000偏移里值为多少吧。

  • vim -b

因为我的是Linux系统,所以用vim方便,你也可以用别的二进制查看工具

用法就是

vim -b 文件名

然后输入 %!xxd 就可以查看了

$ vim -b boot-android.hardware.noname-V1.0-java.art
%!xxd
00000000: 6172 740a 3034 3600 0000 8170 0020 0000  art.046....p. ..
......
// 0x2000偏移的值
00002000: 0000 0008 0000 0000 0000 0000 0000 0000  ................

我们终于看到 0x2000~0x2007 这8个字节的值为 0000 0008 0000 0000 0000

还记得之前代码里我们从该地址读了8字节赋给了num_elements_吗?

+ HashSet(const uint8_t* ptr, bool make_copy_of_data, size_t* read_count) (art/runtime/base/hash_set.h)
   offset = ReadFromBytes(ptr, offset, &temp); // 从ptr, offset为0开始读64位,即8字节
   num_elements_ = static_cast(temp); // 读出值给num_elements_

因为大小端的原因,0x2000~0x2007值即为0x0800 0000, 即 134217728,和出错信息里的值num_elements_=134217728是吻合的。

到这里,我们终于搞清楚了,原来 文件本身生成就是有问题,即我们接下来看看

为啥生成会有问题?

文件生成

结合oatdump信息 SectionInternedStrings SectionImageBitmap地址重合,
所以有理由怀疑是读到了ImageBitmap段的数据,那就看看这个写入了些啥数据吧。

调查方向:
性能 -> dalvik cache -> patchoat crash -> num_elements_ <= num_buckets_ -> 为啥num_elements_不为0 -> 确定文件和偏移 -> 文件本身问题 -> ImageBitmap段的数据

通过搜索关键字 kSectionImageBitmap 觉得 art/compiler/image_writer.cc 应该是编译时生成文件的代码所在,

通过进一步分析代码,搜索关键字 image_bitmap_,觉得在

AllocMemory()
CreateHeader()
CopyAndFixupObject()
Write()
...

等地方都有可能,我们可以在可疑地方把数据dump一下,进一步确认,
添加的日志示例如下:

@@ -305,6 +306,17 @@ bool ImageWriter::Write(int image_fd,
......
    const ImageSection& bitmap_section = image_header->GetImageSection(
        ImageHeader::kSectionImageBitmap); // bitmap_section
    // Align up since data size may be unaligned if the image is compressed.
    size_t bitmap_position_in_file = RoundUp(sizeof(ImageHeader) + data_size, kPageSize); // <-- ** 他的位置要进行Page对齐

     // 添加这个是为了方便确定当前输出文件,确定其偏移
+    PLOG(ERROR) << "testlog write image file " << image_filename << " position:0x" << std::hex << bitmap_position_in_file << std::endl;
    // dump 代码省略
    // 写文件
    if (!image_file->PwriteFully(reinterpret_cast(image_info.image_bitmap_->Begin()),
                                 bitmap_section.Size(),
                                 bitmap_position_in_file)) {
......
@@ -2210,8 +2236,29 @@ void ImageWriter::CopyAndFixupObject(Object* obj) {
   auto* dst = reinterpret_cast(image_info.image_->Begin() + offset);
......
+  {
     // char *p = reinterpret_cast(image_info.image_bitmap_->Begin());
+    for (size_t tempi=0; tempi < obj->SizeOf() && tempi < 5; ++tempi) {
+      int what = *p;
+      PLOG(ERROR) << "testlog CopyAndFixupObject dump3 " << what << " p:" << (void *) p << std::endl;
+      p++;
+    }
+  }
   // Set()前后都添加了dump, 以进一步确认
   image_info.image_bitmap_->Set(dst);  // Mark the obj as live.
+  {
+    // char *p = reinterpret_cast(image_info.image_bitmap_->Begin());
+    char *p = (char*)(image_info.image_bitmap_->Begin());
+    for (size_t tempi=0; tempi < obj->SizeOf() && tempi < 5; ++tempi) {
+      int what = *p;
+      PLOG(ERROR) << "testlog CopyAndFixupObject dump5 " << what << " p:" << (void *) p << std::endl;
+      p++;
+    }
+  }

通过分析 mm 模块编译的日志

// 先通过write时的文件名和dump数据时位置为0x7f16a82a4003
dex2oatd E 01-07 17:30:44 798494 798494 image_writer.cc:309] testlog write image file out/target/product/.../dex_bootjars/system/framework/arm/boot-android.hardware.noname-V1.0-java.art position:0x2000
dex2oatd E 01-07 17:30:44 798494 798494 image_writer.cc:315] testlog write buffer dump2 0 p:0x7f16a82a4000
dex2oatd E 01-07 17:30:44 798494 798494 image_writer.cc:315] testlog write buffer dump2 0 p:0x7f16a82a4001
dex2oatd E 01-07 17:30:44 798494 798494 image_writer.cc:315] testlog write buffer dump2 0 p:0x7f16a82a4002
dex2oatd E 01-07 17:30:44 798494 798494 image_writer.cc:315] testlog write buffer dump2 8 p:0x7f16a82a4003

// 再次过滤日志,发现 dump3和dump5时数据就变为8了, 对应加的日志,即为 image_info.image_bitmap_->Set(dst); 前后
dex2oatd E 01-07 17:30:41 798494 798494 image_writer.cc:2231] testlog CopyAndFixupObject dump3 0 p:0x7f16a82a4003
dex2oatd E 01-07 17:30:41 798494 798494 image_writer.cc:2242] testlog CopyAndFixupObject dump5 8 p:0x7f16a82a4003

最终定位在image_info.image_bitmap_->Set(dst)之后数据就有问题了,

具体代码:

art/compiler/image_writer.cc
void ImageWriter::CopyAndFixupObject(Object* obj) {...
  image_info.image_bitmap_->Set(dst);  // Mark the obj as live

art/runtime/gc/accounting/space_bitmap.h
bool Set(const mirror::Object* obj) ALWAYS_INLINE {
  return Modify(obj);
}

art/runtime/gc/accounting/space_bitmap-inl.h
inline bool SpaceBitmap::Modify(const mirror::Object* obj) {
  uintptr_t addr = reinterpret_cast(obj);
......
  const uintptr_t mask = OffsetToMask(offset);
  DCHECK_LT(index, bitmap_size_ / sizeof(intptr_t)) << " bitmap_size_ = " << bitmap_size_;
  Atomic* atomic_entry = &bitmap_begin_[index]; // <-- entry为bitmap_begin_, 在SpaceBitmap()构造时赋值
  uintptr_t old_word = atomic_entry->LoadRelaxed();
  if (kSetBit) {
    // Check the bit before setting the word incase we are trying to mark a read only bitmap
    // like an image space bitmap. This bitmap is mapped as read only and will fault if we
    // attempt to change any words. Since all of the objects are marked, this will never
    // occur if we check before setting the bit. This also prevents dirty pages that would
    // occur if the bitmap was read write and we did not check the bit.
    if ((old_word & mask) == 0) {
      atomic_entry->StoreRelaxed(old_word | mask); // <--应该在这儿改变了值

虽然吧……看了老半天也没太懂StoreRelaxed()究竟咋存的,也不太想继续研究,但感觉应该OK的代码这个值也是这样,

为啥OK时读bitmap section时这个值没出错呢?

那就看看OK时的情况吧,编译了OK时的boot-android.hardware.noname-V1.0-java.art,用vim -b查看0x2000的值居然都为0,

00002000: 0000 0000 0000 0000 0000 0000 0000 0000  ................

奇了怪了……

偶然发现,原来!他的偏移为0x3000

// oatdump OK时的信息
SpaceTypeImageSpace begin=0x70810000,end=0x70810138,size=312B,name="/home/.../out/target/product/.../system/framework/arm/boot-android.hardware.noname-V1.0-java.art"]
SectionObjects 0x70810000-0x70810138
SectionArtFields 0x70810138-0x70810138
......
SectionDexCacheArrays 0x70810138-0x70812008
SectionInternedStrings 0x70812008-0x70812008
SectionClassTable 0x70812008-0x70812008 // 结束地址为 0x2008
SectionImageBitmap 0x70813000-0x70814000 // Bitmap起始地址为0x3000

// mm模块时日志输出也表明偏移为0x3000
dex2oatd E 01-07 17:03:19 790902 790902 image_writer.cc:309] testlog write image file out/target/product/.../dex_bootjars/system/framework/arm/boot-android.hardware.noname-V1.0-java.art position:0x3000

那再看0x3000地址的值,果然和NG时0x2000地址值是一样的,都为0x800 0000

00002000: 0000 0000 0000 0000 0000 0000 0000 0000  ................
// 0x2008之后的8字节都为0
00002010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0x3000地址的值0x800 0000
00003000: 0000 0008 0000 0000 0000 0000 0000 0000  ................

这时候就只剩下最后个问题了,

为啥OK时SectionClassTable和SectionImageBitmap的地址没重合呢?

看之前代码分析,ImageWriter::Write()的时要求kPageSize也就是4K(4096)对齐,

art/compiler/image_writer.cc
bool ImageWriter::Write(int image_fd,
......
    // Align up since data size may be unaligned if the image is compressed.
    size_t bitmap_position_in_file = RoundUp(sizeof(ImageHeader) + data_size, kPageSize); 

0x2008(即十进制数8200)不能整除4096, 下一个能整除的是0x3000,

至此,整个事情就明了了,剩下的就是修复问题了。

结果

从以上详细分析来看,NG时编译出来的段的偏移为0x2000, 刚好能4K对齐,InternedStrings和ImageBitmap重叠,

所以如果InternedStrings section 大小为零时继续读,就读到了后面bitmap段的数据了。

而OK时段偏移为0x2008, 下一个4K对齐的地址为0x3000, 0x2008后两个8字节数据都刚好为0,所以刚好就没问题了。

如果非要用个图来表示的话,大概是这样的 (图中数据存储用的大端模式,方便画,数据为16进制)

        NG                              OK
      +----+                             +----+
0x2000| 00 +--+ InternedStrings    0x2000| 00 |
0x2001| 00 |  | ClassTable            ...| 00 |
0x2002| 00 |  + ImageBitmap           ...| 00 |
0x2003| 08 |                          .  | 00 |
   ...| 00 |                          .  | .  |
   ...|... |                          .  | .  |
0x2007| 00 |                       0x2007|    |
      +----+                       0x2008| 00 +--+ InternedStrings
                                      .  | 00 |  | ClassTable
                                   0x2010| 00 |  +
                                      .  | .  |
                                   0x3000| 00 +--+ ImageBitmap
                                   0x3001| 00 |
                                   0x3002| 00 |
                                   0x3003| 08 |
                                   0x3004| 00 |
                                      ...| .  |
                                   0x3007| 00 |
                                         +----+

解决方法:

段的大小为零时直接return,不做处理就行(PatchClassTable()其实也已经这样处理了)。

art/patchoat/patchoat.cc
 void PatchOat::PatchInternedStrings(const ImageHeader* image_header) {
   const auto& section = image_header->GetImageSection(ImageHeader::kSectionInternedStrings);
+  if (section.Size() == 0) {
+    return;
+  }

总结

art对于我来说刚开始感觉很高深,心里就有点怂了,认为搞不定搞不定搞不宝,不过嘛,bug嘛,又不是实现,不用怕。

解决的过程和手段其实和别的问题也差不多:

收集信息,看代码,分析日志,根据新线索调整方向,用工具,加log.....
  • 整个调查方向进程:
  • 工具:

TBD

oatdump
vim -b
readelf

  • 另外,知道了改了与boot相关的需要替换所有的boot*
adb push out/target/product/.../system/framework/arm/boot* /system/framework/arm/

你可能感兴趣的:(android,性能,虚拟机,art)